// Copyright (c) 2012, 2015 Kiel University and others.
// Copyright (c) 2015, 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 core implements ELK metadata, JSON conversion, and recursive layout.
package core import ( ) type Targets uint8 const ( Parents Targets = 1 << iota Nodes Edges Ports Labels ) type OptionKind uint8 const ( StringOption OptionKind = iota BooleanOption IntegerOption DoubleOption EnumOption EnumSetOption ObjectOption UndefinedOption ) // OptionData describes one layout option and how its JSON spelling is parsed. type OptionData struct { ID string Group string Kind OptionKind Targets Targets // MetadataOnly marks UI or programmatic metadata that the headless JSON // layout engine preserves but does not interpret during layout. MetadataOnly bool LegacyIDs []string EnumValues []string Parse func(string) (any, error) Format func(any) string } func ( *OptionData) ( string) (any, bool) { if == "null" || ( == "" && .Kind != EnumSetOption) { return nil, false } if .Parse != nil { , := .Parse() return , == nil } switch .Kind { case StringOption: return , true case BooleanOption: if strings.EqualFold(, "true") { return true, true } if strings.EqualFold(, "false") { return false, true } case IntegerOption: , := strconv.ParseInt(strings.TrimSpace(), 10, 32) if == nil { return int(), true } case DoubleOption: := strings.TrimSpace() = strings.TrimSuffix(strings.TrimSuffix(strings.TrimSuffix(strings.TrimSuffix(, "d"), "D"), "f"), "F") , := strconv.ParseFloat(, 64) if == nil { return , true } case EnumOption: if , := parseEnum(, .EnumValues); { return , true } case EnumSetOption: := strings.FieldsFunc(, func( rune) bool { return strings.ContainsRune("[] ,\t\r\n", ) }) := make([]string, 0, len()) for , := range { , := parseEnum(, .EnumValues) if ! { return nil, false } = append(, ) } return , true case ObjectOption: // Programmatic metadata objects have no portable native Go type. Keep // their JSON spelling so the option is registered and preserved instead // of silently disappearing at the codec boundary. return , true } return nil, false } func ( *OptionData) ( any) string { if .Format != nil { return .Format() } if , := .([]string); { return "[" + strings.Join(, ", ") + "]" } return graph.JavaString() } func parseEnum( string, []string) (string, bool) { for , := range { if == { return , true } } , := strconv.Atoi() if == nil && >= 0 && < len() { return [], true } return "", false } // OptionRegistry is insertion ordered and performs ELK's unique dot-boundary // suffix lookup. type OptionRegistry struct { options []*OptionData byID map[string]*OptionData } func () *OptionRegistry { := &OptionRegistry{byID: make(map[string]*OptionData)} RegisterCoreOptions() return } func ( *OptionRegistry) ( OptionData) error { if .ID == "" { return fmt.Errorf("layout option id must not be empty") } if , := .byID[.ID]; { return fmt.Errorf("layout option %q is already registered", .ID) } := .LegacyIDs = append([]string(nil), .LegacyIDs...) .EnumValues = append([]string(nil), .EnumValues...) := & .options = append(.options, ) .byID[.ID] = for , := range .LegacyIDs { .byID[] = } return nil } func ( *OptionRegistry) ( string) *OptionData { return .byID[] } func ( *OptionRegistry) () []*OptionData { return append([]*OptionData(nil), .options...) } func ( *OptionRegistry) ( string) *OptionData { if == "" { return nil } if := .byID[]; != nil { return } var *OptionData for , := range .options { if dotSuffix(.ID, ) { if != nil && != { return nil } = } } if != nil { return } for , := range .options { for , := range .LegacyIDs { if dotSuffix(, ) { if != nil && != { return nil } = } } } return } func dotSuffix(, string) bool { if !strings.HasSuffix(, ) { return false } return len() == len() || [len()-len()-1] == '.' } type GraphFeature uint32 const ( FeatureSelfLoops GraphFeature = 1 << iota FeatureInsideSelfLoops FeatureMultiEdges FeatureEdgeLabels FeaturePorts FeatureCompound FeatureClusters FeatureDisconnected FeatureHyperedges ) func ( GraphFeature) ( GraphFeature) bool { return & == } // LayoutProvider lays out one parent node and its children. type LayoutProvider interface { Layout(model *graph.Model, parent graph.NodeID, monitor ProgressMonitor) error } type AlgorithmData struct { ID string Features GraphFeature Create func() LayoutProvider KnownOptions []string } type AlgorithmRegistry struct { algorithms []*AlgorithmData byID map[string]*AlgorithmData } func () *AlgorithmRegistry { return &AlgorithmRegistry{byID: make(map[string]*AlgorithmData)} } func ( *AlgorithmRegistry) ( AlgorithmData) error { if .ID == "" || .Create == nil { return fmt.Errorf("layout algorithm id and provider are required") } if , := .byID[.ID]; { return fmt.Errorf("layout algorithm %q is already registered", .ID) } := .KnownOptions = append([]string(nil), .KnownOptions...) := & .algorithms = append(.algorithms, ) .byID[.ID] = return nil } func ( *AlgorithmRegistry) ( string) *AlgorithmData { return .byID[] } func ( *AlgorithmRegistry) ( string) *AlgorithmData { if == "" { return nil } if := .byID[]; != nil { return } var *AlgorithmData for , := range .algorithms { if dotSuffix(.ID, ) { if != nil { return nil } = } } return } func ( *AlgorithmRegistry) () []*AlgorithmData { return append([]*AlgorithmData(nil), .algorithms...) } // ProgressMonitor is deliberately small; providers can report work while // callers that do not need progress use NullProgressMonitor. type ProgressMonitor interface { Begin(task string, totalWork float64) Worked(work float64) Done() Canceled() bool Subtask(work float64) ProgressMonitor } type nullProgressMonitor struct{} func (nullProgressMonitor) (string, float64) {} func (nullProgressMonitor) (float64) {} func (nullProgressMonitor) () {} func (nullProgressMonitor) () bool { return false } func ( nullProgressMonitor) (float64) ProgressMonitor { return } var NullProgressMonitor ProgressMonitor = nullProgressMonitor{}