view src/layout/Layout.ts @ 121:3584f24becf4

cleanup
author drewp@bigasterisk.com
date Sun, 20 Mar 2022 14:10:56 -0700
parents a7519d92dbc6
children 2e8fa3fec0c8
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 { ViewConfig } from "./ViewConfig";

type UriSet = Immutable.Set<NamedNode>;

interface ColumnHeader {
  rdfTypes: 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)[];
}

function addToValues<K, V>(
  imap: Immutable.Map<K, V[]>,
  key: K,
  newValue: V
): Immutable.Map<K, V[]> {
  let cur = imap.get(key, undefined);
  let ret = imap;
  if (cur === undefined) {
    cur = [];
    ret = imap.set(key, cur);
  }
  cur.push(newValue);
  return ret;
}

function multiColumnSort<T>(
  elems: T[],
  makeSortCols: (elem: T, index: number) => Array<number | string>
): T[] {
  const tagged = elems.map((p, i) => {
    return {
      sort: makeSortCols(p, i),
      val: p,
    };
  });
  tagged.sort((e1, e2) => {
    let index = 0;
    for (let k1 of e1.sort) {
      const k2 = e2.sort[index];
      if (!Immutable.is(k1, k2)) {
        if (typeof k1 === "number") {
          if (typeof k2 === "number") {
            return k1 - k2;
          } else {
            throw new Error(`${k1} vs ${k2}`);
          }
        } else {
          return k1.localeCompare(k2 as string);
        }
      }
      index++;
    }
    return 0;
  });
  return tagged.map((e) => e.val);
}

class AlignedTableBuilder {
  subjSet: UriSet = Immutable.Set();
  predSet: UriSet = Immutable.Set();
  subjsSeenWithPred: Immutable.Map<NamedNode, NamedNode[]> = Immutable.Map();
  typesSeenForSubj: Immutable.Map<NamedNode, NamedNode[]> = Immutable.Map();

  cell = new UriPairMap();
  constructor(public primaryType: NamedNode, public joinTypes: NamedNode[]) {}

  showsType(rdfType: NamedNode): boolean {
    return (
      this.primaryType.equals(rdfType) ||
      Immutable.Set<NamedNode>(this.joinTypes).has(rdfType)
    );
  }

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

    if (pred.equals(rdf.type)) {
      this.trackTypes(subj, q.object as NamedNode);
    }
    this.trackSubjs(subj, pred);
  }

  private trackTypes(subj: NamedNode<string>, rdfType: NamedNode) {
    let cur = this.typesSeenForSubj.get(subj, undefined);
    if (cur === undefined) {
      cur = [];
      this.typesSeenForSubj = this.typesSeenForSubj.set(subj, cur);
    }
    cur.push(rdfType);
  }

  private trackTypes(unaliasedSubj: NamedNode<string>, rdfType: NamedNode) {
    this.typesSeenForSubj = addToValues(
      this.typesSeenForSubj,
      unaliasedSubj,
      rdfType
    );
  }

  private trackSubjs(unaliasedSubj: NamedNode, pred: NamedNode<string>) {
    this.subjsSeenWithPred = addToValues(
      this.subjsSeenWithPred,
      pred,
      unaliasedSubj
    );
  }

  _displayedPreds(): NamedNode[] {
    let preds = uniqueSortedTerms(this.predSet);
    preds = preds.filter((p) => {
      if (p.equals(rdf.type)) return false;
      return true;
    });
    preds = multiColumnSort(preds, (elem: NamedNode, index: number) => {
      const types = this.typesSeenWithPred(elem);
      return [
        elem.equals(rdfs.label) ? 0 : 1,
        types.length == 1 ? 0 : 1,
        types[0].equals(this.primaryType) ? 0 : 1,
        types[0].id,
        index,
      ];
    });
    return preds;
  }

  gotStatements(): boolean {
    return !this.subjSet.isEmpty();
  }

  private typesSeenWithPred(pred: NamedNode): NamedNode[] {
    const subs = this.subjsSeenWithPred.get(pred, []);
    const types: NamedNode[] = [];
    subs.forEach((s) => {
      this.typesSeenForSubj.get(s, []).forEach((t) => {
        types.push(t);
      });
    });
    return uniqueSortedTerms(types);
  }

  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: ColumnHeader[] = preds.map((pred) => {
      return { rdfTypes: this.typesSeenWithPred(pred), pred: pred };
    });
    return { columnHeaders: headers, rowHeaders: subjs, rows: outputGrid };
  }
}

type SubjectTableBuilders = Immutable.Map<
  NamedNode<string>,
  AlignedTableBuilder[]
>;

function subjectsToTablesMap(
  graph: Store,
  tableBuilders: AlignedTableBuilder[]
): SubjectTableBuilders {
  let out: SubjectTableBuilders = Immutable.Map();
  graph.forEach(
    (q: Quad) => {
      const s = q.subject as NamedNode;
      const rdfType = q.object as NamedNode;
      tableBuilders.forEach((tb) => {
        if (tb.showsType(rdfType)) {
          out = addToValues(out, s, tb);
        }
      });
    },
    null,
    rdf.type,
    null,
    null
  );
  return out;
}

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 = addToValues(po, p, q.object as NamedNode);
        }
      });

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

  _groupAllStatements(
    graph: Store,
    tablesWantingSubject: SubjectTableBuilders,
    ungrouped: Quad[]
  ) {
    graph.forEach(
      (q: Quad) => {
        const tables = tablesWantingSubject.get(q.subject as NamedNode);

        if (tables && tables.length) {
          tables.forEach((t: AlignedTableBuilder) => t.addQuad(q));
        } else {
          ungrouped.push(q);
        }
      },
      null,
      null,
      null,
      null
    );
  }

  plan(graph: Store): LayoutResult {
    const ungrouped: Quad[] = [];

    const tableBuilders = this.viewConfig
      ? this.viewConfig.tables.map(
          (t) => new AlignedTableBuilder(t.primary, t.joins)
        )
      : [];

    const tablesWantingSubject = subjectsToTablesMap(graph, tableBuilders);
    this._groupAllStatements(graph, tablesWantingSubject, ungrouped);
    const res: LayoutResult = { sections: [] };
    for (const t of tableBuilders) {
      if (t.gotStatements()) {
        res.sections.push(t.value());
      }
    }
    if (ungrouped.length) {
      res.sections.push(freeStatmentsSection(ungrouped));
    }
    return res;
  }
}