tbox: import initial meta model v1.0 - 5 couches, axe curated/observed, couches physique et consommation modelisees, proprietes de pontage declarees. Etat connu : instances/ contient un TTL v0.6 et un back-doc v0.7 divergents, migration BR-013 au backlog
This commit is contained in:
@@ -0,0 +1,979 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PR Data Meta Model - T-Box viewer generator
|
||||
===========================================
|
||||
Reads the T-Box Turtle file and emits a self-contained, zero-dependency HTML
|
||||
viewer. The TTL is the source of truth; the viewer is a build artefact and must
|
||||
never be edited by hand.
|
||||
|
||||
USAGE
|
||||
python generate_tbox_viewer.py [tbox.ttl] [out.html]
|
||||
|
||||
Defaults: pr_datametamodel_v1_0.ttl -> pr_metamodel_viewer_v1_0.html
|
||||
|
||||
Uses rdflib when available; otherwise falls back to a restricted Turtle reader
|
||||
that handles the subset of syntax used by this T-Box. Run it in CI on every
|
||||
merge so the viewer can never drift from the model.
|
||||
"""
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
|
||||
PR = "https://ontology.pernod-ricard.com/metamodel/"
|
||||
|
||||
WANTED = {
|
||||
"a": "type", "rdf:type": "type",
|
||||
"rdfs:label": "label", "rdfs:comment": "comment",
|
||||
"rdfs:subClassOf": "subClassOf", "rdfs:subPropertyOf": "subPropertyOf",
|
||||
"rdfs:domain": "domain", "rdfs:range": "range",
|
||||
"owl:inverseOf": "inverseOf", "owl:deprecated": "deprecated",
|
||||
"dcterms:isReplacedBy": "isReplacedBy",
|
||||
"pr:harvestSource": "harvestSource", "pr:curationMode": "curationMode",
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Turtle reading
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def strip_comments(text):
|
||||
"""Remove # comments that sit outside string literals."""
|
||||
out, i, n = [], 0, len(text)
|
||||
while i < n:
|
||||
c = text[i]
|
||||
if text.startswith('"""', i):
|
||||
j = text.find('"""', i + 3)
|
||||
j = n if j == -1 else j + 3
|
||||
out.append(text[i:j]); i = j; continue
|
||||
if c == '"':
|
||||
j = i + 1
|
||||
while j < n and text[j] != '"':
|
||||
j += 2 if text[j] == "\\" else 1
|
||||
j = min(j + 1, n)
|
||||
out.append(text[i:j]); i = j; continue
|
||||
if c == "#":
|
||||
j = text.find("\n", i)
|
||||
i = n if j == -1 else j
|
||||
continue
|
||||
out.append(c); i += 1
|
||||
return "".join(out)
|
||||
|
||||
|
||||
def split_top(text, sep):
|
||||
"""Split on `sep` at nesting depth 0, ignoring string literals."""
|
||||
parts, buf, depth, i, n = [], [], 0, 0, len(text)
|
||||
while i < n:
|
||||
c = text[i]
|
||||
if text.startswith('"""', i):
|
||||
j = text.find('"""', i + 3)
|
||||
j = n if j == -1 else j + 3
|
||||
buf.append(text[i:j]); i = j; continue
|
||||
if c == '"':
|
||||
j = i + 1
|
||||
while j < n and text[j] != '"':
|
||||
j += 2 if text[j] == "\\" else 1
|
||||
j = min(j + 1, n)
|
||||
buf.append(text[i:j]); i = j; continue
|
||||
if c in "([":
|
||||
depth += 1
|
||||
elif c in ")]":
|
||||
depth -= 1
|
||||
if c == sep and depth == 0:
|
||||
parts.append("".join(buf)); buf = []; i += 1; continue
|
||||
buf.append(c); i += 1
|
||||
if "".join(buf).strip():
|
||||
parts.append("".join(buf))
|
||||
return parts
|
||||
|
||||
|
||||
def tokens(text):
|
||||
"""Split an object list into individual terms."""
|
||||
return [t.strip() for t in split_top(text, ",") if t.strip()]
|
||||
|
||||
|
||||
def unquote(tok):
|
||||
tok = tok.strip()
|
||||
if tok.startswith('"""'):
|
||||
return tok[3:tok.rindex('"""')] if tok.count('"""') > 1 else tok[3:]
|
||||
if tok.startswith('"'):
|
||||
end = tok.rindex('"')
|
||||
return tok[1:end] if end > 0 else tok[1:]
|
||||
return tok
|
||||
|
||||
|
||||
def parse_ttl_fallback(text):
|
||||
text = strip_comments(text)
|
||||
subjects = OrderedDict()
|
||||
for stmt in split_top(text, "."):
|
||||
stmt = stmt.strip()
|
||||
if not stmt:
|
||||
continue
|
||||
chunks = split_top(stmt, ";")
|
||||
head = chunks[0].strip()
|
||||
m = re.match(r"^(\S+)\s+(.*)$", head, re.S)
|
||||
if not m:
|
||||
continue
|
||||
subj, rest = m.group(1), m.group(2)
|
||||
if not subj.startswith("pr:"):
|
||||
continue
|
||||
rec = subjects.setdefault(subj, {})
|
||||
for chunk in [rest] + [c.strip() for c in chunks[1:]]:
|
||||
chunk = chunk.strip()
|
||||
if not chunk:
|
||||
continue
|
||||
pm = re.match(r"^(\S+)\s+(.*)$", chunk, re.S)
|
||||
if not pm:
|
||||
continue
|
||||
pred, objs = pm.group(1), pm.group(2)
|
||||
key = WANTED.get(pred)
|
||||
if not key:
|
||||
continue
|
||||
rec.setdefault(key, []).extend(unquote(t) for t in tokens(objs))
|
||||
return subjects
|
||||
|
||||
|
||||
def parse_ttl_rdflib(path):
|
||||
from rdflib import Graph, RDF, RDFS, OWL, URIRef, Namespace
|
||||
DCT = Namespace("http://purl.org/dc/terms/")
|
||||
g = Graph(); g.parse(path, format="turtle")
|
||||
inv = {
|
||||
RDF.type: "type", RDFS.label: "label", RDFS.comment: "comment",
|
||||
RDFS.subClassOf: "subClassOf", RDFS.subPropertyOf: "subPropertyOf",
|
||||
RDFS.domain: "domain", RDFS.range: "range",
|
||||
OWL.inverseOf: "inverseOf", OWL.deprecated: "deprecated",
|
||||
DCT.isReplacedBy: "isReplacedBy",
|
||||
URIRef(PR + "harvestSource"): "harvestSource",
|
||||
URIRef(PR + "curationMode"): "curationMode",
|
||||
}
|
||||
|
||||
def short(term):
|
||||
s = str(term)
|
||||
for pfx, ns in (("pr:", PR),
|
||||
("owl:", "http://www.w3.org/2002/07/owl#"),
|
||||
("rdfs:", "http://www.w3.org/2000/01/rdf-schema#"),
|
||||
("xsd:", "http://www.w3.org/2001/XMLSchema#"),
|
||||
("prov:", "http://www.w3.org/ns/prov#")):
|
||||
if s.startswith(ns):
|
||||
return pfx + s[len(ns):]
|
||||
return s
|
||||
|
||||
subjects = OrderedDict()
|
||||
for s, p, o in g:
|
||||
key = inv.get(p)
|
||||
if key is None or not str(s).startswith(PR):
|
||||
continue
|
||||
rec = subjects.setdefault(short(s), {})
|
||||
rec.setdefault(key, []).append(str(o) if key in ("label", "comment", "harvestSource", "curationMode", "deprecated") else short(o))
|
||||
return subjects
|
||||
|
||||
|
||||
def read_tbox(path):
|
||||
try:
|
||||
return parse_ttl_rdflib(path), "rdflib"
|
||||
except ImportError:
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
return parse_ttl_fallback(fh.read()), "built-in reader"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Model assembly
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
BANDS = [
|
||||
("roots", "Model roots", "How a term got here", "#0A1F44"),
|
||||
("ownership", "Ownership & Categorization", "Who is accountable", "#B8935A"),
|
||||
("business", "Business", "What the business means", "#8B3A3A"),
|
||||
("logical", "Logical", "What we commit to deliver", "#7A4E8B"),
|
||||
("physical", "Physical", "What actually runs", "#1E4B9B"),
|
||||
("consumption", "Consumption", "What people open in the morning", "#1E7A5F"),
|
||||
("context", "Actors & vocabularies", "Outside the object stack", "#6B6B6B"),
|
||||
]
|
||||
|
||||
LAYER_ROOT = {
|
||||
"pr:OwnershipLayerObject": "ownership",
|
||||
"pr:BusinessLayerObject": "business",
|
||||
"pr:LogicalLayerObject": "logical",
|
||||
"pr:PhysicalLayerObject": "physical",
|
||||
"pr:ConsumptionLayerObject": "consumption",
|
||||
}
|
||||
|
||||
|
||||
def first(rec, key, default=""):
|
||||
v = rec.get(key)
|
||||
return v[0] if v else default
|
||||
|
||||
|
||||
def build(subjects):
|
||||
classes, objprops, dataprops, annprops, individuals = {}, {}, {}, {}, {}
|
||||
for term, rec in subjects.items():
|
||||
types = rec.get("type", [])
|
||||
if "owl:Class" in types:
|
||||
classes[term] = rec
|
||||
elif "owl:ObjectProperty" in types:
|
||||
objprops[term] = rec
|
||||
elif "owl:DatatypeProperty" in types:
|
||||
dataprops[term] = rec
|
||||
elif "owl:AnnotationProperty" in types:
|
||||
annprops[term] = rec
|
||||
elif types and types[0].startswith("pr:"):
|
||||
individuals.setdefault(types[0], []).append(term)
|
||||
|
||||
# layer + provenance by walking subClassOf upward
|
||||
def ancestry(term, seen=None):
|
||||
seen = seen or set()
|
||||
if term in seen:
|
||||
return []
|
||||
seen.add(term)
|
||||
out = [term]
|
||||
for parent in classes.get(term, {}).get("subClassOf", []):
|
||||
out += ancestry(parent, seen)
|
||||
return out
|
||||
|
||||
nodes = []
|
||||
for term, rec in classes.items():
|
||||
anc = ancestry(term)
|
||||
band = next((LAYER_ROOT[a] for a in anc if a in LAYER_ROOT), None)
|
||||
if band is None:
|
||||
if "pr:Actor" in anc or term in ("pr:ActivationStatus", "pr:Environment", "pr:SystemType"):
|
||||
band = "context"
|
||||
else:
|
||||
band = "roots"
|
||||
provenance = ("observed" if "pr:ObservedObject" in anc
|
||||
else "curated" if "pr:CuratedObject" in anc else "neutral")
|
||||
nodes.append({
|
||||
"id": term,
|
||||
"label": first(rec, "label", term.split(":")[1]),
|
||||
"band": band,
|
||||
"prov": provenance,
|
||||
"parent": first(rec, "subClassOf"),
|
||||
"comment": first(rec, "comment"),
|
||||
"harvest": first(rec, "harvestSource"),
|
||||
"curation": first(rec, "curationMode"),
|
||||
"deprecated": first(rec, "deprecated") in ("true", "True"),
|
||||
"replacedBy": first(rec, "isReplacedBy"),
|
||||
"members": [
|
||||
{"id": m, "label": first(subjects.get(m, {}), "label", m.split(":")[1])}
|
||||
for m in individuals.get(term, [])
|
||||
],
|
||||
})
|
||||
|
||||
known = {n["id"] for n in nodes}
|
||||
edges, universal = [], []
|
||||
for term, rec in objprops.items():
|
||||
dom, rng = first(rec, "domain"), first(rec, "range")
|
||||
item = {
|
||||
"id": term,
|
||||
"label": first(rec, "label", term.split(":")[1]),
|
||||
"s": dom, "t": rng,
|
||||
"comment": first(rec, "comment"),
|
||||
"harvest": first(rec, "harvestSource"),
|
||||
"curation": first(rec, "curationMode"),
|
||||
"inverseOf": first(rec, "inverseOf"),
|
||||
"subPropertyOf": first(rec, "subPropertyOf"),
|
||||
"functional": "owl:FunctionalProperty" in rec.get("type", []),
|
||||
"transitive": "owl:TransitiveProperty" in rec.get("type", []),
|
||||
"deprecated": first(rec, "deprecated") in ("true", "True"),
|
||||
"replacedBy": first(rec, "isReplacedBy"),
|
||||
}
|
||||
if dom in known and rng in known:
|
||||
edges.append(item)
|
||||
else:
|
||||
item["s"] = dom or "pr:MetaModelObject"
|
||||
universal.append(item)
|
||||
|
||||
datatypes = [{
|
||||
"id": t, "label": first(r, "label", t.split(":")[1]),
|
||||
"range": first(r, "range"), "comment": first(r, "comment"),
|
||||
"harvest": first(r, "harvestSource"),
|
||||
"functional": "owl:FunctionalProperty" in r.get("type", []),
|
||||
"deprecated": first(r, "deprecated") in ("true", "True"),
|
||||
"replacedBy": first(r, "isReplacedBy"),
|
||||
} for t, r in dataprops.items()]
|
||||
|
||||
bridges = [{
|
||||
"id": t, "label": first(r, "label", t.split(":")[1]),
|
||||
"comment": first(r, "comment"),
|
||||
} for t, r in annprops.items() if t.startswith("pr:denotes")]
|
||||
|
||||
onto = subjects.get("<https://ontology.pernod-ricard.com/metamodel/>", {})
|
||||
return {
|
||||
"bands": [{"id": b[0], "title": b[1], "tag": b[2], "color": b[3]} for b in BANDS],
|
||||
"nodes": nodes, "edges": edges, "universal": universal,
|
||||
"datatypes": datatypes, "bridges": bridges,
|
||||
}
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# HTML emission
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def emit(model, version, src_name, out_path):
|
||||
payload = json.dumps(model, ensure_ascii=False, separators=(",", ":"))
|
||||
html = (TEMPLATE.replace("__MODEL__", payload)
|
||||
.replace("__SRC__", src_name)
|
||||
.replace("__VERSION__", version))
|
||||
with open(out_path, "w", encoding="utf-8") as fh:
|
||||
fh.write(html)
|
||||
|
||||
|
||||
TEMPLATE = r"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PR Data Meta Model · T-Box __VERSION__</title>
|
||||
<style>
|
||||
:root{
|
||||
--navy:#0A1F44; --gold:#B8935A; --ink:#1A1A1A;
|
||||
--bg:#F7F6F2; --panel:#FFFFFF; --rule:#E4E2DA; --muted:#6B6B6B;
|
||||
--c-roots:#0A1F44; --c-ownership:#B8935A; --c-business:#8B3A3A;
|
||||
--c-logical:#7A4E8B; --c-physical:#1E4B9B; --c-consumption:#1E7A5F;
|
||||
--c-context:#6B6B6B;
|
||||
--mono:ui-monospace,"SF Mono",SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace;
|
||||
--sans:-apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif;
|
||||
}
|
||||
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
|
||||
html,body{height:100%;margin:0}
|
||||
body{font-family:var(--sans);background:var(--bg);color:var(--ink);
|
||||
display:flex;flex-direction:column;overflow:hidden}
|
||||
:focus-visible{outline:2px solid var(--gold);outline-offset:2px}
|
||||
|
||||
/* ---------- chrome ---------- */
|
||||
header{background:var(--navy);color:#fff;padding:10px 18px;display:flex;
|
||||
align-items:center;gap:16px;flex-shrink:0;box-shadow:0 2px 6px rgba(0,0,0,.15)}
|
||||
header h1{margin:0;font-family:var(--mono);font-size:13px;font-weight:600;
|
||||
letter-spacing:.16em;text-transform:uppercase}
|
||||
header .sub{font-size:11px;color:rgba(255,255,255,.62);margin-top:3px}
|
||||
.stats{margin-left:auto;display:flex;gap:20px}
|
||||
.stat{text-align:right;line-height:1.15}
|
||||
.stat b{display:block;font-family:var(--mono);font-size:16px;color:var(--gold)}
|
||||
.stat span{font-size:9px;text-transform:uppercase;letter-spacing:.09em;
|
||||
color:rgba(255,255,255,.55)}
|
||||
#mtoggle{display:none;background:transparent;color:#fff;
|
||||
border:1px solid rgba(255,255,255,.4);padding:5px 10px;border-radius:3px;
|
||||
font-size:11px;font-weight:600;cursor:pointer;font-family:inherit}
|
||||
|
||||
main{flex:1;display:flex;min-height:0;position:relative}
|
||||
|
||||
/* ---------- sidebar ---------- */
|
||||
aside{width:255px;background:var(--panel);border-right:1px solid var(--rule);
|
||||
overflow-y:auto;padding:16px;flex-shrink:0}
|
||||
aside h2{font-family:var(--mono);font-size:9.5px;text-transform:uppercase;
|
||||
letter-spacing:.14em;color:var(--muted);margin:22px 0 9px;font-weight:600}
|
||||
aside h2:first-child{margin-top:0}
|
||||
#q{width:100%;padding:8px 10px;border:1px solid var(--rule);border-radius:3px;
|
||||
font-family:var(--mono);font-size:12px;background:var(--bg)}
|
||||
#q:focus{border-color:var(--navy);outline:none;background:#fff}
|
||||
.chip{display:flex;align-items:center;gap:9px;padding:5px 0;font-size:12.5px;
|
||||
cursor:pointer;user-select:none;border:none;background:none;width:100%;
|
||||
text-align:left;font-family:inherit;color:inherit}
|
||||
.chip .swatch{width:11px;height:11px;flex-shrink:0;border-radius:2px}
|
||||
.chip .n{margin-left:auto;font-family:var(--mono);font-size:10px;color:var(--muted)}
|
||||
.chip.off{opacity:.34}
|
||||
.seg{display:flex;border:1px solid var(--rule);border-radius:3px;overflow:hidden}
|
||||
.seg button{flex:1;padding:7px 4px;background:#fff;border:none;cursor:pointer;
|
||||
font-family:var(--mono);font-size:9.5px;letter-spacing:.07em;
|
||||
text-transform:uppercase;color:var(--muted)}
|
||||
.seg button+button{border-left:1px solid var(--rule)}
|
||||
.seg button.on{background:var(--navy);color:#fff}
|
||||
.opt{display:flex;align-items:center;gap:8px;padding:4px 0;font-size:12px;cursor:pointer}
|
||||
.opt input{accent-color:var(--navy);width:15px;height:15px;cursor:pointer}
|
||||
.note{font-size:11px;line-height:1.55;color:var(--muted)}
|
||||
.note code{font-family:var(--mono);font-size:10.5px;background:var(--bg);padding:1px 4px}
|
||||
|
||||
/* ---------- canvas ---------- */
|
||||
#wrap{flex:1;min-width:0;position:relative;overflow:hidden;touch-action:none;
|
||||
background:
|
||||
linear-gradient(90deg,rgba(10,31,68,.035) 1px,transparent 1px) 0 0/28px 28px,
|
||||
linear-gradient(180deg,#FCFBF7,#EFEDE3)}
|
||||
svg{display:block;width:100%;height:100%;cursor:grab}
|
||||
svg.drag{cursor:grabbing}
|
||||
.bandrect{opacity:.5}
|
||||
.bandline{stroke:var(--rule);stroke-width:1}
|
||||
.bandlabel{font-family:var(--mono);font-size:10px;letter-spacing:.15em;
|
||||
text-transform:uppercase;font-weight:600}
|
||||
.bandtag{font-family:var(--sans);font-size:9.5px;fill:var(--muted);font-style:italic}
|
||||
.node{cursor:pointer}
|
||||
.node rect{stroke-width:1.5}
|
||||
.node text{font-size:10px;pointer-events:none;text-anchor:middle;font-weight:500}
|
||||
.node.dim{opacity:.13}
|
||||
.node.sel rect{stroke-width:3}
|
||||
.edge{fill:none;stroke:#9AA0AB;stroke-width:1.2;marker-end:url(#ar)}
|
||||
.edge.bridge{stroke:var(--navy);stroke-width:2;marker-end:url(#arb)}
|
||||
.edge.uni{stroke:#C3BFB4;stroke-dasharray:3 3;marker-end:url(#aru)}
|
||||
.edge.dim{opacity:.07}
|
||||
.edge.hot{stroke:var(--gold);stroke-width:2.6;marker-end:url(#arg)}
|
||||
.elabel{font-family:var(--mono);font-size:8px;fill:#5A6270;text-anchor:middle;
|
||||
pointer-events:none}
|
||||
.elabel.dim{opacity:.06}
|
||||
.ebg{fill:#F4F2EC}
|
||||
|
||||
#zoom{position:absolute;left:14px;bottom:14px;display:flex;flex-direction:column;
|
||||
gap:3px;background:#fff;border:1px solid var(--rule);border-radius:4px;padding:3px}
|
||||
#zoom button{width:32px;height:32px;background:#fff;border:none;border-radius:3px;
|
||||
font-size:16px;cursor:pointer;color:var(--navy);font-weight:600;font-family:inherit}
|
||||
#zoom button:hover{background:var(--bg)}
|
||||
#hint{position:absolute;right:14px;bottom:14px;font-family:var(--mono);
|
||||
font-size:9.5px;color:var(--muted);background:rgba(255,255,255,.82);
|
||||
padding:5px 9px;border-radius:3px;letter-spacing:.04em}
|
||||
|
||||
/* ---------- inspector ---------- */
|
||||
#insp{position:absolute;top:0;right:0;width:352px;max-width:100%;height:100%;
|
||||
background:var(--panel);border-left:1px solid var(--rule);
|
||||
box-shadow:-6px 0 24px rgba(10,31,68,.10);overflow-y:auto;padding:20px 20px 40px;
|
||||
transform:translateX(100%);transition:transform .22s ease}
|
||||
#insp.open{transform:translateX(0)}
|
||||
#insp .close{position:absolute;top:12px;right:14px;border:none;background:none;
|
||||
font-size:22px;cursor:pointer;color:var(--muted);line-height:1;padding:2px 6px}
|
||||
#insp .kicker{font-family:var(--mono);font-size:9px;letter-spacing:.16em;
|
||||
text-transform:uppercase;font-weight:600}
|
||||
#insp h3{margin:6px 0 3px;font-size:19px;letter-spacing:-.01em}
|
||||
#insp .iri{font-family:var(--mono);font-size:10.5px;color:var(--muted);
|
||||
word-break:break-all;margin-bottom:14px}
|
||||
#insp h4{font-family:var(--mono);font-size:9px;text-transform:uppercase;
|
||||
letter-spacing:.14em;color:var(--muted);margin:20px 0 7px;font-weight:600;
|
||||
border-top:1px solid var(--rule);padding-top:10px}
|
||||
#insp p{font-size:12.5px;line-height:1.62;margin:0 0 8px}
|
||||
.pill{display:inline-block;padding:3px 8px;border-radius:2px;font-family:var(--mono);
|
||||
font-size:9px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;
|
||||
margin:0 5px 5px 0;border:1px solid}
|
||||
.rel{display:grid;grid-template-columns:1fr auto;gap:4px 10px;align-items:baseline;
|
||||
padding:5px 0;border-bottom:1px dotted var(--rule);font-size:12px}
|
||||
.rel .p{font-family:var(--mono);font-size:10.5px;color:var(--navy)}
|
||||
.rel .o{color:var(--muted);font-size:11.5px;text-align:right}
|
||||
.rel .flag{font-family:var(--mono);font-size:8.5px;color:var(--gold);
|
||||
letter-spacing:.06em;text-transform:uppercase}
|
||||
.jump{background:none;border:none;padding:0;font:inherit;color:var(--navy);
|
||||
cursor:pointer;text-decoration:underline;text-underline-offset:2px}
|
||||
.harvest{background:var(--bg);border-left:2px solid var(--gold);padding:9px 11px;
|
||||
font-family:var(--mono);font-size:10.5px;line-height:1.55;color:#4A5160}
|
||||
|
||||
footer{background:var(--navy);color:rgba(255,255,255,.55);padding:6px 18px;
|
||||
font-family:var(--mono);font-size:9.5px;letter-spacing:.06em;display:flex;
|
||||
justify-content:space-between;flex-shrink:0}
|
||||
|
||||
@media(max-width:860px){
|
||||
aside{position:absolute;z-index:30;height:100%;transform:translateX(-100%);
|
||||
transition:transform .22s;box-shadow:4px 0 18px rgba(0,0,0,.16)}
|
||||
aside.open{transform:translateX(0)}
|
||||
#mtoggle{display:inline-block}
|
||||
.stats{display:none}
|
||||
#insp{width:100%}
|
||||
#hint{display:none}
|
||||
}
|
||||
@media(prefers-reduced-motion:reduce){*{transition:none!important}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header>
|
||||
<button id="mtoggle" aria-label="Toggle filters">☰</button>
|
||||
<div>
|
||||
<h1>PR Data Meta Model · T-Box</h1>
|
||||
<div class="sub">Five layers, crossed with a provenance axis: what governance asserts, what harvesters observe</div>
|
||||
</div>
|
||||
<div class="stats">
|
||||
<div class="stat"><b id="s-cls">0</b><span>Classes</span></div>
|
||||
<div class="stat"><b id="s-obj">0</b><span>Relations</span></div>
|
||||
<div class="stat"><b id="s-dat">0</b><span>Attributes</span></div>
|
||||
<div class="stat"><b id="s-brg">0</b><span>Seam edges</span></div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<aside id="side">
|
||||
<h2>Find a term</h2>
|
||||
<input id="q" type="search" placeholder="metric, field, lineage…" autocomplete="off">
|
||||
|
||||
<h2>Layout</h2>
|
||||
<div class="seg" role="group">
|
||||
<button id="m-strata" class="on">Strata</button>
|
||||
<button id="m-tax">Taxonomy</button>
|
||||
<button id="m-free">Network</button>
|
||||
</div>
|
||||
|
||||
<h2>Layers</h2>
|
||||
<div id="bands"></div>
|
||||
|
||||
<h2>Show</h2>
|
||||
<label class="opt"><input type="checkbox" id="o-uni"> Universal relations</label>
|
||||
<label class="opt"><input type="checkbox" id="o-dep"> Deprecated terms</label>
|
||||
<label class="opt"><input type="checkbox" id="o-lab" checked> Relation names</label>
|
||||
|
||||
<h2>Reading it</h2>
|
||||
<p class="note">
|
||||
Solid fill is <b>curated</b>: governance writes it, a pull request reviews it.
|
||||
Hatched outline is <b>observed</b>: a harvester writes it nightly and no one
|
||||
edits it by hand.<br><br>
|
||||
The two navy edges are the seam — <code>materializedAs</code> and
|
||||
<code>physicalizedIn</code>. They are the only place the two worlds meet,
|
||||
and the only mapping a steward has to confirm.
|
||||
</p>
|
||||
</aside>
|
||||
|
||||
<div id="wrap">
|
||||
<svg id="svg" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<marker id="ar" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#9AA0AB"/></marker>
|
||||
<marker id="arb" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#0A1F44"/></marker>
|
||||
<marker id="aru" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#C3BFB4"/></marker>
|
||||
<marker id="arg" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#B8935A"/></marker>
|
||||
<pattern id="hatch" width="5" height="5" patternTransform="rotate(45)" patternUnits="userSpaceOnUse">
|
||||
<line x1="0" y1="0" x2="0" y2="5" stroke="#0A1F44" stroke-width="1.1" opacity=".16"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<g id="vp"><g id="bL"></g><g id="eL"></g><g id="tL"></g><g id="nL"></g></g>
|
||||
</svg>
|
||||
<div id="zoom">
|
||||
<button id="z-in" aria-label="Zoom in">+</button>
|
||||
<button id="z-out" aria-label="Zoom out">−</button>
|
||||
<button id="z-fit" aria-label="Fit to view" style="font-size:13px">↻</button>
|
||||
</div>
|
||||
<div id="hint">drag to pan · scroll to zoom · click a class</div>
|
||||
</div>
|
||||
|
||||
<div id="insp" role="dialog" aria-label="Term details">
|
||||
<button class="close" id="insp-x" aria-label="Close">×</button>
|
||||
<div id="insp-body"></div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<span>Build artefact of __SRC__ — regenerate, never edit</span>
|
||||
<span id="f-ver">T-Box __VERSION__</span>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
var M = __MODEL__;
|
||||
|
||||
/* ---------------- state ---------------- */
|
||||
var NW=168, NH=46, BANDGAP=52, GUT=206;
|
||||
var byId={}, off={}, mode='strata', showUni=false, showDep=false, showLab=true;
|
||||
var sel=null, hot=null, view={x:0,y:0,s:1};
|
||||
var svg=document.getElementById('svg'), vp=document.getElementById('vp');
|
||||
var bL=document.getElementById('bL'), eL=document.getElementById('eL');
|
||||
var tL=document.getElementById('tL'), nL=document.getElementById('nL');
|
||||
var SEAM={'pr:materializedAs':1,'pr:physicalizedIn':1};
|
||||
|
||||
M.nodes.forEach(function(n){byId[n.id]=n;});
|
||||
document.getElementById('s-cls').textContent=M.nodes.length;
|
||||
document.getElementById('s-obj').textContent=M.edges.length+M.universal.length;
|
||||
document.getElementById('s-dat').textContent=M.datatypes.length;
|
||||
document.getElementById('s-brg').textContent=M.edges.filter(function(e){return SEAM[e.id];}).length;
|
||||
|
||||
function el(t,a){var e=document.createElementNS('http://www.w3.org/2000/svg',t);
|
||||
if(a)for(var k in a)e.setAttribute(k,a[k]);return e;}
|
||||
function bandOf(id){return M.bands.filter(function(b){return b.id===id;})[0];}
|
||||
function visible(n){ if(off[n.band])return false; if(n.deprecated&&!showDep)return false; return true; }
|
||||
|
||||
/* ---------------- layout ---------------- */
|
||||
function layout(){
|
||||
var live=M.nodes.filter(visible);
|
||||
if(mode==='tax'){ layoutTax(live); return live; }
|
||||
var y=0;
|
||||
M.bands.forEach(function(b){
|
||||
var group=live.filter(function(n){return n.band===b.id;});
|
||||
if(!group.length){b._h=0;b._y=y;return;}
|
||||
var perRow=Math.max(3,Math.ceil(Math.sqrt(group.length*2.1)));
|
||||
var rows=Math.ceil(group.length/perRow);
|
||||
b._y=y; b._h=rows*(NH+20)+48; b._rows=rows; b._per=perRow;
|
||||
group.forEach(function(n,i){
|
||||
n.row=Math.floor(i/perRow);
|
||||
n.x=GUT+(i%perRow)*(NW+26)+NW/2;
|
||||
n.y=y+34+n.row*(NH+20)+NH/2;
|
||||
});
|
||||
y+=b._h+BANDGAP;
|
||||
});
|
||||
if(mode==='strata') relax(live);
|
||||
else force(live);
|
||||
return live;
|
||||
}
|
||||
/* barycentre relaxation on x only: connected classes drift together,
|
||||
strata stay intact, which is the whole point of the drawing */
|
||||
function relax(live){
|
||||
var idx={}; live.forEach(function(n){idx[n.id]=n;});
|
||||
var adj={}; live.forEach(function(n){adj[n.id]=[];});
|
||||
M.edges.forEach(function(e){
|
||||
if(idx[e.s]&&idx[e.t]&&(showDep||!e.deprecated)){
|
||||
adj[e.s].push(e.t); adj[e.t].push(e.s);}
|
||||
});
|
||||
for(var it=0;it<70;it++){
|
||||
live.forEach(function(n){
|
||||
var ns=adj[n.id]; if(!ns.length)return;
|
||||
var sum=0,c=0;
|
||||
ns.forEach(function(m){if(idx[m]){sum+=idx[m].x;c++;}});
|
||||
if(c) n.x += (sum/c - n.x)*0.22;
|
||||
});
|
||||
/* de-overlap inside each row */
|
||||
var rows={};
|
||||
live.forEach(function(n){var k=n.band+'|'+n.row;(rows[k]=rows[k]||[]).push(n);});
|
||||
for(var k in rows){
|
||||
var r=rows[k].sort(function(a,b){return a.x-b.x;});
|
||||
for(var i=1;i<r.length;i++){
|
||||
var need=(NW+22)-(r[i].x-r[i-1].x);
|
||||
if(need>0){ r[i].x+=need*0.55; r[i-1].x-=need*0.45; }
|
||||
}
|
||||
r.forEach(function(n){ if(n.x<GUT+NW/2) n.x=GUT+NW/2; });
|
||||
}
|
||||
}
|
||||
/* final strict pass: guarantee no overlap, preserve the order relaxation found */
|
||||
var rows2={};
|
||||
live.forEach(function(n){var k=n.band+'|'+n.row;(rows2[k]=rows2[k]||[]).push(n);});
|
||||
for(var k2 in rows2){
|
||||
var r2=rows2[k2].sort(function(a,b){return a.x-b.x;});
|
||||
var cursor=Math.max(GUT+NW/2, r2[0].x);
|
||||
r2.forEach(function(n){ n.x=Math.max(n.x,cursor); cursor=n.x+NW+22; });
|
||||
}
|
||||
}
|
||||
/* plain force-directed network: no strata, for people who want the usual view */
|
||||
function force(live){
|
||||
var idx={}; live.forEach(function(n){idx[n.id]=n;});
|
||||
var links=M.edges.filter(function(e){
|
||||
return idx[e.s]&&idx[e.t]&&e.s!==e.t&&(showDep||!e.deprecated);});
|
||||
var cx=live.reduce(function(a,n){return a+n.x;},0)/live.length;
|
||||
var cy=live.reduce(function(a,n){return a+n.y;},0)/live.length;
|
||||
for(var it=0;it<420;it++){
|
||||
var k=1-it/420;
|
||||
for(var i=0;i<live.length;i++){
|
||||
var a=live[i];
|
||||
for(var j=i+1;j<live.length;j++){
|
||||
var b=live[j], dx=b.x-a.x, dy=(b.y-a.y)*1.9;
|
||||
var d2=dx*dx+dy*dy||1, f=42000/d2;
|
||||
if(f>9)f=9;
|
||||
var d=Math.sqrt(d2);
|
||||
a.x-=dx/d*f; a.y-=dy/d*f*0.5; b.x+=dx/d*f; b.y+=dy/d*f*0.5;
|
||||
}
|
||||
a.x+=(cx-a.x)*0.004*k; a.y+=(cy-a.y)*0.006*k;
|
||||
}
|
||||
links.forEach(function(e){
|
||||
var a=idx[e.s], b=idx[e.t], dx=b.x-a.x, dy=b.y-a.y;
|
||||
var d=Math.sqrt(dx*dx+dy*dy)||1, f=(d-235)*0.018*k;
|
||||
a.x+=dx/d*f; a.y+=dy/d*f; b.x-=dx/d*f; b.y-=dy/d*f;
|
||||
});
|
||||
}
|
||||
}
|
||||
function layoutTax(live){
|
||||
var kids={}, roots=[];
|
||||
live.forEach(function(n){ if(n.parent&&byId[n.parent]&&visible(byId[n.parent]))
|
||||
(kids[n.parent]=kids[n.parent]||[]).push(n); else roots.push(n); });
|
||||
var cursor=0;
|
||||
function place(n,depth){
|
||||
var ch=kids[n.id]||[];
|
||||
n.x=depth*(NW+58)+NW/2+30;
|
||||
if(!ch.length){ n.y=cursor*(NH+13)+NH/2+20; cursor++; }
|
||||
else{ ch.forEach(function(c){place(c,depth+1);});
|
||||
n.y=(ch[0].y+ch[ch.length-1].y)/2; }
|
||||
}
|
||||
roots.forEach(function(r){place(r,0); cursor+=0.7;});
|
||||
M.bands.forEach(function(b){b._h=0;});
|
||||
}
|
||||
|
||||
/* ---------------- render ---------------- */
|
||||
function render(){
|
||||
var live=layout();
|
||||
var idx={}; live.forEach(function(n){idx[n.id]=n;});
|
||||
bL.innerHTML=''; eL.innerHTML=''; tL.innerHTML=''; nL.innerHTML='';
|
||||
|
||||
if(mode==='strata'){
|
||||
var maxx=Math.max.apply(null,live.map(function(n){return n.x;}).concat([600]))+NW;
|
||||
M.bands.forEach(function(b){
|
||||
if(!b._h)return;
|
||||
bL.appendChild(el('rect',{x:0,y:b._y,width:maxx+60,height:b._h,
|
||||
fill:b.color,opacity:.045,class:'bandrect'}));
|
||||
bL.appendChild(el('line',{x1:0,y1:b._y,x2:maxx+60,y2:b._y,class:'bandline'}));
|
||||
bL.appendChild(el('rect',{x:GUT-5,y:b._y+10,width:3,height:b._h-22,
|
||||
fill:b.color,opacity:.85}));
|
||||
var t=el('text',{x:GUT-20,y:b._y+26,class:'bandlabel',fill:b.color,
|
||||
'text-anchor':'end'});
|
||||
t.textContent=b.title; bL.appendChild(t);
|
||||
var g=el('text',{x:GUT-20,y:b._y+41,class:'bandtag','text-anchor':'end'});
|
||||
g.textContent=b.tag; bL.appendChild(g);
|
||||
});
|
||||
}
|
||||
|
||||
var list=M.edges.slice();
|
||||
if(showUni) list=list.concat(M.universal);
|
||||
list.forEach(function(e){
|
||||
if(!idx[e.s]||!idx[e.t])return;
|
||||
if(e.deprecated&&!showDep)return;
|
||||
var a=idx[e.s], b=idx[e.t], uni=M.universal.indexOf(e)>-1;
|
||||
var cls='edge'+(uni?' uni':SEAM[e.id]?' bridge':'');
|
||||
var p, mx, my;
|
||||
if(a===b){
|
||||
p='M'+(a.x-24)+','+(a.y-NH/2)+' C'+(a.x-70)+','+(a.y-70)+' '+(a.x+70)+','+(a.y-70)+' '+(a.x+24)+','+(a.y-NH/2);
|
||||
mx=a.x; my=a.y-52;
|
||||
}else{
|
||||
var dx=b.x-a.x, dy=b.y-a.y, k=Math.abs(dy)>4?0.38:0;
|
||||
var cx=a.x+dx/2+(-dy)*k*0.14, cy=a.y+dy/2+dx*k*0.05;
|
||||
p='M'+a.x+','+a.y+' Q'+cx+','+cy+' '+b.x+','+b.y;
|
||||
mx=(a.x+2*cx+b.x)/4; my=(a.y+2*cy+b.y)/4;
|
||||
}
|
||||
var path=el('path',{d:p,class:cls}); path.dataset.e=e.id;
|
||||
path.dataset.a=e.s; path.dataset.b=e.t; eL.appendChild(path);
|
||||
if(showLab&&!uni){
|
||||
var w=e.label.length*4.6+8;
|
||||
var bg=el('rect',{x:mx-w/2,y:my-6,width:w,height:11,rx:2,class:'ebg'});
|
||||
bg.dataset.el=e.id; tL.appendChild(bg);
|
||||
var tx=el('text',{x:mx,y:my+2.5,class:'elabel'});
|
||||
tx.dataset.el=e.id; tx.textContent=e.label; tL.appendChild(tx);
|
||||
}
|
||||
});
|
||||
|
||||
live.forEach(function(n){
|
||||
var col=bandOf(n.band).color;
|
||||
var g=el('g',{class:'node','tabindex':'0','role':'button',
|
||||
'aria-label':n.label,transform:'translate('+(n.x-NW/2)+','+(n.y-NH/2)+')'});
|
||||
g.dataset.id=n.id;
|
||||
g.appendChild(el('rect',{width:NW,height:NH,rx:3,
|
||||
fill:n.prov==='observed'?'#fff':col,
|
||||
'fill-opacity':n.prov==='observed'?1:.13,
|
||||
stroke:col,'stroke-dasharray':n.deprecated?'4 3':'none'}));
|
||||
if(n.prov==='observed')
|
||||
g.appendChild(el('rect',{width:NW,height:NH,rx:3,fill:'url(#hatch)',stroke:'none'}));
|
||||
var lines=wrap(n.label,22);
|
||||
lines.forEach(function(s,i){
|
||||
var t=el('text',{x:NW/2,y:NH/2+4+(i-(lines.length-1)/2)*11.8,
|
||||
fill:n.deprecated?'#9A9A9A':'#1A1A1A',
|
||||
'text-decoration':n.deprecated?'line-through':'none'});
|
||||
t.textContent=s; g.appendChild(t);
|
||||
});
|
||||
nL.appendChild(g);
|
||||
});
|
||||
applyHot();
|
||||
}
|
||||
function wrap(s,max){
|
||||
var w=s.split(' '), out=[], cur='';
|
||||
w.forEach(function(x){
|
||||
if((cur+' '+x).trim().length>max){ if(cur)out.push(cur); cur=x; }
|
||||
else cur=(cur+' '+x).trim();
|
||||
});
|
||||
if(cur)out.push(cur);
|
||||
return out.slice(0,3);
|
||||
}
|
||||
|
||||
/* ---------------- focus / highlight ---------------- */
|
||||
function neighbours(id){
|
||||
var s={}; s[id]=1;
|
||||
M.edges.concat(M.universal).forEach(function(e){
|
||||
if(e.s===id)s[e.t]=1; if(e.t===id)s[e.s]=1;});
|
||||
return s;
|
||||
}
|
||||
function applyHot(){
|
||||
var id=hot||sel;
|
||||
var nodes=nL.querySelectorAll('.node');
|
||||
var paths=eL.querySelectorAll('.edge');
|
||||
var labs=tL.querySelectorAll('[data-el]');
|
||||
if(!id){
|
||||
nodes.forEach(function(n){n.classList.remove('dim','sel');});
|
||||
paths.forEach(function(p){p.classList.remove('dim','hot');});
|
||||
labs.forEach(function(l){l.classList.remove('dim');});
|
||||
return;
|
||||
}
|
||||
var keep=neighbours(id), live={};
|
||||
paths.forEach(function(p){
|
||||
var on=(p.dataset.a===id||p.dataset.b===id);
|
||||
p.classList.toggle('hot',on); p.classList.toggle('dim',!on);
|
||||
if(on)live[p.dataset.e]=1;
|
||||
});
|
||||
labs.forEach(function(l){l.classList.toggle('dim',!live[l.dataset.el]);});
|
||||
nodes.forEach(function(n){
|
||||
n.classList.toggle('dim',!keep[n.dataset.id]);
|
||||
n.classList.toggle('sel',n.dataset.id===sel);
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------------- inspector ---------------- */
|
||||
var insp=document.getElementById('insp'), ibody=document.getElementById('insp-body');
|
||||
function esc(s){return (s||'').replace(/&/g,'&').replace(/</g,'<');}
|
||||
function open(id){
|
||||
var n=byId[id]; if(!n)return;
|
||||
sel=id; var b=bandOf(n.band), h=[];
|
||||
h.push('<div class="kicker" style="color:'+b.color+'">'+esc(b.title)+'</div>');
|
||||
h.push('<h3>'+esc(n.label)+'</h3>');
|
||||
h.push('<div class="iri">'+esc(n.id)+'</div>');
|
||||
h.push('<span class="pill" style="color:'+b.color+';border-color:'+b.color+'">'+
|
||||
(n.prov==='observed'?'Observed · harvested':n.prov==='curated'?'Curated · asserted':'Structural')+'</span>');
|
||||
if(n.curation) h.push('<span class="pill" style="color:#B8935A;border-color:#B8935A">'+esc(n.curation)+'</span>');
|
||||
if(n.deprecated) h.push('<span class="pill" style="color:#8B3A3A;border-color:#8B3A3A">Deprecated</span>');
|
||||
if(n.comment) h.push('<h4>What it is for</h4><p>'+esc(n.comment)+'</p>');
|
||||
if(n.replacedBy) h.push('<p><b>Replaced by</b> '+link(n.replacedBy)+'</p>');
|
||||
if(n.harvest) h.push('<h4>Harvested from</h4><div class="harvest">'+esc(n.harvest)+'</div>');
|
||||
if(n.parent) h.push('<h4>Specialises</h4><div class="rel"><span class="p">rdfs:subClassOf</span><span class="o">'+link(n.parent)+'</span></div>');
|
||||
var kids=M.nodes.filter(function(m){return m.parent===id;});
|
||||
if(kids.length) h.push('<h4>Specialised by</h4>'+kids.map(function(k){
|
||||
return '<div class="rel"><span class="p">'+esc(k.label)+'</span><span class="o">'+
|
||||
(k.deprecated?'deprecated':bandOf(k.band).title)+'</span></div>';}).join(''));
|
||||
if(n.members&&n.members.length) h.push('<h4>Allowed values</h4>'+n.members.map(function(m){
|
||||
return '<div class="rel"><span class="p">'+esc(m.label)+'</span><span class="o">'+esc(m.id)+'</span></div>';}).join(''));
|
||||
var out=M.edges.concat(M.universal).filter(function(e){return e.s===id&&(showDep||!e.deprecated);});
|
||||
var inc=M.edges.concat(M.universal).filter(function(e){return e.t===id&&(showDep||!e.deprecated);});
|
||||
if(out.length) h.push('<h4>Points to</h4>'+out.map(rel).join(''));
|
||||
if(inc.length) h.push('<h4>Pointed to by</h4>'+inc.map(function(e){
|
||||
return '<div class="rel"><span class="p">'+esc(e.label)+'</span><span class="o">'+link(e.s)+'</span></div>';}).join(''));
|
||||
ibody.innerHTML=h.join('');
|
||||
insp.classList.add('open');
|
||||
bindJumps(); applyHot();
|
||||
}
|
||||
function rel(e){
|
||||
var flags=[]; if(e.functional)flags.push('exactly one');
|
||||
if(e.transitive)flags.push('transitive');
|
||||
if(SEAM[e.id])flags.push('seam');
|
||||
return '<div class="rel"><span class="p">'+esc(e.label)+
|
||||
(flags.length?' <span class="flag">'+flags.join(' · ')+'</span>':'')+
|
||||
'</span><span class="o">'+link(e.t)+'</span>'+
|
||||
(e.comment?'<p style="grid-column:1/-1;margin:4px 0 0;font-size:11.5px;color:#6B6B6B">'+esc(e.comment)+'</p>':'')+
|
||||
'</div>';
|
||||
}
|
||||
function link(id){
|
||||
var n=byId[id];
|
||||
return n?'<button class="jump" data-j="'+id+'">'+esc(n.label)+'</button>':esc(id);
|
||||
}
|
||||
function bindJumps(){
|
||||
ibody.querySelectorAll('.jump').forEach(function(b){
|
||||
b.onclick=function(){open(b.dataset.j); focusOn(b.dataset.j);};});
|
||||
}
|
||||
document.getElementById('insp-x').onclick=function(){
|
||||
insp.classList.remove('open'); sel=null; applyHot();};
|
||||
document.addEventListener('keydown',function(e){
|
||||
if(e.key==='Escape'){insp.classList.remove('open'); sel=null; applyHot();}});
|
||||
|
||||
/* ---------------- view ---------------- */
|
||||
function apply(){vp.setAttribute('transform','translate('+view.x+','+view.y+') scale('+view.s+')');}
|
||||
function fit(){
|
||||
var live=M.nodes.filter(visible); if(!live.length)return;
|
||||
var xs=live.map(function(n){return n.x;}), ys=live.map(function(n){return n.y;});
|
||||
var pad=(mode==='strata')?GUT+30:NW;
|
||||
var mnx=Math.min.apply(null,xs)-pad, mxx=Math.max.apply(null,xs)+NW;
|
||||
var mny=Math.min.apply(null,ys)-90, mxy=Math.max.apply(null,ys)+70;
|
||||
var r=svg.getBoundingClientRect();
|
||||
view.s=Math.min(r.width/(mxx-mnx),r.height/(mxy-mny),1.25);
|
||||
view.x=(r.width-(mxx-mnx)*view.s)/2-mnx*view.s;
|
||||
view.y=(r.height-(mxy-mny)*view.s)/2-mny*view.s;
|
||||
apply();
|
||||
}
|
||||
function focusOn(id){
|
||||
var n=byId[id]; if(!n)return;
|
||||
var r=svg.getBoundingClientRect();
|
||||
view.x=r.width/2-n.x*view.s-120; view.y=r.height/2-n.y*view.s; apply();
|
||||
}
|
||||
document.getElementById('z-in').onclick=function(){zoom(1.22);};
|
||||
document.getElementById('z-out').onclick=function(){zoom(.82);};
|
||||
document.getElementById('z-fit').onclick=fit;
|
||||
function zoom(f){
|
||||
var r=svg.getBoundingClientRect(), cx=r.width/2, cy=r.height/2;
|
||||
var ns=Math.max(.18,Math.min(view.s*f,2.6));
|
||||
view.x=cx-(cx-view.x)*(ns/view.s); view.y=cy-(cy-view.y)*(ns/view.s);
|
||||
view.s=ns; apply();
|
||||
}
|
||||
var drag=false,ds,vs,pinch=null;
|
||||
svg.addEventListener('mousedown',function(e){drag=true;svg.classList.add('drag');
|
||||
ds={x:e.clientX,y:e.clientY};vs={x:view.x,y:view.y};});
|
||||
window.addEventListener('mousemove',function(e){if(!drag)return;
|
||||
view.x=vs.x+(e.clientX-ds.x);view.y=vs.y+(e.clientY-ds.y);apply();});
|
||||
window.addEventListener('mouseup',function(){drag=false;svg.classList.remove('drag');});
|
||||
svg.addEventListener('wheel',function(e){e.preventDefault();
|
||||
var f=e.deltaY<0?1.1:.9, r=svg.getBoundingClientRect();
|
||||
var mx=e.clientX-r.left,my=e.clientY-r.top;
|
||||
var ns=Math.max(.18,Math.min(view.s*f,2.6));
|
||||
view.x=mx-(mx-view.x)*(ns/view.s);view.y=my-(my-view.y)*(ns/view.s);
|
||||
view.s=ns;apply();},{passive:false});
|
||||
svg.addEventListener('touchstart',function(e){
|
||||
if(e.touches.length===1){drag=true;ds={x:e.touches[0].clientX,y:e.touches[0].clientY};
|
||||
vs={x:view.x,y:view.y};}
|
||||
else if(e.touches.length===2){drag=false;
|
||||
var dx=e.touches[0].clientX-e.touches[1].clientX,dy=e.touches[0].clientY-e.touches[1].clientY;
|
||||
pinch={d:Math.sqrt(dx*dx+dy*dy),s:view.s};}},{passive:true});
|
||||
svg.addEventListener('touchmove',function(e){
|
||||
if(e.touches.length===1&&drag){view.x=vs.x+(e.touches[0].clientX-ds.x);
|
||||
view.y=vs.y+(e.touches[0].clientY-ds.y);apply();e.preventDefault();}
|
||||
else if(e.touches.length===2&&pinch){
|
||||
var dx=e.touches[0].clientX-e.touches[1].clientX,dy=e.touches[0].clientY-e.touches[1].clientY;
|
||||
var nd=Math.sqrt(dx*dx+dy*dy),ns=Math.max(.18,Math.min(pinch.s*(nd/pinch.d),2.6));
|
||||
var r=svg.getBoundingClientRect(),cx=r.width/2,cy=r.height/2;
|
||||
view.x=cx-(cx-view.x)*(ns/view.s);view.y=cy-(cy-view.y)*(ns/view.s);
|
||||
view.s=ns;apply();e.preventDefault();}},{passive:false});
|
||||
svg.addEventListener('touchend',function(){drag=false;pinch=null;});
|
||||
|
||||
nL.addEventListener('click',function(e){
|
||||
var g=e.target.closest('.node'); if(g)open(g.dataset.id);});
|
||||
nL.addEventListener('keydown',function(e){
|
||||
var g=e.target.closest('.node');
|
||||
if(g&&(e.key==='Enter'||e.key===' ')){e.preventDefault();open(g.dataset.id);}});
|
||||
nL.addEventListener('mouseover',function(e){
|
||||
var g=e.target.closest('.node'); if(g&&!sel){hot=g.dataset.id;applyHot();}});
|
||||
nL.addEventListener('mouseout',function(e){
|
||||
if(!e.relatedTarget||!e.relatedTarget.closest||!e.relatedTarget.closest('.node')){
|
||||
if(!sel){hot=null;applyHot();}}});
|
||||
|
||||
/* ---------------- controls ---------------- */
|
||||
var bandsBox=document.getElementById('bands');
|
||||
M.bands.forEach(function(b){
|
||||
var n=M.nodes.filter(function(x){return x.band===b.id;}).length;
|
||||
if(!n)return;
|
||||
var btn=document.createElement('button');
|
||||
btn.className='chip'; btn.setAttribute('aria-pressed','true');
|
||||
btn.innerHTML='<span class="swatch" style="background:'+b.color+'"></span>'+
|
||||
b.title+'<span class="n">'+n+'</span>';
|
||||
btn.onclick=function(){
|
||||
off[b.id]=!off[b.id];
|
||||
btn.classList.toggle('off',!!off[b.id]);
|
||||
btn.setAttribute('aria-pressed',String(!off[b.id]));
|
||||
render(); fit();};
|
||||
bandsBox.appendChild(btn);
|
||||
});
|
||||
function setMode(m){
|
||||
mode=m;
|
||||
['strata','tax','free'].forEach(function(k){
|
||||
document.getElementById('m-'+k).classList.toggle('on',k===m);});
|
||||
render(); fit();
|
||||
}
|
||||
document.getElementById('m-strata').onclick=function(){setMode('strata');};
|
||||
document.getElementById('m-tax').onclick=function(){setMode('tax');};
|
||||
document.getElementById('m-free').onclick=function(){setMode('free');};
|
||||
document.getElementById('o-uni').onchange=function(){showUni=this.checked;render();};
|
||||
document.getElementById('o-dep').onchange=function(){showDep=this.checked;render();fit();};
|
||||
document.getElementById('o-lab').onchange=function(){showLab=this.checked;render();};
|
||||
document.getElementById('mtoggle').onclick=function(){
|
||||
document.getElementById('side').classList.toggle('open');};
|
||||
|
||||
document.getElementById('q').addEventListener('input',function(){
|
||||
var v=this.value.trim().toLowerCase();
|
||||
if(!v){hot=null;applyHot();return;}
|
||||
var hit=M.nodes.filter(visible).filter(function(n){
|
||||
return (n.label+' '+n.id+' '+(n.comment||'')).toLowerCase().indexOf(v)>-1;});
|
||||
nL.querySelectorAll('.node').forEach(function(g){
|
||||
var on=hit.some(function(n){return n.id===g.dataset.id;});
|
||||
g.classList.toggle('dim',!on);});
|
||||
if(hit.length===1){open(hit[0].id);focusOn(hit[0].id);}
|
||||
});
|
||||
|
||||
render(); fit();
|
||||
window.addEventListener('resize',fit);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
def main():
|
||||
src = sys.argv[1] if len(sys.argv) > 1 else "pr_datametamodel_v1_0.ttl"
|
||||
out = sys.argv[2] if len(sys.argv) > 2 else "pr_metamodel_viewer_v1_0.html"
|
||||
subjects, engine = read_tbox(src)
|
||||
model = build(subjects)
|
||||
import os
|
||||
emit(model, "v1.0", os.path.basename(src), out)
|
||||
print("PR META MODEL - T-Box viewer")
|
||||
print(" source : %s (parsed with %s)" % (src, engine))
|
||||
print(" output : %s" % out)
|
||||
print(" classes %d | object properties %d | datatype properties %d | bridge annotations %d"
|
||||
% (len(model["nodes"]),
|
||||
len(model["edges"]) + len(model["universal"]),
|
||||
len(model["datatypes"]), len(model["bridges"])))
|
||||
for b in model["bands"]:
|
||||
c = len([n for n in model["nodes"] if n["band"] == b["id"]])
|
||||
if c:
|
||||
print(" %-26s %d" % (b["title"], c))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user