8caf0ac3aa
Added framework to generate DOT and png files of character map chart from data folder. Added all figures in the 1-2 chapters. Added font file SimSum for render chart
226 lines
5.9 KiB
Go
226 lines
5.9 KiB
Go
package batch
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"xyj-figures/pkg/entity"
|
|
"xyj-figures/pkg/relationship"
|
|
|
|
findfont "github.com/flopp/go-findfont"
|
|
graphviz "github.com/goccy/go-graphviz"
|
|
"github.com/goccy/go-graphviz/cgraph"
|
|
"github.com/golang/freetype/truetype"
|
|
log "github.com/sirupsen/logrus"
|
|
"golang.org/x/image/font"
|
|
)
|
|
|
|
const (
|
|
CLUSTER_PREFIX = "cluster_"
|
|
)
|
|
|
|
func LoadFolder(path string) ([]entity.Entity, []relationship.Relationship, error) {
|
|
es := []entity.Entity{}
|
|
rs := []relationship.Relationship{}
|
|
errs := []string{}
|
|
|
|
// Triverse all files in path and load all entities and relationships
|
|
err := filepath.Walk(path, func(subpath string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
errs = append(errs, err.Error())
|
|
return nil
|
|
}
|
|
if !info.IsDir() {
|
|
e, err := entity.Load(subpath)
|
|
if err != nil {
|
|
errs = append(errs, err.Error())
|
|
return nil
|
|
}
|
|
es = append(es, *e)
|
|
rs = append(rs, e.Details.(entity.EntityDetails).GetRelationships()...)
|
|
}
|
|
return nil
|
|
})
|
|
if len(errs) > 0 {
|
|
return es, rs, fmt.Errorf("multiple errors: %v", strings.Join(errs, "; "))
|
|
}
|
|
return es, rs, err
|
|
}
|
|
|
|
// DistillData takes loaded data and do all needed optimizations and extract only needed info for maps
|
|
func DistillData(es []entity.Entity, rs []relationship.Relationship) ([]entity.Entity, []relationship.Relationship, error) {
|
|
// Create a set of all entities, including characters and every members
|
|
// TODO: Duplicated name
|
|
nodes := map[string]entity.Entity{}
|
|
edges := map[string]relationship.Relationship{}
|
|
for _, e := range es {
|
|
switch e.Type {
|
|
case entity.EntityTypeCharacter:
|
|
if val, ok := nodes[e.Details.(entity.Character).Name]; ok {
|
|
if err := val.Merge(e); err != nil {
|
|
log.Warnf("failed to merge: %v", err)
|
|
}
|
|
continue
|
|
}
|
|
nodes[e.Details.(entity.Character).Name] = e
|
|
case entity.EntityTypeGroup:
|
|
nodes[e.Details.(entity.Group).Name] = e
|
|
for _, m := range e.Details.(entity.Group).Members {
|
|
nodes[m] = entity.Entity{
|
|
Schema: "v1",
|
|
Type: entity.EntityTypeCharacter,
|
|
Details: entity.Character{
|
|
Name: m,
|
|
},
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, r := range rs {
|
|
key1, key2 := r.GetKey()
|
|
val1, ok1 := edges[key1]
|
|
val2, ok2 := edges[key2]
|
|
// Both keys don't exist
|
|
if !ok1 && !ok2 {
|
|
edges[key1] = r
|
|
continue
|
|
}
|
|
// Logs all unexpected edges
|
|
if ok1 && !r.IsSame(val1) {
|
|
log.Warningf("Found ambigious edges, existing: %+v, new: %+v", val1, r)
|
|
}
|
|
if ok2 && !r.IsSame(val2) {
|
|
log.Warningf("Found ambigious edges, existing: %+v, new: %+v", val2, r)
|
|
}
|
|
}
|
|
|
|
retns := make([]entity.Entity, 0, len(nodes))
|
|
for _, n := range nodes {
|
|
retns = append(retns, n)
|
|
}
|
|
retes := make([]relationship.Relationship, 0, len(edges))
|
|
for _, v := range edges {
|
|
retes = append(retes, v)
|
|
}
|
|
|
|
return retns, retes, nil
|
|
}
|
|
|
|
// Save distilled info into DOT type files
|
|
func Save(path string, ents []entity.Entity, rels []relationship.Relationship) error {
|
|
g := graphviz.New()
|
|
g.SetLayout(graphviz.TWOPI)
|
|
useSystemFont(g, "SimSum.ttc")
|
|
rootgraph, err := g.Graph()
|
|
rootgraph.SafeSet("fontname", "NSimSun", "Times-Roman")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create graph: %v", err)
|
|
}
|
|
defer func() {
|
|
if err := rootgraph.Close(); err != nil {
|
|
log.Errorf("failed to close graph: %v", err)
|
|
}
|
|
g.Close()
|
|
}()
|
|
|
|
nodesPtr := map[string]*cgraph.Node{}
|
|
subGraphPtr := map[string]*cgraph.Graph{}
|
|
nodeGraphMap := map[string]string{}
|
|
for _, e := range ents {
|
|
if e.Type == entity.EntityTypeGroup {
|
|
subGraph := rootgraph.SubGraph(CLUSTER_PREFIX+e.Details.(entity.Group).Name, 1)
|
|
subGraph.SetLabel(e.Details.(entity.Group).Name)
|
|
// subGraph.SafeSet("fontname", "NSimSun", "Times-Roman")
|
|
subGraphPtr[e.Details.(entity.Group).Name] = subGraph
|
|
// Record all members
|
|
for _, m := range e.Details.(entity.Group).Members {
|
|
nodeGraphMap[m] = e.Details.(entity.Group).Name
|
|
}
|
|
}
|
|
}
|
|
fmt.Println(subGraphPtr)
|
|
fmt.Println(nodeGraphMap)
|
|
|
|
for _, e := range ents {
|
|
if e.Type == entity.EntityTypeCharacter {
|
|
log.Debugf("entity: %+v", e)
|
|
if group, ok := nodeGraphMap[e.Details.(entity.Character).Name]; ok {
|
|
ptr, ok := subGraphPtr[group]
|
|
if !ok {
|
|
log.Debugf("can't find sub graph %s", group)
|
|
continue
|
|
}
|
|
if ptr == nil {
|
|
log.Debugf("sub graph %s is nil", group)
|
|
continue
|
|
}
|
|
n, err := ptr.CreateNode(e.Details.(entity.Character).Name)
|
|
if err != nil {
|
|
log.Errorf("failed to create node in sub graph %s: %v", group, err)
|
|
continue
|
|
}
|
|
nodesPtr[e.Details.(entity.Character).Name] = n
|
|
continue
|
|
}
|
|
node, err := rootgraph.CreateNode(e.Details.(entity.Character).Name)
|
|
if err != nil {
|
|
log.Errorf("failed to create node: %v", err)
|
|
continue
|
|
}
|
|
nodesPtr[e.Details.(entity.Character).Name] = node
|
|
continue
|
|
}
|
|
}
|
|
fmt.Println(nodesPtr)
|
|
|
|
for _, r := range rels {
|
|
key, _ := r.GetKey()
|
|
srcPtr := nodesPtr[r.Src]
|
|
dstPtr := nodesPtr[r.Dst]
|
|
fmt.Printf("src: %s, dst: %s\n", r.Src, r.Dst)
|
|
e, err := rootgraph.CreateEdge(key, srcPtr, dstPtr)
|
|
if err != nil {
|
|
log.Errorf("failed to create edge: %v", err)
|
|
continue
|
|
}
|
|
e.SetLabel(r.Description)
|
|
e.SafeSet("fontname", "NSimSun", "Times-Roman")
|
|
e.SafeSet("fontsize", "12", "10")
|
|
if r.Bidirection {
|
|
e.SetDir(cgraph.NoneDir)
|
|
}
|
|
}
|
|
if err := g.RenderFilename(rootgraph, "dot", path); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
if err := g.RenderFilename(rootgraph, graphviz.PNG, path+".png"); err != nil {
|
|
return fmt.Errorf("failed to save graph to disk: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func useSystemFont(g *graphviz.Graphviz, name string) {
|
|
fontPath, err := findfont.Find(name)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
fontData, err := os.ReadFile(fontPath)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
ft, err := truetype.Parse(fontData)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
g.SetFontFace(func(size float64) (font.Face, error) {
|
|
opt := &truetype.Options{
|
|
Size: size,
|
|
}
|
|
return truetype.NewFace(ft, opt), nil
|
|
})
|
|
}
|