netx/netx_api/ume_topology_world.py
2026-08-07 14:41:06 +08:00

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),
}