902 lines
26 KiB
Go
902 lines
26 KiB
Go
package gliffy2drawio
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"html"
|
|
"math"
|
|
"os"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
type GliffyDiagramConverter struct {
|
|
diagramString string
|
|
gliffyDiagram Diagram
|
|
drawioDiagram *MxGraph
|
|
vertices map[string]*GliffyObject
|
|
layers map[string]*GliffyLayer
|
|
|
|
rotationPattern *regexp.Regexp
|
|
pageIDPattern *regexp.Regexp
|
|
namePattern *regexp.Regexp
|
|
|
|
report strings.Builder
|
|
translator *StencilTranslator
|
|
}
|
|
|
|
func NewGliffyDiagramConverter(gliffyDiagramString string) (*GliffyDiagramConverter, error) {
|
|
customTrans := os.Getenv("GLIFFY_TRANSLATIONS_JSON")
|
|
c := &GliffyDiagramConverter{
|
|
diagramString: gliffyDiagramString,
|
|
drawioDiagram: NewMxGraph(),
|
|
vertices: make(map[string]*GliffyObject),
|
|
layers: make(map[string]*GliffyLayer),
|
|
rotationPattern: regexp.MustCompile(`rotation=(\-?\w+)`),
|
|
pageIDPattern: regexp.MustCompile(`pageId=([^&]+)`),
|
|
namePattern: regexp.MustCompile(`name=([^&]+)`),
|
|
translator: NewStencilTranslator("").WithCustomMapping(customTrans),
|
|
}
|
|
if err := c.start(); err != nil {
|
|
return nil, err
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
func (c *GliffyDiagramConverter) start() error {
|
|
if err := json.Unmarshal([]byte(c.diagramString), &c.gliffyDiagram); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Support newer Gliffy files that wrap content in pages/scene.
|
|
if len(c.gliffyDiagram.Stage.Objects) == 0 && len(c.gliffyDiagram.Pages) > 0 {
|
|
selected := c.gliffyDiagram.Pages[0]
|
|
if c.gliffyDiagram.DefaultPage != "" {
|
|
for _, p := range c.gliffyDiagram.Pages {
|
|
if p.ID == c.gliffyDiagram.DefaultPage {
|
|
selected = p
|
|
break
|
|
}
|
|
}
|
|
}
|
|
scene := selected.Scene
|
|
c.gliffyDiagram.Stage = Stage{
|
|
Background: scene.Background,
|
|
Width: scene.Width,
|
|
Height: scene.Height,
|
|
AutoFit: scene.AutoFit,
|
|
GridOn: scene.GridOn,
|
|
DrawingGuidesOn: scene.DrawingGuidesOn,
|
|
Objects: scene.Objects,
|
|
Layers: scene.Layers,
|
|
TextStyles: scene.TextStyles,
|
|
}
|
|
}
|
|
|
|
c.collectLayersAndConvert(c.layers, c.gliffyDiagram.Stage.Layers)
|
|
c.collectVerticesAndConvert(c.vertices, c.gliffyDiagram.Stage.Objects, nil)
|
|
|
|
sortObjectsByOrder(c.gliffyDiagram.Stage.Objects)
|
|
|
|
c.importLayers()
|
|
for _, obj := range c.gliffyDiagram.Stage.Objects {
|
|
if err := c.importObject(obj, obj.Parent); err != nil {
|
|
c.report.WriteString("-- Warning, Object " + obj.ID + " cannot be transformed.\n")
|
|
}
|
|
}
|
|
// Shift everything to origin to avoid bottom-right offset in draw.io.
|
|
c.drawioDiagram.NormalizeOrigin()
|
|
return nil
|
|
}
|
|
|
|
func (c *GliffyDiagramConverter) importLayers() {
|
|
if len(c.gliffyDiagram.Stage.Layers) == 0 {
|
|
return
|
|
}
|
|
sortLayersByOrder(c.gliffyDiagram.Stage.Layers)
|
|
for i, layer := range c.gliffyDiagram.Stage.Layers {
|
|
// Avoid duplicating the default layer (id=1 already in root).
|
|
if i == 0 && layer.MxObject != nil && layer.MxObject.ID == "1" && c.drawioDiagram.HasCell("1") {
|
|
continue
|
|
}
|
|
c.drawioDiagram.AddCell(layer.MxObject, c.drawioDiagram.root)
|
|
if i == 0 {
|
|
c.drawioDiagram.SetDefaultParent(layer.MxObject)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *GliffyDiagramConverter) importObject(obj *GliffyObject, gliffyParent *GliffyObject) error {
|
|
var parent *MxCell
|
|
if gliffyParent != nil {
|
|
parent = gliffyParent.MxObject
|
|
}
|
|
if parent == nil && obj.LayerID != nil {
|
|
if layer, ok := c.layers[*obj.LayerID]; ok && layer != nil {
|
|
parent = layer.MxObject
|
|
}
|
|
}
|
|
|
|
c.drawioDiagram.AddCell(obj.MxObject, parent)
|
|
|
|
if obj.hasChildren() {
|
|
if obj.isSwimlane() {
|
|
if obj.Rotation != 0 {
|
|
reverse(obj.Children)
|
|
}
|
|
} else {
|
|
sortObjectsByOrder(obj.Children)
|
|
}
|
|
for _, child := range obj.Children {
|
|
if err := c.importObject(child, obj); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
if obj.isLine() {
|
|
startTerminal := c.getTerminalCell(obj, true)
|
|
endTerminal := c.getTerminalCell(obj, false)
|
|
if startTerminal != nil {
|
|
obj.MxObject.Source = startTerminal.ID
|
|
}
|
|
if endTerminal != nil {
|
|
obj.MxObject.Target = endTerminal.ID
|
|
}
|
|
c.setWaypoints(obj, startTerminal, endTerminal)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *GliffyDiagramConverter) getTerminalCell(edge *GliffyObject, start bool) *MxCell {
|
|
cons := edge.GetConstraints()
|
|
if cons == nil {
|
|
return nil
|
|
}
|
|
var con *Constraint
|
|
if start {
|
|
con = cons.StartConstraint
|
|
} else {
|
|
con = cons.EndConstraint
|
|
}
|
|
if con == nil {
|
|
return nil
|
|
}
|
|
var data *ConstraintData
|
|
if start {
|
|
data = con.StartPositionConstraint
|
|
} else {
|
|
data = con.EndPositionConstraint
|
|
}
|
|
if data == nil {
|
|
return nil
|
|
}
|
|
terminal := c.vertices[data.NodeID]
|
|
if terminal == nil {
|
|
return nil
|
|
}
|
|
return terminal.MxObject
|
|
}
|
|
|
|
func (c *GliffyDiagramConverter) collectLayersAndConvert(layerMap map[string]*GliffyLayer, layers []*GliffyLayer) {
|
|
if len(layers) == 0 {
|
|
return
|
|
}
|
|
|
|
// Ensure only a single default layer (id=1, parent=0) exists.
|
|
if len(c.drawioDiagram.Model.Root.Cells) > 2 {
|
|
trimmed := []*MxCell{}
|
|
for _, cell := range c.drawioDiagram.Model.Root.Cells {
|
|
if cell == nil {
|
|
continue
|
|
}
|
|
if cell.ID == "0" || cell.ID == "1" {
|
|
trimmed = append(trimmed, cell)
|
|
}
|
|
}
|
|
if len(trimmed) < 2 {
|
|
trimmed = append(trimmed, &MxCell{ID: "1", Parent: c.drawioDiagram.root.ID})
|
|
}
|
|
c.drawioDiagram.Model.Root.Cells = trimmed
|
|
}
|
|
|
|
// Reuse existing default layer (id 1, parent 0) for the first Gliffy layer to match draw.io expectations.
|
|
var defaultLayer *MxCell
|
|
if len(c.drawioDiagram.Model.Root.Cells) >= 2 {
|
|
if cell := c.drawioDiagram.Model.Root.Cells[1]; cell != nil && cell.Parent == c.drawioDiagram.root.ID {
|
|
defaultLayer = cell
|
|
}
|
|
}
|
|
if defaultLayer == nil {
|
|
defaultLayer = &MxCell{ID: "1", Parent: c.drawioDiagram.root.ID}
|
|
c.drawioDiagram.Model.Root.Cells = append(c.drawioDiagram.Model.Root.Cells, defaultLayer)
|
|
}
|
|
|
|
for i, layer := range layers {
|
|
var cell *MxCell
|
|
if i == 0 {
|
|
cell = defaultLayer
|
|
} else {
|
|
cell = &MxCell{}
|
|
c.drawioDiagram.AddCell(cell, c.drawioDiagram.root)
|
|
}
|
|
cell.Value = layer.Name
|
|
if layer.Locked {
|
|
cell.Style = joinStyle(cell.Style, "locked=1")
|
|
}
|
|
if !layer.Visible {
|
|
cell.Style = joinStyle(cell.Style, "visible=0")
|
|
}
|
|
layer.MxObject = cell
|
|
layerMap[layer.GUID] = layer
|
|
if i == 0 {
|
|
c.drawioDiagram.SetDefaultParent(cell)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *GliffyDiagramConverter) collectVerticesAndConvert(vertices map[string]*GliffyObject, objects []*GliffyObject, parent *GliffyObject) {
|
|
for _, obj := range objects {
|
|
obj.Parent = parent
|
|
obj.MxObject = c.convertGliffyObject(obj, parent)
|
|
if !obj.isLine() {
|
|
vertices[obj.ID] = obj
|
|
}
|
|
if obj.isGroup() || obj.isSelection() || (obj.isLine() && obj.hasChildren()) {
|
|
c.collectVerticesAndConvert(vertices, obj.Children, obj)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *GliffyDiagramConverter) GraphXML() (string, error) {
|
|
grid := c.gliffyDiagram.Stage.GridOn
|
|
guides := c.gliffyDiagram.Stage.DrawingGuidesOn
|
|
w := c.gliffyDiagram.Stage.Width
|
|
h := c.gliffyDiagram.Stage.Height
|
|
if w == 0 {
|
|
w = 1200
|
|
}
|
|
if h == 0 {
|
|
h = 900
|
|
}
|
|
xml, err := c.drawioDiagram.ToXML("default-style2", c.gliffyDiagram.Stage.Background, grid, guides, w, h)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
name := c.diagramName()
|
|
return fmt.Sprintf(`<mxfile><diagram name="%s">%s</diagram></mxfile>`, html.EscapeString(name), xml), nil
|
|
}
|
|
|
|
func (c *GliffyDiagramConverter) diagramName() string {
|
|
if c.gliffyDiagram.Title != "" {
|
|
return c.gliffyDiagram.Title
|
|
}
|
|
if len(c.gliffyDiagram.Pages) > 0 {
|
|
selected := c.gliffyDiagram.Pages[0]
|
|
if c.gliffyDiagram.DefaultPage != "" {
|
|
for _, p := range c.gliffyDiagram.Pages {
|
|
if p.ID == c.gliffyDiagram.DefaultPage {
|
|
selected = p
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if selected.Title != "" {
|
|
return selected.Title
|
|
}
|
|
}
|
|
if c.gliffyDiagram.Metadata.Title != "" {
|
|
return c.gliffyDiagram.Metadata.Title
|
|
}
|
|
return "Page-1"
|
|
}
|
|
|
|
func (c *GliffyDiagramConverter) convertGliffyObject(obj *GliffyObject, parent *GliffyObject) *MxCell {
|
|
cell := &MxCell{}
|
|
if obj.ID != "" {
|
|
cell.ID = obj.ID
|
|
}
|
|
if obj.IsUnrecognizedGraphicType() {
|
|
return cell
|
|
}
|
|
|
|
var style strings.Builder
|
|
geo := &MxGeometry{X: obj.X, Y: obj.Y, Width: obj.Width, Height: obj.Height}
|
|
obj.adjustGeo(geo)
|
|
cell.Geometry = geo
|
|
|
|
var textObject *GliffyObject
|
|
var link string
|
|
graphic := obj.GraphicOrChildGraphic()
|
|
translatedStyle := c.translator.Translate(obj.UID, obj.TID)
|
|
|
|
if obj.isGroup() {
|
|
if graphic == nil || translatedStyle == "" {
|
|
style.WriteString("group;")
|
|
}
|
|
cell.Vertex = true
|
|
} else {
|
|
textObject = obj.TextObject()
|
|
}
|
|
|
|
if graphic != nil {
|
|
link = obj.AdjustedLink()
|
|
switch graphic.GetType() {
|
|
case GraphicTypeShape, GraphicTypeMindmap:
|
|
shape := graphic.Shape
|
|
if shape == nil {
|
|
shape = &GliffyShape{}
|
|
}
|
|
cell.Vertex = true
|
|
isChevron := strings.Contains(obj.UID, "chevron")
|
|
if translatedStyle != "" {
|
|
style.WriteString("shape=" + translatedStyle + ";")
|
|
} else {
|
|
// Fallback if stencil translation is missing: render as a basic rect so the shape is visible.
|
|
style.WriteString("shape=rect;")
|
|
}
|
|
if !strings.Contains(style.String(), "shadow=") {
|
|
style.WriteString("shadow=" + intToString(boolToInt(shape.DropShadow)) + ";")
|
|
}
|
|
if !strings.Contains(style.String(), "strokeWidth") {
|
|
style.WriteString("strokeWidth=" + intToString(shape.StrokeWidthValue()) + ";")
|
|
if shape.StrokeWidthValue() == 0 && !isChevron {
|
|
style.WriteString("strokeColor=none;")
|
|
}
|
|
}
|
|
if !strings.Contains(style.String(), "fillColor") {
|
|
if shape.NoFill() && !isChevron {
|
|
style.WriteString("fillColor=none;")
|
|
} else {
|
|
style.WriteString("fillColor=" + shape.FillColor + ";")
|
|
}
|
|
if shape.FillColor == "none" {
|
|
style.WriteString("pointerEvents=0;")
|
|
}
|
|
}
|
|
if !strings.Contains(style.String(), "strokeColor") && !shape.NoFill() {
|
|
stroke := shape.StrokeColor
|
|
if obj.IsUseFillColorForStroke() {
|
|
stroke = shape.FillColor
|
|
}
|
|
style.WriteString("strokeColor=" + stroke + ";")
|
|
}
|
|
if !strings.Contains(style.String(), "gradient") && shape.Gradient && !obj.GradientIgnored() {
|
|
style.WriteString("gradientColor=" + obj.GradientColor() + ";gradientDirection=north;")
|
|
}
|
|
if !obj.isVennCircle() && !strings.Contains(style.String(), "opacity") {
|
|
style.WriteString("opacity=" + floatToString(shape.Opacity*100) + ";")
|
|
}
|
|
style.WriteString(DashStyleMapping(shape.DashStyle, 1))
|
|
if obj.IsSubRoutine() && obj.Width != 0 {
|
|
style.WriteString("size=" + floatToString(10/obj.Width) + ";")
|
|
}
|
|
if fragment := obj.UMLSequenceCombinedFragmentText(); fragment != "" && len(obj.Children) > 0 {
|
|
cell.Value = fragment
|
|
obj.Children = obj.Children[1:]
|
|
}
|
|
case GraphicTypeLine:
|
|
if graphic.Line == nil {
|
|
break
|
|
}
|
|
line := graphic.Line
|
|
cell.Edge = true
|
|
style.WriteString("shape=filledEdge;")
|
|
style.WriteString("strokeWidth=" + intToString(line.StrokeWidthValue()) + ";")
|
|
style.WriteString("strokeColor=" + line.StrokeColor + ";")
|
|
style.WriteString("fillColor=" + line.FillColor + ";")
|
|
style.WriteString(ArrowMapping(line.StartArrow).ToString(true))
|
|
style.WriteString(ArrowMapping(line.EndArrow).ToString(false))
|
|
if line.CornerRadius != nil {
|
|
style.WriteString("rounded=1;")
|
|
} else {
|
|
style.WriteString("rounded=0;")
|
|
}
|
|
style.WriteString(DashStyleMapping(line.DashStyle, line.StrokeWidthValue()))
|
|
style.WriteString(LineMapping(line.Interpolation))
|
|
cell.Geometry.X = 0
|
|
cell.Geometry.Y = 0
|
|
case GraphicTypeText:
|
|
textObject = obj
|
|
cell.Vertex = true
|
|
style.WriteString("text;html=1;nl2Br=0;")
|
|
cell.Value = obj.TextHTML()
|
|
if obj.Parent != nil && !obj.Parent.isGroup() {
|
|
parentGeo := obj.Parent.MxObject.Geometry
|
|
if obj.Parent.isLine() {
|
|
mxGeo := &MxGeometry{X: 0, Y: 0, Width: 0, Height: 0}
|
|
lineT := 0.0
|
|
if graphic.Text != nil {
|
|
lineT = graphic.Text.LineTValue*2 - 1
|
|
}
|
|
mxGeo.X = lineT
|
|
var lblY, lblX float64
|
|
if graphic.Text != nil && graphic.Text.LinePerpValue != nil {
|
|
control := obj.Parent.Graphic.Line.ControlPath
|
|
if len(control) >= 2 {
|
|
i1 := 0
|
|
i2 := len(control) - 1
|
|
noCardinal := false
|
|
switch graphic.Text.CardinalityType {
|
|
case "begin":
|
|
i2 = 1
|
|
case "end":
|
|
i1 = len(control) - 2
|
|
default:
|
|
noCardinal = true
|
|
}
|
|
if noCardinal || control[i1][1] == control[i2][1] {
|
|
lblY = *graphic.Text.LinePerpValue
|
|
if control[i1][0]-control[i2][0] > 0 {
|
|
lblY = -lblY
|
|
}
|
|
} else {
|
|
lblX = *graphic.Text.LinePerpValue
|
|
if control[i1][1]-control[i2][1] < 0 {
|
|
lblX = -lblX
|
|
}
|
|
}
|
|
}
|
|
}
|
|
mxGeo.SourcePoint = &MxPoint{X: lblX, Y: lblY}
|
|
setRelative(mxGeo, true)
|
|
cell.Geometry = mxGeo
|
|
style.WriteString("labelBackgroundColor=" + c.gliffyDiagram.Stage.Background + ";")
|
|
if graphic.Text != nil {
|
|
graphic.Text.SetHAlign("")
|
|
}
|
|
} else {
|
|
cell.Geometry = &MxGeometry{X: 0, Y: 0, Width: parentGeo.Width, Height: parentGeo.Height}
|
|
setRelative(cell.Geometry, true)
|
|
}
|
|
}
|
|
case GraphicTypeImage:
|
|
img := graphic.Image
|
|
cell.Vertex = true
|
|
style.WriteString("shape=" + c.translator.Translate(obj.UID, obj.TID) + ";")
|
|
style.WriteString("image=" + img.CleanURL() + ";")
|
|
case GraphicTypeSVG:
|
|
svg := graphic.Svg
|
|
cell.Vertex = true
|
|
style.WriteString("shape=image;imageAspect=0;")
|
|
if svg != nil && svg.EmbeddedResourceID != nil {
|
|
if res, ok := c.gliffyDiagram.EmbeddedResources.Get(*svg.EmbeddedResourceID); ok {
|
|
svgUtil := SVGImporterUtils{}
|
|
data := svgUtil.SetViewBox(res.Data)
|
|
style.WriteString("image=data:image/svg+xml," + EmbeddedResource{Data: data}.Base64EncodedData() + ";")
|
|
}
|
|
}
|
|
default:
|
|
// Fallback: render as image if possible or apply translated style if found.
|
|
if graphic.Image != nil && graphic.Image.URL != "" {
|
|
cell.Vertex = true
|
|
style.WriteString("shape=image;imageAspect=0;image=" + graphic.Image.CleanURL() + ";")
|
|
} else if graphic.Svg != nil && graphic.Svg.EmbeddedResourceID != nil {
|
|
cell.Vertex = true
|
|
style.WriteString("shape=image;imageAspect=0;")
|
|
if res, ok := c.gliffyDiagram.EmbeddedResources.Get(*graphic.Svg.EmbeddedResourceID); ok {
|
|
svgUtil := SVGImporterUtils{}
|
|
data := svgUtil.SetViewBox(res.Data)
|
|
style.WriteString("image=data:image/svg+xml," + EmbeddedResource{Data: data}.Base64EncodedData() + ";")
|
|
}
|
|
} else if translatedStyle != "" {
|
|
cell.Vertex = true
|
|
style.WriteString("shape=" + translatedStyle + ";")
|
|
}
|
|
}
|
|
} else if obj.isSwimlane() && len(obj.Children) > 0 {
|
|
cell.Vertex = true
|
|
style.WriteString(c.translator.Translate(obj.UID, "") + ";")
|
|
if obj.Rotation == 0 {
|
|
style.WriteString("childLayout=stackLayout;resizeParent=1;resizeParentMax=0;")
|
|
}
|
|
header := obj.Children[0]
|
|
shape := header.Graphic.Shape
|
|
style.WriteString("strokeWidth=" + intToString(shape.StrokeWidthValue()) + ";")
|
|
style.WriteString("shadow=" + intToString(boolToInt(shape.DropShadow)) + ";")
|
|
style.WriteString("fillColor=" + shape.FillColor + ";")
|
|
style.WriteString("strokeColor=" + shape.StrokeColor + ";")
|
|
style.WriteString("startSize=" + floatToString(header.Height) + ";")
|
|
style.WriteString("whiteSpace=wrap;")
|
|
|
|
for i := 1; i < len(obj.Children); i++ {
|
|
gLane := obj.Children[i]
|
|
gLane.Parent = obj
|
|
gs := gLane.Graphic.Shape
|
|
var laneStyle strings.Builder
|
|
laneStyle.WriteString("swimlane;collapsible=0;swimlaneLine=0;")
|
|
laneStyle.WriteString("strokeWidth=" + intToString(gs.StrokeWidthValue()) + ";")
|
|
laneStyle.WriteString("shadow=" + intToString(boolToInt(gs.DropShadow)) + ";")
|
|
laneStyle.WriteString("fillColor=" + gs.FillColor + ";")
|
|
laneStyle.WriteString("strokeColor=" + gs.StrokeColor + ";")
|
|
laneStyle.WriteString("whiteSpace=wrap;html=1;fontStyle=0;")
|
|
|
|
childGeometry := &MxGeometry{X: gLane.X, Y: gLane.Y, Width: gLane.Width, Height: gLane.Height}
|
|
if obj.Rotation != 0 {
|
|
if obj.Rotation == 270 {
|
|
laneStyle.WriteString("horizontal=0;")
|
|
w := childGeometry.Width
|
|
childGeometry.Width = childGeometry.Height
|
|
childGeometry.Height = w
|
|
x := childGeometry.X
|
|
childGeometry.X = childGeometry.Y
|
|
childGeometry.Y = obj.Width - w - x
|
|
} else {
|
|
laneStyle.WriteString("rotation=" + floatToString(obj.Rotation) + ";")
|
|
rotateGeometry(childGeometry, obj.Rotation, obj.Width/2, obj.Height/2)
|
|
}
|
|
}
|
|
mxLane := &MxCell{Vertex: true}
|
|
laneTxt := gLane.Children[0]
|
|
mxLane.Value = laneTxt.TextHTML()
|
|
if laneTxt.Graphic != nil && laneTxt.Graphic.Text != nil {
|
|
laneStyle.WriteString(laneTxt.Graphic.Text.GetStyle(0, 0))
|
|
}
|
|
laneStyle.WriteString("gliffyId=" + gLane.ID + ";")
|
|
mxLane.Style = laneStyle.String()
|
|
mxLane.Geometry = childGeometry
|
|
gLane.MxObject = mxLane
|
|
cell.Geometry = geo
|
|
}
|
|
} else if obj.isMindmap() && len(obj.Children) > 0 {
|
|
rectangle := obj.Children[0]
|
|
if rectangle.Graphic == nil || rectangle.Graphic.Mindmap == nil {
|
|
obj.MxObject = cell
|
|
return cell
|
|
}
|
|
mindmap := rectangle.Graphic.Mindmap
|
|
style.WriteString("shape=" + c.translator.Translate(obj.UID, "") + ";")
|
|
style.WriteString("shadow=" + intToString(boolToInt(mindmap.DropShadow)) + ";")
|
|
style.WriteString("strokeWidth=" + intToString(mindmap.StrokeWidthValue()) + ";")
|
|
style.WriteString("fillColor=" + mindmap.FillColor + ";")
|
|
style.WriteString("strokeColor=" + mindmap.StrokeColor + ";")
|
|
style.WriteString(DashStyleMapping(mindmap.DashStyle, 1))
|
|
if mindmap.Gradient {
|
|
style.WriteString("gradientColor=#FFFFFF;gradientDirection=north;")
|
|
}
|
|
cell.Vertex = true
|
|
}
|
|
|
|
if !obj.isLine() {
|
|
if strings.Contains(style.String(), "rotation") {
|
|
if m := c.rotationPattern.FindStringSubmatch(style.String()); len(m) > 1 {
|
|
initial, _ := strconv.ParseFloat(m[1], 64)
|
|
rotation := initial + obj.Rotation
|
|
styleStr := c.rotationPattern.ReplaceAllString(style.String(), "rotation="+floatToString(rotation))
|
|
style.Reset()
|
|
style.WriteString(styleStr)
|
|
}
|
|
} else if obj.Rotation != 0 {
|
|
if strings.Contains(style.String(), "swimlane;collapsible=0;") && obj.Rotation == 270 {
|
|
w := geo.Width
|
|
h := geo.Height
|
|
geo.X = geo.X + (w-h)/2
|
|
geo.Y = geo.Y + (h-w)/2
|
|
geo.Width = h
|
|
geo.Height = w
|
|
style.WriteString("childLayout=stackLayout;resizeParent=1;resizeParentMax=0;horizontal=0;horizontalStack=0;")
|
|
} else {
|
|
if obj.isGroup() {
|
|
for _, child := range obj.Children {
|
|
c.rotateGroupedObject(obj, child)
|
|
}
|
|
}
|
|
style.WriteString("rotation=" + floatToString(obj.Rotation) + ";")
|
|
}
|
|
}
|
|
}
|
|
|
|
if textObject != nil {
|
|
style.WriteString("html=1;nl2Br=0;")
|
|
if !obj.isLine() && textObject.Graphic != nil && textObject.Graphic.Text != nil {
|
|
txt := textObject.Graphic.Text
|
|
if obj.isSwimlane() {
|
|
txt.SetForceTopPaddingShift(true)
|
|
txt.SetVAlign("middle")
|
|
}
|
|
cell.Value = textObject.TextHTML()
|
|
obj.adjustTextPos(textObject)
|
|
if obj.ContainsTextBracket() {
|
|
c.fixFrameTextBorder(obj, &style)
|
|
style.WriteString(strings.ReplaceAll(txt.GetStyle(0, 0), "verticalAlign=middle", "verticalAlign=top"))
|
|
} else {
|
|
isChevron := strings.Contains(obj.UID, "chevron")
|
|
if textObject == obj || isChevron {
|
|
style.WriteString(txt.GetStyle(0, 0))
|
|
} else {
|
|
style.WriteString(txt.GetStyle(textObject.X, textObject.Y))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
popup := c.getGliffyPopup(obj)
|
|
if link != "" || popup != nil {
|
|
userObj := &UserObject{}
|
|
if link != "" {
|
|
if lb := c.extractLightboxDataFromGliffyUrl(link); lb != nil {
|
|
link = "/plugins/drawio/lightbox.action?ceoId=" + intToString(int(lb.Key)) + "&diagramName=" + lb.Value + ".drawio"
|
|
}
|
|
userObj.Link = link
|
|
if textObject != nil {
|
|
userObj.Label = textObject.TextHTML()
|
|
}
|
|
}
|
|
if popup != nil && popup.Graphic != nil && popup.Graphic.PopupNote != nil {
|
|
userObj.Tooltip = popup.Graphic.PopupNote.Text
|
|
}
|
|
cell.UserObject = userObj
|
|
}
|
|
|
|
style.WriteString("gliffyId=" + obj.ID + ";")
|
|
cell.Style = style.String()
|
|
obj.MxObject = cell
|
|
return cell
|
|
}
|
|
|
|
func (c *GliffyDiagramConverter) getGliffyPopup(obj *GliffyObject) *GliffyObject {
|
|
if !obj.hasChildren() {
|
|
return nil
|
|
}
|
|
for _, child := range obj.Children {
|
|
if child.Graphic != nil && child.Graphic.GetType() == GraphicTypePopup {
|
|
return child
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *GliffyDiagramConverter) rotateGroupedObject(group, child *GliffyObject) {
|
|
pivot := &MxPoint{X: group.Width/2 - child.Width/2, Y: group.Height/2 - child.Height/2}
|
|
temp := &MxPoint{X: child.X, Y: child.Y}
|
|
if group.Rotation != 0 {
|
|
rads := group.Rotation * math.Pi / 180
|
|
cos := math.Cos(rads)
|
|
sin := math.Sin(rads)
|
|
temp = &MxPoint{
|
|
X: temp.X*cos - temp.Y*sin + pivot.X,
|
|
Y: temp.X*sin + temp.Y*cos + pivot.Y,
|
|
}
|
|
child.X = temp.X
|
|
child.Y = temp.Y
|
|
child.Rotation += group.Rotation
|
|
}
|
|
}
|
|
|
|
func (c *GliffyDiagramConverter) fixFrameTextBorder(obj *GliffyObject, style *strings.Builder) {
|
|
if obj.TextObject() == nil {
|
|
return
|
|
}
|
|
wrong := "labelX=32"
|
|
correct := "labelX=" + floatToString(obj.TextObject().Width*1.1)
|
|
styleStr := strings.Replace(style.String(), wrong, correct, 1)
|
|
style.Reset()
|
|
style.WriteString(styleStr)
|
|
}
|
|
|
|
func (c *GliffyDiagramConverter) addConstraint(obj *GliffyObject, terminal *MxCell, source bool, orthogonal bool) bool {
|
|
cons := obj.GetConstraints()
|
|
if cons == nil {
|
|
return orthogonal
|
|
}
|
|
var con *Constraint
|
|
if source {
|
|
con = cons.StartConstraint
|
|
} else {
|
|
con = cons.EndConstraint
|
|
}
|
|
if con == nil {
|
|
return orthogonal
|
|
}
|
|
var data *ConstraintData
|
|
if source {
|
|
data = con.StartPositionConstraint
|
|
} else {
|
|
data = con.EndPositionConstraint
|
|
}
|
|
if data != nil {
|
|
direction := getStyleValue(terminal, "direction", "east")
|
|
temp := &MxPoint{X: data.PX, Y: data.PY}
|
|
rotation := 0
|
|
switch strings.ToLower(direction) {
|
|
case "south":
|
|
rotation = 270
|
|
case "west":
|
|
rotation = 180
|
|
case "north":
|
|
rotation = 90
|
|
}
|
|
if rotation != 0 {
|
|
rad := float64(rotation) * math.Pi / 180
|
|
temp = &MxPoint{
|
|
X: temp.X*math.Cos(rad) - temp.Y*math.Sin(rad),
|
|
Y: temp.X*math.Sin(rad) + temp.Y*math.Cos(rad),
|
|
}
|
|
}
|
|
if !orthogonal || (temp.X == 0.5 && temp.Y == 0.5) || obj.ForceConstraints() {
|
|
cell := obj.MxObject
|
|
if source {
|
|
cell.Style += "exitX=" + floatToString(temp.X) + ";exitY=" + floatToString(temp.Y) + ";exitPerimeter=0;"
|
|
} else {
|
|
cell.Style += "entryX=" + floatToString(temp.X) + ";entryY=" + floatToString(temp.Y) + ";entryPerimeter=0;"
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
return orthogonal
|
|
}
|
|
|
|
func (c *GliffyDiagramConverter) setWaypoints(obj *GliffyObject, start, end *MxCell) {
|
|
if obj.Graphic == nil || obj.Graphic.Line == nil {
|
|
return
|
|
}
|
|
cell := obj.MxObject
|
|
geo := cell.Geometry
|
|
if geo == nil {
|
|
geo = &MxGeometry{}
|
|
cell.Geometry = geo
|
|
}
|
|
setRelative(geo, true)
|
|
|
|
points := obj.Graphic.Line.ControlPath
|
|
if len(points) < 2 {
|
|
return
|
|
}
|
|
mxPoints := make([]*MxPoint, 0, len(points))
|
|
pivot := &MxPoint{X: obj.X + obj.Width/2, Y: obj.Y + obj.Height/2}
|
|
for _, p := range points {
|
|
wp := &MxPoint{X: p[0] + obj.X, Y: p[1] + obj.Y}
|
|
if obj.Rotation != 0 {
|
|
rads := obj.Rotation * math.Pi / 180
|
|
cos := math.Cos(rads)
|
|
sin := math.Sin(rads)
|
|
wp = &MxPoint{
|
|
X: pivot.X + (wp.X-pivot.X)*cos - (wp.Y-pivot.Y)*sin,
|
|
Y: pivot.Y + (wp.X-pivot.X)*sin + (wp.Y-pivot.Y)*cos,
|
|
}
|
|
}
|
|
mxPoints = append(mxPoints, wp)
|
|
}
|
|
|
|
orthogonal := true
|
|
last := mxPoints[0]
|
|
for _, p := range mxPoints[1:] {
|
|
orthogonal = orthogonal && (last.X == p.X || last.Y == p.Y)
|
|
last = p
|
|
}
|
|
|
|
p0 := mxPoints[0]
|
|
pe := mxPoints[len(mxPoints)-1]
|
|
if start == nil {
|
|
geo.SourcePoint = &MxPoint{X: p0.X, Y: p0.Y, As: "sourcePoint"}
|
|
mxPoints = mxPoints[1:]
|
|
} else {
|
|
if c.addConstraint(obj, start, true, orthogonal) {
|
|
// keep point
|
|
}
|
|
}
|
|
if end == nil {
|
|
geo.TargetPoint = &MxPoint{X: pe.X, Y: pe.Y, As: "targetPoint"}
|
|
mxPoints = mxPoints[:len(mxPoints)-1]
|
|
} else {
|
|
if c.addConstraint(obj, end, false, orthogonal) {
|
|
// keep
|
|
}
|
|
}
|
|
|
|
if orthogonal {
|
|
cell.Style += "edgeStyle=orthogonalEdgeStyle;"
|
|
result := make([]*MxPoint, 0, len(mxPoints))
|
|
var prev *MxPoint
|
|
for _, p := range mxPoints {
|
|
if prev == nil || prev.X != p.X || prev.Y != p.Y {
|
|
result = append(result, p)
|
|
prev = p
|
|
}
|
|
}
|
|
if len(result) == 0 && ((start == nil && end != nil) || (start != nil && end == nil)) {
|
|
center := &MxPoint{X: p0.X + (pe.X-p0.X)/2, Y: p0.Y + (pe.Y-p0.Y)/2}
|
|
result = []*MxPoint{center, center}
|
|
}
|
|
mxPoints = result
|
|
}
|
|
|
|
if len(mxPoints) > 0 {
|
|
geo.Points = &MxPointArray{Points: mxPoints}
|
|
}
|
|
cell.Geometry = geo
|
|
}
|
|
|
|
type lightboxInfo struct {
|
|
Key int64
|
|
Value string
|
|
}
|
|
|
|
func (c *GliffyDiagramConverter) extractLightboxDataFromGliffyUrl(link string) *lightboxInfo {
|
|
pagem := c.pageIDPattern.FindStringSubmatch(link)
|
|
namem := c.namePattern.FindStringSubmatch(link)
|
|
if len(pagem) > 1 {
|
|
oldPageID, err := strconv.ParseInt(pagem[1], 10, 64)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
if len(namem) > 1 {
|
|
return &lightboxInfo{Key: oldPageID, Value: namem[1]}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getStyleValue(cell *MxCell, key, defaultValue string) string {
|
|
if cell == nil {
|
|
return defaultValue
|
|
}
|
|
style := cell.Style
|
|
if style == "" {
|
|
return defaultValue
|
|
}
|
|
pairs := strings.Split(style, ";")
|
|
for _, p := range pairs {
|
|
if p == "" {
|
|
continue
|
|
}
|
|
parts := strings.SplitN(p, "=", 2)
|
|
if len(parts) == 2 && strings.EqualFold(parts[0], key) {
|
|
return parts[1]
|
|
}
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
func sortObjectsByOrder(objs []*GliffyObject) {
|
|
sort.SliceStable(objs, func(i, j int) bool {
|
|
o1 := objs[i].Order.String()
|
|
o2 := objs[j].Order.String()
|
|
if o1 == "" && o2 == "" {
|
|
return false
|
|
}
|
|
if o1 == "" {
|
|
return false
|
|
}
|
|
if o2 == "" {
|
|
return true
|
|
}
|
|
f1, err1 := strconv.ParseFloat(o1, 64)
|
|
f2, err2 := strconv.ParseFloat(o2, 64)
|
|
if err1 == nil && err2 == nil {
|
|
return f1 < f2
|
|
}
|
|
return o1 < o2
|
|
})
|
|
}
|
|
|
|
func sortLayersByOrder(layers []*GliffyLayer) {
|
|
sort.SliceStable(layers, func(i, j int) bool {
|
|
return layers[i].Order < layers[j].Order
|
|
})
|
|
}
|
|
|
|
func reverse(objs []*GliffyObject) {
|
|
for i, j := 0, len(objs)-1; i < j; i, j = i+1, j-1 {
|
|
objs[i], objs[j] = objs[j], objs[i]
|
|
}
|
|
}
|
|
|
|
func boolToInt(v bool) int {
|
|
if v {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// Convenience entry point for users wanting a single call.
|
|
func ConvertGliffyJSONToDrawioXML(gliffyJSON string) (string, error) {
|
|
converter, err := NewGliffyDiagramConverter(gliffyJSON)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return converter.GraphXML()
|
|
}
|