view src/graph_view.ts @ 96:4d19759d0d9a

factor NodeDisplay
author drewp@bigasterisk.com
date Thu, 13 Jan 2022 21:32:18 -0800
parents a5f53d397526
children 26c55d5d5202
line wrap: on
line source

import { html, TemplateResult } from "lit";
import { DataFactory, Literal, NamedNode, Quad, Store, Term, Util } from "n3";
import { NodeDisplay } from "./NodeDisplay";
import { SuffixLabels } from "./suffixLabels";
import {
  groupByRdfType,
  MultiSubjsTypeBlockLayout,
  predsForSubj,
  TypeToSubjs,
} from "./tabulate";
import { 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(labels);
    this.nodeDisplay = new NodeDisplay(labels);
  }

  _addLabelsForAllTerms(labels: SuffixLabels) {
    return this.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, typeUri: NamedNode) {
    const layout = new MultiSubjsTypeBlockLayout(this.graph, byType, typeUri);

    return html`
      <div>[icon] ${this.nodeDisplay.render(typeUri)} type resources</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 typesToShow = this.view.typesToShow(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>
        ${typesToShow.map((t: NamedNode) =>
          this._multiSubjsTypeBlock(byType, t)
        )}
        <div class="spoGrid">
          ${untypedSubjs.map(this._subjPredObjsBlock.bind(this))}
        </div>
      </section>
    `;
  }
}