ea 0.6.136 → 0.6.138
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/ea/fonts/cambria_7_regular_widths.json +1 -0
- data/lib/ea/model/diagram_connector.rb +4 -0
- data/lib/ea/sources/xmi/diagram_builder/association_class_metadata.rb +127 -0
- data/lib/ea/sources/xmi/diagram_builder/umldi_edges/property_roles.rb +71 -0
- data/lib/ea/sources/xmi/diagram_builder/umldi_edges.rb +56 -0
- data/lib/ea/sources/xmi/diagram_builder.rb +203 -14
- data/lib/ea/sources/xmi/extension_elements.rb +5 -4
- data/lib/ea/sources/xmi/relationship_builder.rb +20 -2
- data/lib/ea/sources/xmi/stored_route.rb +194 -1
- data/lib/ea/svg/ea_emitter/compartment/enum_literals.rb +3 -2
- data/lib/ea/svg/ea_emitter/connector_redock.rb +2 -1
- data/lib/ea/svg/ea_emitter/element/tagged_value_renderer.rb +10 -5
- data/lib/ea/svg/ea_emitter/label/end_label.rb +119 -20
- data/lib/ea/svg/ea_emitter/labels.rb +15 -2
- data/lib/ea/svg/ea_emitter/render_context.rb +7 -2
- data/lib/ea/theme/definition.rb +2 -2
- data/lib/ea/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0c2231b8d009e8d99c841920776cbe8539e09b76d512beed109f073fb7904282
|
|
4
|
+
data.tar.gz: 3feade927368b299842047c9c0e31aa0cb8473e7dad6870e5626fb369e1503de
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 025f36b456316a6fcd801318f17c27363d62d3ee7355cd303c1b7147081f44d3b70fdf711966ae4a5a7fc33e022dc097ba4d6f15f6c3ad03e237d59318b89fac
|
|
7
|
+
data.tar.gz: d2487edce57559735c9cdf42984dd16ad3597985fb5d2f01ddeb9289c893b423a986f8587b3788d0cd759aaca93661c8e0a86262a4939f503d21e9495e29db9f
|
|
@@ -38,6 +38,9 @@ module Ea
|
|
|
38
38
|
attribute :hidden, :boolean
|
|
39
39
|
attribute :is_substitutable, :boolean, default: false
|
|
40
40
|
attribute :label_boxes, :hash, default: -> { {} }
|
|
41
|
+
# Absolute labels saved by UML-DI when the EA extension omits the
|
|
42
|
+
# corresponding relative label boxes.
|
|
43
|
+
attribute :umldi_label_bounds, :hash, default: -> { {} }
|
|
41
44
|
attribute :ghost_labels, GhostLabel, collection: true, initialize_empty: true
|
|
42
45
|
# True when the t_diagramlinks.Geometry string carries explicit
|
|
43
46
|
# SX/SY/EX/EY offset fields. EA renders the «import» package_anchor
|
|
@@ -86,6 +89,7 @@ module Ea
|
|
|
86
89
|
map "lineWidth", to: :line_width
|
|
87
90
|
map "hidden", to: :hidden, render_default: true
|
|
88
91
|
map "labelBoxes", to: :label_boxes
|
|
92
|
+
map "umldiLabelBounds", to: :umldi_label_bounds
|
|
89
93
|
map "ghostLabels", to: :ghost_labels, render_empty: true
|
|
90
94
|
map "hasGeometryOffsets", to: :has_geometry_offsets, render_default: true
|
|
91
95
|
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
|
|
5
|
+
module Ea
|
|
6
|
+
module Sources
|
|
7
|
+
module Xmi
|
|
8
|
+
class DiagramBuilder
|
|
9
|
+
# EA stores the association-class reference, connector subtype and proxy
|
|
10
|
+
# classifiers in extension attributes omitted by the XMI model gem.
|
|
11
|
+
class AssociationClassMetadata
|
|
12
|
+
def initialize(xmi_path)
|
|
13
|
+
@xmi_path = xmi_path
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def for(connector, placed_subjects:, waypoints:)
|
|
17
|
+
return unless valid_tether?(connector, waypoints)
|
|
18
|
+
|
|
19
|
+
attached_id = connector_data.dig(connector.idref, :association_class)
|
|
20
|
+
return unless attached_id && class_ids.include?(attached_id)
|
|
21
|
+
return unless placed_subjects.any? { |subject| guid_key(subject) == guid_key(attached_id) }
|
|
22
|
+
|
|
23
|
+
attached_id
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def valid_tether?(connector, waypoints)
|
|
29
|
+
@xmi_path && connector && association_class?(connector) &&
|
|
30
|
+
proxy_pair?(connector) && coincident_route?(waypoints)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def association_class?(connector)
|
|
34
|
+
connector.properties&.ea_type == "Association" &&
|
|
35
|
+
connector_data.dig(connector.idref, :subtype) == "Class" &&
|
|
36
|
+
connector_data.dig(connector.idref, :private_data).to_s.match?(/\A[1-9]\d*\z/)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def proxy_pair?(connector)
|
|
40
|
+
endpoints = [connector.source, connector.target]
|
|
41
|
+
return false unless proxy_endpoints?(endpoints)
|
|
42
|
+
|
|
43
|
+
classifiers = endpoints.map { |endpoint| proxy_classifiers[endpoint.idref] }
|
|
44
|
+
matching_classifiers?(classifiers)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def proxy_endpoints?(endpoints)
|
|
48
|
+
endpoints.all? { |endpoint| endpoint&.model&.type == "ProxyConnector" }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def matching_classifiers?(classifiers)
|
|
52
|
+
classifiers.none? { |classifier| classifier.to_s.empty? } && classifiers.uniq.one?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def coincident_route?(waypoints)
|
|
56
|
+
first = waypoints&.first&.position
|
|
57
|
+
last = waypoints&.last&.position
|
|
58
|
+
first && last && first.x == last.x && first.y == last.y
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def connector_data
|
|
62
|
+
@connector_data ||= read_metadata.fetch(:connectors)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def proxy_classifiers
|
|
66
|
+
@proxy_classifiers ||= read_metadata.fetch(:proxies)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def class_ids
|
|
70
|
+
@class_ids ||= read_metadata.fetch(:classes)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def read_metadata
|
|
74
|
+
@read_metadata ||= scan_metadata
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def scan_metadata
|
|
78
|
+
data = { connectors: {}, proxies: {}, classes: [] }
|
|
79
|
+
File.open(@xmi_path) do |file|
|
|
80
|
+
Nokogiri::XML::Reader(file).each { |node| read_node(node, data) }
|
|
81
|
+
end
|
|
82
|
+
data
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def read_node(node, data)
|
|
86
|
+
return unless node.node_type == Nokogiri::XML::Reader::TYPE_ELEMENT
|
|
87
|
+
|
|
88
|
+
read_element(node, data) if node.local_name == "element"
|
|
89
|
+
read_connector(node, data) if node.local_name == "connector"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def read_element(node, data)
|
|
93
|
+
element = Nokogiri::XML(node.outer_xml).root
|
|
94
|
+
id = attr(element, "idref")
|
|
95
|
+
return unless id
|
|
96
|
+
|
|
97
|
+
type = attr(element, "type")
|
|
98
|
+
data[:classes] << id if type == "uml:Class"
|
|
99
|
+
data[:proxies][id] = attr(element, "classifier") if type == "uml:ProxyConnector"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def read_connector(node, data)
|
|
103
|
+
connector = Nokogiri::XML(node.outer_xml).root
|
|
104
|
+
id = attr(connector, "idref")
|
|
105
|
+
return unless id
|
|
106
|
+
|
|
107
|
+
extended = connector.at_xpath("./*[local-name()='extendedProperties']")
|
|
108
|
+
properties = connector.at_xpath("./*[local-name()='properties']")
|
|
109
|
+
data[:connectors][id] = {
|
|
110
|
+
subtype: attr(properties, "subtype"),
|
|
111
|
+
association_class: attr(extended, "associationclass"),
|
|
112
|
+
private_data: attr(extended, "privatedata1")
|
|
113
|
+
}
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def attr(node, name)
|
|
117
|
+
node&.attribute_nodes&.find { |attribute| attribute.name == name }&.value
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def guid_key(subject)
|
|
121
|
+
subject&.sub(/\AEA(?:ID|PK)_/, "")&.upcase
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Sources
|
|
5
|
+
module Xmi
|
|
6
|
+
class DiagramBuilder
|
|
7
|
+
class UmldiEdges
|
|
8
|
+
# Association membership proves which saved role bounds an edge owns.
|
|
9
|
+
class PropertyRoles
|
|
10
|
+
def initialize(doc)
|
|
11
|
+
@doc = doc
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def keys_for(diagram_id, connector_id)
|
|
15
|
+
property_roles.dig(diagram_id, connector_id) || []
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def property_roles
|
|
21
|
+
@property_roles ||= begin
|
|
22
|
+
document = @doc
|
|
23
|
+
declared = declared_properties(document)
|
|
24
|
+
document.xpath("//*[local-name()=\"Diagram\"]").to_h do |diagram|
|
|
25
|
+
[diagram["id"] || diagram["xmi:id"], diagram_property_roles(diagram, declared)]
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def declared_properties(document)
|
|
31
|
+
members = document.xpath("//*[local-name()='memberEnd']").to_set do |node|
|
|
32
|
+
[node["xmi:idref"], node.parent["xmi:id"]]
|
|
33
|
+
end
|
|
34
|
+
document.xpath("//*[@association][@xmi:id]", "xmi" => "http://www.omg.org/spec/XMI/20131001")
|
|
35
|
+
.filter_map do |node|
|
|
36
|
+
pair = [node["xmi:id"], node["association"]]
|
|
37
|
+
pair if node.attributes["type"]&.value == "uml:Property" && members.include?(pair)
|
|
38
|
+
end.to_set
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def diagram_property_roles(diagram, declared)
|
|
42
|
+
unique_edges(diagram).transform_values do |edge|
|
|
43
|
+
edge.xpath("./ownedElement[@modelElement]").filter_map do |label|
|
|
44
|
+
key = role_key(label)
|
|
45
|
+
key if key &&
|
|
46
|
+
declared.include?([label["modelElement"], edge["modelElement"]])
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def unique_edges(diagram)
|
|
52
|
+
diagram.xpath("./ownedElement[@modelElement]")
|
|
53
|
+
.select { |node| node.attributes["type"]&.value.to_s.end_with?("UMLEdge") }
|
|
54
|
+
.group_by { |edge| edge["modelElement"] }
|
|
55
|
+
.select { |_, edges| edges.one? }.transform_values(&:first)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def role_key(label)
|
|
59
|
+
return unless label.attributes["type"]&.value.to_s.end_with?("UMLAssociationEndLabel")
|
|
60
|
+
|
|
61
|
+
case label["xmi:id"] || label["id"]
|
|
62
|
+
when /\AEAID_SEL/ then :source_role
|
|
63
|
+
when /\AEAID_TEL/ then :target_role
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -11,6 +11,8 @@ module Ea
|
|
|
11
11
|
# drawn by more than one edge in the same diagram has neither; an edge
|
|
12
12
|
# with fewer than two waypoints has no route.
|
|
13
13
|
class UmldiEdges
|
|
14
|
+
autoload :PropertyRoles, "ea/sources/xmi/diagram_builder/umldi_edges/property_roles"
|
|
15
|
+
|
|
14
16
|
def initialize(xmi_path)
|
|
15
17
|
@xmi_path = xmi_path
|
|
16
18
|
end
|
|
@@ -23,6 +25,20 @@ module Ea
|
|
|
23
25
|
ends.dig(diagram_id, connector_id)
|
|
24
26
|
end
|
|
25
27
|
|
|
28
|
+
# Absolute saved UMLDI bounds for connector label boxes. These are
|
|
29
|
+
# kept separate from extension label offsets: callers may use them
|
|
30
|
+
# when extension geometry is absent or the association owns the role.
|
|
31
|
+
def label_bounds_for(diagram_id, connector_id)
|
|
32
|
+
label_bounds.dig(diagram_id, connector_id)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Role label keys whose UMLDI label is bound to the property that
|
|
36
|
+
# the connector's association declares, so the bounds describe that
|
|
37
|
+
# end's own role label rather than an unrelated label.
|
|
38
|
+
def property_role_keys_for(diagram_id, connector_id)
|
|
39
|
+
(@property_roles ||= PropertyRoles.new(doc)).keys_for(diagram_id, connector_id)
|
|
40
|
+
end
|
|
41
|
+
|
|
26
42
|
private
|
|
27
43
|
|
|
28
44
|
def ends
|
|
@@ -31,6 +47,46 @@ module Ea
|
|
|
31
47
|
end
|
|
32
48
|
end
|
|
33
49
|
|
|
50
|
+
def label_bounds
|
|
51
|
+
@label_bounds ||= doc.xpath("//*[local-name()=\"Diagram\"]").to_h do |diagram|
|
|
52
|
+
[diagram["id"] || diagram["xmi:id"], diagram_label_bounds(diagram)]
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def diagram_label_bounds(diagram)
|
|
57
|
+
boxes_by_connector = unique_edges(diagram).to_h do |connector_id, edge|
|
|
58
|
+
[connector_id, edge_label_bounds(edge)]
|
|
59
|
+
end
|
|
60
|
+
boxes_by_connector.reject { |_, boxes| boxes.empty? }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def edge_label_bounds(edge)
|
|
64
|
+
edge.xpath("./ownedElement[@modelElement]").each_with_object({}) do |label, found|
|
|
65
|
+
bounds = label.at_xpath("./*[local-name()=\"bounds\"]")
|
|
66
|
+
key = label_key(label)
|
|
67
|
+
next unless key && bounds
|
|
68
|
+
|
|
69
|
+
found[key] = bounds_values(bounds)
|
|
70
|
+
rescue ArgumentError, TypeError
|
|
71
|
+
next
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def bounds_values(bounds)
|
|
76
|
+
%w[x y width height].to_h { |attribute| [attribute.to_sym, Integer(bounds[attribute])] }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def label_key(label)
|
|
80
|
+
type = label.attributes["type"]&.value.to_s
|
|
81
|
+
role = (label["id"] || label["xmi:id"]).to_s
|
|
82
|
+
end_name = { "EAID_SEL" => "source", "EAID_SML" => "source",
|
|
83
|
+
"EAID_TEL" => "target", "EAID_TML" => "target" }
|
|
84
|
+
side = end_name.find { |prefix, _| role.start_with?(prefix) }&.last
|
|
85
|
+
suffix = { "UMLAssociationEndLabel" => "role", "UMLMultiplicityLabel" => "multiplicity" }
|
|
86
|
+
label_kind = suffix[type.split(":").last]
|
|
87
|
+
"#{side}_#{label_kind}".to_sym if side && label_kind
|
|
88
|
+
end
|
|
89
|
+
|
|
34
90
|
def diagram_ends(diagram)
|
|
35
91
|
unique_edges(diagram).each_with_object({}) do |(connector_id, edge), acc|
|
|
36
92
|
acc[connector_id] = [edge["source"], edge["target"]] if edge["source"] && edge["target"]
|
|
@@ -10,6 +10,7 @@ module Ea
|
|
|
10
10
|
# the UML-DI owned_element bounds otherwise.
|
|
11
11
|
class DiagramBuilder
|
|
12
12
|
autoload :ConnectorMetadata, "ea/sources/xmi/diagram_builder/connector_metadata"
|
|
13
|
+
autoload :AssociationClassMetadata, "ea/sources/xmi/diagram_builder/association_class_metadata"
|
|
13
14
|
autoload :FontSettings, "ea/sources/xmi/diagram_builder/font_settings"
|
|
14
15
|
autoload :StereotypeProfiles, "ea/sources/xmi/diagram_builder/stereotype_profiles"
|
|
15
16
|
autoload :UmldiEdges, "ea/sources/xmi/diagram_builder/umldi_edges"
|
|
@@ -44,11 +45,12 @@ module Ea
|
|
|
44
45
|
|
|
45
46
|
def build_one(ext_diagram)
|
|
46
47
|
id = IdNormalizer.from_xmi_id(ext_diagram.id)
|
|
47
|
-
ext_elements = ExtensionElements.new(ext_diagram)
|
|
48
|
+
ext_elements = ExtensionElements.new(ext_diagram, connector_subjects: connector_index)
|
|
48
49
|
placed_elements = ext_elements.placed_elements
|
|
49
50
|
placed_connectors = ext_elements.placed_connectors
|
|
50
51
|
props = ext_diagram.properties
|
|
51
52
|
umldi_keywords = keywords_for(id)
|
|
53
|
+
context = context_diagram?(ext_diagram)
|
|
52
54
|
|
|
53
55
|
Ea::Model::Diagram.new(
|
|
54
56
|
id: id,
|
|
@@ -64,8 +66,7 @@ module Ea
|
|
|
64
66
|
show_parents: !style_flag?(ext_diagram, "HideParents"),
|
|
65
67
|
hand_draw: style_flag?(ext_diagram, "HandDraw"),
|
|
66
68
|
elements: build_elements(placed_elements, id, umldi_keywords),
|
|
67
|
-
connectors: build_connectors(placed_connectors,
|
|
68
|
-
placed_elements, id),
|
|
69
|
+
connectors: build_connectors(placed_connectors, placed_elements, id, context),
|
|
69
70
|
annotations: []
|
|
70
71
|
)
|
|
71
72
|
end
|
|
@@ -101,6 +102,13 @@ module Ea
|
|
|
101
102
|
value.to_i.nonzero? ? true : false
|
|
102
103
|
end
|
|
103
104
|
|
|
105
|
+
def context_diagram?(ext_diagram)
|
|
106
|
+
name = ext_diagram.properties&.name.to_s
|
|
107
|
+
return true if name.match?(/\AContext Diagram: Package (Clients|Suppliers) of /i)
|
|
108
|
+
|
|
109
|
+
style_ex_for(ext_diagram).to_s.match(/(?:\A|;)SuppressFOC=(\d)/)&.public_send(:[], 1) != "1"
|
|
110
|
+
end
|
|
111
|
+
|
|
104
112
|
def page_bounds(ext_diagram)
|
|
105
113
|
style = style_for(ext_diagram).to_s
|
|
106
114
|
width = style[/(?:\A|;)DocSize\.cx=(\d+)/, 1]
|
|
@@ -130,6 +138,7 @@ module Ea
|
|
|
130
138
|
line_width: style.line_width,
|
|
131
139
|
font_family: font[:family],
|
|
132
140
|
font_size: font[:size],
|
|
141
|
+
font_size_unit: "pt",
|
|
133
142
|
font_logical_height: font[:logical_height],
|
|
134
143
|
font_size_inherited: ExtensionStyleParser.split_pairs(style.raw.to_s)["fontsz"] == "0",
|
|
135
144
|
font_bold: style.bold,
|
|
@@ -140,10 +149,32 @@ module Ea
|
|
|
140
149
|
z_order: placed.seqno,
|
|
141
150
|
duid: style.duid,
|
|
142
151
|
umldi_keyword: umldi_keywords[placed.subject],
|
|
143
|
-
style: (
|
|
152
|
+
style: element_style(placed.subject)
|
|
144
153
|
)
|
|
145
154
|
end
|
|
146
155
|
|
|
156
|
+
# XMI preserves UML member order in ownedAttribute. Carry that
|
|
157
|
+
# provenance to the renderer so it does not replace it with a name sort.
|
|
158
|
+
def element_style(subject)
|
|
159
|
+
style = (@stereotype_profiles ||= StereotypeProfiles.new(root)).for(subject)
|
|
160
|
+
return style unless source_ordered_classifiers.include?(subject)
|
|
161
|
+
|
|
162
|
+
style.merge(stored_attribute_order: true)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def source_ordered_classifiers
|
|
166
|
+
@source_ordered_classifiers ||= begin
|
|
167
|
+
ids = []
|
|
168
|
+
visit = lambda do |element|
|
|
169
|
+
ids << element.id if TypeClassifierMap.classifier_type?(element.type)
|
|
170
|
+
element.packaged_element.to_a.each { |child| visit.call(child) }
|
|
171
|
+
element.nested_classifier.to_a.each { |child| visit.call(child) }
|
|
172
|
+
end
|
|
173
|
+
root.model&.packaged_element.to_a.each { |element| visit.call(element) }
|
|
174
|
+
ids
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
147
178
|
def build_bounds(left, top, right, bottom)
|
|
148
179
|
return nil unless left && top && right && bottom
|
|
149
180
|
|
|
@@ -152,7 +183,7 @@ module Ea
|
|
|
152
183
|
height: bottom - top)
|
|
153
184
|
end
|
|
154
185
|
|
|
155
|
-
def build_connectors(placed_connectors, placed_elements, owner_id)
|
|
186
|
+
def build_connectors(placed_connectors, placed_elements, owner_id, context)
|
|
156
187
|
# Index elements by DUID so we can resolve SOID/EOID.
|
|
157
188
|
by_duid = placed_elements.each_with_object({}) do |pe, acc|
|
|
158
189
|
acc[pe.style.duid] = pe if pe.style.duid
|
|
@@ -160,23 +191,46 @@ module Ea
|
|
|
160
191
|
by_subject = placed_elements.group_by { |pe| guid_key(pe.subject) }
|
|
161
192
|
|
|
162
193
|
placed_connectors.each_with_index.map do |placed, index|
|
|
163
|
-
build_connector(placed, by_duid, by_subject, owner_id, index)
|
|
194
|
+
connector = build_connector(placed, by_duid, by_subject, owner_id, rank: index, context: context)
|
|
195
|
+
mark_association_class(connector, placed, placed_elements, context)
|
|
164
196
|
end
|
|
165
197
|
end
|
|
166
198
|
|
|
167
|
-
def
|
|
199
|
+
def mark_association_class(connector, placed, placed_elements, context)
|
|
200
|
+
mark_plain_end_seats(connector, placed, context)
|
|
201
|
+
reference = association_class_metadata.for(
|
|
202
|
+
connector_index[placed.subject],
|
|
203
|
+
placed_subjects: placed_elements.map(&:subject),
|
|
204
|
+
waypoints: connector.waypoints
|
|
205
|
+
)
|
|
206
|
+
return connector unless reference
|
|
207
|
+
|
|
208
|
+
connector.style[:association_class_ref] = reference
|
|
209
|
+
connector.source_anchor = connector.waypoints.first.position
|
|
210
|
+
connector
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def mark_plain_end_seats(connector, placed, context)
|
|
214
|
+
mark_plain_source_seat(connector, placed, context)
|
|
215
|
+
mark_plain_target_seat(connector, placed, context)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def association_class_metadata
|
|
219
|
+
@association_class_metadata ||= AssociationClassMetadata.new(xmi_path)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def build_connector(placed, by_duid, by_subject, owner_id, options)
|
|
168
223
|
geom = placed.geometry
|
|
169
224
|
style = placed.style
|
|
170
|
-
edge_source, edge_target =
|
|
171
|
-
source =
|
|
172
|
-
target = by_duid[style.eoid] || sole_placement(by_subject, edge_target)
|
|
225
|
+
edge_source, edge_target = connector_ends(owner_id, placed)
|
|
226
|
+
source, target = placed_ends(style, by_duid, by_subject, edge_source, edge_target)
|
|
173
227
|
ext_meta = connector_meta(placed.subject)
|
|
174
228
|
route = connector_route(placed, source, target, owner_id, ext_meta[:ea_type])
|
|
175
|
-
Ea::Model::DiagramConnector.new(
|
|
229
|
+
connector = Ea::Model::DiagramConnector.new(
|
|
176
230
|
id: IdNormalizer.synthetic_id(owner_id, "conn", placed.subject),
|
|
177
231
|
diagram_id: owner_id,
|
|
178
232
|
relationship_ref: placed.subject,
|
|
179
|
-
paint_order: [rank],
|
|
233
|
+
paint_order: [options.fetch(:rank)],
|
|
180
234
|
source_duid: style.soid,
|
|
181
235
|
target_duid: style.eoid,
|
|
182
236
|
source_element_ref: source && synthetic_element_id(owner_id, source),
|
|
@@ -193,8 +247,111 @@ module Ea
|
|
|
193
247
|
line_color: style.color,
|
|
194
248
|
line_width: style.line_width,
|
|
195
249
|
hidden: style.hidden,
|
|
196
|
-
label_boxes:
|
|
250
|
+
label_boxes: connector_label_boxes(placed, ext_meta[:ea_type],
|
|
251
|
+
options.fetch(:context) ? nil : owner_id)
|
|
197
252
|
)
|
|
253
|
+
connector.umldi_label_bounds = if options.fetch(:context)
|
|
254
|
+
{}
|
|
255
|
+
else
|
|
256
|
+
umldi_label_bounds_for(owner_id, placed.subject,
|
|
257
|
+
connector.label_boxes)
|
|
258
|
+
end
|
|
259
|
+
mark_stored_ray(connector, placed, source, target) unless options.fetch(:context)
|
|
260
|
+
mark_association_route(connector, placed, source, target) unless options.fetch(:context)
|
|
261
|
+
connector
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def umldi_label_bounds_for(owner_id, connector_id, label_boxes)
|
|
265
|
+
return {} unless xmi_path
|
|
266
|
+
|
|
267
|
+
bounds = umldi_edges.label_bounds_for(owner_id, connector_id)
|
|
268
|
+
return {} unless bounds
|
|
269
|
+
|
|
270
|
+
property_roles = umldi_edges.property_role_keys_for(owner_id, connector_id)
|
|
271
|
+
bounds.reject do |key, _|
|
|
272
|
+
extension_key = { source_role: :llt, source_multiplicity: :llb,
|
|
273
|
+
target_role: :lrt, target_multiplicity: :lrb }.fetch(key)
|
|
274
|
+
label_boxes.key?(extension_key) && !property_roles.include?(key)
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def mark_stored_ray(connector, placed, source, target)
|
|
279
|
+
return if connector.style[:umldi_route]
|
|
280
|
+
|
|
281
|
+
metadata = stored_ray_metadata(connector, placed, source, target)
|
|
282
|
+
metadata.each { |name, value| connector.public_send("#{name}=", value) }
|
|
283
|
+
connector.style[:association_ray_outline] = true unless metadata.empty?
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def stored_ray_metadata(connector, placed, source, target, stage: :ray_metadata)
|
|
287
|
+
geom = placed.geometry
|
|
288
|
+
link = StoredRoute::Link.new(placed.style.raw, geom.path.to_s.tr("$", ";"))
|
|
289
|
+
bounds = { source: bounds_from_placed(source), target: bounds_from_placed(target) }
|
|
290
|
+
offsets = { source: [geom.sx, geom.sy], target: [geom.ex, geom.ey] }
|
|
291
|
+
StoredRoute.new.public_send(stage, connector, link: link, bounds: bounds, offsets: offsets, edge: geom.edge)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def mark_association_route(connector, placed, source, target)
|
|
295
|
+
metadata = stored_ray_metadata(connector, placed, source, target, stage: :association_route)
|
|
296
|
+
metadata.each { |name, value| connector.public_send("#{name}=", value) }
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def mark_plain_source_seat(connector, placed, context)
|
|
300
|
+
return if context || !placed.geometry.path.to_s.empty? || connector.connector_type != "Aggregation"
|
|
301
|
+
return unless connector.style.to_h.values_at(:mode, :lwidth, :tree) == ["3", "2", nil]
|
|
302
|
+
return unless connector.source_stereotype.to_s.empty?
|
|
303
|
+
|
|
304
|
+
connector.style[:plain_source_seat] = true
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def mark_plain_target_seat(connector, placed, context)
|
|
308
|
+
return if context || !placed.geometry.path.to_s.empty? || connector.connector_type != "Association"
|
|
309
|
+
return unless connector.style.to_h.values_at(:mode, :lwidth, :tree) == ["3", "2", nil]
|
|
310
|
+
return unless connector.target_stereotype.to_s.empty?
|
|
311
|
+
|
|
312
|
+
connector.style[:plain_target_seat] = true
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
PHANTOM_TYPES = %w[Association Aggregation].freeze
|
|
316
|
+
|
|
317
|
+
# EA exports a model relationship drawn without a diagram link row as
|
|
318
|
+
# an Association or Aggregation link with the default style and an
|
|
319
|
+
# all-zero, EDGE-less, bendless geometry; the QEA source builds it as
|
|
320
|
+
# a phantom connector.
|
|
321
|
+
def phantom_link?(placed, type)
|
|
322
|
+
PHANTOM_TYPES.include?(type) && placed.style.raw == ";Hidden=0;" && default_geometry?(placed.geometry)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def default_geometry?(geom)
|
|
326
|
+
geom.edge.nil? && [geom.sx, geom.sy, geom.ex, geom.ey].all?(&:zero?) &&
|
|
327
|
+
geom.path.to_s.empty? && geom.label_boxes.to_h.empty?
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def connector_label_boxes(placed, type, owner_id)
|
|
331
|
+
return serialize_label_boxes(placed.geometry.label_boxes) unless phantom_link?(placed, type)
|
|
332
|
+
|
|
333
|
+
# An empty extension placement can still have saved UMLDI labels.
|
|
334
|
+
# Synthetic phantom offsets must not suppress those authored bounds.
|
|
335
|
+
return {} if owner_id && xmi_path && umldi_edges.label_bounds_for(owner_id, placed.subject)
|
|
336
|
+
|
|
337
|
+
Qea::DiagramBuilder::DEFAULT_LABEL_BOXES
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# The saved SOID/EOID can name the model connector's ends in reverse
|
|
341
|
+
# order; the model connector decides which placement is the source.
|
|
342
|
+
def placed_ends(style, by_duid, by_subject, edge_source, edge_target)
|
|
343
|
+
source = by_duid[style.soid] || sole_placement(by_subject, edge_source)
|
|
344
|
+
target = by_duid[style.eoid] || sole_placement(by_subject, edge_target)
|
|
345
|
+
return [source, target] unless source && target &&
|
|
346
|
+
reversed_ends?(source, target, edge_source, edge_target)
|
|
347
|
+
|
|
348
|
+
[target, source]
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def reversed_ends?(source, target, edge_source, edge_target)
|
|
352
|
+
guid_key(edge_source) != guid_key(edge_target) &&
|
|
353
|
+
guid_key(source.subject) == guid_key(edge_target) &&
|
|
354
|
+
guid_key(target.subject) == guid_key(edge_source)
|
|
198
355
|
end
|
|
199
356
|
|
|
200
357
|
def connector_meta(subject_id)
|
|
@@ -238,11 +395,34 @@ module Ea
|
|
|
238
395
|
end
|
|
239
396
|
|
|
240
397
|
def connector_route(placed, source, target, owner_id, type)
|
|
398
|
+
phantom_route(placed, source, target, type) || saved_route(placed, source, target, owner_id, type)
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
def saved_route(placed, source, target, owner_id, type)
|
|
241
402
|
native = umldi_waypoints(owner_id, placed) if placed.geometry.bend_points.to_a.empty?
|
|
242
403
|
points = native || compute_waypoints(placed.geometry, source, target,
|
|
243
404
|
style: placed.style, connector_type: type)
|
|
244
405
|
native ||= umldi_waypoints(owner_id, placed) if points.empty?
|
|
245
|
-
{ waypoints: native || points, style: native
|
|
406
|
+
{ waypoints: native || points, style: connector_route_style(placed, native) }
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def phantom_route(placed, source, target, type)
|
|
410
|
+
return unless source && target && phantom_link?(placed, type)
|
|
411
|
+
|
|
412
|
+
points = StoredRoute.new.phantom_waypoints(bounds_from_placed(source), bounds_from_placed(target))
|
|
413
|
+
{ waypoints: points.map { |x, y| Ea::Model::Waypoint.new(position: Ea::Model::Point.new(x: x, y: y)) },
|
|
414
|
+
style: { lwidth: "1", phantom_outline: true } }
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def connector_route_style(placed, native)
|
|
418
|
+
route_style = native ? { umldi_route: true } : {}
|
|
419
|
+
# Label seats use EA's saved routing mode on every route source;
|
|
420
|
+
# umldi_route still marks UMLDI waypoints as authoritative.
|
|
421
|
+
route_style.merge!(Qea::DiagramStyleParser.parse(placed.style.raw).slice(:mode, :lwidth, :tree))
|
|
422
|
+
# Source-known links without DUIDs use classifier endpoint metadata,
|
|
423
|
+
# not a saved label box that might refer to the opposite end.
|
|
424
|
+
route_style[:explicit_end_roles] = true unless placed.style.soid && placed.style.eoid
|
|
425
|
+
route_style
|
|
246
426
|
end
|
|
247
427
|
|
|
248
428
|
# EA's unique exported edge supplies bendless routes and recovers
|
|
@@ -254,6 +434,15 @@ module Ea
|
|
|
254
434
|
route&.map { |x, y| Ea::Model::Waypoint.new(position: Ea::Model::Point.new(x: x, y: y)) }
|
|
255
435
|
end
|
|
256
436
|
|
|
437
|
+
def connector_ends(diagram_id, placed)
|
|
438
|
+
umldi_ends(diagram_id, placed) || extension_ends(placed.subject)
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
def extension_ends(subject)
|
|
442
|
+
connector = connector_index[subject]
|
|
443
|
+
[connector&.source&.idref, connector&.target&.idref]
|
|
444
|
+
end
|
|
445
|
+
|
|
257
446
|
def umldi_ends(diagram_id, placed)
|
|
258
447
|
return unless xmi_path
|
|
259
448
|
|
|
@@ -12,8 +12,9 @@ module Ea
|
|
|
12
12
|
class ExtensionElements
|
|
13
13
|
attr_reader :diagram
|
|
14
14
|
|
|
15
|
-
def initialize(diagram)
|
|
15
|
+
def initialize(diagram, connector_subjects: {})
|
|
16
16
|
@diagram = diagram
|
|
17
|
+
@connector_subjects = connector_subjects
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def placed_elements
|
|
@@ -53,9 +54,9 @@ module Ea
|
|
|
53
54
|
|
|
54
55
|
placement = ExtensionGeometryParser.parse(row.geometry)
|
|
55
56
|
style = ExtensionStyleParser.parse(row.style)
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
return nil unless style.soid && style.eoid
|
|
57
|
+
# Some EA placements omit both DUIDs. The extension connector
|
|
58
|
+
# index still identifies these rows without treating shapes as links.
|
|
59
|
+
return nil unless (style.soid && style.eoid) || @connector_subjects.key?(row.subject)
|
|
59
60
|
|
|
60
61
|
PlacedConnector.new(
|
|
61
62
|
subject: row.subject,
|