mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 03:10:46 +08:00
615 lines
20 KiB
Python
615 lines
20 KiB
Python
"""Seed UME hierarchical canvases: World (level) + World Flat + per-SBN views."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any
|
|
from uuid import uuid4
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
from .models import TopoFabricNode, TopoFolder, TopoView, UmeTopoNode
|
|
from .topology_common import _utcnow
|
|
from .topology_membership import VIEW_KIND_PHYSICAL
|
|
from .topology_views_tree import bootstrap_topology_tree
|
|
|
|
_log = logging.getLogger("netx.ume.topo_world")
|
|
|
|
WORLD_FOLDER_NAME = "UME World"
|
|
WORLD_DRILL_FOLDER_NAME = "World"
|
|
WORLD_FOLDER_REF = "ume:world"
|
|
WORLD_DRILL_REF = "ume:world:drill"
|
|
WORLD_VIEW_NAME = "World"
|
|
WORLD_FLAT_VIEW_NAME = "世界地图"
|
|
# Pre-rename / locale labels kept so existing TopoView rows still match.
|
|
WORLD_FLAT_VIEW_NAME_LEGACY = "完整世界地图"
|
|
WORLD_FLAT_VIEW_NAME_EN = "World map"
|
|
WORLD_FLAT_VIEW_NAMES = frozenset(
|
|
{WORLD_FLAT_VIEW_NAME, WORLD_FLAT_VIEW_NAME_LEGACY, WORLD_FLAT_VIEW_NAME_EN}
|
|
)
|
|
# Drill-down root: children of MD (root SBNs + rare direct MEs).
|
|
WORLD_LEVEL_FILTER = {"ume_level": True, "parent": "md"}
|
|
WORLD_FLAT_FILTER = {"world_flat": True}
|
|
|
|
|
|
def is_world_flat_view_name(name: str | None) -> bool:
|
|
return str(name or "").strip() in WORLD_FLAT_VIEW_NAMES
|
|
|
|
|
|
def _filt(view: TopoView | None) -> dict[str, Any]:
|
|
if view is None:
|
|
return {}
|
|
from .topology_common import view_filter_dict
|
|
|
|
return view_filter_dict(getattr(view, "filter", None))
|
|
|
|
|
|
def is_ume_level_view(view: TopoView | None) -> bool:
|
|
return bool(_filt(view).get("ume_level"))
|
|
|
|
|
|
def is_world_flat_view(view: TopoView | None) -> bool:
|
|
filt = _filt(view)
|
|
return bool(filt.get("world_flat")) or is_world_flat_view_name(str(getattr(view, "name", "") or ""))
|
|
|
|
|
|
def is_ume_world_container(folder: TopoFolder | None) -> bool:
|
|
if folder is None:
|
|
return False
|
|
ext = str(getattr(folder, "external_ref", None) or "").strip()
|
|
if ext == WORLD_FOLDER_REF:
|
|
return True
|
|
return str(folder.name or "").strip() == WORLD_FOLDER_NAME and bool(folder.is_system)
|
|
|
|
|
|
def is_world_drill_folder(folder: TopoFolder | None) -> bool:
|
|
if folder is None:
|
|
return False
|
|
ext = str(getattr(folder, "external_ref", None) or "").strip()
|
|
return ext == WORLD_DRILL_REF or (
|
|
str(folder.name or "") == WORLD_DRILL_FOLDER_NAME and bool(folder.is_system)
|
|
)
|
|
|
|
|
|
def is_world_view(view: TopoView | None) -> bool:
|
|
"""True for UME World drill-root (legacy `world` key or ume_level parent=md)."""
|
|
if view is None:
|
|
return False
|
|
filt = _filt(view)
|
|
if filt.get("world_flat"):
|
|
return False
|
|
if filt.get("ume_level") and str(filt.get("parent") or "") == "md":
|
|
return True
|
|
# Legacy flat-world filter — treat as flat, not drill root.
|
|
if bool(filt.get("world")) and not filt.get("ume_level"):
|
|
return False
|
|
return str(view.name or "") == WORLD_VIEW_NAME and not is_world_flat_view(view)
|
|
|
|
|
|
def is_ume_canvas_view(view: TopoView | None) -> bool:
|
|
"""Any UME-driven virtual canvas (level or flat) — bypass membership graph."""
|
|
if view is None:
|
|
return False
|
|
filt = _filt(view)
|
|
if filt.get("ume_level") or filt.get("world_flat"):
|
|
return True
|
|
# Legacy world viewport
|
|
return bool(filt.get("world"))
|
|
|
|
|
|
def get_world_container_folder(db: Session) -> TopoFolder | None:
|
|
for f in db.query(TopoFolder).filter(TopoFolder.kind == "region").all():
|
|
if is_ume_world_container(f):
|
|
return f
|
|
return None
|
|
|
|
|
|
def get_world_drill_folder(db: Session) -> TopoFolder | None:
|
|
row = (
|
|
db.query(TopoFolder)
|
|
.filter(TopoFolder.external_ref == WORLD_DRILL_REF)
|
|
.first()
|
|
)
|
|
if row is not None:
|
|
return row
|
|
for f in db.query(TopoFolder).filter(TopoFolder.kind == "region").all():
|
|
if is_world_drill_folder(f):
|
|
return f
|
|
return None
|
|
|
|
|
|
def get_world_view(db: Session) -> TopoView | None:
|
|
"""Drill-down World root canvas (not the flat all-NE map)."""
|
|
for v in db.query(TopoView).all():
|
|
if is_world_view(v) or (
|
|
is_ume_level_view(v) and str(_filt(v).get("parent") or "") == "md"
|
|
):
|
|
return v
|
|
return None
|
|
|
|
|
|
def get_world_flat_view(db: Session) -> TopoView | None:
|
|
for v in db.query(TopoView).all():
|
|
if is_world_flat_view(v):
|
|
return v
|
|
return None
|
|
|
|
|
|
def _folder_subtree_ids(
|
|
db: Session,
|
|
folder_id: str,
|
|
*,
|
|
children_map: dict[str | None, list[str]] | None = None,
|
|
) -> set[str]:
|
|
if children_map is None:
|
|
children_map = {}
|
|
for fid, parent_id in db.query(TopoFolder.id, TopoFolder.parent_id).all():
|
|
children_map.setdefault(parent_id, []).append(fid)
|
|
ids = {folder_id}
|
|
stack = [folder_id]
|
|
while stack:
|
|
cur = stack.pop()
|
|
for cid in children_map.get(cur, []):
|
|
if cid not in ids:
|
|
ids.add(cid)
|
|
stack.append(cid)
|
|
return ids
|
|
|
|
|
|
def _child_region_has_nes(db: Session, child: TopoFolder) -> bool:
|
|
"""True when the child region subtree owns at least one fabric NE."""
|
|
ids = _folder_subtree_ids(db, child.id)
|
|
if ids and (
|
|
db.query(TopoFabricNode.id)
|
|
.filter(TopoFabricNode.region_folder_id.in_(list(ids)))
|
|
.limit(1)
|
|
.first()
|
|
is not None
|
|
):
|
|
return True
|
|
# Cheap fallback: direct UME ME under this SBN (never scan all fabric attrs).
|
|
ref = str(getattr(child, "external_ref", None) or "").strip()
|
|
if ref and not ref.startswith("ume:"):
|
|
if (
|
|
db.query(UmeTopoNode.node_id)
|
|
.filter(
|
|
UmeTopoNode.node_type == "TOPO_NODE_ME",
|
|
UmeTopoNode.parent_node == ref,
|
|
)
|
|
.limit(1)
|
|
.first()
|
|
is not None
|
|
):
|
|
return True
|
|
return False
|
|
|
|
|
|
def world_map_should_exist(db: Session) -> bool:
|
|
"""Rule 2A: L2 has nested child regions and at least one of those has NEs.
|
|
|
|
If the unique L2 canvas only has direct NEs (no child regions), world map is
|
|
redundant with the L2 canvas and must not appear.
|
|
"""
|
|
drill = get_world_drill_folder(db)
|
|
if drill is None:
|
|
return False
|
|
children = (
|
|
db.query(TopoFolder)
|
|
.filter(TopoFolder.parent_id == drill.id, TopoFolder.kind == "region")
|
|
.all()
|
|
)
|
|
if not children:
|
|
return False
|
|
children_map: dict[str | None, list[str]] = {}
|
|
for fid, parent_id in db.query(TopoFolder.id, TopoFolder.parent_id).all():
|
|
children_map.setdefault(parent_id, []).append(fid)
|
|
all_ids: set[str] = set()
|
|
for c in children:
|
|
all_ids |= _folder_subtree_ids(db, c.id, children_map=children_map)
|
|
if all_ids and (
|
|
db.query(TopoFabricNode.id)
|
|
.filter(TopoFabricNode.region_folder_id.in_(list(all_ids)))
|
|
.limit(1)
|
|
.first()
|
|
is not None
|
|
):
|
|
return True
|
|
refs = [
|
|
str(getattr(c, "external_ref", None) or "").strip()
|
|
for c in children
|
|
if str(getattr(c, "external_ref", None) or "").strip()
|
|
and not str(getattr(c, "external_ref", None) or "").startswith("ume:")
|
|
]
|
|
if refs and (
|
|
db.query(UmeTopoNode.node_id)
|
|
.filter(
|
|
UmeTopoNode.node_type == "TOPO_NODE_ME",
|
|
UmeTopoNode.parent_node.in_(refs),
|
|
)
|
|
.limit(1)
|
|
.first()
|
|
is not None
|
|
):
|
|
return True
|
|
return False
|
|
|
|
|
|
def reconcile_world_flat_view(db: Session) -> TopoView | None:
|
|
"""Create flat view when 2A holds; keep row but mark suppressed when not.
|
|
|
|
Soft-hide via filter.suppressed so drag overrides on TopoViewNode survive.
|
|
"""
|
|
container = get_world_container_folder(db)
|
|
if container is None:
|
|
return None
|
|
should = world_map_should_exist(db)
|
|
existing = get_world_flat_view(db)
|
|
now = _utcnow()
|
|
if should:
|
|
if existing is None:
|
|
existing = _ensure_view(
|
|
db,
|
|
folder_id=container.id,
|
|
name=WORLD_FLAT_VIEW_NAME,
|
|
filt=dict(WORLD_FLAT_FILTER),
|
|
remark="All NEs with composed flat-world coordinates (no regions)",
|
|
sort_order=1,
|
|
match=is_world_flat_view,
|
|
)
|
|
else:
|
|
existing.folder_id = container.id
|
|
filt = dict(existing.filter or {})
|
|
filt.update(WORLD_FLAT_FILTER)
|
|
filt.pop("suppressed", None)
|
|
existing.filter = filt
|
|
existing.name = WORLD_FLAT_VIEW_NAME
|
|
existing.updated_at = now
|
|
return existing
|
|
if existing is not None:
|
|
filt = dict(existing.filter or {})
|
|
filt.update(WORLD_FLAT_FILTER)
|
|
filt["suppressed"] = True
|
|
existing.filter = filt
|
|
existing.updated_at = now
|
|
return existing
|
|
|
|
|
|
def is_world_flat_visible(view: TopoView | None) -> bool:
|
|
"""Tree/hex should show flat only when not suppressed and 2A would hold."""
|
|
if view is None or not is_world_flat_view(view):
|
|
return False
|
|
if bool(_filt(view).get("suppressed")):
|
|
return False
|
|
return True
|
|
|
|
|
|
def _ensure_view(
|
|
db: Session,
|
|
*,
|
|
folder_id: str,
|
|
name: str,
|
|
filt: dict[str, Any],
|
|
remark: str,
|
|
sort_order: int,
|
|
match,
|
|
) -> TopoView:
|
|
"""Find view under folder matching predicate, or create."""
|
|
now = _utcnow()
|
|
found: TopoView | None = None
|
|
for v in db.query(TopoView).filter(TopoView.folder_id == folder_id).all():
|
|
if match(v):
|
|
found = v
|
|
break
|
|
if found is None:
|
|
found = TopoView(
|
|
id=uuid4().hex,
|
|
folder_id=folder_id,
|
|
kind=VIEW_KIND_PHYSICAL,
|
|
role="core",
|
|
name=name,
|
|
remark=remark,
|
|
sort_order=sort_order,
|
|
filter=dict(filt),
|
|
viewport={},
|
|
created_at=now,
|
|
updated_at=now,
|
|
)
|
|
db.add(found)
|
|
db.flush()
|
|
return found
|
|
found.kind = VIEW_KIND_PHYSICAL
|
|
found.name = name
|
|
found.remark = remark
|
|
found.sort_order = sort_order
|
|
cur = dict(found.filter or {})
|
|
cur.update(filt)
|
|
found.filter = cur
|
|
found.updated_at = now
|
|
return found
|
|
|
|
|
|
def ensure_ume_world_and_sbn_folders(db: Session) -> dict[str, Any]:
|
|
"""Create/update UME World container + unique L2 World drill + conditional Flat.
|
|
|
|
Tree shape::
|
|
|
|
UME World (nav only — hex browse)
|
|
├─ World (unique L2 folder ume:world:drill — SBNs hang here)
|
|
│ └─ <root SBNs…>
|
|
└─ 世界地图 (flat view — only when rule 2A holds)
|
|
"""
|
|
now = _utcnow()
|
|
stats: dict[str, Any] = {
|
|
"world_folder_id": "",
|
|
"world_drill_folder_id": "",
|
|
"world_view_id": "",
|
|
"world_flat_view_id": "",
|
|
"world_map_visible": False,
|
|
"sbn_folders": 0,
|
|
"sbn_updated": 0,
|
|
"sbn_views": 0,
|
|
}
|
|
bootstrap_topology_tree(db)
|
|
root = db.query(TopoFolder).filter(TopoFolder.kind == "root").first()
|
|
if root is None:
|
|
raise RuntimeError("topology_root_missing")
|
|
|
|
world_folder = (
|
|
db.query(TopoFolder)
|
|
.filter(TopoFolder.kind == "region", TopoFolder.name == WORLD_FOLDER_NAME)
|
|
.first()
|
|
)
|
|
if world_folder is None:
|
|
world_folder = TopoFolder(
|
|
id=uuid4().hex,
|
|
parent_id=root.id,
|
|
kind="region",
|
|
name=WORLD_FOLDER_NAME,
|
|
sort_order=0,
|
|
is_system=True,
|
|
external_ref=WORLD_FOLDER_REF,
|
|
created_at=now,
|
|
updated_at=now,
|
|
)
|
|
db.add(world_folder)
|
|
db.flush()
|
|
else:
|
|
world_folder.is_system = True
|
|
world_folder.external_ref = world_folder.external_ref or WORLD_FOLDER_REF
|
|
world_folder.updated_at = now
|
|
|
|
# Drill folder "World" under UME World — owns the level canvas + SBN children.
|
|
drill_folder = (
|
|
db.query(TopoFolder)
|
|
.filter(TopoFolder.external_ref == WORLD_DRILL_REF)
|
|
.first()
|
|
)
|
|
if drill_folder is None:
|
|
drill_folder = (
|
|
db.query(TopoFolder)
|
|
.filter(
|
|
TopoFolder.parent_id == world_folder.id,
|
|
TopoFolder.name == WORLD_DRILL_FOLDER_NAME,
|
|
TopoFolder.is_system.is_(True),
|
|
)
|
|
.first()
|
|
)
|
|
if drill_folder is None:
|
|
drill_folder = TopoFolder(
|
|
id=uuid4().hex,
|
|
parent_id=world_folder.id,
|
|
kind="region",
|
|
name=WORLD_DRILL_FOLDER_NAME,
|
|
sort_order=0,
|
|
is_system=True,
|
|
external_ref=WORLD_DRILL_REF,
|
|
created_at=now,
|
|
updated_at=now,
|
|
)
|
|
db.add(drill_folder)
|
|
db.flush()
|
|
else:
|
|
drill_folder.parent_id = world_folder.id
|
|
drill_folder.name = WORLD_DRILL_FOLDER_NAME
|
|
drill_folder.is_system = True
|
|
drill_folder.external_ref = WORLD_DRILL_REF
|
|
drill_folder.sort_order = 0
|
|
drill_folder.updated_at = now
|
|
|
|
world_view = _ensure_view(
|
|
db,
|
|
folder_id=drill_folder.id,
|
|
name=WORLD_VIEW_NAME,
|
|
filt=dict(WORLD_LEVEL_FILTER),
|
|
remark="UME drill-down root (child regions + direct MEs)",
|
|
sort_order=0,
|
|
match=lambda v: is_world_view(v)
|
|
or (is_ume_level_view(v) and str(_filt(v).get("parent") or "") == "md")
|
|
or (bool(_filt(v).get("world")) and not _filt(v).get("world_flat")),
|
|
)
|
|
# Migrate legacy World view that still hangs on UME World container.
|
|
for v in db.query(TopoView).filter(TopoView.folder_id == world_folder.id).all():
|
|
if is_world_flat_view(v):
|
|
continue
|
|
if is_world_view(v) or (
|
|
is_ume_level_view(v) and str(_filt(v).get("parent") or "") == "md"
|
|
):
|
|
if v.id != world_view.id:
|
|
v.folder_id = drill_folder.id
|
|
vf = dict(v.filter or {})
|
|
vf.pop("world", None)
|
|
vf.pop("mode", None)
|
|
vf.update(WORLD_LEVEL_FILTER)
|
|
v.filter = vf
|
|
v.updated_at = now
|
|
wf = dict(world_view.filter or {})
|
|
wf.pop("world", None)
|
|
wf.pop("mode", None)
|
|
wf.update(WORLD_LEVEL_FILTER)
|
|
world_view.filter = wf
|
|
world_view.folder_id = drill_folder.id
|
|
|
|
stats["world_folder_id"] = world_folder.id
|
|
stats["world_drill_folder_id"] = drill_folder.id
|
|
stats["world_view_id"] = world_view.id
|
|
|
|
sbns = (
|
|
db.query(UmeTopoNode)
|
|
.filter(UmeTopoNode.node_type == "TOPO_NODE_SBN")
|
|
.all()
|
|
)
|
|
sbn_ids = {str(s.node_id) for s in sbns if str(s.node_id or "").strip()}
|
|
by_ref: dict[str, TopoFolder] = {
|
|
str(f.external_ref): f
|
|
for f in db.query(TopoFolder)
|
|
.filter(TopoFolder.external_ref.isnot(None), TopoFolder.external_ref != "")
|
|
.all()
|
|
if str(f.external_ref or "").strip() and not str(f.external_ref).startswith("ume:")
|
|
}
|
|
|
|
for sbn in sbns:
|
|
sid = str(sbn.node_id or "").strip()
|
|
if not sid:
|
|
continue
|
|
label = (sbn.user_label or sid)[:256]
|
|
folder = by_ref.get(sid)
|
|
if folder is None:
|
|
folder = TopoFolder(
|
|
id=uuid4().hex,
|
|
parent_id=drill_folder.id,
|
|
kind="region",
|
|
name=label,
|
|
sort_order=100,
|
|
is_system=True,
|
|
external_ref=sid,
|
|
created_at=now,
|
|
updated_at=now,
|
|
)
|
|
db.add(folder)
|
|
by_ref[sid] = folder
|
|
stats["sbn_folders"] += 1
|
|
else:
|
|
folder.name = label
|
|
folder.is_system = True
|
|
folder.updated_at = now
|
|
stats["sbn_updated"] += 1
|
|
|
|
db.flush()
|
|
|
|
for sbn in sbns:
|
|
sid = str(sbn.node_id or "").strip()
|
|
folder = by_ref.get(sid)
|
|
if folder is None:
|
|
continue
|
|
parent = str(sbn.parent_node or "").strip()
|
|
if parent in sbn_ids and parent in by_ref:
|
|
folder.parent_id = by_ref[parent].id
|
|
else:
|
|
# Root SBNs hang under World (drill), not UME World container.
|
|
folder.parent_id = drill_folder.id
|
|
|
|
label = (sbn.user_label or sid)[:256]
|
|
level_filt = {
|
|
"ume_level": True,
|
|
"sbn_id": sid,
|
|
"ume_x": float(sbn.x_pos) if sbn.x_pos is not None else None,
|
|
"ume_y": float(sbn.y_pos) if sbn.y_pos is not None else None,
|
|
}
|
|
_ensure_view(
|
|
db,
|
|
folder_id=folder.id,
|
|
name=label[:200] or "Topology",
|
|
filt=level_filt,
|
|
remark=f"UME SBN level canvas ({sid})",
|
|
sort_order=0,
|
|
match=lambda v, _sid=sid: is_ume_level_view(v)
|
|
and str(_filt(v).get("sbn_id") or "") == _sid,
|
|
)
|
|
stats["sbn_views"] += 1
|
|
|
|
# Tag fabric ME nodes with direct SBN for region filter / flat packing.
|
|
me_parent = {
|
|
str(n.ume_ne_id or n.node_id): str(n.parent_node or "")
|
|
for n in db.query(UmeTopoNode).filter(UmeTopoNode.node_type == "TOPO_NODE_ME").all()
|
|
if str(n.ume_ne_id or n.node_id or "").strip()
|
|
}
|
|
for fn in db.query(TopoFabricNode).filter(TopoFabricNode.ume_ne_id.isnot(None)).all():
|
|
uid = str(fn.ume_ne_id or "").strip()
|
|
parent = me_parent.get(uid, "")
|
|
if not parent:
|
|
continue
|
|
attrs = dict(fn.attrs or {})
|
|
attrs["ume_sbn_id"] = parent[:128]
|
|
folder = by_ref.get(parent)
|
|
if folder is not None:
|
|
fn.region_folder_id = folder.id
|
|
fn.region_source = "ume"
|
|
fn.attrs = attrs
|
|
|
|
db.flush()
|
|
flat_view = reconcile_world_flat_view(db)
|
|
stats["world_map_visible"] = world_map_should_exist(db)
|
|
if flat_view is not None and stats["world_map_visible"]:
|
|
stats["world_flat_view_id"] = flat_view.id
|
|
elif flat_view is not None:
|
|
stats["world_flat_view_id"] = flat_view.id
|
|
|
|
db.commit()
|
|
try:
|
|
from .ume_topology_flat_coords import recompute_flat_world_coords
|
|
|
|
stats["flat_coords"] = recompute_flat_world_coords(db)
|
|
except Exception:
|
|
_log.exception("recompute_flat_world_coords after world ensure failed")
|
|
_log.info("ume world/sbn folders ready: %s", stats)
|
|
return stats
|
|
|
|
|
|
def folder_bbox(db: Session, folder_id: str) -> dict[str, float | int] | None:
|
|
"""Bounding box of fabric nodes tagged with this SBN folder (or descendants)."""
|
|
folder = db.get(TopoFolder, folder_id)
|
|
if folder is None:
|
|
return None
|
|
all_folders = db.query(TopoFolder).all()
|
|
children: dict[str | None, list[str]] = {}
|
|
for f in all_folders:
|
|
children.setdefault(f.parent_id, []).append(f.id)
|
|
ids = {folder_id}
|
|
stack = [folder_id]
|
|
while stack:
|
|
cur = stack.pop()
|
|
for cid in children.get(cur, []):
|
|
if cid not in ids:
|
|
ids.add(cid)
|
|
stack.append(cid)
|
|
|
|
q = (
|
|
db.query(TopoFabricNode)
|
|
.filter(
|
|
TopoFabricNode.region_folder_id.in_(list(ids)),
|
|
TopoFabricNode.world_x.isnot(None),
|
|
TopoFabricNode.world_y.isnot(None),
|
|
)
|
|
.all()
|
|
)
|
|
if not q:
|
|
ref = str(folder.external_ref or "").strip()
|
|
if ref:
|
|
q = [
|
|
n
|
|
for n in db.query(TopoFabricNode)
|
|
.filter(TopoFabricNode.world_x.isnot(None), TopoFabricNode.world_y.isnot(None))
|
|
.all()
|
|
if str((n.attrs or {}).get("ume_sbn_id") or "") == ref
|
|
]
|
|
if not q:
|
|
return None
|
|
xs = [float(n.world_x) for n in q]
|
|
ys = [float(n.world_y) for n in q]
|
|
return {
|
|
"min_x": min(xs),
|
|
"max_x": max(xs),
|
|
"min_y": min(ys),
|
|
"max_y": max(ys),
|
|
"n": len(q),
|
|
}
|