diff src/layout/Layout.ts @ 106:2468f2227d22

make src/layout/ and src/render/ separation
author drewp@bigasterisk.com
date Sun, 13 Mar 2022 22:00:30 -0700
parents src/Layout.ts@1aea03d306af
children 5e6840229a05
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/layout/Layout.ts	Sun Mar 13 22:00:30 2022 -0700
@@ -0,0 +1,200 @@
+// Organize graph data into tables (column orders, etc) for the view layer.
+
+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 { rdf } from "./namespaces";
+import { TableDesc, ViewConfig } from "./ViewConfig";
+
+type UriSet = Immutable.Set<NamedNode>;
+export type TypeToSubjs = Immutable.Map<NamedNode, UriSet>;
+
+// https://github.com/rdfjs/N3.js/issues/265
+(NamedNode.prototype as any).hashCode = () => 0;
+
+interface ColumnHeader {
+  rdfType: NamedNode;
+  pred: NamedNode;
+}
+export interface AlignedTable {
+  columnHeaders: ColumnHeader[];
+  rows: (Term | null)[][]; // each row is 1 wider than columnHeaders since the 1st element is the subject for that row
+}
+interface FreeStatements {
+  statements: Quad[];
+}
+export interface LayoutResult {
+  sections: (AlignedTable | FreeStatements)[];
+}
+
+class AlignedTableBuilder {
+  columnPreds = Immutable.List<NamedNode>();
+  subjRowIndices = Immutable.Map<NamedNode, number>();
+  rows: (Term | null)[][] = [];
+  constructor(
+    public rdfType: NamedNode /* plus join types, sort instructions */
+  ) {}
+
+  addQuad(q: Quad) {
+    const pred = q.predicate as NamedNode;
+    const omittedColumn = pred.equals(rdf.type);
+    if (!this.columnPreds.contains(pred) && !omittedColumn) {
+      this.columnPreds = this.columnPreds.push(pred); // this is putting cols in random order
+      this.rows.forEach((r) => r.push(null));
+    }
+
+    const predIndex = omittedColumn ? null : this.columnPreds.indexOf(pred);
+    let rowIndex = this.subjRowIndices.get(q.subject as NamedNode);
+    if (rowIndex === undefined) {
+      const newRow = new Array(1 + this.columnPreds.size).fill(null);
+      newRow[0] = q.subject;
+      this.rows.push(newRow);
+      rowIndex = this.rows.length - 1;
+      this.subjRowIndices = this.subjRowIndices.set(
+        q.subject as NamedNode,
+        rowIndex
+      );
+    }
+    if (predIndex !== null) {
+      this.rows[rowIndex][1 + predIndex] = q.object;
+    }
+  }
+
+  value(): AlignedTable {
+    this.rows.sort((a, b) => {
+      const uriA = (a[0] as NamedNode).value,
+        uriB = (b[0] as NamedNode).value;
+      return uriA.localeCompare(uriB);
+    });
+    const headers = this.columnPreds
+      .map((pred) => {
+        return { rdfType: this.rdfType, pred: pred };
+      })
+      .toArray();
+    return { columnHeaders: headers, rows: this.rows };
+  }
+}
+
+function findTypesNeededForTables(viewConfig?: ViewConfig): UriSet {
+  const typesToGather: NamedNode[] = [];
+  if (viewConfig) {
+    viewConfig.tables.forEach((t: TableDesc) => {
+      typesToGather.push(t.primary);
+    });
+  }
+  return Immutable.Set(typesToGather);
+}
+
+function findSubjectsWithTypes(graph: Store, typesToGatherSet: UriSet): UriSet {
+  const subjectsToGather: NamedNode[] = [];
+  graph.forEach(
+    (q: Quad) => {
+      if (typesToGatherSet.contains(q.object as NamedNode)) {
+        subjectsToGather.push(q.subject as NamedNode);
+      }
+    },
+    null,
+    rdf.type,
+    null,
+    null
+  );
+  return Immutable.Set(subjectsToGather);
+}
+
+export class Layout {
+  constructor(public viewConfig?: ViewConfig) {}
+  plan(graph: Store): LayoutResult {
+    const typesToGatherSet = findTypesNeededForTables(this.viewConfig);
+
+    const subjectsToGatherSet = findSubjectsWithTypes(graph, typesToGatherSet);
+    const ungrouped: Quad[] = [];
+    const vc = this.viewConfig;
+    const table =
+      vc && vc.tables.length > 0
+        ? new AlignedTableBuilder(vc.tables[0].primary)
+        : null;
+
+    graph.forEach(
+      (q: Quad) => {
+        if (!subjectsToGatherSet.contains(q.subject as NamedNode) || !table) {
+          ungrouped.push(q);
+        } else {
+          table.addQuad(q);
+        }
+      },
+      null,
+      null,
+      null,
+      null
+    );
+    const res: LayoutResult = { sections: [] };
+    if (table) {
+      res.sections.push(table.value());
+    }
+    res.sections.push({ statements: ungrouped });
+    return res;
+  }
+}
+
+// interface ISP {
+//   subj: NamedNode;
+//   pred: NamedNode;
+// }
+// const SP = Immutable.Record<ISP>({
+//   subj: new NamedNode(""),
+//   pred: new NamedNode(""),
+// });
+
+// // One table of rows with a common rdf:type.
+// export class MultiSubjsTypeBlockLayout {
+//   subjs: NamedNode[];
+//   preds: NamedNode[];
+//   graphCells: Immutable.Map<ISP, Immutable.Set<Term>>;
+//   constructor(graph: Store, byType: TypeToSubjs, table: TableDesc) {
+//     const subjSet = byType.get(table.primary);
+//     this.subjs = subjSet ? Array.from(subjSet) : [];
+//     this.subjs.sort();
+
+//     let preds = Immutable.Set<NamedNode>();
+
+//     this.graphCells = Immutable.Map<ISP, Immutable.Set<Term>>().withMutations(
+//       (mutGraphCells) => {
+//         this.subjs.forEach((subj: NamedNode) => {
+//           graph.forEach(
+//             (q: Quad) => {
+//               if (!Util.isNamedNode(q.predicate)) {
+//                 throw new Error();
+//               }
+
+//               const pred = q.predicate as NamedNode;
+//               if (pred.equals(rdf.type)) {
+//                 // the whole block is labeled with the type
+//                 return;
+//               }
+//               preds = preds.add(pred);
+//               const cellKey = this.makeCellKey(subj, pred);
+//               mutGraphCells.set(
+//                 cellKey,
+//                 mutGraphCells.get(cellKey, Immutable.Set<Term>()).add(q.object)
+//               );
+//             },
+//             subj,
+//             null,
+//             null,
+//             null
+//           );
+//         });
+//       }
+//     );
+//     this.preds = Array.from(preds);
+//     this.preds.splice(this.preds.indexOf(rdf.type), 1);
+//     // also pull out label, which should be used on 1st column
+//     this.preds.sort();
+//   }
+
+//   makeCellKey(subj: NamedNode, pred: NamedNode): ISP {
+//     return SP({
+//       subj: subj,
+//       pred: pred,
+//     });
+//   }
+// }