Expand architecture documentation: add support for labels (archimate:Note), update SVG rendering, extend node group definitions, and refine inter-service relationship mappings.
This commit is contained in:
parent
c5f95ee66f
commit
8357ccee8a
@ -130,6 +130,13 @@ def obj(v, eid, x, y, w, h, parent=None):
|
||||
(parent["children"] if parent else v["objects"]).append(o)
|
||||
return o
|
||||
|
||||
def note(v, text, x, y, w, h, parent=None):
|
||||
"""Пояснительная надпись на схеме. Элементом модели не является: нужна легенде,
|
||||
где подписать надо не архитектуру, а саму нотацию."""
|
||||
o = obj(v, None, x, y, w, h, parent)
|
||||
o["note"] = text
|
||||
return o
|
||||
|
||||
def conn(rtype, so, to):
|
||||
key = (rtype, so["el"], to["el"])
|
||||
if key not in REL_INDEX:
|
||||
@ -141,6 +148,12 @@ def conn(rtype, so, to):
|
||||
|
||||
SERVICES = PROFILE["services"]
|
||||
SW = {s["id"]: s for s in PROFILE["software"]}
|
||||
MIN_LINKS = 3 # порог, с которого сервису отводится отдельное представление
|
||||
_tno = [2] # T1 и T2 названы явно, дальше нумерация сквозная по семейству
|
||||
|
||||
def tnum():
|
||||
_tno[0] += 1
|
||||
return f"T{_tno[0]}"
|
||||
|
||||
v1 = view("T1. Технологический слой",
|
||||
"Что развёрнуто в кластере и какие технологические сервисы это даёт. "
|
||||
@ -242,7 +255,7 @@ if GROUPS:
|
||||
# --- представление «Внешние интеграции»
|
||||
IV = PROFILE.get("integration_view")
|
||||
if IV:
|
||||
vi = view(f"T{len(views)+1}. {IV['name']}", IV.get("doc", ""))
|
||||
vi = view(f"{tnum()}. {IV['name']}", IV.get("doc", ""))
|
||||
CW, CH = 320, 110
|
||||
shown = {}
|
||||
for ci, col in enumerate(IV["columns"]):
|
||||
@ -254,6 +267,44 @@ if IV:
|
||||
if src in shown and tgt in shown:
|
||||
conn(rtype, shown[src], shown[tgt])
|
||||
|
||||
# --- прикладной слой: смысловые домены
|
||||
# Строится последним из прикладного, чтобы захватить и компоненты, заданные
|
||||
# в node_groups вручную: в матрице контура их нет.
|
||||
DOMAINS = PROFILE.get("domains", [])
|
||||
if DOMAINS:
|
||||
app_ids = {e[1] for e in elements if e[0] == "application"}
|
||||
claimed = {app_id.get(a, a) for g in DOMAINS
|
||||
if isinstance(g.get("apps"), list) for a in g["apps"]}
|
||||
for g in DOMAINS:
|
||||
want = ([e for e in app_ids if e not in claimed] if g.get("apps") == "*"
|
||||
else [app_id.get(a, a) for a in g.get("apps") or []])
|
||||
missing = [e for e in want if e not in app_ids]
|
||||
if missing:
|
||||
sys.exit(f"домен {g['id']}: нет таких компонентов — {', '.join(missing)}")
|
||||
g["_apps"] = sorted(want)
|
||||
# пустая группа — не элемент модели, а незаполненная строка профиля
|
||||
DOMAINS = [g for g in DOMAINS if g["_apps"]]
|
||||
for g in DOMAINS:
|
||||
el("other", g["id"], "Grouping", g["name"], g.get("doc", ""))
|
||||
for eid in g["_apps"]:
|
||||
R("Aggregation", g["id"], eid)
|
||||
|
||||
va = view("A0. Карта прикладного слоя",
|
||||
f"Из чего состоит прикладной слой: {len(app_ids)} "
|
||||
f"{plural(len(app_ids), 'компонент', 'компонента', 'компонентов')} по смысловым "
|
||||
f"группам. Связи здесь намеренно не показаны — на таком числе компонентов они "
|
||||
f"делают схему нечитаемой; для них есть представления A1 и далее. Вложенность "
|
||||
f"означает aggregation: группа объединяет компоненты, но не владеет ими.")
|
||||
PER = 7
|
||||
y = 20
|
||||
for g in DOMAINS:
|
||||
rows = (len(g["_apps"]) + PER - 1) // PER
|
||||
h = 34 + rows * 62 + 8
|
||||
og = obj(va, g["id"], 20, y, PER * 180 + 28, h)
|
||||
for i, eid in enumerate(g["_apps"]):
|
||||
obj(va, eid, 14 + (i % PER) * 180, 34 + (i // PER) * 62, 165, 50, og)
|
||||
y += h + 20
|
||||
|
||||
# --- прикладной слой: межсервисные вызовы
|
||||
if CALLS:
|
||||
import math, collections
|
||||
@ -286,19 +337,48 @@ if CALLS:
|
||||
conn("Serving", core_obj[b], core_obj[a])
|
||||
|
||||
# A2…: по представлению на каждый сервис с тремя и более потребителями
|
||||
a_no = 2
|
||||
a_no, hub_view = 2, {}
|
||||
for hub, n in inbound.most_common():
|
||||
if n < 3:
|
||||
if n < MIN_LINKS:
|
||||
break
|
||||
callers = sorted(a for a, b, _ in calls if b == hub)
|
||||
v = view(f"A{a_no}. Зависят от: {hub}",
|
||||
f"К {hub} обращаются {n} {plural(n, 'сервис', 'сервиса', 'сервисов')}.")
|
||||
hub_view[hub] = f"A{a_no}"
|
||||
a_no += 1
|
||||
oh = obj(v, app_id[hub], 20, 20, 260, 60)
|
||||
for i, a in enumerate(callers):
|
||||
conn("Serving", oh,
|
||||
obj(v, app_id[a], 20 + (i % 6) * 190, 150 + (i // 6) * 80, 170, 55))
|
||||
|
||||
# P1…: зеркало к A-представлениям. Те отвечают «кто сломается без сервиса»,
|
||||
# эти — «без чего не работает он сам»: технологические сервисы сверху,
|
||||
# вызываемые им приложения снизу.
|
||||
p_no = 1
|
||||
for hub, n in outbound.most_common():
|
||||
if n < MIN_LINKS:
|
||||
break
|
||||
callees = sorted({b for a, b, _ in calls if a == hub})
|
||||
uses = [ts for ts in SERVICES if hub in consumers[ts["id"]]]
|
||||
back = (f"Кто зависит от самого {hub} — представление {hub_view[hub]}."
|
||||
if hub in hub_view else
|
||||
f"Входящих вызовов у {hub} меньше {MIN_LINKS}, отдельного представления для них нет.")
|
||||
m = len(callees)
|
||||
v = view(f"P{p_no}. Паспорт: {hub}",
|
||||
f"Что нужно, чтобы {hub} работал: {len(uses)} "
|
||||
f"{plural(len(uses), 'технологический сервис', 'технологических сервиса', 'технологических сервисов')} "
|
||||
f"и {m} {plural(m, 'прикладной сервис', 'прикладных сервиса', 'прикладных сервисов')}. "
|
||||
f"Стрелки, как и везде, идут от обслуживающего к обслуживаемому. {back}")
|
||||
p_no += 1
|
||||
wide = min(6, max(len(uses), m))
|
||||
oh = obj(v, app_id[hub], 20 + max(0, (wide * 190 - 260) // 2),
|
||||
20 + 80 * ((len(uses) + 5) // 6), 260, 60)
|
||||
for i, ts in enumerate(uses):
|
||||
conn("Serving", obj(v, ts["id"], 20 + (i % 6) * 190, 20 + (i // 6) * 80, 170, 55), oh)
|
||||
base = oh["b"][1] + 130
|
||||
for i, b in enumerate(callees):
|
||||
conn("Serving", obj(v, app_id[b], 20 + (i % 6) * 190, base + (i // 6) * 80, 170, 55), oh)
|
||||
|
||||
PER_ROW = 6
|
||||
for ts in SERVICES:
|
||||
lst = consumers[ts["id"]]
|
||||
@ -306,7 +386,7 @@ for ts in SERVICES:
|
||||
if not lst and not extra:
|
||||
continue
|
||||
n = len(lst)
|
||||
v = view(f"T{len(views)+1}. Потребители: {ts['name']}",
|
||||
v = view(f"{tnum()}. Потребители: {ts['name']}",
|
||||
f"Сервисом {plural(n, 'пользуется', 'пользуются', 'пользуются')} "
|
||||
f"{n} из {len(DATA['apps'])} приложений контура.")
|
||||
ots = obj(v, ts["id"], 20, 20, 260, 60)
|
||||
@ -316,8 +396,79 @@ for ts in SERVICES:
|
||||
conn("Serving", ots,
|
||||
obj(v, eid, 20 + (i % PER_ROW) * 190, 150 + (i // PER_ROW) * 80, 170, 55))
|
||||
|
||||
# --- легенда нотации
|
||||
# Собрана из настоящих элементов модели, а не из выдуманных ради неё: так легенда
|
||||
# заодно оказывается разобранным примером и не может разойтись с остальными схемами.
|
||||
LEG_TS = max((t for t in SERVICES if consumers[t["id"]] and t["realizers"][0] in SW),
|
||||
key=lambda t: len(consumers[t["id"]]), default=None)
|
||||
if LEG_TS and GROUPS and DOMAINS:
|
||||
l_ss = LEG_TS["realizers"][0]
|
||||
l_app = app_id[consumers[LEG_TS["id"]][0]]
|
||||
l_gsw = next((g for g in GROUPS if l_ss in (g.get("runs_software") or [])), None)
|
||||
l_gap = next((g for g in GROUPS if any(app_id[a] == l_app for a in g["_apps"])), None)
|
||||
l_dm = next((g for g in DOMAINS if l_app in g["_apps"]), None)
|
||||
l_art = next((e[1] for e in elements if e[2] == "Artifact"), None)
|
||||
|
||||
vl = view("L1. Как читать схемы",
|
||||
"Легенда нотации. Показан настоящий фрагмент модели, подписанный по типам: "
|
||||
"каждый элемент и каждая линия здесь встречаются на остальных представлениях "
|
||||
"в том же начертании.")
|
||||
W, COL = 260, 320
|
||||
note(vl, "Стрелка serving читается не как «кто кого вызывает», а как «кто без кого не "
|
||||
"работает»: она идёт от обслуживающего к обслуживаемому. Направление выбрано так "
|
||||
"потому, что от схемы обычно нужен ответ на вопрос «что откажет, если убрать этот "
|
||||
"элемент», — и он читается по стрелкам вперёд.", 20, 20, COL * 4 - 60, 66)
|
||||
|
||||
row_a = [
|
||||
(nd_cluster, "Узел (Node) — вычислительная среда: кластер, группа серверов или "
|
||||
"отдельный хост. Ромб на линии — composition: узел состоит из этого, "
|
||||
"и отдельно от узла содержимое не существует."),
|
||||
(l_ss, "Системное ПО (System Software) — платформенный компонент внутри узла. "
|
||||
"Пунктир с полым треугольником — realization: компонент реализует "
|
||||
"технологический сервис."),
|
||||
(LEG_TS["id"], "Технологический сервис (Technology Service) — что платформа даёт "
|
||||
"приложениям, без указания, чем именно это сделано. Приложение зависит "
|
||||
"от сервиса, а не от конкретного продукта под ним."),
|
||||
(l_app, "Прикладной компонент (Application Component) — сервис прикладного слоя. "
|
||||
"Цветом различаются слои: голубой — прикладной, зелёный — технологический."),
|
||||
]
|
||||
o_leg = {}
|
||||
for i, (eid, txt) in enumerate(row_a):
|
||||
o_leg[eid] = obj(vl, eid, 20 + i * COL, 120, W, 60)
|
||||
note(vl, txt, 20 + i * COL, 190, W, 96)
|
||||
conn("Composition", o_leg[nd_cluster], o_leg[l_ss])
|
||||
conn("Realization", o_leg[l_ss], o_leg[LEG_TS["id"]])
|
||||
conn("Serving", o_leg[LEG_TS["id"]], o_leg[l_app])
|
||||
|
||||
row_b = [
|
||||
(1, l_gsw and l_gsw["id"], l_ss, "Aggregation",
|
||||
"Линия с полым ромбом — aggregation: группа объединяет элемент, но тот может "
|
||||
"существовать и без неё. Так показано, на каких узлах работает системное ПО."),
|
||||
(2, l_gap and l_gap["id"], l_app, "Assignment",
|
||||
"Линия с шариком и закрашенной стрелкой — assignment: узел выделен под компонент, "
|
||||
"и компонент на нём исполняется."),
|
||||
(3, l_dm and l_dm["id"], l_app, "Aggregation",
|
||||
"Смысловая группа (Grouping) — домен прикладного слоя. Тоже aggregation: "
|
||||
"компонент принадлежит домену по смыслу, а не по месту размещения."),
|
||||
]
|
||||
for i, src, tgt, rtype, txt in row_b:
|
||||
if not src:
|
||||
continue
|
||||
os_ = obj(vl, src, 20 + i * COL, 320, W, 60)
|
||||
note(vl, txt, 20 + i * COL, 390, W, 96)
|
||||
conn(rtype, os_, o_leg[tgt])
|
||||
if l_art:
|
||||
obj(vl, l_art, 20, 320, W, 60)
|
||||
note(vl, "Артефакт (Artifact) — то, что физически лежит в хранилище: файл, "
|
||||
"образ контейнера, дистрибутив.", 20, 390, W, 96)
|
||||
|
||||
# --- порядок представлений в файле: легенда, технологический слой, прикладной, паспорта
|
||||
ORDER = {"L": 0, "T": 1, "A": 2, "P": 3}
|
||||
views.sort(key=lambda v: (ORDER.get(v["name"][0], 9),
|
||||
int(re.match(r"[A-Z](\d+)", v["name"]).group(1))))
|
||||
|
||||
# ---------------------------------------------------------------- сериализация
|
||||
FOLDERS = [("Strategy", "strategy"), ("Business", "business"), ("Application", "application"),
|
||||
FOLDERS =[("Strategy", "strategy"), ("Business", "business"), ("Application", "application"),
|
||||
("Technology & Physical", "technology"), ("Motivation", "motivation"),
|
||||
("Implementation & Migration", "implementation_migration"), ("Other", "other"),
|
||||
("Relations", "relations"), ("Views", "diagrams")]
|
||||
@ -329,6 +480,11 @@ def emit_obj(o, ind):
|
||||
p = " " * ind
|
||||
tgt = f' targetConnections={quoteattr(" ".join(o["tgt"]))}' if o["tgt"] else ""
|
||||
x, y, w, h = o["b"]
|
||||
if "note" in o:
|
||||
return (f'{p}<child xsi:type="archimate:Note" id="{o["id"]}" textAlignment="1">\n'
|
||||
f'{p} <bounds x="{x}" y="{y}" width="{w}" height="{h}"/>\n'
|
||||
f'{p} <content>{escape(o["note"])}</content>\n'
|
||||
f'{p}</child>\n')
|
||||
s = f'{p}<child xsi:type="archimate:DiagramObject" id="{o["id"]}" archimateElement="{o["el"]}"{tgt}>\n'
|
||||
s += f'{p} <bounds x="{x}" y="{y}" width="{w}" height="{h}"/>\n'
|
||||
for c in o["src"]:
|
||||
@ -346,7 +502,7 @@ out = ['<?xml version="1.0" encoding="UTF-8"?>',
|
||||
|
||||
for fname, ftype in FOLDERS:
|
||||
inner = ""
|
||||
if ftype in ("technology", "application"):
|
||||
if ftype in ("technology", "application", "other"):
|
||||
for folder, eid, etype, name, doc in elements:
|
||||
if folder != ftype:
|
||||
continue
|
||||
|
||||
@ -265,6 +265,88 @@
|
||||
]
|
||||
]
|
||||
},
|
||||
"_домены": "Смысловая группировка прикладного слоя. Описывает продукт, а не инсталляцию, и от контура к контуру обычно не меняется. apps — имена приложений либо идентификаторы компонентов, заданных в node_groups; \"*\" в последней группе собирает всё, что не попало в предыдущие, — так новое приложение не исчезает с карты молча. Пустые группы в представление не выводятся.",
|
||||
"domains": [
|
||||
{
|
||||
"id": "dm-core",
|
||||
"name": "Платформа",
|
||||
"doc": "Общие механизмы, на которые опираются все прикладные домены: единая точка входа в интерфейс, вход и права, справочник атрибутов, рабочие пространства и проекты как контейнеры данных, журнал действий, уведомления, оркестрация процессов.\n\nПочти все связи прикладного слоя ведут сюда, поэтому изменения в этой группе стоят дороже всего: отказ любого её компонента виден сразу во всех доменах, тогда как отказ прикладного сервиса ограничен своей областью.",
|
||||
"apps": [
|
||||
"control-interface",
|
||||
"auth-flow",
|
||||
"iam",
|
||||
"ams-sync",
|
||||
"django",
|
||||
"eav",
|
||||
"workspaces",
|
||||
"projects",
|
||||
"flows",
|
||||
"faas",
|
||||
"message-hub",
|
||||
"subscriptions",
|
||||
"system-log"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "dm-pm",
|
||||
"name": "Управление проектом",
|
||||
"doc": "Планирование и учёт: календарно-сетевые планы, договоры и обязательства по ним, справочник ресурсов, рабочие заметки.\n\nГруппа потребляет данные остальных доменов чаще, чем отдаёт: план и договор ссылаются на документы, замечания и полевые измерения, обратные ссылки редки.",
|
||||
"apps": [
|
||||
"pm",
|
||||
"contracts",
|
||||
"resources",
|
||||
"notes"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "dm-cde",
|
||||
"name": "Документы и общая среда данных",
|
||||
"doc": "Ядро CDE: хранение документов и их версий, связи между документами, вложения, комплекты передачи, чертежи и трёхмерные модели, подготовка САПР-моделей к просмотру, проверка штампов.\n\nСамая нагруженная группа по числу входящих ссылок — на документ ссылается почти каждый прикладной сценарий. Хранилище документов здесь же является и самым связанным компонентом всей платформы.",
|
||||
"apps": [
|
||||
"cde",
|
||||
"documentations",
|
||||
"document-link",
|
||||
"attachments",
|
||||
"transmittal",
|
||||
"drawings",
|
||||
"bim",
|
||||
"ac-bim-converter",
|
||||
"stamp-verification"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "dm-qc",
|
||||
"name": "Контроль качества",
|
||||
"doc": "Работа с несоответствиями: инспекции и чек-листы, замечания, проблемы, запросы информации, ревью, предписания, сравнение редакций чертежей.\n\nГруппа однородна по устройству — почти каждый её сервис читает документ, пишет запись о несоответствии и поднимает процесс в оркестраторе, — поэтому её компоненты сильнее прочих зависят от домена документов и от платформенных процессов.",
|
||||
"apps": [
|
||||
"inspections",
|
||||
"checklists",
|
||||
"remarks",
|
||||
"issues",
|
||||
"rfi",
|
||||
"reviews",
|
||||
"prescriptions",
|
||||
"comparisons"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "dm-field",
|
||||
"name": "Полевые данные",
|
||||
"doc": "Данные, приходящие со стройплощадки: замеры, привязка к местности, сечения, а также обработка тяжёлых входных файлов.\n\nЕдинственная группа с существенной вычислительной нагрузкой: обработка вынесена на отдельные узлы, потому что её всплески не должны мешать интерактивной работе остальных сервисов.",
|
||||
"apps": [
|
||||
"measurements",
|
||||
"mapper",
|
||||
"cross-section",
|
||||
"processing"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "dm-other",
|
||||
"name": "Не отнесено",
|
||||
"doc": "Приложения, не попавшие ни в одну группу выше. В норме группа пуста; появление компонента здесь означает, что группировку пора дополнить.",
|
||||
"apps": "*"
|
||||
}
|
||||
],
|
||||
"services": [
|
||||
{
|
||||
"id": "ts-ingress",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@ -12,8 +12,9 @@ LAYER = { # цвета Archi по слоям
|
||||
"ApplicationComponent": "#B5FFFF", "ApplicationService": "#B5FFFF",
|
||||
"Node": "#C9E7B7", "SystemSoftware": "#C9E7B7",
|
||||
"TechnologyService": "#C9E7B7", "Artifact": "#C9E7B7",
|
||||
"Grouping": "#F4F4F4",
|
||||
}
|
||||
STROKE = {"Application": "#2a7f8f", "Technology": "#4a7f3a"}
|
||||
STROKE = {"Application": "#2a7f8f", "Technology": "#4a7f3a", "Grouping": "#888"}
|
||||
|
||||
concepts, rels = {}, {}
|
||||
for folder in root.findall("folder"):
|
||||
@ -24,17 +25,20 @@ for folder in root.findall("folder"):
|
||||
elif et != "ArchimateDiagramModel":
|
||||
concepts[e.get("id")] = (et, e.get("name") or "")
|
||||
|
||||
def collect(node, ox, oy, nodes, conns):
|
||||
def collect(node, ox, oy, nodes, conns, notes):
|
||||
b = node.find("bounds")
|
||||
x, y = ox + int(b.get("x", 0)), oy + int(b.get("y", 0))
|
||||
w, h = int(b.get("width", 120)), int(b.get("height", 55))
|
||||
if (node.get(XSI) or "") == "archimate:Note":
|
||||
notes.append((node.findtext("content") or "", x, y, w, h))
|
||||
return
|
||||
nodes[node.get("id")] = (node.get("archimateElement"), x, y, w, h)
|
||||
for c in node.findall("sourceConnection"):
|
||||
conns.append((c.get("relationship"), c.get("source"), c.get("target")))
|
||||
for ch in node.findall("child"):
|
||||
collect(ch, x, y, nodes, conns)
|
||||
collect(ch, x, y, nodes, conns, notes)
|
||||
|
||||
def wrap(s, width):
|
||||
def wrap(s, width, limit=4):
|
||||
out, line = [], ""
|
||||
for word in s.split():
|
||||
if len(line) + len(word) + 1 > width and line:
|
||||
@ -43,7 +47,7 @@ def wrap(s, width):
|
||||
line = f"{line} {word}".strip()
|
||||
if line:
|
||||
out.append(line)
|
||||
return out[:4]
|
||||
return out[:limit]
|
||||
|
||||
def edge_point(x, y, w, h, tx, ty):
|
||||
"""Точка пересечения луча к (tx,ty) с границей прямоугольника."""
|
||||
@ -68,6 +72,8 @@ MARKERS = """
|
||||
<path d="M0,5 L6,1 L13,5 L6,9 Z" fill="#555"/></marker>
|
||||
<marker id="ball" markerWidth="10" markerHeight="10" refX="2" refY="5" orient="auto">
|
||||
<circle cx="4" cy="5" r="3.2" fill="#555"/></marker>
|
||||
<marker id="odiamond" markerWidth="16" markerHeight="12" refX="1" refY="5" orient="auto">
|
||||
<path d="M0,5 L6,1 L13,5 L6,9 Z" fill="#fff" stroke="#555" stroke-width="1.2"/></marker>
|
||||
</defs>"""
|
||||
|
||||
STYLE = {
|
||||
@ -75,6 +81,7 @@ STYLE = {
|
||||
"Realization": ('stroke-dasharray="6,4"', 'marker-end="url(#hollow)"', ""),
|
||||
"Composition": ('stroke-dasharray="none"', "", 'marker-start="url(#diamond)"'),
|
||||
"Assignment": ('stroke-dasharray="none"', 'marker-end="url(#filled)"', 'marker-start="url(#ball)"'),
|
||||
"Aggregation": ('stroke-dasharray="none"', "", 'marker-start="url(#odiamond)"'),
|
||||
}
|
||||
|
||||
parts = []
|
||||
@ -82,13 +89,15 @@ views = [e for f in root.findall("folder") for e in f.findall("element")
|
||||
if (e.get(XSI) or "").endswith("ArchimateDiagramModel")]
|
||||
|
||||
for v in views:
|
||||
nodes, conns = {}, []
|
||||
nodes, conns, notes = {}, [], []
|
||||
for ch in v.findall("child"):
|
||||
collect(ch, 0, 0, nodes, conns)
|
||||
collect(ch, 0, 0, nodes, conns, notes)
|
||||
if not nodes:
|
||||
continue
|
||||
maxx = max(x + w for _, x, _, w, _ in nodes.values()) + 40
|
||||
maxy = max(y + h for _, _, y, _, h in nodes.values()) + 40
|
||||
boxes = [(x, y, w, h) for _, x, y, w, h in nodes.values()] + \
|
||||
[(x, y, w, h) for _, x, y, w, h in notes]
|
||||
maxx = max(x + w for x, _, w, _ in boxes) + 40
|
||||
maxy = max(y + h for _, y, _, h in boxes) + 40
|
||||
|
||||
svg = [f'<svg viewBox="0 0 {maxx} {maxy}" xmlns="http://www.w3.org/2000/svg" '
|
||||
f'font-family="system-ui, sans-serif">', MARKERS]
|
||||
@ -110,7 +119,8 @@ for v in views:
|
||||
for nid, (eref, x, y, w, h) in sorted(nodes.items(), key=lambda kv: -kv[1][3] * kv[1][4]):
|
||||
etype, name = concepts.get(eref, ("", eref or "?"))
|
||||
fill = LAYER.get(etype, "#EEEEEE")
|
||||
stroke = STROKE["Application"] if etype.startswith("Application") else STROKE["Technology"]
|
||||
stroke = (STROKE["Grouping"] if etype == "Grouping" else
|
||||
STROKE["Application"] if etype.startswith("Application") else STROKE["Technology"])
|
||||
big = w * h > 60000
|
||||
svg.append(f'<rect x="{x}" y="{y}" width="{w}" height="{h}" rx="4" fill="{fill}" '
|
||||
f'fill-opacity="{0.35 if big else 1}" stroke="{stroke}" stroke-width="1.2"/>')
|
||||
@ -125,6 +135,14 @@ for v in views:
|
||||
svg.append(f'<text x="{x + w/2}" y="{y0 + i*fs*1.25:.0f}" font-size="{fs}" '
|
||||
f'text-anchor="middle" dominant-baseline="middle" fill="#111">'
|
||||
f'{html.escape(ln)}</text>')
|
||||
|
||||
# надписи — поверх всего, без заливки слоя: это не элементы модели
|
||||
for content, x, y, w, h in notes:
|
||||
svg.append(f'<rect x="{x}" y="{y}" width="{w}" height="{h}" rx="3" fill="#fffbe8" '
|
||||
f'stroke="#d8cfa8" stroke-width="1"/>')
|
||||
for i, ln in enumerate(wrap(content, max(16, int(w / 6.2)), limit=int(h // 15))):
|
||||
svg.append(f'<text x="{x+9}" y="{y + 17 + i*14}" font-size="11" fill="#4a4632">'
|
||||
f'{html.escape(ln)}</text>')
|
||||
svg.append("</svg>")
|
||||
|
||||
doc = v.find("documentation")
|
||||
|
||||
@ -45,8 +45,10 @@ def flatten(node, ox=0, oy=0, out=None, conns=None):
|
||||
conns = [] if conns is None else conns
|
||||
b = node.find("bounds")
|
||||
x = ox + int(b.get("x", 0)); y = oy + int(b.get("y", 0))
|
||||
# у надписей (Note) нет archimateElement — в обменном формате это узел типа Label
|
||||
out.append((node.get("id"), node.get("archimateElement"),
|
||||
x, y, int(b.get("width", 120)), int(b.get("height", 55))))
|
||||
x, y, int(b.get("width", 120)), int(b.get("height", 55)),
|
||||
text(node, "content")))
|
||||
for c in node.findall("sourceConnection"):
|
||||
conns.append((c.get("id"), c.get("relationship"),
|
||||
c.get("source"), c.get("target")))
|
||||
@ -88,9 +90,15 @@ for v in views:
|
||||
nodes, conns = [], []
|
||||
for child in v.findall("child"):
|
||||
flatten(child, 0, 0, nodes, conns)
|
||||
for nid, eref, x, y, w, h in nodes:
|
||||
out.append(f' <node identifier="{nid}" elementRef="{eref}" xsi:type="Element" '
|
||||
f'x="{x}" y="{y}" w="{w}" h="{h}"/>')
|
||||
for nid, eref, x, y, w, h, content in nodes:
|
||||
if eref:
|
||||
out.append(f' <node identifier="{nid}" elementRef="{eref}" xsi:type="Element" '
|
||||
f'x="{x}" y="{y}" w="{w}" h="{h}"/>')
|
||||
else:
|
||||
out.append(f' <node identifier="{nid}" xsi:type="Label" '
|
||||
f'x="{x}" y="{y}" w="{w}" h="{h}">')
|
||||
out.append(f' <label xml:lang="ru">{escape(content)}</label>')
|
||||
out.append(' </node>')
|
||||
for cid, rref, s, tg in conns:
|
||||
out.append(f' <connection identifier="{cid}" relationshipRef="{rref}" '
|
||||
f'xsi:type="Relationship" source="{s}" target="{tg}"/>')
|
||||
|
||||
BIN
legend.png
Normal file
BIN
legend.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
Loading…
Reference in New Issue
Block a user