view src/graph_view.ts @ 49:c16a331f42e5

rewrap; stylesheet
author drewp@bigasterisk.com
date Thu, 09 Jan 2020 00:05:32 -0800
parents 648bd89f9d47
children 490f569bb0c9
line wrap: on
line source

import { html, TemplateResult } from "lit-html";
import { Quad, Term, N3Store } from "n3";
import { DataFactory, Util } from "n3";
const { namedNode } = DataFactory;
import * as RDF from "rdf-js";
type NamedNode = RDF.NamedNode;

import { SuffixLabels } from "./suffixLabels";
// import ns from 'n3/src/IRIs';
// const { rdf } = ns;
const rdf = {
  type: namedNode("http://www.w3.org/1999/02/22-rdf-syntax-ns#type")
};

type TypeToSubjs = Map<NamedNode, Set<NamedNode>>;
function groupByRdfType(
  graph: N3Store
): { byType: TypeToSubjs; untyped: Set<NamedNode> } {
  const rdfType = rdf.type;
  const byType: TypeToSubjs = new Map();
  const untyped: Set<NamedNode> = new Set(); // subjs
  const internSubjs = new Map<string, NamedNode>();
  graph.forEach(
    q => {
      if (!Util.isNamedNode(q.subject)) {
        throw new Error("unsupported " + q.subject.value);
      }
      const subj = q.subject as NamedNode;

      let subjType: NamedNode | null = null;

      graph.forObjects(
        (o: Quad) => {
          if (Util.isNamedNode(o.object)) {
            subjType = o.object as NamedNode;
          }
        },
        subj,
        rdfType,
        null
      );

      if (subjType !== null) {
        // (subj, rdf:type, subjType) in graph
        if (!byType.has(subjType)) {
          byType.set(subjType, new Set());
        }
        (byType.get(subjType) as Set<NamedNode>).add(subj);
      } else {
        // no rdf:type stmt in graph
        if (!internSubjs.has(subj.value)) {
          internSubjs.set(subj.value, subj);
        }
        const intSubj: NamedNode = internSubjs.get(
          subj.value as string
        ) as NamedNode;
        untyped.add(intSubj);
      }
    },
    null,
    null,
    null,
    null
  );
  return { byType: byType, untyped: untyped };
}

class NodeDisplay {
  labels: SuffixLabels;
  constructor(labels: SuffixLabels) {
    this.labels = labels;
  }
  getHtml(n: Term | NamedNode): TemplateResult {
    if (n.termType == "Literal") {
      let dtPart: any = "";
      if (n.datatype) {
        dtPart = html`
          ^^<span class="literalType">
            ${this.getHtml(n.datatype)}
          </span>
        `;
      }
      return html`
        <span class="literal">${n.value}${dtPart}</span>
      `;
    }

    if (n.termType == "NamedNode") {
      let shortened = false;
      let uriValue: string = n.value;
      for (let [long, short] of [
        ["http://www.w3.org/1999/02/22-rdf-syntax-ns#", "rdf:"],
        ["http://www.w3.org/2000/01/rdf-schema#", "rdfs:"],
        ["http://purl.org/dc/elements/1.1/", "dc:"],
        ["http://www.w3.org/2001/XMLSchema#", "xsd:"]
      ]) {
        if (uriValue?.startsWith(long)) {
          uriValue = short + uriValue.substr(long.length);
          shortened = true;
          break;
        }
      }
      if (!shortened) {
        let dn: string | undefined = this.labels.getLabelForNode(uriValue);
        if (dn === undefined) {
          throw new Error(`dn=${dn}`);
        }
        uriValue = dn;
      }

      return html`
        <a class="graphUri" href="${n.value}">${uriValue}</a>
      `;
    }

    return html`
      [${n.termType} ${n.value}]
    `;
  }
}

