From e1cd5efe261f2f4ba986b67409a19b9e392510ed Mon Sep 17 00:00:00 2001 From: seancarroll Date: Sun, 3 Jan 2021 21:47:23 -0600 Subject: more organization --- src/lib.rs | 2045 ++++++++++++++++++++++++++++++------------------------------ 1 file changed, 1025 insertions(+), 1020 deletions(-) (limited to 'src/lib.rs') diff --git a/src/lib.rs b/src/lib.rs index c4c8cc9..2e75d18 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,292 +26,180 @@ static INDENT: &str = " "; // Point(Point), // } -/// Modifier indicating where on a node an edge should be aimed. -/// If Port is used, the corresponding node must either have record shape with one of its -/// fields having the given portname, or have an HTML-like label, one of whose components has a -/// PORT attribute set to portname. -#[derive(Clone, PartialEq, Eq, Debug)] -pub enum PortPosition { - Port { - port_name: String, - compass_point: Option, - }, - Compass(CompassPoint), + +trait DotString<'a> { + fn dot_string(&self) -> Cow<'a, str>; } -// TODO: AsRef vs this? -impl<'a> DotString<'a> for PortPosition { +pub enum ArrowType { + Normal, + Dot, + Odot, + None, + Empty, + Diamond, + Ediamond, + Box, + Open, + Vee, + Inv, + Invdot, + Invodot, + Tee, + Invempty, + Odiamond, + Crow, + Obox, + Halfopen, +} + +impl<'a> DotString<'a> for ArrowType { fn dot_string(&self) -> Cow<'a, str> { match self { - PortPosition::Port { - port_name, - compass_point, - } => { - let mut dot_string = port_name.to_owned(); - if let Some(compass_point) = compass_point { - dot_string - .push_str(format!(":{}", compass_point.dot_string()).as_str()); - } - dot_string.into() - } - PortPosition::Compass(p) => p.dot_string().into(), + ArrowType::Normal => "normal".into(), + ArrowType::Dot => "dot".into(), + ArrowType::Odot => "odot".into(), + ArrowType::None => "none".into(), + ArrowType::Empty => "empty".into(), + ArrowType::Diamond => "diamond".into(), + ArrowType::Ediamond => "ediamond".into(), + ArrowType::Box => "box".into(), + ArrowType::Open => "open".into(), + ArrowType::Vee => "vee".into(), + ArrowType::Inv => "inv".into(), + ArrowType::Invdot => "invdot".into(), + ArrowType::Invodot => "invodot".into(), + ArrowType::Tee => "tee".into(), + ArrowType::Invempty => "invempty".into(), + ArrowType::Odiamond => "odiamond".into(), + ArrowType::Crow => "crow".into(), + ArrowType::Obox => "obox".into(), + ArrowType::Halfopen => "halfopen".into(), } } } -/// The text for a graphviz label on a node or edge. -#[derive(Clone, PartialEq, Eq, Debug)] -pub enum AttributeText<'a> { - /// Preserves the text directly as is. - AttrStr(Cow<'a, str>), - - /// This kind of label uses the graphviz label escString type: - /// - /// - /// Occurrences of backslashes (`\`) are not escaped; instead they - /// are interpreted as initiating an escString escape sequence. - /// - /// Escape sequences of particular interest: in addition to `\n` - /// to break a line (centering the line preceding the `\n`), there - /// are also the escape sequences `\l` which left-justifies the - /// preceding line and `\r` which right-justifies it. - EscStr(Cow<'a, str>), - - /// This uses a graphviz [HTML string label][html]. - /// The string is printed exactly as given, but between `<` and `>`. - /// **No escaping is performed.** - /// - /// [html]: https://graphviz.org/doc/info/shapes.html#html - HtmlStr(Cow<'a, str>), - - /// Preserves the text directly as is but wrapped in quotes. - /// - /// Occurrences of backslashes (`\`) are escaped, and thus appear - /// as backslashes in the rendered label. - QuotedStr(Cow<'a, str>), +pub enum ClusterMode { + Local, + Global, + None, } -impl<'a> AttributeText<'a> { - pub fn attr>>(s: S) -> AttributeText<'a> { - AttrStr(s.into()) - } - - pub fn escaped>>(s: S) -> AttributeText<'a> { - EscStr(s.into()) - } - - pub fn html>>(s: S) -> AttributeText<'a> { - HtmlStr(s.into()) +impl<'a> DotString<'a> for ClusterMode { + fn dot_string(&self) -> Cow<'a, str> { + match self { + ClusterMode::Local => "local".into(), + ClusterMode::Global => "global".into(), + ClusterMode::None => "none".into(), + } } +} - pub fn quoted>>(s: S) -> AttributeText<'a> { - QuotedStr(s.into()) - } +#[derive(Copy, Clone, PartialEq, Debug)] +pub enum Color<'a> { + RGB { + red: u8, + green: u8, + blue: u8, + }, + RGBA { + red: u8, + green: u8, + blue: u8, + alpha: u8, + }, + // TODO: constrain? + // Hue-Saturation-Value (HSV) 0.0 <= H,S,V <= 1.0 + HSV { + hue: f32, + saturation: f32, + value: f32, + }, + Named(&'a str), +} - fn escape_char(c: char, mut f: F) - where - F: FnMut(char), - { - match c { - // not escaping \\, since Graphviz escString needs to - // interpret backslashes; see EscStr above. - '\\' => f(c), - _ => { - for c in c.escape_default() { - f(c) - } +impl<'a> DotString<'a> for Color<'a> { + fn dot_string(&self) -> Cow<'a, str> { + match self { + Color::RGB { red, green, blue } => { + format!("#{:02x?}{:02x?}{:02x?}", red, green, blue).into() + } + Color::RGBA { + red, + green, + blue, + alpha, + } => { + format!("#{:02x?}{:02x?}{:02x?}{:02x?}", red, green, blue, alpha).into() } + Color::HSV { + hue, + saturation, + value, + } => format!("{} {} {}", hue, saturation, value).into(), + Color::Named(color) => (*color).into(), } } +} - fn escape_str(s: &str) -> String { - let mut out = String::with_capacity(s.len()); - for c in s.chars() { - AttributeText::escape_char(c, |c| out.push(c)); - } - out - } +// The sum of the optional weightings must sum to at most 1. +pub struct WeightedColor<'a> { + color: Color<'a>, - /// Renders text as string suitable for a attribute in a .dot file. - /// This includes quotes or suitable delimiters. - pub fn dot_string(&self) -> String { - match *self { - AttrStr(ref s) => format!("{}", s), - EscStr(ref s) => format!("\"{}\"", AttributeText::escape_str(&s)), - HtmlStr(ref s) => format!("<{}>", s), - QuotedStr(ref s) => format!("\"{}\"", s.escape_default()), - } - } + // TODO: constrain + /// Must be in range 0 <= W <= 1. + weight: Option, } -impl<'a> From for AttributeText<'a> { - fn from(arrow_type: ArrowType) -> Self { - AttributeText::attr(arrow_type.dot_string()) +impl<'a> DotString<'a> for WeightedColor<'a> { + fn dot_string(&self) -> Cow<'a, str> { + let mut dot_string = self.color.dot_string().to_string(); + if let Some(weight) = &self.weight { + dot_string.push_str(format!(";{}", weight).as_str()); + } + dot_string.into() } } -impl<'a> From for AttributeText<'a> { - fn from(v: bool) -> Self { - AttributeText::attr(v.to_string()) - } +pub struct ColorList<'a> { + colors: Vec>, } -impl<'a> From for AttributeText<'a> { - fn from(mode: ClusterMode) -> Self { - AttributeText::quoted(mode.dot_string()) +impl<'a> DotString<'a> for ColorList<'a> { + /// A colon-separated list of weighted color values: WC(:WC)* where each WC has the form C(;F)? + /// Ex: fillcolor=yellow;0.3:blue + fn dot_string(&self) -> Cow<'a, str> { + let mut dot_string = String::new(); + let mut iter = self.colors.iter(); + let first = iter.next(); + if first.is_none() { + return dot_string.into(); + } + dot_string.push_str(&first.unwrap().dot_string()); + for weighted_color in iter { + dot_string.push_str(":"); + dot_string.push_str(&weighted_color.dot_string()) + } + + dot_string.into() } } -impl<'a> From> for AttributeText<'a> { - fn from(color: Color<'a>) -> Self { - AttributeText::quoted(color.dot_string()) - } +/// Convert an element like `(i, j)` into a WeightedColor +pub trait IntoWeightedColor<'a> { + fn into_weighted_color(self) -> WeightedColor<'a>; } -impl<'a> From> for AttributeText<'a> { - fn from(color_list: ColorList<'a>) -> Self { - AttributeText::quoted(color_list.dot_string()) +impl<'a> IntoWeightedColor<'a> for &'a (Color<'a>, Option) { + fn into_weighted_color(self) -> WeightedColor<'a> { + let (s, t) = *self; + WeightedColor { + color: s, + weight: t, + } } } -impl<'a> From for AttributeText<'a> { - fn from(compass: CompassPoint) -> Self { - AttributeText::quoted(compass.dot_string()) - } -} - -impl<'a> From for AttributeText<'a> { - fn from(direction: Direction) -> Self { - AttributeText::attr(direction.dot_string()) - } -} - -impl<'a> From for AttributeText<'a> { - fn from(style: EdgeStyle) -> Self { - AttributeText::attr(style.dot_string()) - } -} - -impl<'a> From for AttributeText<'a> { - fn from(v: f32) -> Self { - AttributeText::attr(v.to_string()) - } -} - -impl<'a> From for AttributeText<'a> { - fn from(style: GraphStyle) -> Self { - AttributeText::attr(style.dot_string()) - } -} - -impl<'a> From for AttributeText<'a> { - fn from(label_justification: LabelJustification) -> Self { - AttributeText::attr(label_justification.dot_string()) - } -} - -impl<'a> From for AttributeText<'a> { - fn from(label_location: LabelLocation) -> Self { - AttributeText::attr(label_location.dot_string()) - } -} - -impl<'a> From for AttributeText<'a> { - fn from(style: NodeStyle) -> Self { - AttributeText::attr(style.dot_string()) - } -} - -impl<'a> From for AttributeText<'a> { - fn from(ordering: Ordering) -> Self { - AttributeText::quoted(ordering.dot_string()) - } -} - -impl<'a> From for AttributeText<'a> { - fn from(mode: OutputMode) -> Self { - AttributeText::quoted(mode.dot_string()) - } -} - -impl<'a> From for AttributeText<'a> { - fn from(mode: PackMode) -> Self { - AttributeText::quoted(mode.dot_string()) - } -} - -impl<'a> From for AttributeText<'a> { - fn from(page_direction: PageDirection) -> Self { - AttributeText::attr(page_direction.dot_string()) - } -} - -impl<'a> From for AttributeText<'a> { - fn from(point: Point) -> Self { - AttributeText::quoted(point.dot_string()) - } -} - -impl<'a> From for AttributeText<'a> { - fn from(port_position: PortPosition) -> Self { - AttributeText::quoted(port_position.dot_string()) - } -} - -impl<'a> From for AttributeText<'a> { - fn from(rank_dir: RankDir) -> Self { - AttributeText::attr(rank_dir.dot_string()) - } -} - -impl<'a> From for AttributeText<'a> { - fn from(ratio: Ratio) -> Self { - match ratio { - Ratio::Aspect(_aspect) => AttributeText::attr(ratio.dot_string()), - _ => AttributeText::quoted(ratio.dot_string()), - } - } -} - -impl<'a> From for AttributeText<'a> { - fn from(rectangle: Rectangle) -> Self { - AttributeText::quoted(rectangle.dot_string()) - } -} - -impl<'a> From for AttributeText<'a> { - fn from(shape: Shape) -> Self { - AttributeText::attr(shape.dot_string()) - } -} - -impl<'a> From for AttributeText<'a> { - fn from(splines: Splines) -> Self { - AttributeText::quoted(splines.dot_string()) - } -} - -impl<'a> From for AttributeText<'a> { - fn from(spline_type: SplineType) -> Self { - AttributeText::quoted(spline_type.dot_string()) - } -} - -impl<'a> From for AttributeText<'a> { - fn from(styles: Styles) -> Self { - match styles { - Styles::Edge(s) => AttributeText::from(s), - Styles::Node(s) => AttributeText::from(s), - Styles::Graph(s) => AttributeText::from(s), - } - } -} - -impl<'a> From for AttributeText<'a> { - fn from(v: u32) -> Self { - AttributeText::attr(v.to_string()) - } -} // TODO: not sure we need this enum but should support setting nodeport either via // headport / tailport attributes e.g. a -> b [tailport=se] @@ -352,60 +240,743 @@ impl<'a> DotString<'a> for CompassPoint { } } -// TODO: probably dont need this struct and can move impl methods into lib module -pub struct Dot<'a> { - graph: Graph<'a>, +pub enum Direction { + Forward, + Back, + Both, + None, } -impl<'a> Dot<'a> { - /// Renders directed graph `g` into the writer `w` in DOT syntax. - /// (Simple wrapper around `render_opts` that passes a default set of options.) - //pub fn render(self, g: Graph, w: &mut W) -> io::Result<()> - pub fn render(self, w: &mut W) -> io::Result<()> - where - W: Write, - { - // TODO: use default_options? - self.render_opts(w, &[]) +impl<'a> DotString<'a> for Direction { + fn dot_string(&self) -> Cow<'a, str> { + match self { + Direction::Forward => "forward".into(), + Direction::Back => "back".into(), + Direction::Both => "both".into(), + Direction::None => "none".into(), + } } +} - // io::Result<()> vs Result<(), Box> - // https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html#the--operator-can-be-used-in-functions-that-return-result - /// Renders directed graph `g` into the writer `w` in DOT syntax. - /// (Main entry point for the library.) - // pub fn render_opts(self, graph: Graph, w: &mut W, options: &[RenderOption]) -> io::Result<()> - pub fn render_opts(self, w: &mut W, options: &[RenderOption]) -> io::Result<()> - where - W: Write, - { - if let Some(comment) = &self.graph.comment { - // TODO: split comment into lines of 80 or so characters - writeln!(w, "// {}", comment)?; +pub enum ImagePosition { + TopLeft, + TopCentered, + TopRight, + MiddleLeft, + MiddleCentered, + MiddleRight, + BottomLeft, + BottomCentered, + BottomRight, +} + +// TODO: change to AsRef? What about something for &'static str? +impl ImagePosition { + pub fn as_slice(&self) -> &'static str { + match self { + ImagePosition::TopLeft => "tl", + ImagePosition::TopCentered => "tc", + ImagePosition::TopRight => "tr", + ImagePosition::MiddleLeft => "ml", + ImagePosition::MiddleCentered => "mc", + ImagePosition::MiddleRight => "mr", + ImagePosition::BottomLeft => "bl", + ImagePosition::BottomCentered => "bc", + ImagePosition::BottomRight => "br", } + } +} - let edge_op = &self.graph.edge_op(); - let strict = if self.graph.strict { "strict " } else { "" }; - write!(w, "{}{}", strict, &self.graph.graph_type())?; +pub enum ImageScale { + Width, + Height, + Both, +} - if let Some(id) = &self.graph.id { - write!(w, " {}", id)?; +impl ImageScale { + pub fn as_slice(&self) -> &'static str { + match self { + ImageScale::Width => "width", + ImageScale::Height => "height", + ImageScale::Both => "both", } + } +} - writeln!(w, " {{")?; +pub enum LabelJustification { + Left, + Right, + Center, +} - if let Some(graph_attributes) = self.graph.graph_attributes { - write!(w, "{}{}\n", INDENT, graph_attributes.dot_string())?; +impl<'a> DotString<'a> for LabelJustification { + fn dot_string(&self) -> Cow<'a, str> { + match self { + LabelJustification::Left => "l".into(), + LabelJustification::Right => "r".into(), + LabelJustification::Center => "c".into(), } + } +} - if let Some(node_attributes) = self.graph.node_attributes { - write!(w, "{}{}\n", INDENT, node_attributes.dot_string())?; - } +pub enum LabelLocation { + Top, + Center, + Bottom, +} - if let Some(edge_attributes) = self.graph.edge_attributes { - write!(w, "{}{}\n", INDENT, edge_attributes.dot_string())?; +impl<'a> DotString<'a> for LabelLocation { + fn dot_string(&self) -> Cow<'a, str> { + match self { + LabelLocation::Top => "t".into(), + LabelLocation::Center => "c".into(), + LabelLocation::Bottom => "b".into(), } + } +} - for n in self.graph.nodes { +pub enum Ordering { + In, + Out, +} + +impl<'a> DotString<'a> for Ordering { + fn dot_string(&self) -> Cow<'a, str> { + match self { + Ordering::In => "in".into(), + Ordering::Out => "out".into(), + } + } +} + +/// These specify the order in which nodes and edges are drawn in concrete output. +/// +/// The default "breadthfirst" is the simplest, but when the graph layout does not avoid edge-node +/// overlap, this mode will sometimes have edges drawn over nodes and sometimes on top of nodes. +/// +/// If the mode "nodesfirst" is chosen, all nodes are drawn first, followed by the edges. +/// This guarantees an edge-node overlap will not be mistaken for an edge ending at a node. +/// +/// On the other hand, usually for aesthetic reasons, it may be desirable that all edges appear +/// beneath nodes, even if the resulting drawing is ambiguous. +/// This can be achieved by choosing "edgesfirst". +pub enum OutputMode { + BreadthFirst, + NodesFirst, + EdgesFirst, +} + +impl<'a> DotString<'a> for OutputMode { + fn dot_string(&self) -> Cow<'a, str> { + match self { + OutputMode::BreadthFirst => "breadthfirst".into(), + OutputMode::NodesFirst => "nodesfirst".into(), + OutputMode::EdgesFirst => "edgesfirst".into(), + } + } +} + +/// The modes "node", "clust" or "graph" specify that the components should be packed together +/// tightly, using the specified granularity. +pub enum PackMode { + /// causes packing at the node and edge level, with no overlapping of these objects. + /// This produces a layout with the least area, but it also allows interleaving, + /// where a node of one component may lie between two nodes in another component. + Node, + + /// guarantees that top-level clusters are kept intact. + /// What effect a value has also depends on the layout algorithm. + Cluster, + + /// does a packing using the bounding box of the component. + /// Thus, there will be a rectangular region around a component free of elements of any other component. + Graph, + // TODO: array - "array(_flags)?(%d)?" +} + +impl<'a> DotString<'a> for PackMode { + fn dot_string(&self) -> Cow<'a, str> { + match self { + PackMode::Node => "node".into(), + PackMode::Cluster => "clust".into(), + PackMode::Graph => "graph".into(), + } + } +} + +/// These specify the 8 row or column major orders for traversing a rectangular array, +/// the first character corresponding to the major order and the second to the minor order. +/// Thus, for “BL”, the major order is from bottom to top, and the minor order is from left to right. +/// This means the bottom row is traversed first, from left to right, then the next row up, +/// from left to right, and so on, until the topmost row is traversed +pub enum PageDirection { + BottomLeft, + BottomRight, + TopLeft, + TopRight, + RightBottom, + RightTop, + LeftBottom, + LeftTop, +} + +impl<'a> DotString<'a> for PageDirection { + fn dot_string(&self) -> Cow<'a, str> { + match self { + PageDirection::BottomLeft => "BL".into(), + PageDirection::BottomRight => "BR".into(), + PageDirection::TopLeft => "TL".into(), + PageDirection::TopRight => "TR".into(), + PageDirection::RightBottom => "RB".into(), + PageDirection::RightTop => "RT".into(), + PageDirection::LeftBottom => "LB".into(), + PageDirection::LeftTop => "LT".into(), + } + } +} + +/// Modifier indicating where on a node an edge should be aimed. +/// If Port is used, the corresponding node must either have record shape with one of its +/// fields having the given portname, or have an HTML-like label, one of whose components has a +/// PORT attribute set to portname. +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum PortPosition { + Port { + port_name: String, + compass_point: Option, + }, + Compass(CompassPoint), +} + +// TODO: AsRef vs this? +// See https://github.com/Peternator7/strum/blob/96ee0a9a307ec7d1a39809fb59037bd4e11557cc/strum/src/lib.rs +impl<'a> DotString<'a> for PortPosition { + fn dot_string(&self) -> Cow<'a, str> { + match self { + PortPosition::Port { + port_name, + compass_point, + } => { + let mut dot_string = port_name.to_owned(); + if let Some(compass_point) = compass_point { + dot_string + .push_str(format!(":{}", compass_point.dot_string()).as_str()); + } + dot_string.into() + } + PortPosition::Compass(p) => p.dot_string().into(), + } + } +} + +/// Corresponding to directed graphs drawn from top to bottom, from left to right, +/// from bottom to top, and from right to left, respectively. +pub enum RankDir { + TopBottom, + LeftRight, + BottomTop, + RightLeft, +} + +impl<'a> DotString<'a> for RankDir { + fn dot_string(&self) -> Cow<'a, str> { + match self { + RankDir::TopBottom => "TB".into(), + RankDir::LeftRight => "LR".into(), + RankDir::BottomTop => "BT".into(), + RankDir::RightLeft => "RL".into(), + } + } +} + +pub enum Ratio { + Aspect(f32), + Fill, + Compress, + Expand, + Auto, +} + +impl<'a> DotString<'a> for Ratio { + fn dot_string(&self) -> Cow<'a, str> { + match self { + Ratio::Aspect(aspect) => aspect.to_string().into(), + Ratio::Fill => "fill".into(), + Ratio::Compress => "compress".into(), + Ratio::Expand => "expand".into(), + Ratio::Auto => "auto".into(), + } + } +} + +pub enum Shape { + Box, + Polygon, + Ellipse, + Oval, + Circle, + Point, + Egg, + Triangle, + Plaintext, + Plain, + Diamond, + Trapezium, + Parallelogram, + House, + Pentagon, + Hexagon, + Septagon, + Octagon, + DoubleCircle, + DoubleOctagon, + TripleOctagon, + Invtriangle, + Invtrapezium, + Invhouse, + Mdiamond, + Msquare, + Mcircle, + Record, + Rect, + Rectangle, + Square, + Star, + None, + Underline, + Cylinder, + Note, + Tab, + Folder, + Box3D, + Component, + Promoter, + Cds, + Terminator, + Utr, + Primersite, + Restrictionsite, + FivePoverHang, + ThreePoverHang, + NoverHang, + Assemply, + Signature, + Insulator, + Ribosite, + Rnastab, + Proteasesite, + Proteinstab, + Rpromotor, + Rarrow, + Larrow, + Lpromotor, +} + +impl<'a> DotString<'a> for Shape { + fn dot_string(&self) -> Cow<'a, str> { + match self { + Shape::Box => "box".into(), + Shape::Polygon => "polygon".into(), + Shape::Ellipse => "ellipse".into(), + Shape::Oval => "oval".into(), + Shape::Circle => "circle".into(), + Shape::Point => "point".into(), + Shape::Egg => "egg".into(), + Shape::Triangle => "triangle".into(), + Shape::Plaintext => "plaintext".into(), + Shape::Plain => "plain".into(), + Shape::Diamond => "diamond".into(), + Shape::Trapezium => "trapezium".into(), + Shape::Parallelogram => "parallelogram".into(), + Shape::House => "house".into(), + Shape::Pentagon => "pentagon".into(), + Shape::Hexagon => "hexagon".into(), + Shape::Septagon => "septagon".into(), + Shape::Octagon => "octagon".into(), + Shape::DoubleCircle => "doublecircle".into(), + Shape::DoubleOctagon => "doubleoctagon".into(), + Shape::TripleOctagon => "tripleocctagon".into(), + Shape::Invtriangle => "invtriangle".into(), + Shape::Invtrapezium => "invtrapezium".into(), + Shape::Invhouse => "invhouse".into(), + Shape::Mdiamond => "mdiamond".into(), + Shape::Msquare => "msquare".into(), + Shape::Mcircle => "mcircle".into(), + Shape::Record => "record".into(), + Shape::Rect => "rect".into(), + Shape::Rectangle => "rectangle".into(), + Shape::Square => "square".into(), + Shape::Star => "star".into(), + Shape::None => "none".into(), + Shape::Underline => "underline".into(), + Shape::Cylinder => "cylinder".into(), + Shape::Note => "note".into(), + Shape::Tab => "tab".into(), + Shape::Folder => "folder".into(), + Shape::Box3D => "box3d".into(), + Shape::Component => "component".into(), + Shape::Promoter => "promoter".into(), + Shape::Cds => "cds".into(), + Shape::Terminator => "terminator".into(), + Shape::Utr => "utr".into(), + Shape::Primersite => "primersite".into(), + Shape::Restrictionsite => "restrictionsite".into(), + Shape::FivePoverHang => "fivepoverhang".into(), + Shape::ThreePoverHang => "threepoverhang".into(), + Shape::NoverHang => "noverhang".into(), + Shape::Assemply => "assemply".into(), + Shape::Signature => "signature".into(), + Shape::Insulator => "insulator".into(), + Shape::Ribosite => "ribosite".into(), + Shape::Rnastab => "rnastab".into(), + Shape::Proteasesite => "proteasesite".into(), + Shape::Proteinstab => "proteinstab".into(), + Shape::Rpromotor => "rpromotor".into(), + Shape::Rarrow => "rarrow".into(), + Shape::Larrow => "larrow".into(), + Shape::Lpromotor => "lpromotor".into(), + } + } +} + +/// Spline, edges are drawn as splines routed around nodes +/// Line, edges are drawn as line segments +/// Polygon, specifies that edges should be drawn as polylines. +/// Ortho, specifies edges should be routed as polylines of axis-aligned segments. +/// Curved, specifies edges should be drawn as curved arcs. +/// splines=line and splines=spline can be used as synonyms for +/// splines=false and splines=true, respectively. +pub enum Splines { + Line, + Spline, + None, + Curved, + Polyline, + Ortho, +} + +impl<'a> DotString<'a> for Splines { + fn dot_string(&self) -> Cow<'a, str> { + match self { + Splines::Line => "line".into(), + Splines::Spline => "spline".into(), + Splines::None => "none".into(), + Splines::Curved => "curved".into(), + Splines::Polyline => "polyline".into(), + Splines::Ortho => "ortho".into(), + } + } +} + +/// The text for a graphviz label on a node or edge. +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum AttributeText<'a> { + /// Preserves the text directly as is. + AttrStr(Cow<'a, str>), + + /// This kind of label uses the graphviz label escString type: + /// + /// + /// Occurrences of backslashes (`\`) are not escaped; instead they + /// are interpreted as initiating an escString escape sequence. + /// + /// Escape sequences of particular interest: in addition to `\n` + /// to break a line (centering the line preceding the `\n`), there + /// are also the escape sequences `\l` which left-justifies the + /// preceding line and `\r` which right-justifies it. + EscStr(Cow<'a, str>), + + /// This uses a graphviz [HTML string label][html]. + /// The string is printed exactly as given, but between `<` and `>`. + /// **No escaping is performed.** + /// + /// [html]: https://graphviz.org/doc/info/shapes.html#html + HtmlStr(Cow<'a, str>), + + /// Preserves the text directly as is but wrapped in quotes. + /// + /// Occurrences of backslashes (`\`) are escaped, and thus appear + /// as backslashes in the rendered label. + QuotedStr(Cow<'a, str>), +} + +impl<'a> AttributeText<'a> { + pub fn attr>>(s: S) -> AttributeText<'a> { + AttrStr(s.into()) + } + + pub fn escaped>>(s: S) -> AttributeText<'a> { + EscStr(s.into()) + } + + pub fn html>>(s: S) -> AttributeText<'a> { + HtmlStr(s.into()) + } + + pub fn quoted>>(s: S) -> AttributeText<'a> { + QuotedStr(s.into()) + } + + fn escape_char(c: char, mut f: F) + where + F: FnMut(char), + { + match c { + // not escaping \\, since Graphviz escString needs to + // interpret backslashes; see EscStr above. + '\\' => f(c), + _ => { + for c in c.escape_default() { + f(c) + } + } + } + } + + fn escape_str(s: &str) -> String { + let mut out = String::with_capacity(s.len()); + for c in s.chars() { + AttributeText::escape_char(c, |c| out.push(c)); + } + out + } + + /// Renders text as string suitable for a attribute in a .dot file. + /// This includes quotes or suitable delimiters. + pub fn dot_string(&self) -> String { + match *self { + AttrStr(ref s) => format!("{}", s), + EscStr(ref s) => format!("\"{}\"", AttributeText::escape_str(&s)), + HtmlStr(ref s) => format!("<{}>", s), + QuotedStr(ref s) => format!("\"{}\"", s.escape_default()), + } + } +} + +impl<'a> From for AttributeText<'a> { + fn from(arrow_type: ArrowType) -> Self { + AttributeText::attr(arrow_type.dot_string()) + } +} + +impl<'a> From for AttributeText<'a> { + fn from(v: bool) -> Self { + AttributeText::attr(v.to_string()) + } +} + +impl<'a> From for AttributeText<'a> { + fn from(mode: ClusterMode) -> Self { + AttributeText::quoted(mode.dot_string()) + } +} + +impl<'a> From> for AttributeText<'a> { + fn from(color: Color<'a>) -> Self { + AttributeText::quoted(color.dot_string()) + } +} + +impl<'a> From> for AttributeText<'a> { + fn from(color_list: ColorList<'a>) -> Self { + AttributeText::quoted(color_list.dot_string()) + } +} + +impl<'a> From for AttributeText<'a> { + fn from(compass: CompassPoint) -> Self { + AttributeText::quoted(compass.dot_string()) + } +} + +impl<'a> From for AttributeText<'a> { + fn from(direction: Direction) -> Self { + AttributeText::attr(direction.dot_string()) + } +} + +impl<'a> From for AttributeText<'a> { + fn from(style: EdgeStyle) -> Self { + AttributeText::attr(style.dot_string()) + } +} + +impl<'a> From for AttributeText<'a> { + fn from(v: f32) -> Self { + AttributeText::attr(v.to_string()) + } +} + +impl<'a> From for AttributeText<'a> { + fn from(style: GraphStyle) -> Self { + AttributeText::attr(style.dot_string()) + } +} + +impl<'a> From for AttributeText<'a> { + fn from(label_justification: LabelJustification) -> Self { + AttributeText::attr(label_justification.dot_string()) + } +} + +impl<'a> From for AttributeText<'a> { + fn from(label_location: LabelLocation) -> Self { + AttributeText::attr(label_location.dot_string()) + } +} + +impl<'a> From for AttributeText<'a> { + fn from(style: NodeStyle) -> Self { + AttributeText::attr(style.dot_string()) + } +} + +impl<'a> From for AttributeText<'a> { + fn from(ordering: Ordering) -> Self { + AttributeText::quoted(ordering.dot_string()) + } +} + +impl<'a> From for AttributeText<'a> { + fn from(mode: OutputMode) -> Self { + AttributeText::quoted(mode.dot_string()) + } +} + +impl<'a> From for AttributeText<'a> { + fn from(mode: PackMode) -> Self { + AttributeText::quoted(mode.dot_string()) + } +} + +impl<'a> From for AttributeText<'a> { + fn from(page_direction: PageDirection) -> Self { + AttributeText::attr(page_direction.dot_string()) + } +} + +impl<'a> From for AttributeText<'a> { + fn from(point: Point) -> Self { + AttributeText::quoted(point.dot_string()) + } +} + +impl<'a> From for AttributeText<'a> { + fn from(port_position: PortPosition) -> Self { + AttributeText::quoted(port_position.dot_string()) + } +} + +impl<'a> From for AttributeText<'a> { + fn from(rank_dir: RankDir) -> Self { + AttributeText::attr(rank_dir.dot_string()) + } +} + +impl<'a> From for AttributeText<'a> { + fn from(ratio: Ratio) -> Self { + match ratio { + Ratio::Aspect(_aspect) => AttributeText::attr(ratio.dot_string()), + _ => AttributeText::quoted(ratio.dot_string()), + } + } +} + +impl<'a> From for AttributeText<'a> { + fn from(rectangle: Rectangle) -> Self { + AttributeText::quoted(rectangle.dot_string()) + } +} + +impl<'a> From for AttributeText<'a> { + fn from(shape: Shape) -> Self { + AttributeText::attr(shape.dot_string()) + } +} + +impl<'a> From for AttributeText<'a> { + fn from(splines: Splines) -> Self { + AttributeText::quoted(splines.dot_string()) + } +} + +impl<'a> From for AttributeText<'a> { + fn from(spline_type: SplineType) -> Self { + AttributeText::quoted(spline_type.dot_string()) + } +} + +impl<'a> From for AttributeText<'a> { + fn from(styles: Styles) -> Self { + match styles { + Styles::Edge(s) => AttributeText::from(s), + Styles::Node(s) => AttributeText::from(s), + Styles::Graph(s) => AttributeText::from(s), + } + } +} + +impl<'a> From for AttributeText<'a> { + fn from(v: u32) -> Self { + AttributeText::attr(v.to_string()) + } +} + + +// TODO: probably dont need this struct and can move impl methods into lib module +pub struct Dot<'a> { + graph: Graph<'a>, +} + +impl<'a> Dot<'a> { + /// Renders directed graph `g` into the writer `w` in DOT syntax. + /// (Simple wrapper around `render_opts` that passes a default set of options.) + //pub fn render(self, g: Graph, w: &mut W) -> io::Result<()> + pub fn render(self, w: &mut W) -> io::Result<()> + where + W: Write, + { + // TODO: use default_options? + self.render_opts(w, &[]) + } + + // io::Result<()> vs Result<(), Box> + // https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html#the--operator-can-be-used-in-functions-that-return-result + /// Renders directed graph `g` into the writer `w` in DOT syntax. + /// (Main entry point for the library.) + // pub fn render_opts(self, graph: Graph, w: &mut W, options: &[RenderOption]) -> io::Result<()> + pub fn render_opts(self, w: &mut W, options: &[RenderOption]) -> io::Result<()> + where + W: Write, + { + if let Some(comment) = &self.graph.comment { + // TODO: split comment into lines of 80 or so characters + writeln!(w, "// {}", comment)?; + } + + let edge_op = &self.graph.edge_op(); + let strict = if self.graph.strict { "strict " } else { "" }; + write!(w, "{}{}", strict, &self.graph.graph_type())?; + + if let Some(id) = &self.graph.id { + write!(w, " {}", id)?; + } + + writeln!(w, " {{")?; + + if let Some(graph_attributes) = self.graph.graph_attributes { + write!(w, "{}{}\n", INDENT, graph_attributes.dot_string())?; + } + + if let Some(node_attributes) = self.graph.node_attributes { + write!(w, "{}{}\n", INDENT, node_attributes.dot_string())?; + } + + if let Some(edge_attributes) = self.graph.edge_attributes { + write!(w, "{}{}\n", INDENT, edge_attributes.dot_string())?; + } + + for n in self.graph.nodes { // TODO: handle render options // Are render options something we need? // we could clone the node or and remove the attributes based on render options @@ -871,7 +1442,8 @@ pub trait GraphAttributes<'a> { self.add_attribute("landscape", AttributeText::from(landscape)) } - /// Specifies the separator characters used to split an attribute of type layerRange into a list of ranges. + /// Specifies the separator characters used to split an attribute of type layerRange into a + /// list of ranges. fn layer_list_sep(&mut self, layer_list_sep: String) -> &mut Self { self.add_attribute("layerlistsep", AttributeText::attr(layer_list_sep)) } @@ -1229,222 +1801,81 @@ pub trait GraphAttributes<'a> { attributes: HashMap>, ) -> &mut Self; - fn get_attributes_mut(&mut self) -> &mut IndexMap>; -} - -impl<'a> GraphAttributes<'a> for GraphAttributeStatementBuilder<'a> { - fn add_attribute>( - &mut self, - key: S, - value: AttributeText<'a>, - ) -> &mut Self { - self.attributes.insert(key.into(), value); - self - } - - /// Add multiple attributes to the node. - fn add_attributes( - &'a mut self, - attributes: HashMap>, - ) -> &mut Self { - self.attributes.extend(attributes); - self - } - - fn get_attributes_mut(&mut self) -> &mut IndexMap> { - &mut self.attributes - } -} - -// I'm not a huge fan of needing this builder but having a hard time getting around &mut without it -pub struct GraphAttributeStatementBuilder<'a> { - pub attributes: IndexMap>, -} - -impl<'a> GraphAttributeStatementBuilder<'a> { - pub fn new() -> Self { - Self { - attributes: IndexMap::new(), - } - } - - pub fn build(&self) -> GraphAttributeStatement<'a> { - GraphAttributeStatement { - attributes: self.attributes.clone(), - } - } -} - -#[derive(Clone, Debug)] -pub struct GraphAttributeStatement<'a> { - pub attributes: IndexMap>, -} - -impl<'a> GraphAttributeStatement<'a> { - pub fn new() -> Self { - Self { - attributes: IndexMap::new(), - } - } - - pub fn add_attribute>( - &mut self, - key: S, - value: AttributeText<'a>, - ) -> &mut Self { - self.attributes.insert(key.into(), value); - self - } -} - -impl<'a> AttributeStatement<'a> for GraphAttributeStatement<'a> { - fn get_attribute_statement_type(&self) -> &'static str { - "graph" - } - - fn get_attributes(&self) -> &IndexMap> { - &self.attributes - } -} - -pub enum ClusterMode { - Local, - Global, - None, -} - -impl<'a> DotString<'a> for ClusterMode { - fn dot_string(&self) -> Cow<'a, str> { - match self { - ClusterMode::Local => "local".into(), - ClusterMode::Global => "global".into(), - ClusterMode::None => "none".into(), - } - } -} - -pub enum Ratio { - Aspect(f32), - Fill, - Compress, - Expand, - Auto, -} - -impl<'a> DotString<'a> for Ratio { - fn dot_string(&self) -> Cow<'a, str> { - match self { - Ratio::Aspect(aspect) => aspect.to_string().into(), - Ratio::Fill => "fill".into(), - Ratio::Compress => "compress".into(), - Ratio::Expand => "expand".into(), - Ratio::Auto => "auto".into(), - } - } -} - -trait DotString<'a> { - fn dot_string(&self) -> Cow<'a, str>; -} - -pub enum LabelJustification { - Left, - Right, - Center, + fn get_attributes_mut(&mut self) -> &mut IndexMap>; } -impl<'a> DotString<'a> for LabelJustification { - fn dot_string(&self) -> Cow<'a, str> { - match self { - LabelJustification::Left => "l".into(), - LabelJustification::Right => "r".into(), - LabelJustification::Center => "c".into(), - } +impl<'a> GraphAttributes<'a> for GraphAttributeStatementBuilder<'a> { + fn add_attribute>( + &mut self, + key: S, + value: AttributeText<'a>, + ) -> &mut Self { + self.attributes.insert(key.into(), value); + self } -} -pub enum LabelLocation { - Top, - Center, - Bottom, -} + /// Add multiple attributes to the node. + fn add_attributes( + &'a mut self, + attributes: HashMap>, + ) -> &mut Self { + self.attributes.extend(attributes); + self + } -impl<'a> DotString<'a> for LabelLocation { - fn dot_string(&self) -> Cow<'a, str> { - match self { - LabelLocation::Top => "t".into(), - LabelLocation::Center => "c".into(), - LabelLocation::Bottom => "b".into(), - } + fn get_attributes_mut(&mut self) -> &mut IndexMap> { + &mut self.attributes } } -pub enum Ordering { - In, - Out, +// I'm not a huge fan of needing this builder but having a hard time getting around &mut without it +pub struct GraphAttributeStatementBuilder<'a> { + pub attributes: IndexMap>, } -impl<'a> DotString<'a> for Ordering { - fn dot_string(&self) -> Cow<'a, str> { - match self { - Ordering::In => "in".into(), - Ordering::Out => "out".into(), +impl<'a> GraphAttributeStatementBuilder<'a> { + pub fn new() -> Self { + Self { + attributes: IndexMap::new(), } } -} - -/// These specify the order in which nodes and edges are drawn in concrete output. -/// -/// The default "breadthfirst" is the simplest, but when the graph layout does not avoid edge-node -/// overlap, this mode will sometimes have edges drawn over nodes and sometimes on top of nodes. -/// -/// If the mode "nodesfirst" is chosen, all nodes are drawn first, followed by the edges. -/// This guarantees an edge-node overlap will not be mistaken for an edge ending at a node. -/// -/// On the other hand, usually for aesthetic reasons, it may be desirable that all edges appear -/// beneath nodes, even if the resulting drawing is ambiguous. -/// This can be achieved by choosing "edgesfirst". -pub enum OutputMode { - BreadthFirst, - NodesFirst, - EdgesFirst, -} -impl<'a> DotString<'a> for OutputMode { - fn dot_string(&self) -> Cow<'a, str> { - match self { - OutputMode::BreadthFirst => "breadthfirst".into(), - OutputMode::NodesFirst => "nodesfirst".into(), - OutputMode::EdgesFirst => "edgesfirst".into(), + pub fn build(&self) -> GraphAttributeStatement<'a> { + GraphAttributeStatement { + attributes: self.attributes.clone(), } } } -/// The modes "node", "clust" or "graph" specify that the components should be packed together -/// tightly, using the specified granularity. -pub enum PackMode { - /// causes packing at the node and edge level, with no overlapping of these objects. - /// This produces a layout with the least area, but it also allows interleaving, - /// where a node of one component may lie between two nodes in another component. - Node, +#[derive(Clone, Debug)] +pub struct GraphAttributeStatement<'a> { + pub attributes: IndexMap>, +} - /// guarantees that top-level clusters are kept intact. - /// What effect a value has also depends on the layout algorithm. - Cluster, +impl<'a> GraphAttributeStatement<'a> { + pub fn new() -> Self { + Self { + attributes: IndexMap::new(), + } + } - /// does a packing using the bounding box of the component. - /// Thus, there will be a rectangular region around a component free of elements of any other component. - Graph, - // TODO: array - "array(_flags)?(%d)?" + pub fn add_attribute>( + &mut self, + key: S, + value: AttributeText<'a>, + ) -> &mut Self { + self.attributes.insert(key.into(), value); + self + } } -impl<'a> DotString<'a> for PackMode { - fn dot_string(&self) -> Cow<'a, str> { - match self { - PackMode::Node => "node".into(), - PackMode::Cluster => "clust".into(), - PackMode::Graph => "graph".into(), - } +impl<'a> AttributeStatement<'a> for GraphAttributeStatement<'a> { + fn get_attribute_statement_type(&self) -> &'static str { + "graph" + } + + fn get_attributes(&self) -> &IndexMap> { + &self.attributes } } @@ -1500,86 +1931,6 @@ impl<'a> DotString<'a> for Rectangle { } } -/// These specify the 8 row or column major orders for traversing a rectangular array, -/// the first character corresponding to the major order and the second to the minor order. -/// Thus, for “BL”, the major order is from bottom to top, and the minor order is from left to right. -/// This means the bottom row is traversed first, from left to right, then the next row up, -/// from left to right, and so on, until the topmost row is traversed -pub enum PageDirection { - BottomLeft, - BottomRight, - TopLeft, - TopRight, - RightBottom, - RightTop, - LeftBottom, - LeftTop, -} - -impl<'a> DotString<'a> for PageDirection { - fn dot_string(&self) -> Cow<'a, str> { - match self { - PageDirection::BottomLeft => "BL".into(), - PageDirection::BottomRight => "BR".into(), - PageDirection::TopLeft => "TL".into(), - PageDirection::TopRight => "TR".into(), - PageDirection::RightBottom => "RB".into(), - PageDirection::RightTop => "RT".into(), - PageDirection::LeftBottom => "LB".into(), - PageDirection::LeftTop => "LT".into(), - } - } -} - -/// Corresponding to directed graphs drawn from top to bottom, from left to right, -/// from bottom to top, and from right to left, respectively. -pub enum RankDir { - TopBottom, - LeftRight, - BottomTop, - RightLeft, -} - -impl<'a> DotString<'a> for RankDir { - fn dot_string(&self) -> Cow<'a, str> { - match self { - RankDir::TopBottom => "TB".into(), - RankDir::LeftRight => "LR".into(), - RankDir::BottomTop => "BT".into(), - RankDir::RightLeft => "RL".into(), - } - } -} - -/// Spline, edges are drawn as splines routed around nodes -/// Line, edges are drawn as line segments -/// Polygon, specifies that edges should be drawn as polylines. -/// Ortho, specifies edges should be routed as polylines of axis-aligned segments. -/// Curved, specifies edges should be drawn as curved arcs. -/// splines=line and splines=spline can be used as synonyms for -/// splines=false and splines=true, respectively. -pub enum Splines { - Line, - Spline, - None, - Curved, - Polyline, - Ortho, -} - -impl<'a> DotString<'a> for Splines { - fn dot_string(&self) -> Cow<'a, str> { - match self { - Splines::Line => "line".into(), - Splines::Spline => "spline".into(), - Splines::None => "none".into(), - Splines::Curved => "curved".into(), - Splines::Polyline => "polyline".into(), - Splines::Ortho => "ortho".into(), - } - } -} - /// The number of points in the list must be equivalent to 1 mod 3; note that this is not checked. /// TODO: should we check? pub struct SplineType { @@ -1683,58 +2034,14 @@ impl<'a> NodeBuilder<'a> { Self { id, attributes: IndexMap::new(), - } - } - - pub fn build(&self) -> Node<'a> { - Node { - // TODO: are these to_owned and clones necessary? - id: self.id.to_owned(), - attributes: self.attributes.clone(), - } - } -} - -pub enum ImagePosition { - TopLeft, - TopCentered, - TopRight, - MiddleLeft, - MiddleCentered, - MiddleRight, - BottomLeft, - BottomCentered, - BottomRight, -} - -impl ImagePosition { - pub fn as_slice(&self) -> &'static str { - match self { - ImagePosition::TopLeft => "tl", - ImagePosition::TopCentered => "tc", - ImagePosition::TopRight => "tr", - ImagePosition::MiddleLeft => "ml", - ImagePosition::MiddleCentered => "mc", - ImagePosition::MiddleRight => "mr", - ImagePosition::BottomLeft => "bl", - ImagePosition::BottomCentered => "bc", - ImagePosition::BottomRight => "br", - } - } -} - -pub enum ImageScale { - Width, - Height, - Both, -} + } + } -impl ImageScale { - pub fn as_slice(&self) -> &'static str { - match self { - ImageScale::Width => "width", - ImageScale::Height => "height", - ImageScale::Both => "both", + pub fn build(&self) -> Node<'a> { + Node { + // TODO: are these to_owned and clones necessary? + id: self.id.to_owned(), + attributes: self.attributes.clone(), } } } @@ -2704,438 +3011,136 @@ trait EdgeAttributes<'a> { Attributes::url(self.get_attributes_mut(), url); self } - - // TODO: contrain - /// Weight of edge. - /// The heavier the weight, the shorter, straighter and more vertical the edge is. - /// default: 1, minimum: 0 - fn weight(&mut self, weight: u32) -> &mut Self { - self.add_attribute("weight", AttributeText::attr(weight.to_string())) - } - - /// External label for a node or edge. - /// The label will be placed outside of the node but near it. - /// These labels are added after all nodes and edges have been placed. - /// The labels will be placed so that they do not overlap any node or label. - /// This means it may not be possible to place all of them. - /// To force placing all of them, set forcelabels=true. - fn xlabel(&mut self, xlabel: String) -> &mut Self { - Attributes::xlabel(self.get_attributes_mut(), xlabel); - self - } - - /// Position of an exterior label, in points. - /// The position indicates the center of the label. - fn xlp(&mut self, xlp: Point) -> &mut Self { - Attributes::xlp(self.get_attributes_mut(), xlp); - self - } - - fn add_attribute>( - &mut self, - key: S, - value: AttributeText<'a>, - ) -> &mut Self; - - fn get_attributes_mut(&mut self) -> &mut IndexMap>; - - // fn add_attribute>( - // &self, - // key: S, - // value: AttributeText<'a> - // ) { - // self.get_attributes().insert(key.into(), value); - // } - - // fn get_attributes(&self) -> IndexMap>; - - // fn get_attributes_mut(&self) -> &mut IndexMap>; - - // fn to_dot_string(&self) -> String; -} - -impl<'a> EdgeAttributes<'a> for EdgeAttributeStatementBuilder<'a> { - fn add_attribute>( - &mut self, - key: S, - value: AttributeText<'a>, - ) -> &mut Self { - self.attributes.insert(key.into(), value); - self - } - - fn get_attributes_mut(&mut self) -> &mut IndexMap> { - &mut self.attributes - } -} - -impl<'a> AttributeStatement<'a> for EdgeAttributeStatement<'a> { - fn get_attribute_statement_type(&self) -> &'static str { - "edge" - } - - fn get_attributes(&self) -> &IndexMap> { - &self.attributes - } -} - -// I'm not a huge fan of needing this builder but having a hard time getting around &mut without it -pub struct EdgeAttributeStatementBuilder<'a> { - pub attributes: IndexMap>, -} - -impl<'a> EdgeAttributeStatementBuilder<'a> { - pub fn new() -> Self { - Self { - attributes: IndexMap::new(), - } - } - - pub fn build(&self) -> EdgeAttributeStatement<'a> { - EdgeAttributeStatement { - attributes: self.attributes.clone(), - } - } -} - -#[derive(Clone, Debug)] -pub struct EdgeAttributeStatement<'a> { - pub attributes: IndexMap>, -} - -impl<'a> EdgeAttributeStatement<'a> { - pub fn new() -> Self { - Self { - attributes: IndexMap::new(), - } - } - - pub fn add_attribute>( - &mut self, - key: S, - value: AttributeText<'a>, - ) -> &mut Self { - self.attributes.insert(key.into(), value); - self - } -} - -pub enum Shape { - Box, - Polygon, - Ellipse, - Oval, - Circle, - Point, - Egg, - Triangle, - Plaintext, - Plain, - Diamond, - Trapezium, - Parallelogram, - House, - Pentagon, - Hexagon, - Septagon, - Octagon, - DoubleCircle, - DoubleOctagon, - TripleOctagon, - Invtriangle, - Invtrapezium, - Invhouse, - Mdiamond, - Msquare, - Mcircle, - Record, - Rect, - Rectangle, - Square, - Star, - None, - Underline, - Cylinder, - Note, - Tab, - Folder, - Box3D, - Component, - Promoter, - Cds, - Terminator, - Utr, - Primersite, - Restrictionsite, - FivePoverHang, - ThreePoverHang, - NoverHang, - Assemply, - Signature, - Insulator, - Ribosite, - Rnastab, - Proteasesite, - Proteinstab, - Rpromotor, - Rarrow, - Larrow, - Lpromotor, -} - -impl<'a> DotString<'a> for Shape { - fn dot_string(&self) -> Cow<'a, str> { - match self { - Shape::Box => "box".into(), - Shape::Polygon => "polygon".into(), - Shape::Ellipse => "ellipse".into(), - Shape::Oval => "oval".into(), - Shape::Circle => "circle".into(), - Shape::Point => "point".into(), - Shape::Egg => "egg".into(), - Shape::Triangle => "triangle".into(), - Shape::Plaintext => "plaintext".into(), - Shape::Plain => "plain".into(), - Shape::Diamond => "diamond".into(), - Shape::Trapezium => "trapezium".into(), - Shape::Parallelogram => "parallelogram".into(), - Shape::House => "house".into(), - Shape::Pentagon => "pentagon".into(), - Shape::Hexagon => "hexagon".into(), - Shape::Septagon => "septagon".into(), - Shape::Octagon => "octagon".into(), - Shape::DoubleCircle => "doublecircle".into(), - Shape::DoubleOctagon => "doubleoctagon".into(), - Shape::TripleOctagon => "tripleocctagon".into(), - Shape::Invtriangle => "invtriangle".into(), - Shape::Invtrapezium => "invtrapezium".into(), - Shape::Invhouse => "invhouse".into(), - Shape::Mdiamond => "mdiamond".into(), - Shape::Msquare => "msquare".into(), - Shape::Mcircle => "mcircle".into(), - Shape::Record => "record".into(), - Shape::Rect => "rect".into(), - Shape::Rectangle => "rectangle".into(), - Shape::Square => "square".into(), - Shape::Star => "star".into(), - Shape::None => "none".into(), - Shape::Underline => "underline".into(), - Shape::Cylinder => "cylinder".into(), - Shape::Note => "note".into(), - Shape::Tab => "tab".into(), - Shape::Folder => "folder".into(), - Shape::Box3D => "box3d".into(), - Shape::Component => "component".into(), - Shape::Promoter => "promoter".into(), - Shape::Cds => "cds".into(), - Shape::Terminator => "terminator".into(), - Shape::Utr => "utr".into(), - Shape::Primersite => "primersite".into(), - Shape::Restrictionsite => "restrictionsite".into(), - Shape::FivePoverHang => "fivepoverhang".into(), - Shape::ThreePoverHang => "threepoverhang".into(), - Shape::NoverHang => "noverhang".into(), - Shape::Assemply => "assemply".into(), - Shape::Signature => "signature".into(), - Shape::Insulator => "insulator".into(), - Shape::Ribosite => "ribosite".into(), - Shape::Rnastab => "rnastab".into(), - Shape::Proteasesite => "proteasesite".into(), - Shape::Proteinstab => "proteinstab".into(), - Shape::Rpromotor => "rpromotor".into(), - Shape::Rarrow => "rarrow".into(), - Shape::Larrow => "larrow".into(), - Shape::Lpromotor => "lpromotor".into(), - } + + // TODO: contrain + /// Weight of edge. + /// The heavier the weight, the shorter, straighter and more vertical the edge is. + /// default: 1, minimum: 0 + fn weight(&mut self, weight: u32) -> &mut Self { + self.add_attribute("weight", AttributeText::attr(weight.to_string())) } -} -pub enum ArrowType { - Normal, - Dot, - Odot, - None, - Empty, - Diamond, - Ediamond, - Box, - Open, - Vee, - Inv, - Invdot, - Invodot, - Tee, - Invempty, - Odiamond, - Crow, - Obox, - Halfopen, -} + /// External label for a node or edge. + /// The label will be placed outside of the node but near it. + /// These labels are added after all nodes and edges have been placed. + /// The labels will be placed so that they do not overlap any node or label. + /// This means it may not be possible to place all of them. + /// To force placing all of them, set forcelabels=true. + fn xlabel(&mut self, xlabel: String) -> &mut Self { + Attributes::xlabel(self.get_attributes_mut(), xlabel); + self + } -impl<'a> DotString<'a> for ArrowType { - fn dot_string(&self) -> Cow<'a, str> { - match self { - ArrowType::Normal => "normal".into(), - ArrowType::Dot => "dot".into(), - ArrowType::Odot => "odot".into(), - ArrowType::None => "none".into(), - ArrowType::Empty => "empty".into(), - ArrowType::Diamond => "diamond".into(), - ArrowType::Ediamond => "ediamond".into(), - ArrowType::Box => "box".into(), - ArrowType::Open => "open".into(), - ArrowType::Vee => "vee".into(), - ArrowType::Inv => "inv".into(), - ArrowType::Invdot => "invdot".into(), - ArrowType::Invodot => "invodot".into(), - ArrowType::Tee => "tee".into(), - ArrowType::Invempty => "invempty".into(), - ArrowType::Odiamond => "odiamond".into(), - ArrowType::Crow => "crow".into(), - ArrowType::Obox => "obox".into(), - ArrowType::Halfopen => "halfopen".into(), - } + /// Position of an exterior label, in points. + /// The position indicates the center of the label. + fn xlp(&mut self, xlp: Point) -> &mut Self { + Attributes::xlp(self.get_attributes_mut(), xlp); + self } -} -pub enum Direction { - Forward, - Back, - Both, - None, -} + fn add_attribute>( + &mut self, + key: S, + value: AttributeText<'a>, + ) -> &mut Self; -impl<'a> DotString<'a> for Direction { - fn dot_string(&self) -> Cow<'a, str> { - match self { - Direction::Forward => "forward".into(), - Direction::Back => "back".into(), - Direction::Both => "both".into(), - Direction::None => "none".into(), - } - } -} + fn get_attributes_mut(&mut self) -> &mut IndexMap>; -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub enum RenderOption { - NoEdgeLabels, - NoNodeLabels, - NoEdgeStyles, - NoNodeStyles, + // fn add_attribute>( + // &self, + // key: S, + // value: AttributeText<'a> + // ) { + // self.get_attributes().insert(key.into(), value); + // } - // TODO: replace with Fontname(String), - Monospace, -} + // fn get_attributes(&self) -> IndexMap>; -/// Returns vec holding all the default render options. -pub fn default_options() -> Vec { - vec![] -} + // fn get_attributes_mut(&self) -> &mut IndexMap>; -// TODO: is this a good representation of color? -#[derive(Copy, Clone, PartialEq, Debug)] -pub enum Color<'a> { - RGB { - red: u8, - green: u8, - blue: u8, - }, - RGBA { - red: u8, - green: u8, - blue: u8, - alpha: u8, - }, - // TODO: constrain? - // Hue-Saturation-Value (HSV) 0.0 <= H,S,V <= 1.0 - HSV { - hue: f32, - saturation: f32, - value: f32, - }, - Named(&'a str), + // fn to_dot_string(&self) -> String; } -impl<'a> DotString<'a> for Color<'a> { - fn dot_string(&self) -> Cow<'a, str> { - match self { - Color::RGB { red, green, blue } => { - format!("#{:02x?}{:02x?}{:02x?}", red, green, blue).into() - } - Color::RGBA { - red, - green, - blue, - alpha, - } => { - format!("#{:02x?}{:02x?}{:02x?}{:02x?}", red, green, blue, alpha).into() - } - Color::HSV { - hue, - saturation, - value, - } => format!("{} {} {}", hue, saturation, value).into(), - Color::Named(color) => (*color).into(), - } +impl<'a> EdgeAttributes<'a> for EdgeAttributeStatementBuilder<'a> { + fn add_attribute>( + &mut self, + key: S, + value: AttributeText<'a>, + ) -> &mut Self { + self.attributes.insert(key.into(), value); + self + } + + fn get_attributes_mut(&mut self) -> &mut IndexMap> { + &mut self.attributes } } -// The sum of the optional weightings must sum to at most 1. -pub struct WeightedColor<'a> { - color: Color<'a>, +impl<'a> AttributeStatement<'a> for EdgeAttributeStatement<'a> { + fn get_attribute_statement_type(&self) -> &'static str { + "edge" + } - // TODO: constrain - /// Must be in range 0 <= W <= 1. - weight: Option, + fn get_attributes(&self) -> &IndexMap> { + &self.attributes + } } -impl<'a> DotString<'a> for WeightedColor<'a> { - fn dot_string(&self) -> Cow<'a, str> { - let mut dot_string = self.color.dot_string().to_string(); - if let Some(weight) = &self.weight { - dot_string.push_str(format!(";{}", weight).as_str()); +// I'm not a huge fan of needing this builder but having a hard time getting around &mut without it +pub struct EdgeAttributeStatementBuilder<'a> { + pub attributes: IndexMap>, +} + +impl<'a> EdgeAttributeStatementBuilder<'a> { + pub fn new() -> Self { + Self { + attributes: IndexMap::new(), + } + } + + pub fn build(&self) -> EdgeAttributeStatement<'a> { + EdgeAttributeStatement { + attributes: self.attributes.clone(), } - dot_string.into() } } -pub struct ColorList<'a> { - colors: Vec>, +#[derive(Clone, Debug)] +pub struct EdgeAttributeStatement<'a> { + pub attributes: IndexMap>, } -impl<'a> DotString<'a> for ColorList<'a> { - /// A colon-separated list of weighted color values: WC(:WC)* where each WC has the form C(;F)? - /// Ex: fillcolor=yellow;0.3:blue - fn dot_string(&self) -> Cow<'a, str> { - let mut dot_string = String::new(); - let mut iter = self.colors.iter(); - let first = iter.next(); - if first.is_none() { - return dot_string.into(); - } - dot_string.push_str(&first.unwrap().dot_string()); - for weighted_color in iter { - dot_string.push_str(":"); - dot_string.push_str(&weighted_color.dot_string()) +impl<'a> EdgeAttributeStatement<'a> { + pub fn new() -> Self { + Self { + attributes: IndexMap::new(), } + } - dot_string.into() + pub fn add_attribute>( + &mut self, + key: S, + value: AttributeText<'a>, + ) -> &mut Self { + self.attributes.insert(key.into(), value); + self } } -/// Convert an element like `(i, j)` into a WeightedColor -pub trait IntoWeightedColor<'a> { - fn into_weighted_color(self) -> WeightedColor<'a>; +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum RenderOption { + NoEdgeLabels, + NoNodeLabels, + NoEdgeStyles, + NoNodeStyles, + + // TODO: replace with Fontname(String), + Monospace, } -impl<'a> IntoWeightedColor<'a> for &'a (Color<'a>, Option) { - fn into_weighted_color(self) -> WeightedColor<'a> { - let (s, t) = *self; - WeightedColor { - color: s, - weight: t, - } - } +/// Returns vec holding all the default render options. +pub fn default_options() -> Vec { + vec![] } pub enum NodeStyle { -- cgit v1.2.3