github.com/gopherd/gonum@v0.0.4/graph/formats/rdf/rdf_line_example_test.go (about) 1 // Copyright ©2020 The Gonum Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package rdf_test 6 7 import ( 8 "fmt" 9 "log" 10 "strings" 11 12 "github.com/gopherd/gonum/graph" 13 "github.com/gopherd/gonum/graph/encoding" 14 "github.com/gopherd/gonum/graph/encoding/dot" 15 "github.com/gopherd/gonum/graph/formats/rdf" 16 "github.com/gopherd/gonum/graph/multi" 17 ) 18 19 // foodNode implements graph.Node, dot.Node and encoding.Attributer 20 // to allow the RDF term value to be given to the DOT encoder. 21 type foodNode struct { 22 rdf.Term 23 } 24 25 func (n foodNode) DOTID() string { 26 text, _, kind, err := n.Term.Parts() 27 if err != nil { 28 return fmt.Sprintf("error:%s", n.Term.Value) 29 } 30 switch kind { 31 case rdf.Blank: 32 return n.Term.Value 33 case rdf.IRI: 34 return text 35 case rdf.Literal: 36 return fmt.Sprintf("%q", text) 37 default: 38 return fmt.Sprintf("invalid:%s", n.Term.Value) 39 } 40 } 41 42 func (n foodNode) Attributes() []encoding.Attribute { 43 _, qual, _, err := n.Term.Parts() 44 if err != nil { 45 return []encoding.Attribute{{Key: "error", Value: err.Error()}} 46 } 47 if qual == "" { 48 return nil 49 } 50 parts := strings.Split(qual, ":") 51 return []encoding.Attribute{{Key: parts[0], Value: parts[1]}} 52 } 53 54 // foodLine implements graph.Line and encoding.Attributer to 55 // allow the line's RDF term value to be given to the DOT 56 // encoder and for the nodes to be shimmed to the foodNode 57 // type. 58 // 59 // It also implements line reversal for the semantics of 60 // a food web with some taxonomic information. 61 type foodLine struct { 62 *rdf.Statement 63 } 64 65 func (l foodLine) From() graph.Node { return foodNode{l.Subject} } 66 func (l foodLine) To() graph.Node { return foodNode{l.Object} } 67 func (l foodLine) ReversedLine() graph.Line { 68 if l.Predicate.Value == "<tax:is>" { 69 // This should remain unreversed, so return as is. 70 return l 71 } 72 s := *l.Statement 73 // Reverse the line end points. 74 s.Subject, s.Object = s.Object, s.Subject 75 // Invert the semantics of the predicate. 76 switch s.Predicate.Value { 77 case "<eco:eats>": 78 s.Predicate.Value = "<eco:eaten-by>" 79 case "<eco:eaten-by>": 80 s.Predicate.Value = "<eco:eats>" 81 case "<tax:is-a>": 82 s.Predicate.Value = "<tax:includes>" 83 case "<tax:includes>": 84 s.Predicate.Value = "<tax:is-a>" 85 default: 86 panic("invalid predicate") 87 } 88 // All IDs returned by the RDF parser are positive, so 89 // sign reverse the edge ID to avoid any collisions. 90 s.Predicate.UID *= -1 91 return foodLine{&s} 92 } 93 94 func (l foodLine) Attributes() []encoding.Attribute { 95 text, _, _, err := l.Predicate.Parts() 96 if err != nil { 97 return []encoding.Attribute{{Key: "error", Value: err.Error()}} 98 } 99 parts := strings.Split(text, ":") 100 return []encoding.Attribute{{Key: parts[0], Value: parts[1]}} 101 } 102 103 // expand copies src into dst, adding the reversal of each line if it is 104 // distinct. 105 func expand(dst, src *multi.DirectedGraph) { 106 it := src.Edges() 107 for it.Next() { 108 lit := it.Edge().(multi.Edge) 109 for lit.Next() { 110 l := lit.Line() 111 r := l.ReversedLine() 112 dst.SetLine(l) 113 if l == r { 114 continue 115 } 116 dst.SetLine(r) 117 } 118 } 119 } 120 121 func ExampleStatement_ReversedLine() { 122 const statements = ` 123 _:wolf <tax:is-a> _:animal . 124 _:wolf <tax:is> "Wolf"^^<tax:common> . 125 _:wolf <tax:is> "Canis lupus"^^<tax:binomial> . 126 _:wolf <eco:eats> _:sheep . 127 _:sheep <tax:is-a> _:animal . 128 _:sheep <tax:is> "Sheep"^^<tax:common> . 129 _:sheep <tax:is> "Ovis aries"^^<tax:binomial> . 130 _:sheep <eco:eats> _:grass . 131 _:grass <tax:is-a> _:plant . 132 _:grass <tax:is> "Grass"^^<tax:common> . 133 _:grass <tax:is> "Lolium perenne"^^<tax:binomial> . 134 _:grass <tax:is> "Festuca rubra"^^<tax:binomial> . 135 _:grass <tax:is> "Poa pratensis"^^<tax:binomial> . 136 ` 137 138 // Decode the statement stream and insert the lines into a multigraph. 139 g := multi.NewDirectedGraph() 140 dec := rdf.NewDecoder(strings.NewReader(statements)) 141 for { 142 l, err := dec.Unmarshal() 143 if err != nil { 144 break 145 } 146 147 // Wrap the line with a shim type to allow the RDF values 148 // to be passed to the DOT marshaling routine. 149 g.SetLine(foodLine{l}) 150 } 151 152 h := multi.NewDirectedGraph() 153 expand(h, g) 154 155 // Marshal the graph into DOT. 156 b, err := dot.MarshalMulti(h, "food web", "", "\t") 157 if err != nil { 158 log.Fatal(err) 159 } 160 fmt.Printf("%s\n\n", b) 161 162 // Output: 163 // 164 // digraph "food web" { 165 // // Node definitions. 166 // "_:wolf"; 167 // "_:animal"; 168 // "Wolf" [tax=common]; 169 // "Canis lupus" [tax=binomial]; 170 // "_:sheep"; 171 // "Sheep" [tax=common]; 172 // "Ovis aries" [tax=binomial]; 173 // "_:grass"; 174 // "_:plant"; 175 // "Grass" [tax=common]; 176 // "Lolium perenne" [tax=binomial]; 177 // "Festuca rubra" [tax=binomial]; 178 // "Poa pratensis" [tax=binomial]; 179 // 180 // // Edge definitions. 181 // "_:wolf" -> "_:animal" [tax="is-a"]; 182 // "_:wolf" -> "Wolf" [tax=is]; 183 // "_:wolf" -> "Canis lupus" [tax=is]; 184 // "_:wolf" -> "_:sheep" [eco=eats]; 185 // "_:animal" -> "_:wolf" [tax=includes]; 186 // "_:animal" -> "_:sheep" [tax=includes]; 187 // "_:sheep" -> "_:wolf" [eco="eaten-by"]; 188 // "_:sheep" -> "_:animal" [tax="is-a"]; 189 // "_:sheep" -> "Sheep" [tax=is]; 190 // "_:sheep" -> "Ovis aries" [tax=is]; 191 // "_:sheep" -> "_:grass" [eco=eats]; 192 // "_:grass" -> "_:sheep" [eco="eaten-by"]; 193 // "_:grass" -> "_:plant" [tax="is-a"]; 194 // "_:grass" -> "Grass" [tax=is]; 195 // "_:grass" -> "Lolium perenne" [tax=is]; 196 // "_:grass" -> "Festuca rubra" [tax=is]; 197 // "_:grass" -> "Poa pratensis" [tax=is]; 198 // "_:plant" -> "_:grass" [tax=includes]; 199 // } 200 }