initial
This commit is contained in:
463
mxgraph.go
Normal file
463
mxgraph.go
Normal file
@@ -0,0 +1,463 @@
|
||||
package gliffy2drawio
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"math"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type MxGraph struct {
|
||||
Model *MxGraphModel
|
||||
nextID int
|
||||
root *MxCell
|
||||
defaultParent *MxCell
|
||||
}
|
||||
|
||||
func NewMxGraph() *MxGraph {
|
||||
root := &MxCell{ID: "0"}
|
||||
layer := &MxCell{ID: "1", Parent: "0"}
|
||||
graph := &MxGraph{
|
||||
Model: &MxGraphModel{
|
||||
Root: &mxRoot{Cells: []*MxCell{root, layer}},
|
||||
},
|
||||
nextID: 2,
|
||||
root: root,
|
||||
defaultParent: layer,
|
||||
}
|
||||
return graph
|
||||
}
|
||||
|
||||
func (g *MxGraph) SetDefaultParent(cell *MxCell) {
|
||||
g.defaultParent = cell
|
||||
}
|
||||
|
||||
func (g *MxGraph) NextID() string {
|
||||
id := g.nextID
|
||||
g.nextID++
|
||||
return intToString(id)
|
||||
}
|
||||
|
||||
func (g *MxGraph) AddCell(cell *MxCell, parent *MxCell) {
|
||||
if cell == nil {
|
||||
return
|
||||
}
|
||||
// Allow preassigned IDs; otherwise generate.
|
||||
if cell.ID == "" {
|
||||
cell.ID = g.NextID()
|
||||
}
|
||||
if parent == nil {
|
||||
parent = g.defaultParent
|
||||
}
|
||||
cell.Parent = parent.ID
|
||||
g.Model.Root.Cells = append(g.Model.Root.Cells, cell)
|
||||
}
|
||||
|
||||
// HasCell returns true if a cell with the given ID exists in the root.
|
||||
func (g *MxGraph) HasCell(id string) bool {
|
||||
for _, cell := range g.Model.Root.Cells {
|
||||
if cell != nil && cell.ID == id {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type MxGraphModel struct {
|
||||
XMLName xml.Name `xml:"mxGraphModel"`
|
||||
Style string `xml:"style,attr,omitempty"`
|
||||
Background string `xml:"background,attr,omitempty"`
|
||||
Grid int `xml:"grid,attr,omitempty"`
|
||||
GridSize int `xml:"gridSize,attr,omitempty"`
|
||||
Guides int `xml:"guides,attr,omitempty"`
|
||||
Dx int `xml:"dx,attr,omitempty"`
|
||||
Dy int `xml:"dy,attr,omitempty"`
|
||||
Connect int `xml:"connect,attr,omitempty"`
|
||||
Arrows int `xml:"arrows,attr,omitempty"`
|
||||
Fold int `xml:"fold,attr,omitempty"`
|
||||
Page int `xml:"page,attr,omitempty"`
|
||||
PageScale int `xml:"pageScale,attr,omitempty"`
|
||||
PageWidth int `xml:"pageWidth,attr,omitempty"`
|
||||
PageHeight int `xml:"pageHeight,attr,omitempty"`
|
||||
Tooltips int `xml:"tooltips,attr,omitempty"`
|
||||
Math int `xml:"math,attr,omitempty"`
|
||||
Shadow int `xml:"shadow,attr,omitempty"`
|
||||
Root *mxRoot `xml:"root"`
|
||||
}
|
||||
|
||||
type mxRoot struct {
|
||||
Cells []*MxCell
|
||||
}
|
||||
|
||||
type UserObject struct {
|
||||
Label string
|
||||
Link string
|
||||
Tooltip string
|
||||
}
|
||||
|
||||
type MxCell struct {
|
||||
ID string
|
||||
Value string
|
||||
Style string
|
||||
Vertex bool
|
||||
Edge bool
|
||||
Parent string
|
||||
Source string
|
||||
Target string
|
||||
|
||||
Geometry *MxGeometry
|
||||
UserObject *UserObject
|
||||
}
|
||||
|
||||
type MxGeometry struct {
|
||||
X float64 `xml:"x,attr,omitempty"`
|
||||
Y float64 `xml:"y,attr,omitempty"`
|
||||
Width float64 `xml:"width,attr,omitempty"`
|
||||
Height float64 `xml:"height,attr,omitempty"`
|
||||
Relative int `xml:"relative,attr,omitempty"`
|
||||
As string `xml:"as,attr,omitempty"`
|
||||
|
||||
Points *MxPointArray `xml:"Array,omitempty"`
|
||||
SourcePoint *MxPoint `xml:"-"`
|
||||
TargetPoint *MxPoint `xml:"-"`
|
||||
}
|
||||
|
||||
type MxPoint struct {
|
||||
X float64 `xml:"x,attr,omitempty"`
|
||||
Y float64 `xml:"y,attr,omitempty"`
|
||||
As string `xml:"as,attr,omitempty"`
|
||||
}
|
||||
|
||||
type MxPointArray struct {
|
||||
As string `xml:"as,attr,omitempty"`
|
||||
Points []*MxPoint `xml:"mxPoint"`
|
||||
}
|
||||
|
||||
// MarshalXML customizes geometry output to allow multiple mxPoint children.
|
||||
func (g *MxGeometry) MarshalXML(enc *xml.Encoder, start xml.StartElement) error {
|
||||
if start.Name.Local == "" {
|
||||
start.Name.Local = "mxGeometry"
|
||||
}
|
||||
as := g.As
|
||||
if as == "" {
|
||||
as = "geometry"
|
||||
}
|
||||
start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "as"}, Value: as})
|
||||
if g.X != 0 {
|
||||
start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "x"}, Value: floatToString(g.X)})
|
||||
}
|
||||
if g.Y != 0 {
|
||||
start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "y"}, Value: floatToString(g.Y)})
|
||||
}
|
||||
if g.Width != 0 {
|
||||
start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "width"}, Value: floatToString(g.Width)})
|
||||
}
|
||||
if g.Height != 0 {
|
||||
start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "height"}, Value: floatToString(g.Height)})
|
||||
}
|
||||
if g.Relative != 0 {
|
||||
start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "relative"}, Value: intToString(g.Relative)})
|
||||
}
|
||||
|
||||
if err := enc.EncodeToken(start); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if g.Points != nil && len(g.Points.Points) > 0 {
|
||||
asVal := g.Points.As
|
||||
if asVal == "" {
|
||||
asVal = "points"
|
||||
}
|
||||
arrStart := xml.StartElement{Name: xml.Name{Local: "Array"}, Attr: []xml.Attr{{Name: xml.Name{Local: "as"}, Value: asVal}}}
|
||||
if err := enc.EncodeToken(arrStart); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, p := range g.Points.Points {
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
if err := enc.EncodeElement(p, xml.StartElement{Name: xml.Name{Local: "mxPoint"}}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := enc.EncodeToken(xml.EndElement{Name: arrStart.Name}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if g.SourcePoint != nil {
|
||||
p := *g.SourcePoint
|
||||
if p.As == "" {
|
||||
p.As = "sourcePoint"
|
||||
}
|
||||
if err := enc.EncodeElement(p, xml.StartElement{Name: xml.Name{Local: "mxPoint"}}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if g.TargetPoint != nil {
|
||||
p := *g.TargetPoint
|
||||
if p.As == "" {
|
||||
p.As = "targetPoint"
|
||||
}
|
||||
if err := enc.EncodeElement(p, xml.StartElement{Name: xml.Name{Local: "mxPoint"}}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return enc.EncodeToken(xml.EndElement{Name: start.Name})
|
||||
}
|
||||
|
||||
// MarshalXML customizes how the mxRoot emits cells, handling UserObject wrappers.
|
||||
func (r *mxRoot) MarshalXML(enc *xml.Encoder, start xml.StartElement) error {
|
||||
start.Name.Local = "root"
|
||||
if err := enc.EncodeToken(start); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, cell := range r.Cells {
|
||||
if cell == nil {
|
||||
continue
|
||||
}
|
||||
if cell.UserObject != nil {
|
||||
uoStart := xml.StartElement{Name: xml.Name{Local: "UserObject"}}
|
||||
if cell.UserObject.Label != "" {
|
||||
uoStart.Attr = append(uoStart.Attr, xml.Attr{Name: xml.Name{Local: "label"}, Value: cell.UserObject.Label})
|
||||
}
|
||||
if cell.UserObject.Link != "" {
|
||||
uoStart.Attr = append(uoStart.Attr, xml.Attr{Name: xml.Name{Local: "link"}, Value: cell.UserObject.Link})
|
||||
}
|
||||
if cell.UserObject.Tooltip != "" {
|
||||
uoStart.Attr = append(uoStart.Attr, xml.Attr{Name: xml.Name{Local: "tooltip"}, Value: cell.UserObject.Tooltip})
|
||||
}
|
||||
if err := enc.EncodeToken(uoStart); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := enc.EncodeElement(cell.alias(), xml.StartElement{Name: xml.Name{Local: "mxCell"}}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := enc.EncodeToken(xml.EndElement{Name: uoStart.Name}); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := enc.EncodeElement(cell.alias(), xml.StartElement{Name: xml.Name{Local: "mxCell"}}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return enc.EncodeToken(xml.EndElement{Name: start.Name})
|
||||
}
|
||||
|
||||
func (c *MxCell) alias() cellAlias {
|
||||
return cellAlias{
|
||||
ID: c.ID,
|
||||
Value: c.Value,
|
||||
Style: c.Style,
|
||||
Vertex: boolToAttr(c.Vertex),
|
||||
Edge: boolToAttr(c.Edge),
|
||||
Parent: c.Parent,
|
||||
Source: c.Source,
|
||||
Target: c.Target,
|
||||
Geometry: c.geometryWithDefault(),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *MxCell) geometryWithDefault() *MxGeometry {
|
||||
if c.Geometry == nil {
|
||||
return nil
|
||||
}
|
||||
g := *c.Geometry
|
||||
if g.As == "" {
|
||||
g.As = "geometry"
|
||||
}
|
||||
if g.Points != nil && g.Points.As == "" {
|
||||
g.Points.As = "points"
|
||||
}
|
||||
return &g
|
||||
}
|
||||
|
||||
type cellAlias struct {
|
||||
ID string `xml:"id,attr"`
|
||||
Value string `xml:"value,attr,omitempty"`
|
||||
Style string `xml:"style,attr,omitempty"`
|
||||
Vertex string `xml:"vertex,attr,omitempty"`
|
||||
Edge string `xml:"edge,attr,omitempty"`
|
||||
Parent string `xml:"parent,attr,omitempty"`
|
||||
Source string `xml:"source,attr,omitempty"`
|
||||
Target string `xml:"target,attr,omitempty"`
|
||||
Geometry *MxGeometry `xml:"mxGeometry,omitempty"`
|
||||
}
|
||||
|
||||
func boolToAttr(v bool) string {
|
||||
if v {
|
||||
return "1"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// rotateGeometry rotates a rectangle around the given pivot.
|
||||
func rotateGeometry(geo *MxGeometry, rotation, cx, cy float64) {
|
||||
if geo == nil || rotation == 0 {
|
||||
return
|
||||
}
|
||||
rads := rotation * math.Pi / 180
|
||||
cos := math.Cos(rads)
|
||||
sin := math.Sin(rads)
|
||||
x := geo.X - cx
|
||||
y := geo.Y - cy
|
||||
geo.X = x*cos - y*sin + cx
|
||||
geo.Y = x*sin + y*cos + cy
|
||||
}
|
||||
|
||||
// ToXML renders the graph model to draw.io compatible XML.
|
||||
func (g *MxGraph) ToXML(style, background string, grid, guides bool, width, height float64) (string, error) {
|
||||
model := g.Model
|
||||
model.Style = style
|
||||
model.Background = background
|
||||
if grid {
|
||||
model.Grid = 1
|
||||
}
|
||||
if guides {
|
||||
model.Guides = 1
|
||||
}
|
||||
model.GridSize = 10
|
||||
model.Connect = 1
|
||||
model.Arrows = 1
|
||||
model.Fold = 1
|
||||
model.Page = 1
|
||||
model.PageScale = 1
|
||||
model.Dx = 0
|
||||
model.Dy = 0
|
||||
if width == 0 {
|
||||
width = 850
|
||||
}
|
||||
if height == 0 {
|
||||
height = 1100
|
||||
}
|
||||
model.PageWidth = int(width)
|
||||
model.PageHeight = int(height)
|
||||
model.Tooltips = 1
|
||||
model.Math = 0
|
||||
model.Shadow = 0
|
||||
|
||||
output, err := xml.Marshal(model)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(output), nil
|
||||
}
|
||||
|
||||
// NormalizeOrigin shifts all absolute coordinates so the top-left-most point is at (0,0).
|
||||
func (g *MxGraph) NormalizeOrigin() {
|
||||
minX, minY := math.MaxFloat64, math.MaxFloat64
|
||||
|
||||
for _, cell := range g.Model.Root.Cells {
|
||||
if cell == nil || cell.Geometry == nil {
|
||||
continue
|
||||
}
|
||||
geo := cell.Geometry
|
||||
if geo.Relative == 0 {
|
||||
if geo.X < minX {
|
||||
minX = geo.X
|
||||
}
|
||||
if geo.Y < minY {
|
||||
minY = geo.Y
|
||||
}
|
||||
}
|
||||
if geo.Points != nil {
|
||||
for _, p := range geo.Points.Points {
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
if p.X < minX {
|
||||
minX = p.X
|
||||
}
|
||||
if p.Y < minY {
|
||||
minY = p.Y
|
||||
}
|
||||
}
|
||||
}
|
||||
if geo.SourcePoint != nil {
|
||||
if geo.SourcePoint.X < minX {
|
||||
minX = geo.SourcePoint.X
|
||||
}
|
||||
if geo.SourcePoint.Y < minY {
|
||||
minY = geo.SourcePoint.Y
|
||||
}
|
||||
}
|
||||
if geo.TargetPoint != nil {
|
||||
if geo.TargetPoint.X < minX {
|
||||
minX = geo.TargetPoint.X
|
||||
}
|
||||
if geo.TargetPoint.Y < minY {
|
||||
minY = geo.TargetPoint.Y
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if minX == math.MaxFloat64 || minY == math.MaxFloat64 {
|
||||
return
|
||||
}
|
||||
|
||||
dx, dy := -minX, -minY
|
||||
|
||||
for _, cell := range g.Model.Root.Cells {
|
||||
if cell == nil || cell.Geometry == nil {
|
||||
continue
|
||||
}
|
||||
geo := cell.Geometry
|
||||
if geo.Relative == 0 {
|
||||
geo.X += dx
|
||||
geo.Y += dy
|
||||
}
|
||||
if geo.Points != nil {
|
||||
for _, p := range geo.Points.Points {
|
||||
if p == nil {
|
||||
continue
|
||||
}
|
||||
p.X += dx
|
||||
p.Y += dy
|
||||
}
|
||||
}
|
||||
if geo.SourcePoint != nil {
|
||||
geo.SourcePoint.X += dx
|
||||
geo.SourcePoint.Y += dy
|
||||
}
|
||||
if geo.TargetPoint != nil {
|
||||
geo.TargetPoint.X += dx
|
||||
geo.TargetPoint.Y += dy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// helper to ensure geometry has relative flag set to 1.
|
||||
func setRelative(geo *MxGeometry, v bool) {
|
||||
if geo == nil {
|
||||
return
|
||||
}
|
||||
if v {
|
||||
geo.Relative = 1
|
||||
} else {
|
||||
geo.Relative = 0
|
||||
}
|
||||
}
|
||||
|
||||
// linearly interpolated center used for rotations.
|
||||
func midpoint(p1, p2 *MxPoint) *MxPoint {
|
||||
return &MxPoint{
|
||||
X: (p1.X + p2.X) / 2,
|
||||
Y: (p1.Y + p2.Y) / 2,
|
||||
}
|
||||
}
|
||||
|
||||
// joinStyle appends style fragments with trailing semicolon when needed.
|
||||
func joinStyle(parts ...string) string {
|
||||
var sb strings.Builder
|
||||
for _, p := range parts {
|
||||
if p == "" {
|
||||
continue
|
||||
}
|
||||
if !strings.HasSuffix(p, ";") {
|
||||
p += ";"
|
||||
}
|
||||
sb.WriteString(p)
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
Reference in New Issue
Block a user