view src/layout/Layout.ts @ 108:5e6840229a05

rewrite freeStatements rendering to put more planning in layout
author drewp@bigasterisk.com
date Fri, 18 Mar 2022 11:57:38 -0700
parents 2468f2227d22
children 3cdbbd913f1d
line wrap: on
line source

// 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 { uniqueSortedTerms } from "./rdf_value";
import { TableDesc, ViewConfig } from "./ViewConfig";

type UriSet = Immutable.Set<NamedNode>;
export type TypeToSubjs = Immutable.Map<NamedNode, UriSet>;

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
}

export interface PredRow {
  pred: NamedNode;
  objs: Term[];
}

export interface SubjRow {
  subj: NamedNode;
  predRows: PredRow[];
}

export interface FreeStatements {
  subjRows: SubjRow[];
}

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);
}

function freeStatmentsSection(stmts: Quad[]): FreeStatements {
  const subjs: NamedNode[] = [];
  stmts.forEach((q) => {
    subjs.push(q.subject as NamedNode);
  });
  return {
    subjRows: uniqueSortedTerms(subjs).map((subj) => {
      const preds: NamedNode[] = [];
      let po = Immutable.Map<NamedNode, Term[]>();
      stmts.forEach((q) => {
        if (q.subject.equals(subj)) {
          const p = q.predicate as NamedNode;
          preds.push(p);
          po = po.set(p, po.get(p, []));
          po.get(p)?.push(q.object as Term);
        }
      });

      const rows: PredRow[] = [];
      uniqueSortedTerms(preds).forEach((p) => {
        rows.push({ pred: p, objs: uniqueSortedTerms(po.get(p, [])) });
      });
      return { subj: subj, predRows: rows };
    }),
  };
}

// The description of how this page should look: sections, tables, etc.
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(freeStatmentsSection(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,
//     });
//   }
// }