view src/elements/graph-view/GraphView.ts @ 139:cf642d395be4

new simpler Patch class; fancier 'hide' view config support
author drewp@bigasterisk.com
date Mon, 08 May 2023 13:05:20 -0700
parents 5a1a79f54779
children
line wrap: on
line source

import Immutable from "immutable";
import { LitElement, PropertyValues, TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import { NamedNode, Quad, Store, Term } from "n3";
import { MultiStore } from "../../MultiStore";
import { Patch } from "../../Patch";
import {
  AlignedTable,
  FreeStatements,
  Layout,
  PredRow,
  SubjRow,
} from "../../layout/Layout";
import { ViewConfig } from "../../layout/ViewConfig";
import { uniqueSortedTerms } from "../../layout/rdf_value";
import { SuffixLabels } from "../../layout/suffixLabels";
import { graphViewStyle, pageStyle } from "../../style";
import { NodeDisplay } from "./NodeDisplay";
type UriSet = Immutable.Set<NamedNode>;

@customElement("graph-view")
export class GraphView extends LitElement {
  @property() graph: MultiStore | null = null;
  @property() viewEl: HTMLElement | null = null;

  viewConfig: ViewConfig | null = null;

  nodeDisplay: NodeDisplay | null = null;
  constructor() {
    super();
  }

  static styles = [pageStyle, graphViewStyle];
  update(changedProperties: PropertyValues) {
    if (changedProperties.has("graph") && this.graph) {
      const viewUri = new NamedNode(this.viewEl?.getAttribute('uri') || 'no-view')
      // const viewUri = new NamedNode(new URL("#view", document.baseURI).href);
      this.viewConfig = new ViewConfig(this.graph, viewUri);

      // "when viewconfig is updated..."
      // setTimeout(()=>this.requestUpdate(), 1000)

      this.graph.graphChanged.subscribe(this.onChange?.bind(this));
    }
    super.update(changedProperties);
  }

  onChange(p: Patch) {
    this.requestUpdate();
  }

  render() {
    if (!this.graph) {
      return;
    }
    return this.makeTemplate(this.graph);
  }

  makeTemplate(graph: MultiStore): TemplateResult {
    if (!this.viewConfig) {
      throw new Error();
    }
    const layout = new Layout(this.viewConfig);
    const lr = layout.plan(graph);

    const labels = new SuffixLabels();
    this._addLabelsForAllTerms(graph, labels);
    labels.planDisplayForNode(this.viewConfig.viewRoot); // todo shoudltn be needed
    this.nodeDisplay = new NodeDisplay(labels);
    let viewTitle = html``;
    viewTitle = html`<h2>View 
      <a href="${this.viewConfig.viewRoot.value}"
        >${this.nodeDisplay.render(this.viewConfig.viewRoot)}</a
      >`;
    return html`
      <section>
        ${viewTitle}
        <div>
          <!-- todo: graphs and provenance.
            These statements are all in the
            <span data-bind="html: $root.createCurie(graphUri())">...</span> graph.-->
        </div>
        ${lr.sections.map(this._renderSection.bind(this))}
      </section>
    `;
  }

  _addLabelsForAllTerms(graph: Store, labels: SuffixLabels) {
    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
    );
  }

  _renderSection(section: AlignedTable | FreeStatements) {
    if ((section as any).columnHeaders) {
      return this._renderAlignedTable(section as AlignedTable);
    } else {
      return this._renderFreeStatements(section as FreeStatements);
    }
  }

  _renderAlignedTable(section: AlignedTable): TemplateResult {
    const nodeDisplay = this.nodeDisplay;
    if (!nodeDisplay) throw new Error();
    const tableTypes: NamedNode[][] = [];
    const typeHeads: TemplateResult[] = [];
    const heads: TemplateResult[] = [];
    for (let ch of section.columnHeaders) {
      const colSpan = 1; //todo
      typeHeads.push(
        html`<th colspan="${colSpan}">
          ${ch.rdfTypes.map((n) => nodeDisplay.render(n))}
        </th>`
      );

      tableTypes.push(ch.rdfTypes);
      heads.push(html`<th>${nodeDisplay.render(ch.pred)}</th>`);
    }

    const cells = [];

    for (let rowIndex in section.rows) {
      const headerCol = nodeDisplay.render(section.rowHeaders[rowIndex]);
      const bodyCols = [];
      for (let cellObjs of section.rows[rowIndex]) {
        const display = cellObjs.map(
          (t) => html`<div>${nodeDisplay.render(t)}</div>`
        );
        bodyCols.push(html`<td>${display}</td>`);
      }
      cells.push(
        html`<tr>
          <th>${headerCol}</th>
          ${bodyCols}
        </tr>`
      );
    }
    const tableTypesUnique = uniqueSortedTerms(tableTypes.flat());
    const typesDisplay = html`${tableTypesUnique.length == 1 ? "type" : "types"}
    ${tableTypesUnique.map((n) => nodeDisplay.render(n))}`;

    return html`
      <div>[icon] Resources of ${typesDisplay}</div>
      <div class="typeBlockScroll">
        <table class="typeBlock">
          <thead>
            <tr>
              <th></th>
              ${typeHeads}
            </tr>
            <tr>
              <th>Subject</th>
              ${heads}
            </tr>
          </thead>
          <tbody>
            ${cells}
          </tbody>
        </table>
      </div>
    `;
  }

  _renderFreeStatements(section: FreeStatements): TemplateResult {
    const subjects: NamedNode[] = [];
    let subjPreds = Immutable.Map<NamedNode, UriSet>();

    return html`<div class="spoGrid">
      grid has rowcount ${section.subjRows.length}
      ${section.subjRows.map(this._subjPredObjsBlock.bind(this))}
    </div>`;
  }

  _subjPredObjsBlock(row: SubjRow): TemplateResult {
    return html`
      <div class="subject">
        ${this.nodeDisplay?.render(row.subj)}
        <!-- todo: special section for uri/type-and-icon/label/comment -->
        <div>${row.predRows.map(this._predObjsBlock.bind(this))}</div>
      </div>
    `;
  }

  _predObjsBlock(row: PredRow): TemplateResult {
    return html`
      <div class="predicate">
        ${this.nodeDisplay?.render(row.pred)}
        <div>${row.objs.map(this._objCell.bind(this))}</div>
      </div>
    `;
  }

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

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

declare global {
  interface HTMLElementTagNameMap {
    "graph-view": GraphView;
  }
}