ea 0.6.133 → 0.6.135
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/model/document.rb +2 -11
- data/lib/ea/model/package_alias_index.rb +22 -0
- data/lib/ea/model.rb +1 -0
- data/lib/ea/sources/xmi/adapter.rb +27 -10
- data/lib/ea/sources/xmi/classifier_builder.rb +30 -24
- 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/umldi_edges.rb +32 -6
- data/lib/ea/sources/xmi/diagram_builder.rb +58 -21
- data/lib/ea/sources/xmi/legend_builder.rb +40 -0
- data/lib/ea/sources/xmi/note_builder.rb +16 -7
- data/lib/ea/sources/xmi/package_builder.rb +9 -5
- data/lib/ea/sources/xmi/relationship_builder.rb +50 -12
- data/lib/ea/sources/xmi/umldi_keyword_extractor.rb +13 -3
- data/lib/ea/sources/xmi.rb +1 -0
- 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 +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c90b3802a319e6dec5d22f9d196bfc328c942c679925634774dc55780874deef
|
|
4
|
+
data.tar.gz: 737a502a84e3a08c01070024b0793204ae4c460c928fbd531297fbd3d02fdf82
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a3df522a4725e068ec20bd515db2b35c058f270819cf5f503c187c59dc0d1db7b4b21e3e9d886db33c5960f2510bbe55b47ddddb0731f8c22a55b97e8d25b789
|
|
7
|
+
data.tar.gz: a2319133a4bcc7e0473ec82128b8b23205134cf5e09804f60b15c9e0a9a76ebd3795d6dea3566a4827fcc52b91c65dc14c521d93fbf5133ea63aa189bf229098
|
data/lib/ea/model/document.rb
CHANGED
|
@@ -58,11 +58,8 @@ module Ea
|
|
|
58
58
|
# `model_element_ref` lookups from diagram elements resolve.
|
|
59
59
|
def index_by_id
|
|
60
60
|
@index_by_id ||= begin
|
|
61
|
-
idx =
|
|
62
|
-
packages.each
|
|
63
|
-
idx[p.id] = p
|
|
64
|
-
alias_eaid_for_package(idx, p)
|
|
65
|
-
end
|
|
61
|
+
idx = PackageAliasIndex.new
|
|
62
|
+
packages.each { |p| idx[p.id] = p }
|
|
66
63
|
classifiers.each { |c| idx[c.id] = c }
|
|
67
64
|
relationships.each { |r| idx[r.id] = r }
|
|
68
65
|
stereotypes.each { |s| idx[s.id] = s }
|
|
@@ -98,12 +95,6 @@ module Ea
|
|
|
98
95
|
property_index[id]
|
|
99
96
|
end
|
|
100
97
|
|
|
101
|
-
def alias_eaid_for_package(idx, package)
|
|
102
|
-
return unless package.id&.start_with?("EAPK_")
|
|
103
|
-
|
|
104
|
-
idx["EAID_#{package.id[5..]}"] = package
|
|
105
|
-
end
|
|
106
|
-
|
|
107
98
|
def root_packages
|
|
108
99
|
packages.select { |p| p.parent_id.nil? }
|
|
109
100
|
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Model
|
|
5
|
+
# Id-keyed lookup in which a package stored under `EAPK_<guid>`
|
|
6
|
+
# also answers to `EAID_<guid>`, without a second entry: `values`
|
|
7
|
+
# lists each package once.
|
|
8
|
+
class PackageAliasIndex < Hash
|
|
9
|
+
def [](id)
|
|
10
|
+
super || package_alias(id)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def package_alias(id)
|
|
16
|
+
return unless id.is_a?(String) && id.start_with?("EAID_")
|
|
17
|
+
|
|
18
|
+
fetch("EAPK_#{id[5..]}", nil)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
data/lib/ea/model.rb
CHANGED
|
@@ -19,6 +19,7 @@ module Ea
|
|
|
19
19
|
autoload :Base, "ea/model/base"
|
|
20
20
|
autoload :Metadata, "ea/model/metadata"
|
|
21
21
|
autoload :Point, "ea/model/point"
|
|
22
|
+
autoload :PackageAliasIndex, "ea/model/package_alias_index"
|
|
22
23
|
autoload :Bounds, "ea/model/bounds"
|
|
23
24
|
autoload :Waypoint, "ea/model/waypoint"
|
|
24
25
|
autoload :Annotation, "ea/model/annotation"
|
|
@@ -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),
|
|
@@ -82,18 +90,16 @@ module Ea
|
|
|
82
90
|
{ literals: literals }
|
|
83
91
|
end
|
|
84
92
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
#
|
|
92
|
-
|
|
93
|
-
simple = raw.split(":").last
|
|
94
|
-
simple = raw if simple.nil? || simple.empty?
|
|
93
|
+
# A name that already carries a namespace prefix
|
|
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)
|
|
97
|
+
name = element.name.to_s
|
|
98
|
+
return name if name.include?(":")
|
|
99
|
+
return "#{nested_parent}::#{name}" if nested_parent
|
|
100
|
+
|
|
95
101
|
pkg_name = containing_package&.name
|
|
96
|
-
pkg_name ? "#{pkg_name}::#{
|
|
102
|
+
pkg_name ? "#{pkg_name}::#{name}" : name
|
|
97
103
|
end
|
|
98
104
|
|
|
99
105
|
def boolean(value)
|
|
@@ -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
|
|
@@ -6,9 +6,10 @@ module Ea
|
|
|
6
6
|
module Sources
|
|
7
7
|
module Xmi
|
|
8
8
|
class DiagramBuilder
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
9
|
+
# The UMLDI edge EA exported for a connector on a diagram: its
|
|
10
|
+
# ordered waypoints and its source/target model elements. A connector
|
|
11
|
+
# drawn by more than one edge in the same diagram has neither; an edge
|
|
12
|
+
# with fewer than two waypoints has no route.
|
|
12
13
|
class UmldiEdges
|
|
13
14
|
def initialize(xmi_path)
|
|
14
15
|
@xmi_path = xmi_path
|
|
@@ -18,8 +19,34 @@ module Ea
|
|
|
18
19
|
routes.dig(diagram_id, connector_id)
|
|
19
20
|
end
|
|
20
21
|
|
|
22
|
+
def ends_for(diagram_id, connector_id)
|
|
23
|
+
ends.dig(diagram_id, connector_id)
|
|
24
|
+
end
|
|
25
|
+
|
|
21
26
|
private
|
|
22
27
|
|
|
28
|
+
def ends
|
|
29
|
+
@ends ||= doc.xpath("//*[local-name()=\"Diagram\"]").to_h do |diagram|
|
|
30
|
+
[diagram["id"] || diagram["xmi:id"], diagram_ends(diagram)]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def diagram_ends(diagram)
|
|
35
|
+
unique_edges(diagram).each_with_object({}) do |(connector_id, edge), acc|
|
|
36
|
+
acc[connector_id] = [edge["source"], edge["target"]] if edge["source"] && edge["target"]
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Edges keyed by connector, keeping only connectors drawn by exactly
|
|
41
|
+
# one edge of the diagram, whatever that edge carries.
|
|
42
|
+
def unique_edges(diagram)
|
|
43
|
+
diagram.xpath("./ownedElement[@modelElement]")
|
|
44
|
+
.select { |node| node.attributes["type"]&.value.to_s.end_with?("UMLEdge") }
|
|
45
|
+
.group_by { |edge| edge["modelElement"] }
|
|
46
|
+
.select { |_, same| same.one? }
|
|
47
|
+
.transform_values(&:first)
|
|
48
|
+
end
|
|
49
|
+
|
|
23
50
|
def routes
|
|
24
51
|
@routes ||= doc.xpath("//*[local-name()=\"Diagram\"]").to_h do |diagram|
|
|
25
52
|
[diagram["id"] || diagram["xmi:id"], diagram_routes(diagram)]
|
|
@@ -27,9 +54,8 @@ module Ea
|
|
|
27
54
|
end
|
|
28
55
|
|
|
29
56
|
def diagram_routes(diagram)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
points = same.one? ? waypoints(same.first) : []
|
|
57
|
+
unique_edges(diagram).each_with_object({}) do |(connector_id, edge), acc|
|
|
58
|
+
points = waypoints(edge)
|
|
33
59
|
acc[connector_id] = points if points.size >= 2
|
|
34
60
|
end
|
|
35
61
|
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
|
|
@@ -52,10 +55,14 @@ module Ea
|
|
|
52
55
|
name: props&.name || ext_diagram.id,
|
|
53
56
|
package_id: ext_diagram.model&.package,
|
|
54
57
|
diagram_type: props&.type&.split(":")&.last&.downcase,
|
|
55
|
-
bounds:
|
|
58
|
+
bounds: page_bounds(ext_diagram),
|
|
56
59
|
style: style_for(ext_diagram),
|
|
57
60
|
style_ex: style_ex_for(ext_diagram),
|
|
58
61
|
show_notes: style_ex_for(ext_diagram).to_s.include?("ShowNotes=1"),
|
|
62
|
+
show_package_contents: style_flag?(ext_diagram, "PackageContents"),
|
|
63
|
+
show_foreign: style_flag?(ext_diagram, "HighlightForeign"),
|
|
64
|
+
show_parents: !style_flag?(ext_diagram, "HideParents"),
|
|
65
|
+
hand_draw: style_flag?(ext_diagram, "HandDraw"),
|
|
59
66
|
elements: build_elements(placed_elements, id, umldi_keywords),
|
|
60
67
|
connectors: build_connectors(placed_connectors,
|
|
61
68
|
placed_elements, id),
|
|
@@ -89,18 +96,18 @@ module Ea
|
|
|
89
96
|
|
|
90
97
|
private
|
|
91
98
|
|
|
92
|
-
def
|
|
93
|
-
|
|
99
|
+
def style_flag?(ext_diagram, key)
|
|
100
|
+
value = style_for(ext_diagram).to_s[/(?:\A|;)#{key}=(\d+)/, 1]
|
|
101
|
+
value.to_i.nonzero? ? true : false
|
|
102
|
+
end
|
|
94
103
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return nil unless
|
|
104
|
+
def page_bounds(ext_diagram)
|
|
105
|
+
style = style_for(ext_diagram).to_s
|
|
106
|
+
width = style[/(?:\A|;)DocSize\.cx=(\d+)/, 1]
|
|
107
|
+
height = style[/(?:\A|;)DocSize\.cy=(\d+)/, 1]
|
|
108
|
+
return nil unless width && height
|
|
100
109
|
|
|
101
|
-
Ea::Model::Bounds.new(x: 0, y: 0,
|
|
102
|
-
width: right - left,
|
|
103
|
-
height: bottom - top)
|
|
110
|
+
Ea::Model::Bounds.new(x: 0, y: 0, width: width.to_i, height: height.to_i)
|
|
104
111
|
end
|
|
105
112
|
|
|
106
113
|
def build_elements(placed, owner_id, umldi_keywords = {})
|
|
@@ -110,17 +117,21 @@ module Ea
|
|
|
110
117
|
def build_element(placed, owner_id, umldi_keywords = {})
|
|
111
118
|
geom = placed.geometry
|
|
112
119
|
style = placed.style
|
|
120
|
+
bounds = build_bounds(geom.left, geom.top, geom.right, geom.bottom)
|
|
121
|
+
font = (@font_settings ||= FontSettings.new(xmi_path)).for(placed)
|
|
113
122
|
Ea::Model::DiagramElement.new(
|
|
114
123
|
id: IdNormalizer.synthetic_id(owner_id, "elem", placed.subject),
|
|
115
124
|
diagram_id: owner_id,
|
|
116
125
|
model_element_ref: placed.subject,
|
|
117
|
-
bounds:
|
|
118
|
-
image_bounds: build_bounds(geom.img_left, geom.img_top, geom.img_right, geom.img_bottom),
|
|
126
|
+
bounds: bounds,
|
|
127
|
+
image_bounds: (build_bounds(geom.img_left, geom.img_top, geom.img_right, geom.img_bottom) unless bounds),
|
|
119
128
|
background_color: style.background_color,
|
|
120
129
|
line_color: style.line_color,
|
|
121
130
|
line_width: style.line_width,
|
|
122
|
-
font_family:
|
|
123
|
-
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",
|
|
124
135
|
font_bold: style.bold,
|
|
125
136
|
font_italic: style.italic,
|
|
126
137
|
font_underline: style.underline,
|
|
@@ -129,7 +140,7 @@ module Ea
|
|
|
129
140
|
z_order: placed.seqno,
|
|
130
141
|
duid: style.duid,
|
|
131
142
|
umldi_keyword: umldi_keywords[placed.subject],
|
|
132
|
-
style:
|
|
143
|
+
style: (@stereotype_profiles ||= StereotypeProfiles.new(root)).for(placed.subject)
|
|
133
144
|
)
|
|
134
145
|
end
|
|
135
146
|
|
|
@@ -146,17 +157,19 @@ module Ea
|
|
|
146
157
|
by_duid = placed_elements.each_with_object({}) do |pe, acc|
|
|
147
158
|
acc[pe.style.duid] = pe if pe.style.duid
|
|
148
159
|
end
|
|
160
|
+
by_subject = placed_elements.group_by { |pe| guid_key(pe.subject) }
|
|
149
161
|
|
|
150
162
|
placed_connectors.each_with_index.map do |placed, index|
|
|
151
|
-
build_connector(placed, by_duid, owner_id, index)
|
|
163
|
+
build_connector(placed, by_duid, by_subject, owner_id, index)
|
|
152
164
|
end
|
|
153
165
|
end
|
|
154
166
|
|
|
155
|
-
def build_connector(placed, by_duid, owner_id, rank)
|
|
167
|
+
def build_connector(placed, by_duid, by_subject, owner_id, rank)
|
|
156
168
|
geom = placed.geometry
|
|
157
169
|
style = placed.style
|
|
158
|
-
|
|
159
|
-
|
|
170
|
+
edge_source, edge_target = umldi_ends(owner_id, placed)
|
|
171
|
+
source = by_duid[style.soid] || sole_placement(by_subject, edge_source)
|
|
172
|
+
target = by_duid[style.eoid] || sole_placement(by_subject, edge_target)
|
|
160
173
|
ext_meta = connector_meta(placed.subject)
|
|
161
174
|
route = connector_route(placed, source, target, owner_id, ext_meta[:ea_type])
|
|
162
175
|
Ea::Model::DiagramConnector.new(
|
|
@@ -172,6 +185,9 @@ module Ea
|
|
|
172
185
|
target_edge: geom.edge,
|
|
173
186
|
connector_type: ext_meta[:ea_type],
|
|
174
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),
|
|
175
191
|
waypoints: route[:waypoints],
|
|
176
192
|
style: route[:style],
|
|
177
193
|
line_color: style.color,
|
|
@@ -189,7 +205,11 @@ module Ea
|
|
|
189
205
|
{
|
|
190
206
|
ea_type: props&.ea_type,
|
|
191
207
|
direction: props&.direction
|
|
192
|
-
}
|
|
208
|
+
}.merge(connector_metadata.for(connector))
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def connector_metadata
|
|
212
|
+
@connector_metadata ||= ConnectorMetadata.new(xmi_path)
|
|
193
213
|
end
|
|
194
214
|
|
|
195
215
|
def connector_index
|
|
@@ -234,6 +254,23 @@ module Ea
|
|
|
234
254
|
route&.map { |x, y| Ea::Model::Waypoint.new(position: Ea::Model::Point.new(x: x, y: y)) }
|
|
235
255
|
end
|
|
236
256
|
|
|
257
|
+
def umldi_ends(diagram_id, placed)
|
|
258
|
+
return unless xmi_path
|
|
259
|
+
|
|
260
|
+
umldi_edges.ends_for(diagram_id, placed.subject)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def sole_placement(by_subject, subject)
|
|
264
|
+
placements = by_subject[guid_key(subject)]
|
|
265
|
+
placements.first if placements&.one?
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# EA spells one GUID with an EAID_ or EAPK_ prefix and mixed case
|
|
269
|
+
# depending on the element kind.
|
|
270
|
+
def guid_key(subject)
|
|
271
|
+
subject&.sub(/\AEA(?:ID|PK)_/, "")&.upcase
|
|
272
|
+
end
|
|
273
|
+
|
|
237
274
|
def umldi_edges
|
|
238
275
|
@umldi_edges ||= UmldiEdges.new(xmi_path)
|
|
239
276
|
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ea
|
|
4
|
+
module Sources
|
|
5
|
+
module Xmi
|
|
6
|
+
# Builds an `Ea::Model::Legend` from the `value` of an element's
|
|
7
|
+
# `<xrefs>` element. The CustomProperties description inside it
|
|
8
|
+
# uses the same `@PROP=...@ENDPROP;` encoding as the QEA t_xref
|
|
9
|
+
# row, so parsing is delegated to the QEA legend builder.
|
|
10
|
+
#
|
|
11
|
+
# Returns nil when the value carries no legend properties.
|
|
12
|
+
class LegendBuilder
|
|
13
|
+
CLIENT = "xmi-element"
|
|
14
|
+
|
|
15
|
+
XrefRow = Struct.new(:name, :description)
|
|
16
|
+
|
|
17
|
+
# Answers the one CustomProperties xref lookup the QEA builder makes.
|
|
18
|
+
class SingleElementXrefs
|
|
19
|
+
def initialize(description)
|
|
20
|
+
@rows = [XrefRow.new(Qea::LegendBuilder::LEGEND_XREF_NAME, description)]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def xrefs_for_client(_guid)
|
|
24
|
+
@rows
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def initialize(xrefs_value)
|
|
29
|
+
@xrefs_value = xrefs_value
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def build
|
|
33
|
+
return nil if @xrefs_value.to_s.empty?
|
|
34
|
+
|
|
35
|
+
Qea::LegendBuilder.new(SingleElementXrefs.new(@xrefs_value)).build_for(CLIENT)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -10,9 +10,12 @@ module Ea
|
|
|
10
10
|
# inside `<xmi:Extension>/<elements>`. Each carries a
|
|
11
11
|
# `<properties documentation="..."/>` child with the body text.
|
|
12
12
|
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
13
|
+
# `uml:Text` elements are projected the same way; those whose
|
|
14
|
+
# CustomProperties xref carries legend configuration also
|
|
15
|
+
# get an `Ea::Model::Legend` payload.
|
|
15
16
|
class NoteBuilder
|
|
17
|
+
NOTE_TYPES = { "uml:Note" => "Note", "uml:Text" => "Text" }.freeze
|
|
18
|
+
|
|
16
19
|
attr_reader :root
|
|
17
20
|
|
|
18
21
|
def initialize(root)
|
|
@@ -29,17 +32,23 @@ module Ea
|
|
|
29
32
|
private
|
|
30
33
|
|
|
31
34
|
def build_one(ext_element)
|
|
32
|
-
|
|
33
|
-
return nil unless
|
|
35
|
+
note_type = NOTE_TYPES[ext_element.type]
|
|
36
|
+
return nil unless note_type
|
|
34
37
|
|
|
35
|
-
props = ext_element.properties
|
|
36
38
|
Ea::Model::Note.new(
|
|
37
39
|
id: ext_element.idref,
|
|
38
40
|
name: ext_element.name,
|
|
39
|
-
body:
|
|
40
|
-
note_type:
|
|
41
|
+
body: ext_element.properties&.documentation,
|
|
42
|
+
note_type: note_type,
|
|
43
|
+
legend: legend_for(ext_element, note_type)
|
|
41
44
|
)
|
|
42
45
|
end
|
|
46
|
+
|
|
47
|
+
def legend_for(ext_element, note_type)
|
|
48
|
+
return nil unless note_type == "Text"
|
|
49
|
+
|
|
50
|
+
LegendBuilder.new(ext_element.xrefs&.value).build
|
|
51
|
+
end
|
|
43
52
|
end
|
|
44
53
|
end
|
|
45
54
|
end
|
|
@@ -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
|
|
@@ -9,6 +9,8 @@ module Ea
|
|
|
9
9
|
class RelationshipBuilder
|
|
10
10
|
# Connector types that imply an aggregation on the source end
|
|
11
11
|
# when the end itself records none.
|
|
12
|
+
CONNECTOR_STEREOTYPE = /(?<=@STEREO;Name=)[^;]+/
|
|
13
|
+
|
|
12
14
|
AGGREGATION_FOR_CONNECTOR_TYPE = {
|
|
13
15
|
"Aggregation" => "shared",
|
|
14
16
|
"Composition" => "composite"
|
|
@@ -22,13 +24,13 @@ module Ea
|
|
|
22
24
|
|
|
23
25
|
def build_all
|
|
24
26
|
relationships = []
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
30
32
|
end
|
|
31
|
-
relationships
|
|
33
|
+
relationships + package_import_relationships(relationships)
|
|
32
34
|
end
|
|
33
35
|
|
|
34
36
|
def build_one(element)
|
|
@@ -40,9 +42,44 @@ module Ea
|
|
|
40
42
|
|
|
41
43
|
private
|
|
42
44
|
|
|
45
|
+
def package_import_relationships(built)
|
|
46
|
+
built_ids = built.to_set(&:id)
|
|
47
|
+
connector_index.filter_map do |id, connector|
|
|
48
|
+
next if built_ids.include?(id) || connector.properties&.ea_type != "Package"
|
|
49
|
+
|
|
50
|
+
package_import_relationship(id, connector)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def connector_stereotype(connector)
|
|
55
|
+
connector&.xrefs&.value.to_s[CONNECTOR_STEREOTYPE]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def package_import_relationship(id, connector)
|
|
59
|
+
Ea::Model::Association.new(
|
|
60
|
+
id: id,
|
|
61
|
+
stereotype: connector_stereotype(connector),
|
|
62
|
+
source_id: connector.source&.idref,
|
|
63
|
+
target_id: connector.target&.idref,
|
|
64
|
+
source_navigable: navigable?(id, :source),
|
|
65
|
+
target_navigable: navigable?(id, :target)
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
|
|
43
69
|
def walk(element, &block)
|
|
44
70
|
yield element if block
|
|
45
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
|
|
46
83
|
end
|
|
47
84
|
|
|
48
85
|
def common_args(element)
|
|
@@ -51,6 +88,7 @@ module Ea
|
|
|
51
88
|
id: id,
|
|
52
89
|
name: element.name,
|
|
53
90
|
qualified_name: element.name,
|
|
91
|
+
stereotype: connector_stereotype(connector_index[id]),
|
|
54
92
|
annotations: AnnotationBuilder.from_element(element, id)
|
|
55
93
|
}
|
|
56
94
|
end
|
|
@@ -88,8 +126,8 @@ module Ea
|
|
|
88
126
|
target_multiplicity_raw: raw_card(target.type&.multiplicity),
|
|
89
127
|
source_aggregation: source_aggregation(connector),
|
|
90
128
|
target_aggregation: target.type&.aggregation || "none",
|
|
91
|
-
source_navigable: navigable?(element, :source),
|
|
92
|
-
target_navigable: navigable?(element, :target)
|
|
129
|
+
source_navigable: navigable?(element.id, :source),
|
|
130
|
+
target_navigable: navigable?(element.id, :target)
|
|
93
131
|
}
|
|
94
132
|
end
|
|
95
133
|
|
|
@@ -118,8 +156,8 @@ module Ea
|
|
|
118
156
|
target_multiplicity_upper: parse_upper(target_end),
|
|
119
157
|
source_aggregation: source_end&.aggregation || "none",
|
|
120
158
|
target_aggregation: target_end&.aggregation || "none",
|
|
121
|
-
source_navigable: navigable?(element, :source),
|
|
122
|
-
target_navigable: navigable?(element, :target)
|
|
159
|
+
source_navigable: navigable?(element.id, :source),
|
|
160
|
+
target_navigable: navigable?(element.id, :target)
|
|
123
161
|
}
|
|
124
162
|
end
|
|
125
163
|
|
|
@@ -197,8 +235,8 @@ module Ea
|
|
|
197
235
|
end
|
|
198
236
|
end
|
|
199
237
|
|
|
200
|
-
def navigable?(
|
|
201
|
-
connector = connector_index[
|
|
238
|
+
def navigable?(id, endpoint)
|
|
239
|
+
connector = connector_index[id]
|
|
202
240
|
end_element = connector&.public_send(endpoint)
|
|
203
241
|
flag = end_element&.modifiers&.is_navigable
|
|
204
242
|
flag.nil? || flag
|
|
@@ -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
|
data/lib/ea/sources/xmi.rb
CHANGED
|
@@ -25,6 +25,7 @@ module Ea
|
|
|
25
25
|
autoload :RelationshipBuilder, "ea/sources/xmi/relationship_builder"
|
|
26
26
|
autoload :AnnotationBuilder, "ea/sources/xmi/annotation_builder"
|
|
27
27
|
autoload :TagBuilder, "ea/sources/xmi/tag_builder"
|
|
28
|
+
autoload :LegendBuilder, "ea/sources/xmi/legend_builder"
|
|
28
29
|
autoload :NoteBuilder, "ea/sources/xmi/note_builder"
|
|
29
30
|
autoload :StereotypeBuilder, "ea/sources/xmi/stereotype_builder"
|
|
30
31
|
autoload :DiagramBuilder, "ea/sources/xmi/diagram_builder"
|
|
@@ -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.135
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -680,6 +680,7 @@ files:
|
|
|
680
680
|
- lib/ea/model/note.rb
|
|
681
681
|
- lib/ea/model/operation.rb
|
|
682
682
|
- lib/ea/model/package.rb
|
|
683
|
+
- lib/ea/model/package_alias_index.rb
|
|
683
684
|
- lib/ea/model/parameter.rb
|
|
684
685
|
- lib/ea/model/point.rb
|
|
685
686
|
- lib/ea/model/polymorphic_maps.rb
|
|
@@ -851,6 +852,9 @@ files:
|
|
|
851
852
|
- lib/ea/sources/xmi/annotation_builder.rb
|
|
852
853
|
- lib/ea/sources/xmi/classifier_builder.rb
|
|
853
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
|
|
854
858
|
- lib/ea/sources/xmi/diagram_builder/umldi_edges.rb
|
|
855
859
|
- lib/ea/sources/xmi/extension_elements.rb
|
|
856
860
|
- lib/ea/sources/xmi/extension_geometry_parser.rb
|
|
@@ -858,6 +862,7 @@ files:
|
|
|
858
862
|
- lib/ea/sources/xmi/extension_types.rb
|
|
859
863
|
- lib/ea/sources/xmi/id_normalizer.rb
|
|
860
864
|
- lib/ea/sources/xmi/instance_builder.rb
|
|
865
|
+
- lib/ea/sources/xmi/legend_builder.rb
|
|
861
866
|
- lib/ea/sources/xmi/metadata_builder.rb
|
|
862
867
|
- lib/ea/sources/xmi/note_builder.rb
|
|
863
868
|
- lib/ea/sources/xmi/operation_builder.rb
|