diff src/layout/Layout.ts @ 122:2e8fa3fec0c8

support joining subjects into wider rows
author drewp@bigasterisk.com
date Sun, 20 Mar 2022 14:12:03 -0700
parents 3584f24becf4
children 5a1a79f54779
line wrap: on
line diff
--- a/src/layout/Layout.ts	Sun Mar 20 14:10:56 2022 -0700
+++ b/src/layout/Layout.ts	Sun Mar 20 14:12:03 2022 -0700
@@ -2,9 +2,10 @@
 
 import Immutable from "immutable"; // mostly using this for the builtin equals() testing, since NamedNode(x)!=NamedNode(x)
 import { NamedNode, Quad, Store, Term } from "n3";
+import { addToValues, multiColumnSort } from "./algorithm";
 import { rdf, rdfs } from "./namespaces";
 import { uniqueSortedTerms, UriPairMap } from "./rdf_value";
-import { ViewConfig } from "./ViewConfig";
+import { Link, ViewConfig } from "./ViewConfig";
 
 type UriSet = Immutable.Set<NamedNode>;
 
@@ -37,61 +38,18 @@
   sections: (AlignedTable | FreeStatements)[];
 }
 
-function addToValues<K, V>(
-  imap: Immutable.Map<K, V[]>,
-  key: K,
-  newValue: V
-): Immutable.Map<K, V[]> {
-  let cur = imap.get(key, undefined);
-  let ret = imap;
-  if (cur === undefined) {
-    cur = [];
-    ret = imap.set(key, cur);
-  }
-  cur.push(newValue);
-  return ret;
-}
-
-function multiColumnSort<T>(
-  elems: T[],
-  makeSortCols: (elem: T, index: number) => Array<number | string>
-): T[] {
-  const tagged = elems.map((p, i) => {
-    return {
-      sort: makeSortCols(p, i),
-      val: p,
-    };
-  });
-  tagged.sort((e1, e2) => {
-    let index = 0;
-    for (let k1 of e1.sort) {
-      const k2 = e2.sort[index];
-      if (!Immutable.is(k1, k2)) {
-        if (typeof k1 === "number") {
-          if (typeof k2 === "number") {
-            return k1 - k2;
-          } else {
-            throw new Error(`${k1} vs ${k2}`);
-          }
-        } else {
-          return k1.localeCompare(k2 as string);
-        }
-      }
-      index++;
-    }
-    return 0;
-  });
-  return tagged.map((e) => e.val);
-}
-
 class AlignedTableBuilder {
   subjSet: UriSet = Immutable.Set();
   predSet: UriSet = Immutable.Set();
   subjsSeenWithPred: Immutable.Map<NamedNode, NamedNode[]> = Immutable.Map();
   typesSeenForSubj: Immutable.Map<NamedNode, NamedNode[]> = Immutable.Map();
-
+  aliases: Immutable.Map<NamedNode, NamedNode> = Immutable.Map();
   cell = new UriPairMap();
-  constructor(public primaryType: NamedNode, public joinTypes: NamedNode[]) {}
+  constructor(
+    public primaryType: NamedNode,
+    public joinTypes: NamedNode[],
+    public links: Link[]
+  ) {}
 
   showsType(rdfType: NamedNode): boolean {
     return (
@@ -100,26 +58,54 @@
     );
   }
 
+  // Considering this.links, call adder on more subjs that should be routed to this table.
+  detectLinks(graph: Store, adder: (linkedSubj: NamedNode) => void) {
+    this.links.forEach((link: Link) => {
+      graph.forEach(
+        (q: Quad) => {
+          const linkedSubj = q.object as NamedNode;
+          adder(linkedSubj);
+          this.linkSubjs(q.subject as NamedNode, linkedSubj);
+        },
+        null,
+        link.pred,
+        null,
+        null
+      );
+    });
+    this.chainLinks();
+  }
+
+  // When you make a row for displayedSubj, also include linkedSubj's data
+  private linkSubjs(displayedSubj: NamedNode, linkedSubj: NamedNode) {
+    this.aliases = this.aliases.set(linkedSubj, displayedSubj);
+  }
+
+  // After this.aliases is built; shorten {b: a, c:b} to {b: a, c: a}
+  private chainLinks() {
+    for (let alias of this.aliases.keys()) {
+      let x = this.aliases.get(alias)!;
+      while (this.aliases.has(x)) {
+        x = this.aliases.get(x)!;
+      }
+      this.aliases = this.aliases.set(alias, x);
+    }
+  }
+
+  // Stream in quads that belong to this table (caller has to work out that
+  // part), then call value().
   addQuad(q: Quad) {
-    const subj = q.subject as NamedNode;
+    const unaliasedSubj = q.subject as NamedNode;
+    const aliasedSubj = this.aliases.get(unaliasedSubj, unaliasedSubj);
     const pred = q.predicate as NamedNode;
-    this.subjSet = this.subjSet.add(subj);
+    this.subjSet = this.subjSet.add(aliasedSubj);
     this.predSet = this.predSet.add(pred);
-    this.cell.add(subj, pred, q.object);
+    this.cell.add(aliasedSubj, pred, q.object);
 
     if (pred.equals(rdf.type)) {
-      this.trackTypes(subj, q.object as NamedNode);
+      this.trackTypes(unaliasedSubj, q.object as NamedNode);
     }
-    this.trackSubjs(subj, pred);
-  }
-
-  private trackTypes(subj: NamedNode<string>, rdfType: NamedNode) {
-    let cur = this.typesSeenForSubj.get(subj, undefined);
-    if (cur === undefined) {
-      cur = [];
-      this.typesSeenForSubj = this.typesSeenForSubj.set(subj, cur);
-    }
-    cur.push(rdfType);
+    this.trackSubjs(unaliasedSubj, pred);
   }
 
   private trackTypes(unaliasedSubj: NamedNode<string>, rdfType: NamedNode) {
@@ -138,10 +124,11 @@
     );
   }
 
