diff src/Layout.ts @ 104:1aea03d306af

WIP Layout. tests are passing but they're a little wrong
author drewp@bigasterisk.com
date Sat, 12 Mar 2022 22:41:43 -0800
parents f12feced00ce
children
line wrap: on
line diff
--- a/src/Layout.ts	Sat Mar 12 00:42:00 2022 -0800
+++ b/src/Layout.ts	Sat Mar 12 22:41:43 2022 -0800
@@ -1,35 +1,21 @@
 // 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 {
-  DataFactory,
-  NamedNode,
-  Quad,
-  Quad_Object,
-  Store,
-  Term,
-  Util,
-} from "n3";
-import { ViewConfig } from "./ViewConfig";
-
-const { namedNode } = DataFactory;
-
-// // import ns from 'n3/src/IRIs';
-// // const { rdf } = ns;
+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
-if ((NamedNode.prototype as any).hashCode === undefined) {
-  (NamedNode.prototype as any).hashCode = () => 0;
-}
+(NamedNode.prototype as any).hashCode = () => 0;
 
 interface ColumnHeader {
-  rdfType?: NamedNode; // could be more than one column that introduces an rdf:type for a join
+  rdfType: NamedNode;
   pred: NamedNode;
 }
-interface AlignedTable {
+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
 }
@@ -40,102 +26,115 @@
   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) => {
-        ungrouped.push(q);
+        if (!subjectsToGatherSet.contains(q.subject as NamedNode) || !table) {
+          ungrouped.push(q);
+        } else {
+          table.addQuad(q);
+        }
       },
       null,
       null,
       null,
       null
     );
-    return { sections: [{ statements: ungrouped }] };
+    const res: LayoutResult = { sections: [] };
+    if (table) {
+      res.sections.push(table.value());
+    }
+    res.sections.push({ statements: ungrouped });
+    return res;
   }
 }
 
-// function getType(graph: Store, subj: NamedNode): NamedNode | null {
-//   let subjType: NamedNode | null = null;
-
-//   graph.forObjects(
-//     (o: Quad_Object) => {
-//       subjType = o as NamedNode;
-//     },
-//     subj,
-//     rdf.type,
-//     null
-//   );
-//   return subjType;
-// }
-
-// // When there are multiple types, an arbitrary one is used.
-// export function groupByRdfType(
-//   graph: Store
-// ): {
-//   byType: TypeToSubjs;
-//   typesPresent: NamedNode[];
-//   untypedSubjs: NamedNode[];
-// } {
-//   let byType: TypeToSubjs = Immutable.Map();
-//   let untyped: UriSet = Immutable.Set(); // subjs
-//   const internSubjs = new Map<string, NamedNode>();
-//   graph.forEach(
-//     (q) => {
-//       if (!Util.isNamedNode(q.subject)) {
-//         throw new Error("unsupported " + q.subject.value);
-//       }
-//       const subj = q.subject as NamedNode;
-
-//       const subjType = getType(graph, subj);
-
-//       if (subjType !== null) {
-//         // (subj, rdf:type, subjType) in graph
-//         const oldKeys = Array.from(byType.keys());
-//         const oldVal = byType.get(subjType, Immutable.Set<NamedNode>());
-//         const newVal = oldVal.add(subj);
-//         byType = byType.set(subjType, newVal);
-//       } else {
-//         untyped = untyped.add(subj);
-//       }
-//     },
-//     null,
-//     null,
-//     null,
-//     null
-//   );
-
-//   const typesPresent = Array.from(byType.keys());
-//   typesPresent.sort();
-
-//   const untypedSubjs = Array.from(untyped.values());
-//   untypedSubjs.sort();
-//   return {
-//     byType: byType,
-//     typesPresent: typesPresent,
-//     untypedSubjs: untypedSubjs,
-//   };
-// }
-
-// export function predsForSubj(graph: Store, typeUri: NamedNode): NamedNode[] {
-//   const predsSet: Set<NamedNode> = new Set();
-//   graph.forEach(
-//     (q: Quad) => {
-//       predsSet.add(q.predicate as NamedNode);
-//     },
-//     typeUri,
-//     null,
-//     null,
-//     null
-//   );
-//   const preds = Array.from(predsSet.values());
-//   preds.sort();
-//   return preds;
-// }
-
 // interface ISP {
 //   subj: NamedNode;
 //   pred: NamedNode;