view src/layout/Layout.ts @ 114:4b33a479dc2f

fix layout test to match new layout return types. clean up UriPairMap
author drewp@bigasterisk.com
date Sat, 19 Mar 2022 16:12:49 -0700
parents 3cdbbd913f1d
children 84551452a9c9
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, rdfs } from "./namespaces";
import { uniqueSortedTerms, UriPairMap } 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[];
  rowHeaders: NamedNode[];
  rows: Term[][][];
}

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 {
  subjSet = Immutable.Set<NamedNode>();
  predSet = Immutable.Set<NamedNode>();
  cell = new UriPairMap();
  constructor(
    public rdfType: NamedNode /* plus join types, sort instructions */
  ) {}

  addQuad(q: Quad) {
    const subj = q.subject as NamedNode;
    const pred = q.predicate as NamedNode;
    this.subjSet = this.subjSet.add(subj);
    this.predSet = this.predSet.add(pred);
    this.cell.add(subj, pred, q.object);
  }

  _displayedPreds(): NamedNode[] {
    let preds = uniqueSortedTerms(this.predSet);
    preds = preds.filter((p) => {
      return !p.equals(rdf.type);
    });
    const tagged = preds.map((p, i) => {
      if (p.equals(rdfs.label)) {
        i = -1;
      }
      return { sort: i, val: p };
    });
    tagged.sort((a, b) => {
      return a.sort - b.sort;
    });
    preds = tagged.map((e) => e.val);
    return preds;
  }

  value(): AlignedTable {
    const subjs = uniqueSortedTerms(this.subjSet);
    const preds = this._displayedPreds();
    const outputGrid: Term[][][] = [];
    for (let subj of subjs) {
      const row: Term[][] = [];
      preds.forEach((pred) => {
        const objs = this.cell.get(subj, pred);
        const uniq = uniqueSortedTerms(objs);
        row.push(uniq);
      });
      outputGrid.push(row);
    }

    const headers = preds.map((pred) => {
      return { rdfType: this.rdfType, pred: pred };
    });
    return { columnHeaders: headers, rowHeaders: subjs, rows: outputGrid };
  }
}

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, typesToGather: UriSet): UriSet {
  const subjectsToGather: NamedNode[] = [];
  const ft = typesToGather.toArray()[0];
  graph.forEach(
    (q: Quad) => {
      if (q.object.equals(ft)) {
        //typesToGather.has(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 typesToTable = findTypesNeededForTables(this.viewConfig);

    const subjectsToTable = findSubjectsWithTypes(graph, typesToTable);
    const ungrouped: Quad[] = [];
    const vc = this.viewConfig;
    const table =
      vc && vc.tables.length > 0
        ? new AlignedTableBuilder(vc.tables[0].primary) //todo multiple tables
        : null;

    graph.forEach(
      (q: Quad) => {
        let contains = false;
        subjectsToTable.forEach((s) => {
          if (s.equals(q.subject)) {
            contains = true;
          }
        });

        // if (subjectsToTable.has(q.subject as NamedNode) && table) { // not working
        if (contains && table) {
          table.addQuad(q);
        } else {
          ungrouped.push(q);
        }
      },
      null,
      null,
      null,
      null
    );
    const res: LayoutResult = { sections: [] };
    if (table) {
      console.log("table value");
      res.sections.push(table.value());
    }
    res.sections.push(freeStatmentsSection(ungrouped));
    return res;
  }
}

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