ea 0.1.0 → 0.1.4

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.
Files changed (227) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +125 -0
  3. data/Rakefile +12 -4
  4. data/TODO.next/00-publish-blocking-bugs.md +74 -0
  5. data/TODO.next/01-standalone-ea-gem-identity.md +76 -0
  6. data/TODO.next/02-optional-lutaml-uml-dependency.md +47 -0
  7. data/TODO.next/03-slim-lutaml-uml.md +79 -0
  8. data/TODO.next/04-loader-registry-for-uml-repository.md +49 -0
  9. data/TODO.next/05-extract-shared-transformer-methods.md +14 -0
  10. data/TODO.next/06-deduplicate-stereotype-loading.md +17 -0
  11. data/TODO.next/07-transformer-registry-in-factory.md +20 -0
  12. data/TODO.next/08-connector-type-registry.md +27 -0
  13. data/TODO.next/09-element-renderer-registry.md +29 -0
  14. data/TODO.next/10-connector-renderer-lsp.md +18 -0
  15. data/TODO.next/11-consolidate-style-knowledge.md +33 -0
  16. data/TODO.next/12-data-driven-from-db-row.md +24 -0
  17. data/TODO.next/13-extract-duplicated-methods.md +17 -0
  18. data/TODO.next/14-remove-dead-code.md +10 -0
  19. data/TODO.next/15-narrow-exception-handling.md +39 -0
  20. data/TODO.next/16-repository-indexes.md +28 -0
  21. data/TODO.next/17-fix-spec-quality-and-coverage.md +32 -0
  22. data/TODO.next/18-xmi-tool-specific-parser-architecture.md +172 -0
  23. data/TODO.next/19-fix-ea-gemspec-dependency-declarations.md +56 -0
  24. data/TODO.next/20-ci-requires-unreleased-lutaml-uml.md +63 -0
  25. data/TODO.next/21-qeatoxmi-via-xmi-gem.md +340 -0
  26. data/TODO.next/22-strip-respond-to-from-qeatoxmi-specs.md +32 -0
  27. data/TODO.next/23-cleanup-idallocator.md +41 -0
  28. data/TODO.next/24-tighten-parity-specs.md +42 -0
  29. data/TODO.next/25-sparx-eaid-format-for-synthesized-ids.md +62 -0
  30. data/TODO.next/26-fix-uppervalue-lowervalue-count-gap.md +51 -0
  31. data/TODO.next/27-extract-cardinality-module.md +68 -0
  32. data/TODO.next/28-extract-xml-sanitizer.md +51 -0
  33. data/TODO.next/29-ocp-registry-for-classifier-builders.md +58 -0
  34. data/TODO.next/30-struct-return-for-association-end.md +37 -0
  35. data/TODO.next/31-idallocator-specs.md +27 -0
  36. data/TODO.next/32-phase2-gap-sentinel-specs.md +53 -0
  37. data/TODO.next/33-normalize-lower-cleanup.md +30 -0
  38. data/TODO.next/34-document-member-end-order-rt-prefix.md +29 -0
  39. data/TODO.next/35-walk-runstate-for-instance-slots.md +76 -0
  40. data/TODO.next/36-wire-interface-realization.md +50 -0
  41. data/TODO.next/37-visibility-returns-real-booleans.md +36 -0
  42. data/config/diagram_styles.yml +200 -0
  43. data/config/model_transformations.yml +266 -0
  44. data/config/qea_schema.yml +1024 -0
  45. data/docs/ea_to_uml_type_mapping.md +89 -0
  46. data/docs/xmi_qea_conversion_capabilities.md +99 -0
  47. data/examples/lur/20251010_current_plateau_v5.1.lur +0 -0
  48. data/examples/lur/basic.lur +0 -0
  49. data/examples/lur/test-output.lur +0 -0
  50. data/examples/lur/test.lur +0 -0
  51. data/examples/lur_basic_usage.rb +221 -0
  52. data/examples/lur_cli_workflow.rb +263 -0
  53. data/examples/lur_statistics.rb +326 -0
  54. data/examples/qea/20251010_current_plateau_v5.1.qea +0 -0
  55. data/examples/qea/ArcGISWorkspace_template.qea +0 -0
  56. data/examples/qea/README_qea_parser.adoc +230 -0
  57. data/examples/qea/UmlModel_template.qea +0 -0
  58. data/examples/qea/basic.qea +0 -0
  59. data/examples/qea/simple.qea +0 -0
  60. data/examples/qea/simple_example.qea +0 -0
  61. data/examples/qea/test.qea +0 -0
  62. data/examples/qea_standalone_query.rb +73 -0
  63. data/examples/qea_to_repository.rb +51 -0
  64. data/examples/smoke_test_real_qea.rb +81 -0
  65. data/exe/ea +7 -0
  66. data/lib/ea/cli/app.rb +72 -0
  67. data/lib/ea/cli/command/base.rb +80 -0
  68. data/lib/ea/cli/command/convert.rb +62 -0
  69. data/lib/ea/cli/command/diagrams.rb +81 -0
  70. data/lib/ea/cli/command/list.rb +61 -0
  71. data/lib/ea/cli/command/parse.rb +29 -0
  72. data/lib/ea/cli/command/stats.rb +20 -0
  73. data/lib/ea/cli/command/validate.rb +41 -0
  74. data/lib/ea/cli/command.rb +15 -0
  75. data/lib/ea/cli/error.rb +34 -0
  76. data/lib/ea/cli/output/formatter.rb +34 -0
  77. data/lib/ea/cli/output/json_formatter.rb +20 -0
  78. data/lib/ea/cli/output/table_formatter.rb +42 -0
  79. data/lib/ea/cli/output/yaml_formatter.rb +20 -0
  80. data/lib/ea/cli/output.rb +56 -0
  81. data/lib/ea/cli.rb +17 -0
  82. data/lib/ea/diagram/configuration.rb +379 -0
  83. data/lib/ea/diagram/element_renderers/base_renderer.rb +77 -0
  84. data/lib/ea/diagram/element_renderers/class_renderer.rb +323 -0
  85. data/lib/ea/diagram/element_renderers/connector_renderer.rb +41 -0
  86. data/lib/ea/diagram/element_renderers/package_renderer.rb +61 -0
  87. data/lib/ea/diagram/element_renderers.rb +43 -0
  88. data/lib/ea/diagram/extractor.rb +560 -0
  89. data/lib/ea/diagram/layout_engine.rb +170 -0
  90. data/lib/ea/diagram/path_builder.rb +202 -0
  91. data/lib/ea/diagram/style_parser.rb +42 -0
  92. data/lib/ea/diagram/style_resolver.rb +276 -0
  93. data/lib/ea/diagram/svg_renderer.rb +274 -0
  94. data/lib/ea/diagram/util.rb +73 -0
  95. data/lib/ea/diagram.rb +47 -0
  96. data/lib/ea/qea/benchmark.rb +210 -0
  97. data/lib/ea/qea/database.rb +308 -0
  98. data/lib/ea/qea/factory/association_builder.rb +203 -0
  99. data/lib/ea/qea/factory/association_transformer.rb +91 -0
  100. data/lib/ea/qea/factory/attribute_tag_transformer.rb +57 -0
  101. data/lib/ea/qea/factory/attribute_transformer.rb +93 -0
  102. data/lib/ea/qea/factory/base_transformer.rb +177 -0
  103. data/lib/ea/qea/factory/class_transformer.rb +116 -0
  104. data/lib/ea/qea/factory/constraint_transformer.rb +75 -0
  105. data/lib/ea/qea/factory/data_type_transformer.rb +77 -0
  106. data/lib/ea/qea/factory/diagram_transformer.rb +157 -0
  107. data/lib/ea/qea/factory/document_builder.rb +283 -0
  108. data/lib/ea/qea/factory/ea_to_uml_factory.rb +229 -0
  109. data/lib/ea/qea/factory/enum_transformer.rb +74 -0
  110. data/lib/ea/qea/factory/generalization_builder.rb +227 -0
  111. data/lib/ea/qea/factory/generalization_transformer.rb +98 -0
  112. data/lib/ea/qea/factory/instance_transformer.rb +68 -0
  113. data/lib/ea/qea/factory/object_property_transformer.rb +58 -0
  114. data/lib/ea/qea/factory/operation_transformer.rb +66 -0
  115. data/lib/ea/qea/factory/package_transformer.rb +145 -0
  116. data/lib/ea/qea/factory/reference_resolver.rb +99 -0
  117. data/lib/ea/qea/factory/stereotype_loader.rb +39 -0
  118. data/lib/ea/qea/factory/tagged_value_transformer.rb +38 -0
  119. data/lib/ea/qea/factory/transformer_registry.rb +80 -0
  120. data/lib/ea/qea/factory.rb +37 -0
  121. data/lib/ea/qea/file_detector.rb +178 -0
  122. data/lib/ea/qea/infrastructure/database_connection.rb +100 -0
  123. data/lib/ea/qea/infrastructure/schema_reader.rb +136 -0
  124. data/lib/ea/qea/infrastructure/table_reader.rb +224 -0
  125. data/lib/ea/qea/infrastructure.rb +12 -0
  126. data/lib/ea/qea/models/base_model.rb +59 -0
  127. data/lib/ea/qea/models/ea_attribute.rb +109 -0
  128. data/lib/ea/qea/models/ea_attribute_tag.rb +100 -0
  129. data/lib/ea/qea/models/ea_complexity_type.rb +79 -0
  130. data/lib/ea/qea/models/ea_connector.rb +160 -0
  131. data/lib/ea/qea/models/ea_connector_type.rb +60 -0
  132. data/lib/ea/qea/models/ea_constraint_type.rb +63 -0
  133. data/lib/ea/qea/models/ea_datatype.rb +104 -0
  134. data/lib/ea/qea/models/ea_diagram.rb +115 -0
  135. data/lib/ea/qea/models/ea_diagram_link.rb +78 -0
  136. data/lib/ea/qea/models/ea_diagram_object.rb +73 -0
  137. data/lib/ea/qea/models/ea_diagram_type.rb +56 -0
  138. data/lib/ea/qea/models/ea_document.rb +63 -0
  139. data/lib/ea/qea/models/ea_object.rb +223 -0
  140. data/lib/ea/qea/models/ea_object_constraint.rb +53 -0
  141. data/lib/ea/qea/models/ea_object_property.rb +87 -0
  142. data/lib/ea/qea/models/ea_object_type.rb +73 -0
  143. data/lib/ea/qea/models/ea_operation.rb +127 -0
  144. data/lib/ea/qea/models/ea_operation_param.rb +76 -0
  145. data/lib/ea/qea/models/ea_package.rb +78 -0
  146. data/lib/ea/qea/models/ea_script.rb +62 -0
  147. data/lib/ea/qea/models/ea_status_type.rb +66 -0
  148. data/lib/ea/qea/models/ea_stereotype.rb +57 -0
  149. data/lib/ea/qea/models/ea_tagged_value.rb +99 -0
  150. data/lib/ea/qea/models/ea_xref.rb +165 -0
  151. data/lib/ea/qea/models.rb +35 -0
  152. data/lib/ea/qea/repositories/base_repository.rb +225 -0
  153. data/lib/ea/qea/repositories/object_repository.rb +219 -0
  154. data/lib/ea/qea/repositories.rb +10 -0
  155. data/lib/ea/qea/services/configuration.rb +211 -0
  156. data/lib/ea/qea/services/database_loader.rb +191 -0
  157. data/lib/ea/qea/services.rb +10 -0
  158. data/lib/ea/qea/validation/association_validator.rb +73 -0
  159. data/lib/ea/qea/validation/attribute_validator.rb +91 -0
  160. data/lib/ea/qea/validation/base_validator.rb +331 -0
  161. data/lib/ea/qea/validation/class_validator.rb +121 -0
  162. data/lib/ea/qea/validation/database/circular_reference_validator.rb +109 -0
  163. data/lib/ea/qea/validation/database/orphan_validator.rb +153 -0
  164. data/lib/ea/qea/validation/database/referential_integrity_validator.rb +128 -0
  165. data/lib/ea/qea/validation/database.rb +16 -0
  166. data/lib/ea/qea/validation/diagram_validator.rb +112 -0
  167. data/lib/ea/qea/validation/formatters/json_formatter.rb +137 -0
  168. data/lib/ea/qea/validation/formatters/text_formatter.rb +235 -0
  169. data/lib/ea/qea/validation/formatters.rb +12 -0
  170. data/lib/ea/qea/validation/operation_validator.rb +71 -0
  171. data/lib/ea/qea/validation/package_validator.rb +111 -0
  172. data/lib/ea/qea/validation/validation_engine.rb +387 -0
  173. data/lib/ea/qea/validation/validation_message.rb +144 -0
  174. data/lib/ea/qea/validation/validation_result.rb +210 -0
  175. data/lib/ea/qea/validation/validator_registry.rb +134 -0
  176. data/lib/ea/qea/validation.rb +28 -0
  177. data/lib/ea/qea/verification/comparison_result.rb +264 -0
  178. data/lib/ea/qea/verification/document_normalizer.rb +169 -0
  179. data/lib/ea/qea/verification/document_verifier.rb +322 -0
  180. data/lib/ea/qea/verification/element_comparator.rb +277 -0
  181. data/lib/ea/qea/verification/structure_matcher.rb +287 -0
  182. data/lib/ea/qea/verification.rb +14 -0
  183. data/lib/ea/qea.rb +185 -0
  184. data/lib/ea/transformations/configuration.rb +333 -0
  185. data/lib/ea/transformations/format_registry.rb +366 -0
  186. data/lib/ea/transformations/parsers/base_parser.rb +482 -0
  187. data/lib/ea/transformations/parsers/qea_parser.rb +401 -0
  188. data/lib/ea/transformations/parsers/xmi_parser.rb +243 -0
  189. data/lib/ea/transformations/transformation_engine.rb +390 -0
  190. data/lib/ea/transformations.rb +85 -0
  191. data/lib/ea/transformers/qea_to_xmi/association_end.rb +19 -0
  192. data/lib/ea/transformers/qea_to_xmi/cardinality.rb +96 -0
  193. data/lib/ea/transformers/qea_to_xmi/context.rb +106 -0
  194. data/lib/ea/transformers/qea_to_xmi/guid_format.rb +56 -0
  195. data/lib/ea/transformers/qea_to_xmi/id_allocator.rb +92 -0
  196. data/lib/ea/transformers/qea_to_xmi/run_state.rb +107 -0
  197. data/lib/ea/transformers/qea_to_xmi/transformer.rb +607 -0
  198. data/lib/ea/transformers/qea_to_xmi/visibility.rb +73 -0
  199. data/lib/ea/transformers/qea_to_xmi.rb +29 -0
  200. data/lib/ea/transformers/uml_to_xmi/id_generator.rb +54 -0
  201. data/lib/ea/transformers/uml_to_xmi/transformer.rb +152 -0
  202. data/lib/ea/transformers/uml_to_xmi/writer.rb +96 -0
  203. data/lib/ea/transformers/uml_to_xmi.rb +16 -0
  204. data/lib/ea/transformers.rb +34 -0
  205. data/lib/ea/version.rb +1 -1
  206. data/lib/ea/xmi/liquid_drops/association_drop.rb +56 -0
  207. data/lib/ea/xmi/liquid_drops/attribute_drop.rb +72 -0
  208. data/lib/ea/xmi/liquid_drops/cardinality_drop.rb +35 -0
  209. data/lib/ea/xmi/liquid_drops/connector_drop.rb +54 -0
  210. data/lib/ea/xmi/liquid_drops/constraint_drop.rb +29 -0
  211. data/lib/ea/xmi/liquid_drops/data_type_drop.rb +63 -0
  212. data/lib/ea/xmi/liquid_drops/dependency_drop.rb +36 -0
  213. data/lib/ea/xmi/liquid_drops/diagram_drop.rb +34 -0
  214. data/lib/ea/xmi/liquid_drops/enum_drop.rb +49 -0
  215. data/lib/ea/xmi/liquid_drops/enum_owned_literal_drop.rb +25 -0
  216. data/lib/ea/xmi/liquid_drops/generalization_attribute_drop.rb +87 -0
  217. data/lib/ea/xmi/liquid_drops/generalization_drop.rb +127 -0
  218. data/lib/ea/xmi/liquid_drops/klass_drop.rb +191 -0
  219. data/lib/ea/xmi/liquid_drops/operation_drop.rb +29 -0
  220. data/lib/ea/xmi/liquid_drops/package_drop.rb +108 -0
  221. data/lib/ea/xmi/liquid_drops/root_drop.rb +34 -0
  222. data/lib/ea/xmi/liquid_drops/source_target_drop.rb +43 -0
  223. data/lib/ea/xmi/lookup_service.rb +89 -0
  224. data/lib/ea/xmi/parser.rb +919 -0
  225. data/lib/ea/xmi.rb +35 -0
  226. data/lib/ea.rb +10 -1
  227. metadata +382 -9
