ea 0.6.134 → 0.6.136
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/sources/xmi/adapter.rb +27 -10
- data/lib/ea/sources/xmi/classifier_builder.rb +25 -15
- data/lib/ea/sources/xmi/diagram_builder/connector_metadata.rb +76 -0
- data/lib/ea/sources/xmi/diagram_builder/font_settings.rb +62 -0
- data/lib/ea/sources/xmi/diagram_builder/stereotype_profiles.rb +36 -0
- data/lib/ea/sources/xmi/diagram_builder.rb +19 -4
- data/lib/ea/sources/xmi/package_builder.rb +9 -5
- data/lib/ea/sources/xmi/relationship_builder.rb +16 -5
- data/lib/ea/sources/xmi/umldi_keyword_extractor.rb +13 -3
- data/lib/ea/svg/ea_emitter/compartment/note_text.rb +15 -4
- data/lib/ea/svg/ea_emitter/element/attribute_renderer.rb +7 -15
- data/lib/ea/svg/ea_emitter/element/header_line_provider/stereotype_label.rb +1 -1
- data/lib/ea/svg/ea_emitter/elements.rb +2 -0
- data/lib/ea/svg/ea_emitter/label/end_label.rb +31 -6
- data/lib/ea/version.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 121e254bdf4c232789483be3fc0de9f65e0b70a89c34009db6421d9dcae1337d
|
|
4
|
+
data.tar.gz: 1f11075a36fe5f7208b264f4f10e409230afe184101157ba41a3bf05fd57efb6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f924fea92342d3525a5fb5ea6b1b8c0167afc5f709d1c373a52b178a263efa29c9adc8bf690c4fba1073053a2f75c5888abbd509dcd187c39a57a0ab4c089286
|
|
7
|
+
data.tar.gz: 5c558dcefad07838fd403fa8eae5a9e7bf00e802348de364d28adb692e02bb76a6de2909139570431bb96a364955c276a5a356b05ba5107ce6906f0aa03315a5
|
|
@@ -35,6 +35,7 @@ module Ea
|
|
|
35
35
|
instance_specifications: instance_specifications,
|
|
36
36
|
diagrams: diagrams
|
|
37
37
|
)
|
|
38
|
+
mark_connector_proxies(doc)
|
|
38
39
|
apply_tagged_values(doc)
|
|
39
40
|
apply_receptions(doc)
|
|
40
41
|
apply_stereotypes(doc)
|
|
@@ -44,6 +45,23 @@ module Ea
|
|
|
44
45
|
|
|
45
46
|
private
|
|
46
47
|
|
|
48
|
+
# EA serializes connector anchors as UML class stubs. Keep their
|
|
49
|
+
# identity and geometry while distinguishing their graphical role.
|
|
50
|
+
def mark_connector_proxies(doc)
|
|
51
|
+
proxy_ids = connector_proxy_ids
|
|
52
|
+
return if proxy_ids.empty?
|
|
53
|
+
|
|
54
|
+
doc.diagrams.flat_map(&:elements).each do |element|
|
|
55
|
+
element.style["connector_proxy"] = true if proxy_ids.include?(element.model_element_ref)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def connector_proxy_ids
|
|
60
|
+
(root.extension&.elements&.element || []).filter_map do |element|
|
|
61
|
+
IdNormalizer.from_xmi_id(element.idref) if element.type == "uml:ProxyConnector"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
47
65
|
def apply_receptions(doc)
|
|
48
66
|
grouped = ReceptionBuilder.new(xmi_path).grouped_by_owner
|
|
49
67
|
doc.classifiers.each do |classifier|
|
|
@@ -99,10 +117,9 @@ module Ea
|
|
|
99
117
|
end
|
|
100
118
|
end
|
|
101
119
|
|
|
102
|
-
# Replaces each type reference with
|
|
103
|
-
#
|
|
104
|
-
#
|
|
105
|
-
# extension block.
|
|
120
|
+
# Replaces each type reference with the type name EA stores in
|
|
121
|
+
# the extension block; references the extension does not name
|
|
122
|
+
# fall back to the classifier's in-model qualified name.
|
|
106
123
|
def resolve_type_names(doc)
|
|
107
124
|
qualified_by_id = doc.classifiers.each_with_object({}) do |c, acc|
|
|
108
125
|
acc[c.id] = c.qualified_name if c.id && c.qualified_name
|
|
@@ -111,8 +128,8 @@ module Ea
|
|
|
111
128
|
|
|
112
129
|
doc.classifiers.each do |classifier|
|
|
113
130
|
(classifier.properties || []).each do |prop|
|
|
114
|
-
prop.type_name =
|
|
115
|
-
|
|
131
|
+
prop.type_name = types.attribute_type(prop.id) ||
|
|
132
|
+
qualified_by_id[prop.type_ref] || prop.type_name
|
|
116
133
|
end
|
|
117
134
|
(classifier.operations || []).each do |op|
|
|
118
135
|
resolve_operation_types(op, qualified_by_id, types)
|
|
@@ -121,12 +138,12 @@ module Ea
|
|
|
121
138
|
end
|
|
122
139
|
|
|
123
140
|
def resolve_operation_types(operation, qualified_by_id, types)
|
|
124
|
-
operation.return_type_name =
|
|
125
|
-
|
|
141
|
+
operation.return_type_name = types.return_type(operation.id) ||
|
|
142
|
+
qualified_by_id[operation.return_type_name] ||
|
|
126
143
|
operation.return_type_name
|
|
127
144
|
operation.parameters.each do |param|
|
|
128
|
-
param.type_name =
|
|
129
|
-
|
|
145
|
+
param.type_name = types.parameter_type(param.id) ||
|
|
146
|
+
qualified_by_id[param.type_name] || param.type_name
|
|
130
147
|
end
|
|
131
148
|
end
|
|
132
149
|
|
|
@@ -16,41 +16,49 @@ module Ea
|
|
|
16
16
|
|
|
17
17
|
def build_all
|
|
18
18
|
classifiers = []
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
root.model.packaged_element.each do |top|
|
|
20
|
+
walk(top, top.type == "uml:Package" ? top : nil) do |element, parent_pkg, owner_class|
|
|
21
|
+
next unless TypeClassifierMap.classifier_type?(element.type)
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
classifiers << build_one(element, parent_pkg, owner_class)
|
|
24
|
+
end
|
|
23
25
|
end
|
|
24
26
|
classifiers
|
|
25
27
|
end
|
|
26
28
|
|
|
27
|
-
def build_one(element, containing_package)
|
|
29
|
+
def build_one(element, containing_package, owner_class = nil)
|
|
28
30
|
klass = TypeClassifierMap.class_for(element.type)
|
|
29
|
-
args = common_args(element, containing_package)
|
|
31
|
+
args = common_args(element, containing_package, owner_class)
|
|
30
32
|
args.merge!(enumeration_args(element)) if klass == Ea::Model::Enumeration
|
|
31
33
|
klass.new(**args)
|
|
32
34
|
end
|
|
33
35
|
|
|
34
36
|
private
|
|
35
37
|
|
|
36
|
-
# Depth-first walk of the packaged_element
|
|
37
|
-
# element with its containing uml:Package
|
|
38
|
-
# model root)
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
# Depth-first walk of the packaged_element and nestedClassifier
|
|
39
|
+
# trees. Yields each element with its containing uml:Package
|
|
40
|
+
# (nil at the model root) and the uml:Class that nests it
|
|
41
|
+
# (nil unless it is a nestedClassifier of a class).
|
|
42
|
+
def walk(element, parent_pkg = nil, owner_class = nil, &block)
|
|
43
|
+
yield element, parent_pkg, owner_class if block
|
|
41
44
|
element.packaged_element.each do |inner|
|
|
42
45
|
new_parent = inner.type == "uml:Package" ? inner : parent_pkg
|
|
43
|
-
walk(inner, new_parent, &block)
|
|
46
|
+
walk(inner, new_parent, nil, &block)
|
|
47
|
+
end
|
|
48
|
+
element.nested_classifier.each do |inner|
|
|
49
|
+
walk(inner, parent_pkg, element, &block)
|
|
44
50
|
end
|
|
45
51
|
end
|
|
46
52
|
|
|
47
|
-
def common_args(element, containing_package)
|
|
53
|
+
def common_args(element, containing_package, owner_class)
|
|
48
54
|
id = IdNormalizer.from_xmi_id(element.id)
|
|
55
|
+
nested_parent = owner_class&.name
|
|
49
56
|
{
|
|
50
57
|
id: id,
|
|
51
58
|
name: element.name,
|
|
52
59
|
package_id: containing_package && IdNormalizer.from_xmi_id(containing_package.id),
|
|
53
|
-
|
|
60
|
+
nested_parent_name: nested_parent,
|
|
61
|
+
qualified_name: qualified_name(element, containing_package, nested_parent),
|
|
54
62
|
is_abstract: boolean(element.is_abstract),
|
|
55
63
|
visibility: element.visibility,
|
|
56
64
|
properties: PropertyBuilder.new.build_all_for(element),
|
|
@@ -83,10 +91,12 @@ module Ea
|
|
|
83
91
|
end
|
|
84
92
|
|
|
85
93
|
# A name that already carries a namespace prefix
|
|
86
|
-
# (e.g. "gml:CodeType") is the qualified name as saved
|
|
87
|
-
|
|
94
|
+
# (e.g. "gml:CodeType") is the qualified name as saved; a
|
|
95
|
+
# nested classifier is prefixed with its owner class.
|
|
96
|
+
def qualified_name(element, containing_package, nested_parent)
|
|
88
97
|
name = element.name.to_s
|
|
89
98
|
return name if name.include?(":")
|
|
99
|
+
return "#{nested_parent}::#{name}" if nested_parent
|
|
90
100
|
|
|
91
101
|
pkg_name = containing_package&.name
|
|
92
102
|
pkg_name ? "#{pkg_name}::#{name}" : name
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
|
|
5
|
+
module Ea
|
|
6
|
+
module Sources
|
|
7
|
+
module Xmi
|
|
8
|
+
class DiagramBuilder
|
|
9
|
+
# The XMI gem omits role stereotypes. Stream the saved connector
|
|
10
|
+
# records once, without retaining another full XML document.
|
|
11
|
+
class ConnectorMetadata
|
|
12
|
+
def initialize(xmi_path)
|
|
13
|
+
@xmi_path = xmi_path
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def for(connector)
|
|
17
|
+
{
|
|
18
|
+
source_stereotype: role_stereotypes.dig(connector.idref, :source),
|
|
19
|
+
target_stereotype: role_stereotypes.dig(connector.idref, :target),
|
|
20
|
+
is_substitutable: false_substitutability?(connector.xrefs&.value.to_s)
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def false_substitutability?(xrefs)
|
|
27
|
+
xrefs.scan(/\$XREFPROP=(.*?)\$ENDXREF;/m).any? do |match|
|
|
28
|
+
record = match.first
|
|
29
|
+
next false unless record.include?("$NAM=CustomProperties$NAM;") &&
|
|
30
|
+
record.include?("$TYP=connector property$TYP;")
|
|
31
|
+
|
|
32
|
+
values = record.scan(/@PROP=(.*?)@ENDPROP;/m).filter_map do |property|
|
|
33
|
+
substitutability_value(property.first)
|
|
34
|
+
end
|
|
35
|
+
!values.empty? && !values.include?("-1")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def substitutability_value(property)
|
|
40
|
+
return unless property.include?("@NAME=isSubstitutable@ENDNAME;") &&
|
|
41
|
+
property.include?("@TYPE=Boolean@ENDTYPE;")
|
|
42
|
+
|
|
43
|
+
property[/@VALU=(.*?)@ENDVALU;/m, 1]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def role_stereotypes
|
|
47
|
+
@role_stereotypes ||= read_role_stereotypes
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def read_role_stereotypes
|
|
51
|
+
return {} unless @xmi_path
|
|
52
|
+
|
|
53
|
+
File.open(@xmi_path) do |file|
|
|
54
|
+
Nokogiri::XML::Reader(file).each_with_object({}) do |node, roles|
|
|
55
|
+
next unless node.node_type == Nokogiri::XML::Reader::TYPE_ELEMENT && node.local_name == "connector"
|
|
56
|
+
|
|
57
|
+
read_connector_roles(node, roles)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def read_connector_roles(node, roles)
|
|
63
|
+
connector = Nokogiri::XML(node.outer_xml).root
|
|
64
|
+
id = connector.at_xpath("@*[local-name()='idref']")&.value
|
|
65
|
+
return unless id
|
|
66
|
+
|
|
67
|
+
roles[id] = %i[source target].to_h do |side|
|
|
68
|
+
value = connector.at_xpath("./*[local-name()='#{side}']/*[local-name()='role']/@stereotype")&.value
|
|
69
|
+
[side, value]
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
|
|
5
|
+
module Ea
|
|
6
|
+
module Sources
|
|
7
|
+
module Xmi
|
|
8
|
+
class DiagramBuilder
|
|
9
|
+
# The XMI gem exposes placement fonts but drops object styleex.
|
|
10
|
+
# Read saved defaults once, then let each placement override them.
|
|
11
|
+
class FontSettings
|
|
12
|
+
def initialize(xmi_path)
|
|
13
|
+
@xmi_path = xmi_path
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def for(placed)
|
|
17
|
+
style = placed.style
|
|
18
|
+
defaults = object_styles.fetch(placed.subject, {})
|
|
19
|
+
placement = ExtensionStyleParser.split_pairs(style.raw.to_s)
|
|
20
|
+
raw_size = saved_size(placement) || saved_size(defaults)
|
|
21
|
+
{
|
|
22
|
+
family: presence(style.font_family) || defaults["font"],
|
|
23
|
+
size: style.font_size || ExtensionStyleParser.scale_font_size(raw_size),
|
|
24
|
+
logical_height: logical_height(raw_size)
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def presence(value)
|
|
31
|
+
value unless value.nil? || value.empty?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def saved_size(style)
|
|
35
|
+
value = style["fontsz"]
|
|
36
|
+
value if ExtensionStyleParser.int(value)&.nonzero?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def logical_height(value)
|
|
40
|
+
tenths = ExtensionStyleParser.int(value)
|
|
41
|
+
tenths * 96 / 720 if tenths
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def object_styles
|
|
45
|
+
@object_styles ||= saved_objects.each_with_object({}) do |element, styles|
|
|
46
|
+
id = element.at_xpath("@*[local-name()='idref']")&.value
|
|
47
|
+
style = element.at_xpath("./*[local-name()='style']/@styleex")&.value
|
|
48
|
+
styles[id] = ExtensionStyleParser.split_pairs(style) if id && style
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def saved_objects
|
|
53
|
+
return [] unless @xmi_path
|
|
54
|
+
|
|
55
|
+
doc = Nokogiri::XML(File.read(@xmi_path))
|
|
56
|
+
doc.xpath("//*[local-name()='Extension']/*[local-name()='elements']/*[local-name()='element']")
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Sources
|
|
5
|
+
module Xmi
|
|
6
|
+
class DiagramBuilder
|
|
7
|
+
# Keep graphical spelling provenance separate from semantic stereotype names.
|
|
8
|
+
class StereotypeProfiles
|
|
9
|
+
def initialize(root)
|
|
10
|
+
@elements = root.extension&.elements&.element.to_a.to_h { |element| [element.idref, element] }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def for(subject)
|
|
14
|
+
element = @elements[subject]
|
|
15
|
+
name = element&.properties&.stereotype
|
|
16
|
+
return {} if name.to_s.empty?
|
|
17
|
+
|
|
18
|
+
fqname = matching_profile(element.xrefs&.value.to_s, name)
|
|
19
|
+
fqname ? { stereotype_fqname: fqname } : {}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def matching_profile(value, name)
|
|
25
|
+
value.split("@STEREO;").drop(1).each do |block|
|
|
26
|
+
description = "@STEREO;#{block.partition("@ENDSTEREO;").first}@ENDSTEREO;"
|
|
27
|
+
application = Qea::Xref::Parser.parse(description).stereotype
|
|
28
|
+
return application.fqname if application.name == name
|
|
29
|
+
end
|
|
30
|
+
nil
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -9,6 +9,9 @@ module Ea
|
|
|
9
9
|
# SOID/EOID, EDGE, label boxes) when present. Falls back to
|
|
10
10
|
# the UML-DI owned_element bounds otherwise.
|
|
11
11
|
class DiagramBuilder
|
|
12
|
+
autoload :ConnectorMetadata, "ea/sources/xmi/diagram_builder/connector_metadata"
|
|
13
|
+
autoload :FontSettings, "ea/sources/xmi/diagram_builder/font_settings"
|
|
14
|
+
autoload :StereotypeProfiles, "ea/sources/xmi/diagram_builder/stereotype_profiles"
|
|
12
15
|
autoload :UmldiEdges, "ea/sources/xmi/diagram_builder/umldi_edges"
|
|
13
16
|
|
|
14
17
|
attr_reader :root, :xmi_path
|
|
@@ -58,6 +61,8 @@ module Ea
|
|
|
58
61
|
show_notes: style_ex_for(ext_diagram).to_s.include?("ShowNotes=1"),
|
|
59
62
|
show_package_contents: style_flag?(ext_diagram, "PackageContents"),
|
|
60
63
|
show_foreign: style_flag?(ext_diagram, "HighlightForeign"),
|
|
64
|
+
show_parents: !style_flag?(ext_diagram, "HideParents"),
|
|
65
|
+
hand_draw: style_flag?(ext_diagram, "HandDraw"),
|
|
61
66
|
elements: build_elements(placed_elements, id, umldi_keywords),
|
|
62
67
|
connectors: build_connectors(placed_connectors,
|
|
63
68
|
placed_elements, id),
|
|
@@ -113,6 +118,7 @@ module Ea
|
|
|
113
118
|
geom = placed.geometry
|
|
114
119
|
style = placed.style
|
|
115
120
|
bounds = build_bounds(geom.left, geom.top, geom.right, geom.bottom)
|
|
121
|
+
font = (@font_settings ||= FontSettings.new(xmi_path)).for(placed)
|
|
116
122
|
Ea::Model::DiagramElement.new(
|
|
117
123
|
id: IdNormalizer.synthetic_id(owner_id, "elem", placed.subject),
|
|
118
124
|
diagram_id: owner_id,
|
|
@@ -122,8 +128,10 @@ module Ea
|
|
|
122
128
|
background_color: style.background_color,
|
|
123
129
|
line_color: style.line_color,
|
|
124
130
|
line_width: style.line_width,
|
|
125
|
-
font_family:
|
|
126
|
-
font_size:
|
|
131
|
+
font_family: font[:family],
|
|
132
|
+
font_size: font[:size],
|
|
133
|
+
font_logical_height: font[:logical_height],
|
|
134
|
+
font_size_inherited: ExtensionStyleParser.split_pairs(style.raw.to_s)["fontsz"] == "0",
|
|
127
135
|
font_bold: style.bold,
|
|
128
136
|
font_italic: style.italic,
|
|
129
137
|
font_underline: style.underline,
|
|
@@ -132,7 +140,7 @@ module Ea
|
|
|
132
140
|
z_order: placed.seqno,
|
|
133
141
|
duid: style.duid,
|
|
134
142
|
umldi_keyword: umldi_keywords[placed.subject],
|
|
135
|
-
style:
|
|
143
|
+
style: (@stereotype_profiles ||= StereotypeProfiles.new(root)).for(placed.subject)
|
|
136
144
|
)
|
|
137
145
|
end
|
|
138
146
|
|
|
@@ -177,6 +185,9 @@ module Ea
|
|
|
177
185
|
target_edge: geom.edge,
|
|
178
186
|
connector_type: ext_meta[:ea_type],
|
|
179
187
|
direction: ext_meta[:direction],
|
|
188
|
+
source_stereotype: ext_meta[:source_stereotype],
|
|
189
|
+
target_stereotype: ext_meta[:target_stereotype],
|
|
190
|
+
is_substitutable: ext_meta.fetch(:is_substitutable, false),
|
|
180
191
|
waypoints: route[:waypoints],
|
|
181
192
|
style: route[:style],
|
|
182
193
|
line_color: style.color,
|
|
@@ -194,7 +205,11 @@ module Ea
|
|
|
194
205
|
{
|
|
195
206
|
ea_type: props&.ea_type,
|
|
196
207
|
direction: props&.direction
|
|
197
|
-
}
|
|
208
|
+
}.merge(connector_metadata.for(connector))
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def connector_metadata
|
|
212
|
+
@connector_metadata ||= ConnectorMetadata.new(xmi_path)
|
|
198
213
|
end
|
|
199
214
|
|
|
200
215
|
def connector_index
|
|
@@ -60,12 +60,16 @@ module Ea
|
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
def child_classifier_ids(packaged_element)
|
|
63
|
-
packaged_element.packaged_element
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
packaged_element.packaged_element.flat_map do |element|
|
|
64
|
+
classifier_ids(element)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def classifier_ids(element)
|
|
69
|
+
return [] unless TypeClassifierMap.classifier_type?(element.type)
|
|
66
70
|
|
|
67
|
-
|
|
68
|
-
|
|
71
|
+
[IdNormalizer.from_xmi_id(element.id),
|
|
72
|
+
*element.nested_classifier.flat_map { |inner| classifier_ids(inner) }].compact
|
|
69
73
|
end
|
|
70
74
|
end
|
|
71
75
|
end
|
|
@@ -24,11 +24,11 @@ module Ea
|
|
|
24
24
|
|
|
25
25
|
def build_all
|
|
26
26
|
relationships = []
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
root.model.packaged_element.each do |top|
|
|
28
|
+
walk(top) do |element|
|
|
29
|
+
relationships << build_one(element) if RelationshipTypeMap.relationship_type?(element.type)
|
|
30
|
+
relationships.concat(owned_generalizations(element))
|
|
31
|
+
end
|
|
32
32
|
end
|
|
33
33
|
relationships + package_import_relationships(relationships)
|
|
34
34
|
end
|
|
@@ -69,6 +69,17 @@ module Ea
|
|
|
69
69
|
def walk(element, &block)
|
|
70
70
|
yield element if block
|
|
71
71
|
element.packaged_element.each { |inner| walk(inner, &block) }
|
|
72
|
+
element.nested_classifier.each { |inner| walk(inner, &block) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def owned_generalizations(element)
|
|
76
|
+
element.generalization.map do |generalization|
|
|
77
|
+
Ea::Model::Generalization.new(
|
|
78
|
+
id: IdNormalizer.from_xmi_id(generalization.id),
|
|
79
|
+
specific_id: IdNormalizer.from_xmi_id(element.id),
|
|
80
|
+
general_id: IdNormalizer.from_xmi_id(generalization.general)
|
|
81
|
+
)
|
|
82
|
+
end
|
|
72
83
|
end
|
|
73
84
|
|
|
74
85
|
def common_args(element)
|
|
@@ -40,14 +40,24 @@ module Ea
|
|
|
40
40
|
|
|
41
41
|
private
|
|
42
42
|
|
|
43
|
-
#
|
|
44
|
-
#
|
|
43
|
+
# EA can save both a built-in keyword and a custom stereotype label.
|
|
44
|
+
# Prefer the label matching the element's saved stereotype, if present.
|
|
45
45
|
def keyword_text(shape)
|
|
46
|
-
|
|
46
|
+
labels = shape.xpath("./ownedElement[@*[local-name()=\"type\"]=\"umldi:UMLKeywordLabel\"]")
|
|
47
|
+
stereotype = stereotypes_by_element[shape["modelElement"]]
|
|
48
|
+
label = labels.find { |candidate| stereotype && candidate["text"] == stereotype } || labels.first
|
|
47
49
|
text = label && label["text"]
|
|
48
50
|
text unless text.nil? || text.empty?
|
|
49
51
|
end
|
|
50
52
|
|
|
53
|
+
def stereotypes_by_element
|
|
54
|
+
@stereotypes_by_element ||= doc.xpath("//*[local-name()=\"Extension\"]/elements/element")
|
|
55
|
+
.each_with_object({}) do |element, result|
|
|
56
|
+
stereotype = element.at_xpath("./properties")&.[]("stereotype")
|
|
57
|
+
result[element["idref"] || element["xmi:idref"]] = stereotype if stereotype
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
51
61
|
def doc
|
|
52
62
|
@doc ||= Nokogiri::XML(File.read(xmi_path))
|
|
53
63
|
end
|
|
@@ -11,7 +11,9 @@ module Ea
|
|
|
11
11
|
# boundaries to fit the element bounds.
|
|
12
12
|
module NoteText
|
|
13
13
|
X_PADDING = 5
|
|
14
|
+
HAND_DRAW_X_PADDING = 10
|
|
14
15
|
Y_FIRST_OFFSET = 15
|
|
16
|
+
HAND_DRAW_Y_FIRST_OFFSET = 11
|
|
15
17
|
LINE_OFFSET = 13
|
|
16
18
|
CHAR_WIDTH_FACTOR = 0.55
|
|
17
19
|
|
|
@@ -24,23 +26,30 @@ module Ea
|
|
|
24
26
|
notes = note_bodies(context.classifier)
|
|
25
27
|
return nil if notes.empty?
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
x_padding, y_offset = note_offsets(context)
|
|
30
|
+
first_y = context.geometry.attr_first_y + y_offset
|
|
28
31
|
bounds = context.bounds
|
|
29
32
|
fill = context.theme.attribute_text_color
|
|
30
|
-
usable_width = bounds.width - (
|
|
33
|
+
usable_width = bounds.width - (x_padding * 2)
|
|
31
34
|
max_chars = (usable_width / (context.size * CHAR_WIDTH_FACTOR)).floor
|
|
32
35
|
|
|
33
36
|
texts = []
|
|
34
37
|
y = first_y
|
|
35
38
|
notes.each do |body|
|
|
36
39
|
wrapped_lines(body, max_chars).each do |line|
|
|
37
|
-
texts << build_text(line, bounds.x +
|
|
40
|
+
texts << build_text(line, bounds.x + x_padding, y, context, fill)
|
|
38
41
|
y += LINE_OFFSET
|
|
39
42
|
end
|
|
40
43
|
end
|
|
41
44
|
wrap_group(texts.join("\n"), fill)
|
|
42
45
|
end
|
|
43
46
|
|
|
47
|
+
# Native hand-drawn classifiers use a wider documentation inset.
|
|
48
|
+
def note_offsets(context)
|
|
49
|
+
context.diagram.hand_draw ? [HAND_DRAW_X_PADDING, HAND_DRAW_Y_FIRST_OFFSET] : [X_PADDING, Y_FIRST_OFFSET]
|
|
50
|
+
end
|
|
51
|
+
module_function :note_offsets
|
|
52
|
+
|
|
44
53
|
def show_notes?(context)
|
|
45
54
|
context.diagram && context.diagram.show_notes
|
|
46
55
|
end
|
|
@@ -67,7 +76,9 @@ module Ea
|
|
|
67
76
|
elsif (current + " " + word).length <= max_chars
|
|
68
77
|
current = "#{current} #{word}"
|
|
69
78
|
else
|
|
70
|
-
|
|
79
|
+
# EA retains the consumed word separator on a wrapped
|
|
80
|
+
# documentation row, including it in SVG textLength.
|
|
81
|
+
lines << "#{current} "
|
|
71
82
|
current = word
|
|
72
83
|
end
|
|
73
84
|
end
|
|
@@ -90,8 +90,8 @@ module Ea
|
|
|
90
90
|
end
|
|
91
91
|
|
|
92
92
|
# Builds attribute display lines from classifier properties,
|
|
93
|
-
# hiding properties
|
|
94
|
-
#
|
|
93
|
+
# hiding association-end properties even when their connectors
|
|
94
|
+
# are not drawn on the diagram.
|
|
95
95
|
#
|
|
96
96
|
# When a property carries a stereotype (e.g. «voidable»),
|
|
97
97
|
# EA renders the stereotype label as a separate line above
|
|
@@ -171,19 +171,11 @@ module Ea
|
|
|
171
171
|
end
|
|
172
172
|
private_class_method :build_text
|
|
173
173
|
|
|
174
|
-
# Association
|
|
175
|
-
#
|
|
176
|
-
#
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
def self.displayable_properties(classifier, exclude_association_ids)
|
|
180
|
-
return nil unless classifier.properties
|
|
181
|
-
|
|
182
|
-
classifier.properties.reject do |prop|
|
|
183
|
-
prop.association_id &&
|
|
184
|
-
(exclude_association_ids.nil? ||
|
|
185
|
-
exclude_association_ids.include?(prop.association_id))
|
|
186
|
-
end
|
|
174
|
+
# Association ends remain in the model for connector role labels,
|
|
175
|
+
# but never become class-compartment attribute rows. The public
|
|
176
|
+
# exclude_association_ids keyword remains accepted for callers.
|
|
177
|
+
def self.displayable_properties(classifier, _exclude_association_ids)
|
|
178
|
+
classifier.properties&.reject(&:association_id)
|
|
187
179
|
end
|
|
188
180
|
private_class_method :displayable_properties
|
|
189
181
|
|
|
@@ -31,7 +31,7 @@ module Ea
|
|
|
31
31
|
# @return [Array<String>] «name» labels (EA renders one
|
|
32
32
|
# line per applied stereotype)
|
|
33
33
|
def self.label_for(classifier, umldi_keyword, stereotype_fqname = nil)
|
|
34
|
-
return ["«#{umldi_keyword}»"] if umldi_keyword && !umldi_keyword.empty?
|
|
34
|
+
return ["«#{canonical(umldi_keyword, stereotype_fqname)}»"] if umldi_keyword && !umldi_keyword.empty?
|
|
35
35
|
|
|
36
36
|
explicit = explicit_stereotype(classifier, stereotype_fqname)
|
|
37
37
|
return explicit if explicit
|
|
@@ -341,9 +341,21 @@ module Ea
|
|
|
341
341
|
inset, baseline = property_role_offsets(direction)
|
|
342
342
|
left = property_role_x(direction, x, inset + width)
|
|
343
343
|
left = [left, MIN_ROLE_X].max if direction == :up
|
|
344
|
+
left += source_property_centering(connector, direction, box, role)
|
|
344
345
|
[left, y + baseline]
|
|
345
346
|
end
|
|
346
347
|
|
|
348
|
+
# Center a short role above the wider stereotype without moving
|
|
349
|
+
# the directional cluster's left edge. Other boxes keep their seat.
|
|
350
|
+
def source_property_centering(connector, direction, box, role)
|
|
351
|
+
return 0 unless %i[left down].include?(direction)
|
|
352
|
+
return 0 unless seat_font? && font_weight == 400 && font_style == "normal"
|
|
353
|
+
return 0 unless box.values_at("cy", "alignment") == [35, 1]
|
|
354
|
+
|
|
355
|
+
stereo = "«#{connector_source_dest_stereotype(connector, :source)}»"
|
|
356
|
+
-[stereotype_offset(role, stereo, box), 0].min
|
|
357
|
+
end
|
|
358
|
+
|
|
347
359
|
def source_property_seat?(connector, direction, box)
|
|
348
360
|
phantom_property_box?(connector, direction, box) ||
|
|
349
361
|
(%i[up left right down].include?(direction) &&
|
|
@@ -885,8 +897,7 @@ module Ea
|
|
|
885
897
|
# relationship guid), falling back to the property index
|
|
886
898
|
# lookup by association source/target id.
|
|
887
899
|
if connector&.relationship_ref
|
|
888
|
-
|
|
889
|
-
prop = assoc_end_property(placed, connector.relationship_ref)
|
|
900
|
+
prop = placed_end_property(association, end_kind, connector.relationship_ref)
|
|
890
901
|
return prop if prop
|
|
891
902
|
end
|
|
892
903
|
|
|
@@ -898,13 +909,27 @@ module Ea
|
|
|
898
909
|
fallback_property_lookup(id)
|
|
899
910
|
end
|
|
900
911
|
|
|
912
|
+
def placed_end_property(association, end_kind, relationship_ref)
|
|
913
|
+
placed = placed_classifier_id(association, end_kind)
|
|
914
|
+
property = assoc_end_property(placed, relationship_ref)
|
|
915
|
+
property unless property && opposite_role_property?(association, end_kind, property)
|
|
916
|
+
end
|
|
917
|
+
|
|
918
|
+
# A UML attribute belongs to the classifier opposite its role.
|
|
919
|
+
# Do not draw that same role again at its anonymous owner end.
|
|
920
|
+
def opposite_role_property?(association, end_kind, property)
|
|
921
|
+
own = end_kind == :source ? association.source_role_name : association.target_role_name
|
|
922
|
+
other = end_kind == :source ? association.target_role_name : association.source_role_name
|
|
923
|
+
role_empty?(own) && other == property.name
|
|
924
|
+
end
|
|
925
|
+
|
|
901
926
|
def placed_classifier_id(association, end_kind)
|
|
902
927
|
end_kind == :source ? association.source_id : association.target_id
|
|
903
928
|
end
|
|
904
929
|
|
|
905
|
-
#
|
|
906
|
-
#
|
|
907
|
-
#
|
|
930
|
+
# Named association-end properties carry typed labels. Unnamed
|
|
931
|
+
# XMI member-end references defer to the saved association role
|
|
932
|
+
# and cardinality; direct property-id endpoints are resolved above.
|
|
908
933
|
def assoc_end_property(classifier_id, relationship_ref)
|
|
909
934
|
return nil unless classifier_id
|
|
910
935
|
|
|
@@ -912,7 +937,7 @@ module Ea
|
|
|
912
937
|
return nil unless classifier.is_a?(Ea::Model::Classifier)
|
|
913
938
|
|
|
914
939
|
(classifier.properties || []).find do |p|
|
|
915
|
-
p.association_id == relationship_ref
|
|
940
|
+
p.association_id == relationship_ref && !p.name.to_s.empty?
|
|
916
941
|
end
|
|
917
942
|
end
|
|
918
943
|
|
data/lib/ea/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ea
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.6.
|
|
4
|
+
version: 0.6.136
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -852,6 +852,9 @@ files:
|
|
|
852
852
|
- lib/ea/sources/xmi/annotation_builder.rb
|
|
853
853
|
- lib/ea/sources/xmi/classifier_builder.rb
|
|
854
854
|
- lib/ea/sources/xmi/diagram_builder.rb
|
|
855
|
+
- lib/ea/sources/xmi/diagram_builder/connector_metadata.rb
|
|
856
|
+
- lib/ea/sources/xmi/diagram_builder/font_settings.rb
|
|
857
|
+
- lib/ea/sources/xmi/diagram_builder/stereotype_profiles.rb
|
|
855
858
|
- lib/ea/sources/xmi/diagram_builder/umldi_edges.rb
|
|
856
859
|
- lib/ea/sources/xmi/extension_elements.rb
|
|
857
860
|
- lib/ea/sources/xmi/extension_geometry_parser.rb
|