changeset 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 4bb8c7775c83
files package.json src/Layout.test.ts src/Layout.ts
diffstat 3 files changed, 124 insertions(+), 130 deletions(-) [+]
line wrap: on
line diff
--- a/package.json	Sat Mar 12 00:42:00 2022 -0800
+++ b/package.json	Sat Mar 12 22:41:43 2022 -0800
@@ -25,6 +25,7 @@
     "n3": "^1.13.0",
     "rdf-js": "^4.0.2"
   },
+  "n3 is hacked":"to remove hashCode getter",
   "devDependencies": {
     "@types/jest": "^27.4.0",
     "jest": "^27.5.1",
--- a/src/Layout.test.ts	Sat Mar 12 00:42:00 2022 -0800
+++ b/src/Layout.test.ts	Sat Mar 12 22:41:43 2022 -0800
@@ -1,25 +1,15 @@
-import { Layout, LayoutResult } from "./Layout";
+import { Quad, Store, Term } from "n3";
+import { n3Graph } from "./fetchAndParse";
+import { AlignedTable, Layout, LayoutResult } from "./Layout";
+import { EX, rdf } from "./namespaces";
 import { ViewConfig } from "./ViewConfig";
-import {
-  DataFactory,
-  Store,
-  Prefixes,
-  Parser,
-  Quad,
-  NamedNode,
-  Term,
-} from "n3";
-import { EX, rdf } from "./namespaces";
-import Immutable from "immutable";
-import { n3Graph } from "./fetchAndParse";
-const { namedNode } = DataFactory;
 
 const twoStatements = async (): Promise<Store> => {
   return n3Graph(`
   @prefix : <http://example.com/> .
   :g1 {
-    :a :b :c .
-    :d :e :f .
+    :a0 :b0 :c0 .
+    :d0 :e0 :f0 .
   }
   `);
 };
@@ -58,8 +48,8 @@
       sections: [
         {
           statements: [
-            G1(EX("a"), EX("b"), EX("c")),
-            G1(EX("d"), EX("e"), EX("f")),
+            G1(EX("a0"), EX("b0"), EX("c0")),
+            G1(EX("d0"), EX("e0"), EX("f0")),
           ],
         },
       ],
@@ -76,24 +66,28 @@
   
         <> a :View; :table [ :primaryType :T1 ] .`);
       const layout = new Layout(vc);
-      const lr = layout.plan(await typedStatements());
+      lr = layout.plan(await typedStatements());
+    });
+    it("returns 2 sections", ()=>{
       expect(lr.sections).toHaveLength(2);
-    });
+    })
     it("puts the right type in the table", async () => {
-      expect(lr.sections[0]).toEqual({
-        columnHeaders: [{ rdfType: EX("T1"), pred: EX("color") }],
-        rows: [
-          [EX("a"), EX("red")],
-          [EX("b"), EX("blue")],
-          [EX("c"), null],
-          [EX("e"), null],
-        ],
-      });
+      const sec0 = lr.sections[0] as AlignedTable;
+      expect(sec0.columnHeaders).toEqual([
+        { rdfType: EX("T1"), pred: EX("color") },
+        { rdfType: EX("T1"), pred: EX("size") }
+    ])
+      expect(sec0.rows).toEqual([
+          [EX("a"), EX("red"), null],
+          [EX("b"), EX("blue"),null],
+          [EX("c"), null, null],
+          [EX("e"), null, EX('small')],
+        ]);
     });
     it("leaves the rest ungrouped", async () => {
       expect(lr.sections[1]).toEqual({
         statements: [
-          G1(EX("d"), rdf.type, EX("T1")),
+          G1(EX("d"), rdf.type, EX("T2")),
           G1(EX("d"), EX("size"), EX("big")),
         ],
       });
--- 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;