export class GraphView {
  url: string;
  graph: N3Store;
  nodeDisplay: NodeDisplay;
  constructor(url: string, graph: N3Store) {
    this.url = url;
    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
    );
  }

  _subjBlock(subj: NamedNode) {
    const predsSet: Set<NamedNode> = new Set();
    this.graph.forEach(
      (q: Quad) => {
        predsSet.add(q.predicate as NamedNode);
      },
      subj,
      null,
      null,
      null
    );
    const preds = Array.from(predsSet.values());
    preds.sort();
    return html`
      <div class="subject">
        ${this.nodeDisplay.getHtml(subj)}
        <!-- todo: special section for uri/type-and-icon/label/comment -->
        <div>
          ${preds.map(p => {
            return this._predBlock(subj, p);
          })}
        </div>
      </div>
    `;
  }

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

  _predBlock(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.getHtml(pred)}
        <div>
          ${objs.map(this._objBlock.bind(this))}
        </div>
      </div>
    `;
  }

  byTypeBlock(byType: TypeToSubjs, typeUri: NamedNode) {
    const subjSet = byType.get(typeUri);
    const subjs: Array<NamedNode> = subjSet ? Array.from(subjSet) : [];
    subjs.sort();

    const graphCells = new Map<string, Set<Term>>(); // [subj, pred] : objs
    const makeCellKey = (subj: NamedNode, pred: NamedNode) =>
      subj.value + "|||" + pred.value;
    const preds = new Set<NamedNode>();

    subjs.forEach((subj: NamedNode) => {
      this.graph.forEach(
        (q: Quad) => {
          if (!Util.isNamedNode(q.predicate)) {
            throw new Error();
          }
          preds.add(q.predicate as NamedNode);
          const cellKey = makeCellKey(subj, q.predicate as NamedNode);
          if (!graphCells.has(cellKey)) {
            graphCells.set(cellKey, new Set<Term>());
          }
          graphCells.get(cellKey)!.add(q.object);
        },
        subj,
        null,
        null,
        null
      );
    });
    const predsList = Array.from(preds);
    predsList.splice(predsList.indexOf(rdf.type), 1);
    // also pull out label, which should be used on 1st column
    predsList.sort();

    const thead = () => {
      const predColumnHead = (pred: NamedNode) => {
        return html`
          <th>${this.nodeDisplay.getHtml(pred)}</th>
        `;
      };
      return html`
        <thead>
          <tr>
            <th></th>
            ${predsList.map(predColumnHead)}
          </tr>
        </thead>
      `;
    };

    const instanceRow = (subj: NamedNode) => {
      const cell = (pred: NamedNode) => {
        const objs = graphCells.get(subj + "|||" + pred);
        if (!objs) {
          return html`
            <td></td>
          `;
        }
        const objsList = Array.from(objs);
        objsList.sort();
        const draw = (obj: Term) => {
          return html`
            <div>${this.nodeDisplay.getHtml(obj)}</div>
          `;
        };
        return html`
          <td>${objsList.map(draw)}</td>
        `;
      };

      return html`
        <tr>
          <td>${this.nodeDisplay.getHtml(subj)}</td>
          ${predsList.map(cell)}
        </tr>
      `;
    };

    return html`
      <div>[icon] ${this.nodeDisplay.getHtml(typeUri)} resources</div>
      <div class="typeBlockScroll">
        <table class="typeBlock">
          ${thead()} ${subjs.map(instanceRow)}
        </table>
      </div>
    `;
  }

  makeTemplate(): TemplateResult {
    const { byType, untyped } = groupByRdfType(this.graph);
    const typedSubjs = Array.from(byType.keys());
    typedSubjs.sort();

    const untypedSubjs = Array.from(untyped.values());
    untypedSubjs.sort();

    return html`
      <section>
        <h2>Current graph (<a href="${this.url}">${this.url}</a>)</h2>
        <div>
          <!-- todo: graphs and provenance.
            These statements are all in the
            <span data-bind="html: $root.createCurie(graphUri())">...</span> graph.-->
        </div>
        ${typedSubjs.map((t: NamedNode) => this.byTypeBlock(byType, t))}
        <div class="spoGrid">
          ${untypedSubjs.map(this._subjBlock.bind(this))}
        </div>
      </section>
    `;
  }
}