// Copyright (c) 2008, 2020 Kiel University and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0.
//
// SPDX-License-Identifier: EPL-2.0

// Package elkutil ports the core ElkUtil operations shared by native layout // providers. The implementation intentionally preserves Java arithmetic and // mutation order where those are visible in JSON output.
package elkutil import ( ) const ( defaultMinWidth = 20.0 defaultMinHeight = 20.0 ) func contains( []string, string) bool { for , := range { if == { return true } } return false } // EffectiveMinSizeConstraintFor mirrors ElkUtil.effectiveMinSizeConstraintFor. func ( *graph.Model, graph.NodeID) geometry.Point { := .Node() if !contains(graph.Get(&.Properties, core.NodeSizeConstraints), "MINIMUM_SIZE") { return geometry.Point{} } := graph.Get(&.Properties, core.NodeSizeMinimum) if contains(graph.Get(&.Properties, basicoptions.NodeSizeOptions), "DEFAULT_MINIMUM_SIZE") { if .X <= 0 { .X = defaultMinWidth } if .Y <= 0 { .Y = defaultMinHeight } } return } // ResizeNodeForConstraints is ElkUtil.resizeNode(ElkNode). It reports false // when the node's size is fixed and no mutation is performed. func ( *graph.Model, graph.NodeID) bool { := .Node() := graph.Get(&.Properties, core.NodeSizeConstraints) if len() == 0 { return false } , := 0.0, 0.0 if contains(, "PORTS") { , , , := 2.0, 2.0, 2.0, 2.0 := graph.Get(&.Properties, core.PortConstraints) == "FIXED_POS" := applicableDirection(, ) for , := range .Ports { := .Port() := graph.Get(&.Properties, core.PortSide) if == "UNDEFINED" { = CalcPortSide(, , ) graph.Set(&.Properties, core.PortSide, ) } if { switch { case "NORTH": = math.Max(, .Shape.X+.Shape.Width) case "EAST": = math.Max(, .Shape.Y+.Shape.Height) case "SOUTH": = math.Max(, .Shape.X+.Shape.Width) case "WEST": = math.Max(, .Shape.Y+.Shape.Height) } } else { switch { case "NORTH": += .Shape.Width + 2 case "EAST": += .Shape.Height + 2 case "SOUTH": += .Shape.Width + 2 case "WEST": += .Shape.Height + 2 } } } = math.Max(, ) = math.Max(, ) } ResizeNode(, , , , true, true) return true } // ResizeNode changes a node's dimensions and moves fixed peripheral content // exactly as ElkUtil.resizeNode does. func ( *graph.Model, graph.NodeID, , float64, , bool) geometry.Point { := .Node() := geometry.Point{X: .Shape.Width, Y: .Shape.Height} := EffectiveMinSizeConstraintFor(, ) .X = math.Max(.X, ) .Y = math.Max(.Y, ) := .X / .X := .Y / .Y := .X - .X := .Y - .Y if { := applicableDirection(, ) := graph.Get(&.Properties, core.PortConstraints) == "FIXED_POS" for , := range .Ports { := .Port() := graph.Get(&.Properties, core.PortSide) if == "UNDEFINED" { = CalcPortSide(, , ) graph.Set(&.Properties, core.PortSide, ) } switch { case "NORTH": if ! { .Shape.X *= } case "EAST": .Shape.X += if ! { .Shape.Y *= } case "SOUTH": if ! { .Shape.X *= } .Shape.Y += case "WEST": if ! { .Shape.Y *= } } .Shape.Presence |= graph.HasX | graph.HasY } } .Shape.SetDimensions(.X, .Y) if { for , := range .Labels { := .Label() := .Shape.X + .Shape.Width/2 := .Shape.Y + .Shape.Height/2 := / .X := / .Y if + >= 1 { if - > 0 && >= 0 { .Shape.X += .Shape.Y += * } else if - < 0 && >= 0 { .Shape.X += * .Shape.Y += } .Shape.Presence |= graph.HasX | graph.HasY } } } graph.Set(&.Properties, core.NodeSizeConstraints, []string{}) return geometry.Point{X: , Y: } } func applicableDirection( *graph.Model, graph.NodeID) string { := .Node() if .Parent == nil { return graph.Get(&.Properties, core.Direction) } return graph.Get(&.Node(*.Parent).Properties, core.Direction) } // CalcPortSide mirrors ElkUtil.calcPortSide. func ( *graph.Model, graph.PortID, string) string { := .Port() := .Node(.Parent) if .Shape.Width <= 0 && .Shape.Height <= 0 { return "UNDEFINED" } switch { case "LEFT", "RIGHT": if .Shape.X < 0 { return "WEST" } if .Shape.X+.Shape.Width > .Shape.Width { return "EAST" } case "UP", "DOWN": if .Shape.Y < 0 { return "NORTH" } if .Shape.Y+.Shape.Height > .Shape.Height { return "SOUTH" } } := (.Shape.X + .Shape.Width/2) / .Shape.Width := (.Shape.Y + .Shape.Height/2) / .Shape.Height switch { case + <= 1 && - <= 0: return "WEST" case + >= 1 && - >= 0: return "EAST" case < 0.5: return "NORTH" default: return "SOUTH" } } // Translate moves the contents and contained edges of parent. func ( *graph.Model, graph.NodeID, , float64) { := .Node() for , := range .Children { := .Node() .Shape.X += .Shape.Y += .Shape.Presence |= graph.HasX | graph.HasY } for , := range .ContainedEdges { TranslateEdge(, , , ) } } func ( *graph.Model, graph.EdgeID, , float64) { := .Edge() for , := range .Sections { := .Section() .Start.X += .Start.Y += .End.X += .End.Y += .BendPoints.Offset(geometry.Point{X: , Y: }) .StartPresent, .EndPresent = true, true } for , := range .Labels { := .Label() .Shape.X += .Shape.Y += .Shape.Presence |= graph.HasX | graph.HasY } if , := graph.TryGet(&.Properties, core.JunctionPoints); { .Offset(geometry.Point{X: , Y: }) graph.Set(&.Properties, core.JunctionPoints, ) } } // TranslateAligned applies CoreOptions.CONTENT_ALIGNMENT after growth. func ( *graph.Model, graph.NodeID, , geometry.Point) { := graph.Get(&.Node().Properties, core.ContentAlignment) , := 0.0, 0.0 if .X > .X { if contains(, "H_CENTER") { = (.X - .X) / 2 } else if contains(, "H_RIGHT") { = .X - .X } } if .Y > .Y { if contains(, "V_CENTER") { = (.Y - .Y) / 2 } else if contains(, "V_BOTTOM") { = .Y - .Y } } Translate(, , , ) } func ( *graph.Model, graph.NodeID) []graph.EdgeID { := .Node() := append([]graph.EdgeID(nil), .OutgoingEdges...) for , := range .Ports { = append(, .Port().OutgoingEdges...) } return } func ( *graph.Model, graph.NodeID) []graph.EdgeID { := .Node() := append([]graph.EdgeID(nil), .IncomingEdges...) for , := range .Ports { = append(, .Port().IncomingEdges...) } return } func ( *graph.Model, graph.EdgeID) bool { := .Edge() var *graph.NodeID := false for , := range append(append([]graph.ShapeID(nil), .Sources...), .Targets...) { := .ShapeNode() := .Node().Parent if ! { = true if != nil { := * = & } continue } if ( == nil) != ( == nil) || != nil && * != * { return true } } return false } // ApplyVectorChain writes a fixed route to one edge section. func ( *graph.Model, graph.SectionID, geometry.Chain) { := .Section() .Start = [0] .End = [len()-1] .BendPoints = append(.BendPoints[:0], [1:len()-1]...) .StartPresent, .EndPresent = true, true .BendPointsPresent = len(.BendPoints) > 0 } func sectionPoints( *graph.Model, graph.SectionID) geometry.Chain { := .Section() := make(geometry.Chain, 0, len(.BendPoints)+2) = append(, .Start) = append(, .BendPoints...) = append(, .End) for := 1; < len()-1; { , , := [-1], [], [+1] if .X == .X && .X == .X || .Y == .Y && .Y == .Y { copy([:], [+1:]) = [:len()-1] } else { ++ } } return } // DetermineJunctionPoints implements ElkUtil.determineJunctionPoints. func ( *graph.Model, graph.EdgeID) (geometry.Chain, error) { := .Edge() if len(.Sections) != 1 { return nil, fmt.Errorf("edge needs exactly one edge section, found %d", len(.Sections)) } var geometry.Chain if .Sources[0].Kind == graph.PortShape { , := junctionPointsAtPort(, , .Sources[0].Port, false) if != nil { return nil, } = append(, ...) } if .Targets[0].Kind == graph.PortShape { , := junctionPointsAtPort(, , .Targets[0].Port, true) if != nil { return nil, } = append(, ...) } return , nil } type connectedSection struct { points geometry.Chain offset geometry.Point } func junctionPointsAtPort( *graph.Model, graph.EdgeID, graph.PortID, bool) (geometry.Chain, error) { := sectionPoints(, .Edge().Sections[0]) := .Port() := append(append([]graph.EdgeID(nil), .IncomingEdges...), .OutgoingEdges...) := make([]connectedSection, 0, len()) for , := range { if == { continue } := .Edge() if len(.Sections) != 1 { return nil, fmt.Errorf("connected edge needs exactly one edge section, found %d", len(.Sections)) } := sectionPoints(, .Sections[0]) var geometry.Point if { = [len()-1].Sub([len()-1]) } else { = [0].Sub([0]) } = append(, connectedSection{points: , offset: }) } if len() == 0 { return nil, nil } := func(, int) int { if { return - 1 - } return } := [(0, len())] var geometry.Chain for := 1; < len(); ++ { := [(, len())] := [:0] for , := range { if len(.points) <= { continue } := .points[(, len(.points))].Add(.offset) if != { , := .X-.X, .Y-.Y , := .X-.X, .Y-.Y if * == * && javaSignum() == javaSignum() && javaSignum() == javaSignum() { if math.Abs() < math.Abs() || math.Abs() < math.Abs() { = append(, ) } } else if > 1 { = append(, ) } continue } = append(, ) } = = } return , nil } func javaSignum( float64) float64 { if == 0 || math.IsNaN() { return } if > 0 { return 1 } return -1 }