-  _displayedPreds(): NamedNode[] {
+  private displayedPreds(): NamedNode[] {
     let preds = uniqueSortedTerms(this.predSet);
     preds = preds.filter((p) => {
       if (p.equals(rdf.type)) return false;
+      if (this.links.filter((l) => l.pred.equals(p)).length) return false;
       return true;
     });
     preds = multiColumnSort(preds, (elem: NamedNode, index: number) => {
@@ -172,10 +159,8 @@
     return uniqueSortedTerms(types);
   }
 
-  value(): AlignedTable {
-    const subjs = uniqueSortedTerms(this.subjSet);
-    const preds = this._displayedPreds();
-    const outputGrid: Term[][][] = [];
+  private gatherOutputGrid(subjs: NamedNode[], preds: NamedNode[]): Term[][][] {
+    const out: Term[][][] = [];
     for (let subj of subjs) {
       const row: Term[][] = [];
       preds.forEach((pred) => {
@@ -183,13 +168,20 @@
         const uniq = uniqueSortedTerms(objs);
         row.push(uniq);
       });
-      outputGrid.push(row);
+      out.push(row);
     }
+    return out;
+  }
 
-    const headers: ColumnHeader[] = preds.map((pred) => {
+  value(): AlignedTable {
+    const subjs = uniqueSortedTerms(this.subjSet);
+    const preds = this.displayedPreds();
+    const outputGrid: Term[][][] = this.gatherOutputGrid(subjs, preds);
+
+    const colHeaders: ColumnHeader[] = preds.map((pred) => {
       return { rdfTypes: this.typesSeenWithPred(pred), pred: pred };
     });
-    return { columnHeaders: headers, rowHeaders: subjs, rows: outputGrid };
+    return { columnHeaders: colHeaders, rowHeaders: subjs, rows: outputGrid };
   }
 }
 
@@ -218,6 +210,13 @@
     null,
     null
   );
+
+  tableBuilders.forEach((tb) => {
+    tb.detectLinks(graph, (linkedSubj: NamedNode) => {
+      out = addToValues(out, linkedSubj, tb);
+    });
+  });
+
   return out;
 }
 
@@ -251,7 +250,7 @@
 export class Layout {
   constructor(public viewConfig?: ViewConfig) {}
 
-  _groupAllStatements(
+  private groupAllStatements(
     graph: Store,
     tablesWantingSubject: SubjectTableBuilders,
     ungrouped: Quad[]
@@ -273,26 +272,31 @@
     );
   }
 
-  plan(graph: Store): LayoutResult {
-    const ungrouped: Quad[] = [];
-
-    const tableBuilders = this.viewConfig
-      ? this.viewConfig.tables.map(
-          (t) => new AlignedTableBuilder(t.primary, t.joins)
-        )
-      : [];
-
-    const tablesWantingSubject = subjectsToTablesMap(graph, tableBuilders);
-    this._groupAllStatements(graph, tablesWantingSubject, ungrouped);
-    const res: LayoutResult = { sections: [] };
+  private generateSections(
+    tableBuilders: AlignedTableBuilder[],
+    ungrouped: Quad[]
+  ): (AlignedTable | FreeStatements)[] {
+    const sections = [];
     for (const t of tableBuilders) {
       if (t.gotStatements()) {
-        res.sections.push(t.value());
+        sections.push(t.value());
       }
     }
     if (ungrouped.length) {
-      res.sections.push(freeStatmentsSection(ungrouped));
+      sections.push(freeStatmentsSection(ungrouped));
     }
-    return res;
+    return sections;
+  }
+
+  plan(graph: Store): LayoutResult {
+    const vcTables = this.viewConfig ? this.viewConfig.tables : [];
+    const tableBuilders = vcTables.map(
+      (t) => new AlignedTableBuilder(t.primary, t.joins, t.links)
+    );
+
+    const tablesWantingSubject = subjectsToTablesMap(graph, tableBuilders);
+    const ungrouped: Quad[] = [];
+    this.groupAllStatements(graph, tablesWantingSubject, ungrouped);
+    return { sections: this.generateSections(tableBuilders, ungrouped) };
   }
 }