Alef1986 commited on
Commit
b129529
·
verified ·
1 Parent(s): 4713546

Upload streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +405 -40
src/streamlit_app.py CHANGED
@@ -1,40 +1,405 @@
1
- import altair as alt
2
- import numpy as np
3
- import pandas as pd
4
- import streamlit as st
5
-
6
- """
7
- # Welcome to Streamlit!
8
-
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
12
-
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
15
-
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import xml.etree.ElementTree as ET
3
+ import networkx as nx
4
+ import streamlit.components.v1 as components
5
+ import json
6
+
7
+ # Page config
8
+ st.set_page_config(page_title="ISO 15288 Analyzer", layout="wide")
9
+ st.title("ISO 15288: Architecture Analyzer")
10
+
11
+ def render_mermaid(mermaid_code, nodes_json):
12
+ """Рендерит Mermaid с поддержкой Зума, Экспорта в PNG и боковой панели"""
13
+ safe_mermaid_code = mermaid_code.replace('`', '\\`').replace('$', '\\$')
14
+
15
+ html_code = f"""
16
+ <!DOCTYPE html>
17
+ <html>
18
+ <head>
19
+ <script src="https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.6.1/dist/svg-pan-zoom.min.js"></script>
20
+
21
+ <style>
22
+ body {{ margin: 0; padding: 0; font-family: sans-serif; background: #ffffff; overflow: hidden; }}
23
+ #graphDiv {{ width: 100vw; height: 100vh; cursor: grab; }}
24
+ #graphDiv:active {{ cursor: grabbing; }}
25
+
26
+ /* Стили кнопки экспорта */
27
+ #toolbar {{
28
+ position: absolute;
29
+ bottom: 25px;
30
+ right: 25px;
31
+ z-index: 100;
32
+ }}
33
+ button.export-btn {{
34
+ background: #ffffff; border: 1px solid #ccc; padding: 10px 18px;
35
+ border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: bold;
36
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1); transition: all 0.2s;
37
+ }}
38
+ button.export-btn:hover {{
39
+ background: #f0f0f0;
40
+ transform: translateY(-2px);
41
+ box-shadow: 0 6px 12px rgba(0,0,0,0.15);
42
+ }}
43
+
44
+ /* Стили всплывающей боковой панели */
45
+ #infoPanel {{
46
+ display: none;
47
+ position: absolute;
48
+ top: 20px;
49
+ right: 20px;
50
+ width: 350px;
51
+ max-height: 80vh;
52
+ background: #ffffff;
53
+ border: 1px solid #e0e0e0;
54
+ border-radius: 8px;
55
+ box-shadow: 0 10px 25px rgba(0,0,0,0.2);
56
+ padding: 20px;
57
+ overflow-y: auto;
58
+ z-index: 1000;
59
+ flex-direction: column;
60
+ }}
61
+ #infoPanel h3 {{ margin: 0 0 15px 0; font-size: 18px; color: #333; }}
62
+ #infoPanel p {{ margin: 0; font-size: 14px; color: #555; line-height: 1.6; }}
63
+
64
+ #closeBtn {{
65
+ position: absolute;
66
+ top: 15px;
67
+ right: 15px;
68
+ cursor: pointer;
69
+ font-size: 24px;
70
+ color: #aaa;
71
+ background: none;
72
+ border: none;
73
+ padding: 0;
74
+ line-height: 1;
75
+ }}
76
+ #closeBtn:hover {{ color: #333; }}
77
+
78
+ /* Указатель для кликабельных узлов */
79
+ .node {{ cursor: pointer !important; }}
80
+ </style>
81
+
82
+ <script type="module">
83
+ import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
84
+ mermaid.initialize({{ startOnLoad: false, theme: 'default', securityLevel: 'loose' }});
85
+
86
+ // 1. Рендеринг и подключение Зума
87
+ window.renderDiagram = async function() {{
88
+ const container = document.getElementById('graphDiv');
89
+ const {{ svg }} = await mermaid.render('mermaidSvg', `{safe_mermaid_code}`);
90
+ container.innerHTML = svg;
91
+
92
+ const svgElement = container.querySelector('svg');
93
+ svgElement.style.width = "100%";
94
+ svgElement.style.height = "100%";
95
+
96
+ window.panZoomSetup = svgPanZoom(svgElement, {{
97
+ zoomEnabled: true,
98
+ controlIconsEnabled: true,
99
+ fit: true,
100
+ center: true,
101
+ minZoom: 0.1,
102
+ maxZoom: 10
103
+ }});
104
+ }};
105
+
106
+ window.addEventListener('load', window.renderDiagram);
107
+ </script>
108
+
109
+ <script>
110
+ // 2. Логика Информационной Панели
111
+ const nodeData = {nodes_json};
112
+
113
+ window.handleNodeClick = function(nodeId) {{
114
+ const data = nodeData[nodeId];
115
+ if (data) {{
116
+ document.getElementById('panelTitle').innerText = data.name;
117
+ document.getElementById('panelDoc').innerText = data.doc ? data.doc : "Description is missing in the model.";
118
+ document.getElementById('infoPanel').style.display = 'flex';
119
+ }}
120
+ }};
121
+
122
+ window.closePanel = function() {{
123
+ document.getElementById('infoPanel').style.display = 'none';
124
+ }};
125
+
126
+ // 3. Экспорт в PNG
127
+ window.downloadPng = function() {{
128
+ const svg = document.querySelector('#graphDiv svg');
129
+ if (!svg) return;
130
+
131
+ const oldZoom = window.panZoomSetup.getZoom();
132
+ const oldPan = window.panZoomSetup.getPan();
133
+ window.panZoomSetup.resetZoom();
134
+ window.panZoomSetup.resetPan();
135
+
136
+ setTimeout(() => {{
137
+ const svgData = new XMLSerializer().serializeToString(svg);
138
+ const canvas = document.createElement("canvas");
139
+
140
+ const bbox = svg.getBoundingClientRect();
141
+ canvas.width = bbox.width * 2;
142
+ canvas.height = bbox.height * 2;
143
+
144
+ const ctx = canvas.getContext("2d");
145
+ ctx.scale(2, 2);
146
+ ctx.fillStyle = "white";
147
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
148
+
149
+ const img = new Image();
150
+ img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgData)));
151
+
152
+ img.onload = function() {{
153
+ ctx.drawImage(img, 0, 0);
154
+ const a = document.createElement("a");
155
+ a.download = "Architecture_Diagram.png";
156
+ a.href = canvas.toDataURL("image/png");
157
+ a.click();
158
+
159
+ window.panZoomSetup.zoom(oldZoom);
160
+ window.panZoomSetup.pan(oldPan);
161
+ }};
162
+ }}, 100);
163
+ }};
164
+ </script>
165
+ </head>
166
+ <body>
167
+ <div id="toolbar">
168
+ <button class="export-btn" onclick="downloadPng()">📸 Export as PNG</button>
169
+ </div>
170
+
171
+ <div id="infoPanel">
172
+ <button id="closeBtn" onclick="closePanel()">&times;</button>
173
+ <h3 id="panelTitle">Process Name</h3>
174
+ <p id="panelDoc">Documentation...</p>
175
+ </div>
176
+
177
+ <div id="graphDiv"></div>
178
+ </body>
179
+ </html>
180
+ """
181
+ components.html(html_code, height=800, scrolling=False)
182
+
183
+ @st.cache_data
184
+ def load_and_parse_xml(file_path):
185
+ tree = ET.parse(file_path)
186
+ root = tree.getroot()
187
+
188
+ G = nx.DiGraph()
189
+ processes = {}
190
+
191
+ for elem in root.findall('.//*'):
192
+ if elem.tag.endswith('element'):
193
+ n_id = elem.get('identifier')
194
+ n_type = next((v for k, v in elem.attrib.items() if k.endswith('type')), 'Unknown')
195
+
196
+ name = "Unnamed"
197
+ doc = ""
198
+ for child in elem:
199
+ if child.tag.endswith('name') and child.text:
200
+ name = child.text
201
+ elif child.tag.endswith('documentation') and child.text:
202
+ doc = child.text
203
+
204
+ G.add_node(n_id, name=name, type=n_type, doc=doc)
205
+
206
+ if 'BusinessFunction' in n_type:
207
+ processes[n_id] = name
208
+
209
+ for rel in root.findall('.//*'):
210
+ if rel.tag.endswith('relationship'):
211
+ src = rel.get('source')
212
+ tgt = rel.get('target')
213
+ r_type = next((v for k, v in rel.attrib.items() if k.endswith('type')), 'Association')
214
+ access = rel.get('accessType', '')
215
+
216
+ if 'Access' in r_type and access.lower() == 'read':
217
+ G.add_edge(tgt, src, type=r_type, access=access)
218
+ else:
219
+ G.add_edge(src, tgt, type=r_type, access=access)
220
+
221
+ return G, processes
222
+
223
+ def graph_to_mermaid_and_data(G, nodes_to_include, edges_to_include):
224
+ if not nodes_to_include:
225
+ return "", "{}"
226
+
227
+ lines = [
228
+ "flowchart LR",
229
+ "classDef process fill:#FFF2CC,stroke:#D6B656,stroke-width:2px,color:#000;",
230
+ "classDef object fill:#D5E8D4,stroke:#82B366,stroke-width:2px,color:#000;"
231
+ ]
232
+
233
+ nodes_data = {}
234
+
235
+ for n_id in set(nodes_to_include):
236
+ data = G.nodes.get(n_id, {})
237
+ m_id = n_id.replace("-", "_")
238
+ name = data.get("name", "Unknown").replace('"', "'")
239
+ n_type = data.get("type", "")
240
+ doc = data.get("doc", "").replace('"', "'").replace("\n", " ")
241
+
242
+ if 'BusinessProcess' in n_type or 'BusinessFunction' in n_type:
243
+ lines.append(f' {m_id}("{name}"):::process')
244
+ else:
245
+ lines.append(f' {m_id}["{name}"]:::object')
246
+
247
+ lines.append(f' click {m_id} call handleNodeClick()')
248
+
249
+ nodes_data[m_id] = {"name": name, "doc": doc}
250
+
251
+ added_edges = set()
252
+ for u, v, data in edges_to_include:
253
+ edge_key = (u, v)
254
+ if edge_key not in added_edges:
255
+ u_id = u.replace("-", "_")
256
+ v_id = v.replace("-", "_")
257
+ e_type = data.get("type", "")
258
+ access = data.get("access", "")
259
+
260
+ label = access if access else e_type.replace("Relationship", "").replace("archimate:", "")
261
+ lines.append(f' {u_id} -->|{label}| {v_id}')
262
+ added_edges.add(edge_key)
263
+
264
+ return "\n".join(lines), json.dumps(nodes_data)
265
+
266
+ # Загрузка графов
267
+ try:
268
+ G, processes = load_and_parse_xml("ISO15288_Archi_Import.xml")
269
+ U = G.to_undirected()
270
+ sorted_process_names = sorted(list(processes.values()))
271
+ except FileNotFoundError:
272
+ st.error("File 'ISO15288_Archi_Import.xml' not found.")
273
+ st.stop()
274
+
275
+ # Интерфейс
276
+ mode = st.radio(
277
+ "Select analysis scenario:",
278
+ (
279
+ "Case 1: Interface exchange between selected processes",
280
+ "Case 2: Shortest DATA FLOW path (Directed)",
281
+ "Case 3: Closest STRUCTURAL connection (Undirected)",
282
+ "Case 4: All possible DATA FLOW paths (Directed)"
283
+ )
284
+ )
285
+
286
+ st.markdown("---")
287
+
288
+ if mode == "Case 1: Interface exchange between selected processes":
289
+ st.write("Select 2 to 5 processes. The script will find objects they exchange data through.")
290
+ selected_names = st.multiselect("Select processes:", sorted_process_names, max_selections=5)
291
+
292
+ if st.button("Generate Interface Diagram") and len(selected_names) >= 2:
293
+ selected_ids = [k for k, v in processes.items() if v in selected_names]
294
+ nodes_to_draw = set(selected_ids)
295
+ edges_to_draw = []
296
+
297
+ def safe_add_edge(u, v):
298
+ if G.has_edge(u, v): edges_to_draw.append((u, v, G[u][v]))
299
+
300
+ for i in range(len(selected_ids)):
301
+ for j in range(i + 1, len(selected_ids)):
302
+ p1, p2 = selected_ids[i], selected_ids[j]
303
+
304
+ safe_add_edge(p1, p2)
305
+ safe_add_edge(p2, p1)
306
+
307
+ common_neighbors = set(U.neighbors(p1)).intersection(set(U.neighbors(p2)))
308
+ for neighbor in common_neighbors:
309
+ nodes_to_draw.add(neighbor)
310
+ safe_add_edge(p1, neighbor)
311
+ safe_add_edge(neighbor, p1)
312
+ safe_add_edge(p2, neighbor)
313
+ safe_add_edge(neighbor, p2)
314
+
315
+ if len(nodes_to_draw) == len(selected_ids) and not edges_to_draw:
316
+ st.warning("No direct links or shared exchange objects found between these processes.")
317
+ else:
318
+ st.success("Exchange interfaces found!")
319
+ mermaid_code, json_data = graph_to_mermaid_and_data(G, nodes_to_draw, edges_to_draw)
320
+ render_mermaid(mermaid_code, json_data)
321
+
322
+ elif mode == "Case 2: Shortest DATA FLOW path (Directed)":
323
+ st.write("Finds the shortest valid data sequence (respecting Write -> Read directions).")
324
+ col1, col2 = st.columns(2)
325
+ with col1:
326
+ p1_name = st.selectbox("Process A (From):", sorted_process_names, key="c2_p1")
327
+ with col2:
328
+ p2_name = st.selectbox("Process B (To):", sorted_process_names, key="c2_p2")
329
+
330
+ if st.button("Find Directed Path") and p1_name != p2_name:
331
+ p1_id = [k for k, v in processes.items() if v == p1_name][0]
332
+ p2_id = [k for k, v in processes.items() if v == p2_name][0]
333
+
334
+ try:
335
+ path = nx.shortest_path(G, source=p1_id, target=p2_id)
336
+ edges_in_path = [(path[i], path[i+1], G[path[i]][path[i+1]]) for i in range(len(path)-1)]
337
+
338
+ st.success(f"Data flow path found! Length: {len(path)-1} step(s).")
339
+ mermaid_code, json_data = graph_to_mermaid_and_data(G, path, edges_in_path)
340
+ render_mermaid(mermaid_code, json_data)
341
+
342
+ except nx.NetworkXNoPath:
343
+ st.error("No valid DATA FLOW exists between these processes (direction matters).")
344
+
345
+ elif mode == "Case 3: Closest STRUCTURAL connection (Undirected)":
346
+ st.write("Finds any connection (e.g., sharing the same input document), ignoring direction.")
347
+ col1, col2 = st.columns(2)
348
+ with col1:
349
+ p1_name = st.selectbox("Process A:", sorted_process_names, key="c3_p1")
350
+ with col2:
351
+ p2_name = st.selectbox("Process B:", sorted_process_names, key="c3_p2")
352
+
353
+ if st.button("Find Closest Connection") and p1_name != p2_name:
354
+ p1_id = [k for k, v in processes.items() if v == p1_name][0]
355
+ p2_id = [k for k, v in processes.items() if v == p2_name][0]
356
+
357
+ try:
358
+ path = nx.shortest_path(U, source=p1_id, target=p2_id)
359
+ edges_in_path = []
360
+ for i in range(len(path)-1):
361
+ u, v = path[i], path[i+1]
362
+ if G.has_edge(u, v): edges_in_path.append((u, v, G[u][v]))
363
+ elif G.has_edge(v, u): edges_in_path.append((v, u, G[v][u]))
364
+
365
+ st.success(f"Structural connection found! Length: {len(path)-1} step(s).")
366
+ mermaid_code, json_data = graph_to_mermaid_and_data(G, path, edges_in_path)
367
+ render_mermaid(mermaid_code, json_data)
368
+
369
+ except nx.NetworkXNoPath:
370
+ st.error("These processes are completely disconnected in the model.")
371
+
372
+ elif mode == "Case 4: All possible DATA FLOW paths (Directed)":
373
+ st.write("Finds all valid data flow sequences (respecting directions).")
374
+ col1, col2 = st.columns(2)
375
+ with col1:
376
+ p1_name = st.selectbox("Process A (From):", sorted_process_names, key="c4_p1")
377
+ with col2:
378
+ p2_name = st.selectbox("Process B (To):", sorted_process_names, key="c4_p2")
379
+
380
+ if st.button("Find All Directed Paths") and p1_name != p2_name:
381
+ p1_id = [k for k, v in processes.items() if v == p1_name][0]
382
+ p2_id = [k for k, v in processes.items() if v == p2_name][0]
383
+
384
+ try:
385
+ paths = list(nx.all_simple_paths(G, source=p1_id, target=p2_id, cutoff=6))
386
+
387
+ if not paths:
388
+ st.error("No valid data flow paths found within 6 steps.")
389
+ else:
390
+ st.success(f"Found {len(paths)} unique data flow path(s).")
391
+
392
+ nodes_in_paths = set()
393
+ edges_in_paths = []
394
+
395
+ for path in paths:
396
+ nodes_in_paths.update(path)
397
+ for i in range(len(path)-1):
398
+ u, v = path[i], path[i+1]
399
+ edges_in_paths.append((u, v, G[u][v]))
400
+
401
+ mermaid_code, json_data = graph_to_mermaid_and_data(G, nodes_in_paths, edges_in_paths)
402
+ render_mermaid(mermaid_code, json_data)
403
+
404
+ except nx.NetworkXNoPath:
405
+ st.error("No valid DATA FLOW exists between these processes.")