@@ -0,0 +1,274 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Diagram
5
+ # Main SVG renderer for EA diagrams
6
+ class SvgRenderer
7
+ include Ea::Diagram::Util
8
+
9
+ attr_reader :diagram_renderer, :options, :bounds, :style_resolver
10
+
11
+ DEFAULT_OPTIONS = {
12
+ padding: 20,
13
+ background_color: "#ffffff",
14
+ grid_visible: false,
15
+ interactive: false,
16
+ css_classes: [],
17
+ }.freeze
18
+
19
+ def initialize(diagram_renderer, options = {})
20
+ @diagram_renderer = diagram_renderer
21
+ @options = DEFAULT_OPTIONS.merge(options)
22
+ @bounds = diagram_renderer.bounds
23
+ @style_resolver = StyleResolver.new(options[:config_path])
24
+ end
25
+
26
+ # Render the complete SVG diagram
27
+ # @return [String] Complete SVG content
28
+ def render # rubocop:disable Metrics/AbcSize
29
+ svg_content = +""
30
+ svg_content << svg_header
31
+ svg_content << defs_section
32
+ svg_content << background_layer
33
+ svg_content << grid_layer if options[:grid_visible]
34
+ svg_content << connectors_layer
35
+ svg_content << elements_layer
36
+ svg_content << interactive_layer if options[:interactive]
37
+ svg_content << svg_footer
38
+ svg_content
39
+ end
40
+
41
+ def determine_marker_type(connector_type) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLength
42
+ normalized_type = connector_type.to_s.downcase
43
+
44
+ case normalized_type
45
+ when "generalization", "inheritance"
46
+ { end: "url(#generalization-arrow)" }
47
+ when "aggregation"
48
+ { start: "url(#aggregation-arrow)" }
49
+ when "composition"
50
+ { start: "url(#composition-arrow)" }
51
+ when "dependency"
52
+ { end: "url(#dependency-arrow)" }
53
+ when "realization", "implementation"
54
+ { end: "url(#realization-arrow)" }
55
+ else
56
+ { end: "url(#association-arrow)" }
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def svg_header # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
63
+ # Bounds already include padding from LayoutEngine
64
+ width = bounds[:width]
65
+ height = bounds[:height]
66
+
67
+ # Normalize viewBox to start at 0,0 (matching EA export format)
68
+ # Shift all content to positive coordinates
69
+ offset_x = bounds[:x].negative? ? bounds[:x].abs : 0
70
+ offset_y = bounds[:y].negative? ? bounds[:y].abs : 0
71
+ total_width = width + offset_x
72
+ total_height = height + offset_y
73
+
74
+ view_box = "0 0 #{total_width} #{total_height}"
75
+
76
+ # Format width/height in cm (matching EA export format)
77
+ width_cm = format("%.2f", (total_width / 37.7952755906).round(2))
78
+ height_cm = format("%.2f", (total_height / 37.7952755906).round(2))
79
+
80
+ <<~SVG
81
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
82
+ <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.0//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
83
+
84
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="#{width_cm}cm" height="#{height_cm}cm" viewBox="#{view_box}">
85
+ <title></title>
86
+ <desc>Created with Enterprise Architect (Build: 1624) 2</desc>
87
+ SVG
88
+ end
89
+
90
+ def defs_section
91
+ <<~SVG
92
+ <defs>
93
+ <style type="text/css">
94
+ <![CDATA[
95
+ .lutaml-diagram-element { cursor: pointer; }
96
+ .lutaml-diagram-element:hover { opacity: 0.8; }
97
+ .lutaml-diagram-connector { fill: none; stroke: #000000; stroke-width: 1; }
98
+ .lutaml-diagram-connector:hover { stroke-width: 2; }
99
+ .lutaml-diagram-grid { stroke: #e0e0e0; stroke-width: 0.5; }
100
+ .lutaml-diagram-text { font-family: Arial, sans-serif; font-size: 11px; }
101
+ .lutaml-diagram-stereotype { font-style: italic; font-size: 9px; }
102
+ .lutaml-diagram-class-name { font-weight: bold; font-size: 12px; }
103
+ ]]>
104
+ </style>
105
+ <!-- EA-style arrow markers -->
106
+ <marker id="generalization-arrow" markerWidth="10" markerHeight="7"
107
+ refX="9" refY="3.5" orient="auto">
108
+ <polygon points="0 0, 10 3.5, 0 7" fill="#FFFFFF" stroke="#000000" stroke-width="1" />
109
+ </marker>
110
+ <marker id="association-arrow" markerWidth="10" markerHeight="7"
111
+ refX="9" refY="3.5" orient="auto">
112
+ <polygon points="0 0, 10 3.5, 0 7" fill="#000000" />
113
+ </marker>
114
+ <marker id="aggregation-arrow" markerWidth="12" markerHeight="12"
115
+ refX="6" refY="6" orient="auto">
116
+ <polygon points="6,0 12,6 6,12 0,6" fill="#FFFFFF" stroke="#000000" stroke-width="1" />
117
+ </marker>
118
+ <marker id="composition-arrow" markerWidth="12" markerHeight="12"
119
+ refX="6" refY="6" orient="auto">
120
+ <polygon points="6,0 12,6 6,12 0,6" fill="#000000" stroke="#000000" stroke-width="1" />
121
+ </marker>
122
+ <marker id="dependency-arrow" markerWidth="10" markerHeight="7"
123
+ refX="9" refY="3.5" orient="auto">
124
+ <polygon points="0 0, 10 3.5, 0 7" fill="#000000" />
125
+ </marker>
126
+ <marker id="realization-arrow" markerWidth="10" markerHeight="7"
127
+ refX="9" refY="3.5" orient="auto">
128
+ <polygon points="0 0, 10 3.5, 0 7" fill="#FFFFFF" stroke="#000000" stroke-width="1" />
129
+ </marker>
130
+ </defs>
131
+ SVG
132
+ end
133
+
134
+ def background_layer # rubocop:disable Metrics/AbcSize
135
+ offset_x = bounds[:x].negative? ? bounds[:x].abs : 0
136
+ offset_y = bounds[:y].negative? ? bounds[:y].abs : 0
137
+ total_width = bounds[:width] + offset_x
138
+ total_height = bounds[:height] + offset_y
139
+
140
+ <<~SVG
141
+ <g style="fill:#{options[:background_color]};fill-opacity:1.00;">
142
+ <rect x="0" y="0" width="#{total_width}" height="#{total_height}" shape-rendering="auto"/>
143
+ </g>
144
+ SVG
145
+ end
146
+
147
+ def grid_layer # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
148
+ grid_size = 20
149
+ grid_lines = +""
150
+
151
+ # Vertical lines
152
+ x = bounds[:x]
153
+ while x <= bounds[:x] + bounds[:width]
154
+ grid_lines << "<line x1=\"#{x}\" y1=\"#{bounds[:y]}\" " \
155
+ "x2=\"#{x}\" " \
156
+ "y2=\"#{bounds[:y] + bounds[:height]}\" " \
157
+ "class=\"lutaml-diagram-grid\" />\n"
158
+ x += grid_size
159
+ end
160
+
161
+ # Horizontal lines
162
+ y = bounds[:y]
163
+ while y <= bounds[:y] + bounds[:height]
164
+ grid_lines << "<line x1=\"#{bounds[:x]}\" y1=\"#{y}\" " \
165
+ "x2=\"#{bounds[:x] + bounds[:width]}\" " \
166
+ "y2=\"#{y}\" class=\"lutaml-diagram-grid\" />\n"
167
+ y += grid_size
168
+ end
169
+
170
+ "<g id=\"grid-layer\" " \
171
+ "class=\"lutaml-diagram-grid-layer\">\n#{grid_lines}</g>\n"
172
+ end
173
+
174
+ def connectors_layer
175
+ connectors_svg = diagram_renderer.connectors.map do |connector|
176
+ render_connector(connector)
177
+ end.join("\n")
178
+
179
+ "<g id=\"connectors-layer\" " \
180
+ "class=\"lutaml-diagram-connectors-layer\">\n" \
181
+ "#{connectors_svg}\n</g>\n"
182
+ end
183
+
184
+ def elements_layer
185
+ elements_svg = diagram_renderer.elements.map do |element|
186
+ render_element(element)
187
+ end.join("\n")
188
+
189
+ "<g id=\"elements-layer\" " \
190
+ "class=\"lutaml-diagram-elements-layer\">\n#{elements_svg}\n</g>\n"
191
+ end
192
+
193
+ def interactive_layer
194
+ # Add interactive JavaScript if needed
195
+ <<~SVG
196
+ <script type="text/javascript">
197
+ <![CDATA[
198
+ // Basic interactivity
199
+ document.addEventListener('DOMContentLoaded', function() {
200
+ var elements = document.querySelectorAll('.lutaml-diagram-element');
201
+ elements.forEach(function(el) {
202
+ el.addEventListener('click', function(e) {
203
+ var event = new CustomEvent('lutaml:element:click', {
204
+ detail: {
205
+ elementId: e.target.getAttribute('data-element-id'),
206
+ elementType: e.target.getAttribute('data-element-type')
207
+ }
208
+ });
209
+ document.dispatchEvent(event);
210
+ console.log('Element clicked:', e.target.getAttribute('data-element-id'));
211
+ });
212
+ });
213
+ });
214
+ ]]>
215
+ </script>
216
+ SVG
217
+ end
218
+
219
+ def svg_footer
220
+ "</svg>\n"
221
+ end
222
+
223
+ def render_connector(connector) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
224
+ path_builder = PathBuilder.new(
225
+ connector,
226
+ connector[:source_element],
227
+ connector[:target_element],
228
+ )
229
+ path_data = path_builder.build_path
230
+
231
+ style = style_resolver.resolve_connector_style(connector)
232
+
233
+ # Determine marker based on connector type
234
+ markers = determine_marker_type(connector[:type])
235
+ marker_start = markers[:start] || ""
236
+ marker_end = markers[:end] || ""
237
+
238
+ # Build style string
239
+ style_attrs = []
240
+ style_attrs << "stroke:#{style[:stroke] || '#000000'}"
241
+ style_attrs << "stroke-width:#{style[:stroke_width] || '1'}"
242
+ style_attrs << "stroke-linecap:#{style[:stroke_linecap] || 'round'}"
243
+ style_attrs << "stroke-linejoin:#{style[:stroke_linejoin] || 'bevel'}"
244
+ style_attrs << "fill:#{style[:fill] || 'none'}"
245
+ style_attrs << "shape-rendering:#{style[:shape_rendering] || 'auto'}"
246
+ if style[:stroke_dasharray]
247
+ style_attrs << "stroke-dasharray:#{style[:stroke_dasharray]}"
248
+ end
249
+
250
+ <<~SVG
251
+ <g style="#{style_attrs.join(';')}">
252
+ <path d="#{path_data}"
253
+ class="lutaml-diagram-connector lutaml-diagram-connector-#{connector[:type]}"
254
+ data-connector-id="#{connector[:id]}"
255
+ data-connector-type="#{connector[:type]}"
256
+ #{"marker-start=\"#{marker_start}\"" unless marker_start.empty?}
257
+ #{"marker-end=\"#{marker_end}\"" unless marker_end.empty?}
258
+ shape-rendering="auto" />
259
+ </g>
260
+ SVG
261
+ end
262
+
263
+ def render_element(element)
264
+ registry = ElementRenderers::DEFAULT_REGISTRY
265
+ renderer_class = registry.renderer_for(element[:type]) ||
266
+ ElementRenderers::BaseRenderer
267
+
268
+ renderer = renderer_class.new(element, style_resolver)
269
+ renderer.render
270
+ end
271
+
272
+ end
273
+ end
274
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Diagram
5
+ module Util
6
+ # Convert a style hash to a CSS string
7
+ #
8
+ # @param style_hash [Hash] Style properties (e.g., { stroke: "#000" })
9
+ # @return [String] CSS string (e.g., "stroke:#000;fill:none")
10
+ def style_to_css(style_hash)
11
+ style_hash.map { |k, v| "#{k}:#{v}" }.join(";")
12
+ end
13
+
14
+ # Parse geometry offsets
15
+ def parse_geometry_offsets(geometry_string)
16
+ geometry = parse_ea_geometry(geometry_string)
17
+
18
+ [
19
+ geometry[:source_offset_x].to_i,
20
+ geometry[:source_offset_y].to_i,
21
+ geometry[:target_offset_x].to_i,
22
+ geometry[:target_offset_y].to_i,
23
+ ]
24
+ rescue TypeError, NoMethodError
25
+ [0, 0, 0, 0]
26
+ end
27
+
28
+ def parse_ea_geometry(geometry_string) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
29
+ return nil if geometry_string.nil? || geometry_string.strip.empty?
30
+
31
+ data = {}
32
+ begin
33
+ geometry = geometry_string
34
+ .gsub(/\s/, "")
35
+ .downcase
36
+ .split(";")
37
+ .to_h { |pair| pair.split("=") }
38
+
39
+ geometry.each do |k, v|
40
+ if v.include?(",")
41
+ # waypoints
42
+ data[:waypoints] ||= []
43
+ x_str, y_str = v.split(",")
44
+ data[:waypoints] << { x: x_str.to_i, y: y_str.to_i }
45
+ else
46
+ key = case k
47
+ when "sx"
48
+ data[:has_relative_coords] ||= true
49
+ "source_offset_x"
50
+ when "sy"
51
+ data[:has_relative_coords] ||= true
52
+ "source_offset_y"
53
+ when "ex"
54
+ data[:has_relative_coords] ||= true
55
+ "target_offset_x"
56
+ when "ey"
57
+ data[:has_relative_coords] ||= true
58
+ "target_offset_y"
59
+ else
60
+ k
61
+ end
62
+
63
+ data[key.to_sym] = v.to_i
64
+ end
65
+ end
66
+ rescue ArgumentError, TypeError
67
+ data = {}
68
+ end
69
+ data
70
+ end
71
+ end
72
+ end
73
+ end
data/lib/ea/diagram.rb ADDED
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ea
4
+ module Diagram
5
+ autoload :SvgRenderer, "ea/diagram/svg_renderer"
6
+ autoload :LayoutEngine, "ea/diagram/layout_engine"
7
+ autoload :StyleParser, "ea/diagram/style_parser"
8
+ autoload :PathBuilder, "ea/diagram/path_builder"
9
+ autoload :StyleResolver, "ea/diagram/style_resolver"
10
+ autoload :Configuration, "ea/diagram/configuration"
11
+ autoload :Util, "ea/diagram/util"
12
+ autoload :Extractor, "ea/diagram/extractor"
13
+ autoload :ElementRenderers, "ea/diagram/element_renderers"
14
+
15
+ class DiagramRenderer
16
+ attr_reader :diagram_data, :layout_engine, :style_parser
17
+
18
+ def initialize(diagram_data)
19
+ @diagram_data = diagram_data
20
+ @layout_engine = LayoutEngine.new
21
+ @style_parser = StyleParser.new
22
+ end
23
+
24
+ def render_svg(options = {})
25
+ svg_renderer = SvgRenderer.new(self, options)
26
+ svg_renderer.render
27
+ end
28
+
29
+ def bounds
30
+ layout_engine.calculate_bounds(diagram_data)
31
+ end
32
+
33
+ def elements
34
+ diagram_data[:elements] || []
35
+ end
36
+
37
+ def connectors
38
+ diagram_data[:connectors] || []
39
+ end
40
+ end
41
+
42
+ def self.render(diagram_data, options = {})
43
+ renderer = DiagramRenderer.new(diagram_data)
44
+ renderer.render_svg(options)
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "benchmark"
4
+
5
+ module Ea
6
+ module Qea
7
+ # Performance benchmarking utilities for comparing QEA vs XMI parsing
8
+ class Benchmark
9
+ class << self
10
+ # Compare QEA and XMI parsing performance
11
+ #
12
+ # @param qea_path [String] Path to QEA file
13
+ # @param xmi_path [String] Path to XMI file
14
+ # @return [Hash] Benchmark results
15
+ #
16
+ # @example
17
+ # results = Ea::Qea::Benchmark.compare(
18
+ # "model.qea",
19
+ # "model.xmi"
20
+ # )
21
+ # puts "QEA: #{results[:qea][:time]}s"
22
+ # puts "XMI: #{results[:xmi][:time]}s"
23
+ # puts "Speedup: #{results[:speedup]}x"
24
+ def compare(qea_path, xmi_path) # rubocop:disable Metrics/MethodLength
25
+ qea_result = measure_qea(qea_path)
26
+ xmi_result = measure_xmi(xmi_path)
27
+
28
+ speedup = if qea_result[:time].positive?
29
+ (xmi_result[:time] / qea_result[:time]).round(2)
30
+ else
31
+ 0
32
+ end
33
+
34
+ {
35
+ qea: qea_result,
36
+ xmi: xmi_result,
37
+ speedup: speedup,
38
+ improvement_percent: ((speedup - 1) * 100).round(1),
39
+ }
40
+ end
41
+
42
+ # Measure QEA parsing performance
43
+ #
44
+ # @param path [String] Path to QEA file
45
+ # @return [Hash] Performance metrics
46
+ #
47
+ # @example
48
+ # result = Ea::Qea::Benchmark.measure_qea("model.qea")
49
+ # puts "Time: #{result[:time]}s"
50
+ # puts "Packages: #{result[:stats][:packages]}"
51
+ def measure_qea(path) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
52
+ unless File.exist?(path)
53
+ return { error: "File not found: #{path}" }
54
+ end
55
+
56
+ result = {
57
+ file: path,
58
+ file_size_mb: (File.size(path) / 1024.0 / 1024.0).round(2),
59
+ format: "QEA",
60
+ }
61
+
62
+ # Measure parsing time
63
+ document = nil
64
+ time = ::Benchmark.realtime do
65
+ document = Ea::Qea.parse(path)
66
+ end
67
+
68
+ result[:time] = time.round(3)
69
+ result[:stats] = {
70
+ packages: document.packages&.size || 0,
71
+ classes: document.classes&.size || 0,
72
+ associations: document.associations&.size || 0,
73
+ diagrams: document.diagrams&.size || 0,
74
+ }
75
+
76
+ # Calculate throughput
77
+ if result[:file_size_mb].positive? && time.positive?
78
+ result[:throughput_mb_per_sec] =
79
+ (result[:file_size_mb] / time).round(2)
80
+ end
81
+
82
+ result
83
+ rescue StandardError => e
84
+ {
85
+ error: e.message,
86
+ file: path,
87
+ format: "QEA",
88
+ }
89
+ end
90
+
91
+ # Measure XMI parsing performance
92
+ #
93
+ # @param path [String] Path to XMI file
94
+ # @return [Hash] Performance metrics
95
+ #
96
+ # @example
97
+ # result = Ea::Qea::Benchmark.measure_xmi("model.xmi")
98
+ # puts "Time: #{result[:time]}s"
99
+ def measure_xmi(path) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
100
+ unless File.exist?(path)
101
+ return { error: "File not found: #{path}" }
102
+ end
103
+
104
+ result = {
105
+ file: path,
106
+ file_size_mb: (File.size(path) / 1024.0 / 1024.0).round(2),
107
+ format: "XMI",
108
+ }
109
+
110
+ # Measure parsing time
111
+ document = nil
112
+ time = ::Benchmark.realtime do
113
+ File.open(path) do |file|
114
+ document = Ea::Xmi::Parser.parse(file)
115
+ end
116
+ end
117
+
118
+ result[:time] = time.round(3)
119
+ result[:stats] = {
120
+ packages: document.packages&.size || 0,
121
+ classes: document.classes&.size || 0,
122
+ associations: document.associations&.size || 0,
123
+ diagrams: document.diagrams&.size || 0,
124
+ }
125
+
126
+ # Calculate throughput
127
+ if result[:file_size_mb].positive? && time.positive?
128
+ result[:throughput_mb_per_sec] =
129
+ (result[:file_size_mb] / time).round(2)
130
+ end
131
+
132
+ result
133
+ rescue StandardError => e
134
+ {
135
+ error: e.message,
136
+ file: path,
137
+ format: "XMI",
138
+ }
139
+ end
140
+
141
+ # Format benchmark results for display
142
+ #
143
+ # @param results [Hash] Results from compare method
144
+ # @return [String] Formatted text
145
+ def format_results(results) # rubocop:disable Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
146
+ return results[:error] if results[:error]
147
+
148
+ output = []
149
+ output << ("=" * 80)
150
+ output << "QEA vs XMI Performance Comparison"
151
+ output << ("=" * 80)
152
+ output << ""
153
+
154
+ if results[:qea][:error]
155
+ output << "QEA Error: #{results[:qea][:error]}"
156
+ else
157
+ output << "QEA File:"
158
+ output << " Path: #{results[:qea][:file]}"
159
+ output << " Size: #{results[:qea][:file_size_mb]} MB"
160
+ output << " Parse Time: #{results[:qea][:time]}s"
161
+ if results[:qea][:throughput_mb_per_sec]
162
+ output << " Throughput: " \
163
+ "#{results[:qea][:throughput_mb_per_sec]} MB/s"
164
+ end
165
+ output << " Packages: #{results[:qea][:stats][:packages]}"
166
+ output << " Classes: #{results[:qea][:stats][:classes]}"
167
+ end
168
+
169
+ output << ""
170
+
171
+ if results[:xmi][:error]
172
+ output << "XMI Error: #{results[:xmi][:error]}"
173
+ else
174
+ output << "XMI File:"
175
+ output << " Path: #{results[:xmi][:file]}"
176
+ output << " Size: #{results[:xmi][:file_size_mb]} MB"
177
+ output << " Parse Time: #{results[:xmi][:time]}s"
178
+ if results[:xmi][:throughput_mb_per_sec]
179
+ output << " Throughput: " \
180
+ "#{results[:xmi][:throughput_mb_per_sec]} MB/s"
181
+ end
182
+ output << " Packages: #{results[:xmi][:stats][:packages]}"
183
+ output << " Classes: #{results[:xmi][:stats][:classes]}"
184
+ end
185
+
186
+ output << ""
187
+ output << "Performance Improvement:"
188
+ output << " QEA is #{results[:speedup]}x faster than XMI"
189
+ output << " Improvement: #{results[:improvement_percent]}%"
190
+ output << ""
191
+
192
+ # Add interpretation
193
+ output << if results[:speedup] >= 10
194
+ " ✓ Significant performance improvement with QEA"
195
+ elsif results[:speedup] >= 5
196
+ " ✓ Notable performance improvement with QEA"
197
+ elsif results[:speedup] >= 2
198
+ " ✓ Moderate performance improvement with QEA"
199
+ else
200
+ " ~ Minimal performance difference"
201
+ end
202
+
203
+ output << ("=" * 80)
204
+
205
+ output.join("\n")
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end