view src/graph_view.ts @ 97:26c55d5d5202

WIP on views & joins
author drewp@bigasterisk.com
date Fri, 11 Feb 2022 20:27:02 -0800
parents 4d19759d0d9a
children
line wrap: on
line source

import { html, TemplateResult } from "lit";
import { DataFactory, Literal, NamedNode, Quad, Store, Term } from "n3";
import { NodeDisplay } from "./NodeDisplay";
import { SuffixLabels } from "./suffixLabels";
import {
  groupByRdfType,
  MultiSubjsTypeBlockLayout,
  predsForSubj,
  TypeToSubjs,
} from "./tabulate";
import { TableDesc, View } from "./view_loader";

const { namedNode } = DataFactory;

// https://github.com/rdfjs/N3.js/issues/265
if ((Literal.prototype as any).hashCode === undefined) {
  (Literal.prototype as any).hashCode = () => 0;
}
if ((NamedNode.prototype as any).hashCode === undefined) {
  (NamedNode.prototype as any).hashCode = () => 0;
}
export class GraphView {
  url: string;
  view: View;
  graph: Store;
  nodeDisplay: NodeDisplay;
  constructor(url: string, viewUrl: string, graph: Store) {
    this.url = url;
    this.view = new View(viewUrl);
    this.graph = graph;

    const labels = new SuffixLabels();
    this._addLabelsForAllTerms(this.graph, labels);
    
    if (this.view.graph) {
      this._addLabelsForAllTerms(this.view.graph, labels);
    }
    this.nodeDisplay = new NodeDisplay(labels);
  }

  _addLabelsForAllTerms(graph: Store, labels: SuffixLabels) {
    console.log("_addLabelsForAllTerms");

    graph.forEach(
      (q: Quad) => {
        if (q.subject.termType === "NamedNode") {
          labels.planDisplayForNode(q.subject);
        }
        if (q.predicate.termType === "NamedNode") {
          labels.planDisplayForNode(q.predicate);
        }
        if (q.object.termType === "NamedNode") {
          labels.planDisplayForNode(q.object);
        }
        if (q.object.termType === "Literal" && q.object.datatype) {
          labels.planDisplayForNode(q.object.datatype);
        }
      },
      null,
      null,
      null,
      null
    );
  }

  _subjPredObjsBlock(subj: NamedNode) {
    const columns = predsForSubj(this.graph, subj);
    return html`
      <div class="subject">
        ${this.nodeDisplay.render(subj)}
        <!-- todo: special section for uri/type-and-icon/label/comment -->
        <div>
          ${columns.map((p) => {
            return this._predObjsBlock(subj, p);
          })}
        </div>
      </div>
    `;
  }

  _objCell(obj: Term) {
    return html`
      <div class="object">
        ${this.nodeDisplay.render(obj)}
        <!-- indicate what source or graph said this stmt -->
      </div>
    `;
  }

  _predObjsBlock(subj: NamedNode, pred: NamedNode) {
    const objsSet = new Set<Term>();
    this.graph.forEach(
      (q: Quad) => {
        objsSet.add(q.object);
      },
      subj,
      pred,
      null,
      null
    );
    const objs = Array.from(objsSet.values());
    objs.sort();
    return html`
      <div class="predicate">
        ${this.nodeDisplay.render(pred)}
        <div>${objs.map(this._objCell.bind(this))}</div>
      </div>
    `;
  }

  _drawObj(obj: Term): TemplateResult {
    return html` <div>${this.nodeDisplay.render(obj)}</div> `;
  }

  _drawColumnHead(pred: NamedNode): TemplateResult {
    return html` <th>${this.nodeDisplay.render(pred)}</th> `;
  }

  _thead(layout: MultiSubjsTypeBlockLayout): TemplateResult {
    return html`
      <thead>
        <tr>
          <th></th>
          ${layout.preds.map(this._drawColumnHead.bind(this))}
        </tr>
      </thead>
    `;
  }

  _msbCell(layout: MultiSubjsTypeBlockLayout, subj: NamedNode) {
    return (pred: NamedNode): TemplateResult => {
      const objs = layout.graphCells.get(layout.makeCellKey(subj, pred));
      if (!objs || !objs.size) {
        return html` <td></td> `;
      }
      const objsList = Array.from(objs);
      objsList.sort();
      return html` <td>${objsList.map(this._drawObj.bind(this))}</td> `;
    };
  }

  _instanceRow(layout: MultiSubjsTypeBlockLayout) {
    return (subj: NamedNode): TemplateResult => {
      return html`
        <tr>
          <td>${this.nodeDisplay.render(subj)}</td>
          ${layout.preds.map(this._msbCell(layout, subj))}
        </tr>
      `;
    };
  }

  _multiSubjsTypeBlock(byType: TypeToSubjs, table: TableDesc) {
    const layout = new MultiSubjsTypeBlockLayout(this.graph, byType, table);

    let typeNames = [html`${this.nodeDisplay.render(table.primary)}`];
    if (table.joins) {
      typeNames.push(html` joined with [`);
      for (let j of table.joins) {
        typeNames.push(html`${this.nodeDisplay.render(j)}`);
      }
      typeNames.push(html`]`);
    }

    return html`
      <div>[icon] Resources of type ${typeNames}</div>
      <div class="typeBlockScroll">
        <table class="typeBlock">
          ${this._thead(layout)} ${layout.subjs.map(this._instanceRow(layout))}
        </table>
      </div>
    `;
  }

  async makeTemplate(): Promise<TemplateResult> {
    await this.view.ready;
    const { byType, typesPresent, untypedSubjs } = groupByRdfType(this.graph);
    let viewTitle = html` (no view)`;
    if (this.view.url) {
      viewTitle = html` using view
        <a href="${this.view.url}">${this.view.label()}</a>`;
    }
    const tables = this.view.toplevelTables(typesPresent);
    return html`
      <section>
        <h2>
          Current graph (<a href="${this.url}">${this.url}</a>)${viewTitle}
        </h2>
        <div>
          <!-- todo: graphs and provenance.
            These statements are all in the
            <span data-bind="html: $root.createCurie(graphUri())">...</span> graph.-->
        </div>
        ${tables.map((t: TableDesc) => this._multiSubjsTypeBlock(byType, t))}
        <div class="spoGrid">
          ${untypedSubjs.map(this._subjPredObjsBlock.bind(this))}
        </div>
      </section>
    `;
  }
}