view src/graph_view.ts @ 45:3e414d575d96

can't use that graph property so well if it's non-notifying
author drewp@bigasterisk.com
date Fri, 03 Jan 2020 23:42:04 -0800
parents 648bd89f9d47
children c16a331f42e5
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`
        <link rel="stylesheet" href="../src/streamed-graph.css">

        <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>
      `;
  }
}