UNPKG

graph-explorer

Version:

Graph Explorer can be used to explore and RDF graphs in SPARQL endpoints or on the web.

442 lines (399 loc) 11.9 kB
import { Dictionary, ElementModel, LinkModel, LinkType, ElementIri, LinkTypeIri, ElementTypeIri, PropertyTypeIri, } from "../data/model"; import { DataProvider } from "../data/provider"; import { PLACEHOLDER_LINK_TYPE } from "../data/schema"; import { Element, FatLinkType, FatClassModel, RichProperty, FatLinkTypeEvents, Link, } from "../diagram/elements"; import { CommandHistory, Command } from "../diagram/history"; import { DiagramModel, DiagramModelEvents, placeholderDataFromIri, } from "../diagram/model"; import { EventSource, Events, Listener } from "../viewUtils/events"; import { DataFetcher } from "./dataFetcher"; import { LayoutData, makeLayoutData, emptyDiagram, LinkTypeOptions, SerializedDiagram, makeSerializedDiagram, emptyLayoutData, } from "./serializedDiagram"; export interface GroupBy { linkType: string; linkDirection: "in" | "out"; } export interface AsyncModelEvents extends DiagramModelEvents { loadingStart: { source: AsyncModel }; loadingSuccess: { source: AsyncModel }; loadingError: { source: AsyncModel; error: any; }; createLoadedLink: { source: AsyncModel; model: LinkModel; cancel(): void; }; } export class AsyncModel extends DiagramModel { readonly events: Events<AsyncModelEvents>; private _dataProvider: DataProvider; private fetcher: DataFetcher; private linkSettings: Record<string, LinkTypeOptions> = {}; constructor( history: CommandHistory, private groupByProperties: readonly GroupBy[] ) { super(history); } private get asyncSource(): EventSource<AsyncModelEvents> { return this.source as EventSource<any>; } get dataProvider() { return this._dataProvider; } subscribeGraph() { super.subscribeGraph(); this.graphListener.listen(this.graph.events, "linkTypeEvent", (e) => { if (e.key === "changeVisibility") { this.onLinkTypeVisibilityChanged(e.data[e.key], e.key); } }); } private setDataProvider(dataProvider: DataProvider) { this._dataProvider = dataProvider; this.fetcher = new DataFetcher(this.graph, dataProvider); } createNewDiagram(dataProvider: DataProvider): Promise<void> { this.resetGraph(); this.setDataProvider(dataProvider); this.asyncSource.trigger("loadingStart", { source: this }); return this.dataProvider .linkTypes() .then((linkTypes: LinkType[]) => { const allLinkTypes = this.initLinkTypes(linkTypes); return this.loadAndRenderLayout({ allLinkTypes, markLinksAsLayoutOnly: false, }); }) .catch((error) => { console.error(error); this.asyncSource.trigger("loadingError", { source: this, error }); return Promise.reject(error); }); } importLayout(params: { dataProvider: DataProvider; preloadedElements?: Dictionary<ElementModel>; validateLinks?: boolean; diagram?: SerializedDiagram; hideUnusedLinkTypes?: boolean; }): Promise<void> { this.resetGraph(); this.setDataProvider(params.dataProvider); this.asyncSource.trigger("loadingStart", { source: this }); return this.dataProvider .linkTypes() .then((linkTypes) => { const allLinkTypes = this.initLinkTypes(linkTypes); const diagram = params.diagram ? params.diagram : emptyDiagram(); this.setLinkSettings(diagram.linkTypeOptions); const loadingModels = this.loadAndRenderLayout({ layoutData: diagram.layoutData, preloadedElements: params.preloadedElements || {}, markLinksAsLayoutOnly: params.validateLinks || false, allLinkTypes, hideUnusedLinkTypes: params.hideUnusedLinkTypes, }); const requestingLinks = params.validateLinks ? this.requestLinksOfType() : Promise.resolve(); return Promise.all([loadingModels, requestingLinks]); }) .then(() => { this.asyncSource.trigger("loadingSuccess", { source: this }); }) .catch((error) => { console.error(error); this.asyncSource.trigger("loadingError", { source: this, error }); return Promise.reject(error); }); } exportLayout(): SerializedDiagram { const layoutData = makeLayoutData( this.graph.getElements(), this.graph.getLinks() ); const linkTypeOptions = this.graph .getLinkTypes() // do not serialize default link type options .filter( (linkType) => (!linkType.visible || !linkType.showLabel) && linkType.id !== PLACEHOLDER_LINK_TYPE ) .map( ({ id, visible, showLabel }): LinkTypeOptions => ({ "@type": "LinkTypeOptions", property: id, visible, showLabel, }) ); return makeSerializedDiagram({ layoutData, linkTypeOptions }); } private initLinkTypes(linkTypes: LinkType[]): FatLinkType[] { const types: FatLinkType[] = []; for (const { id, label } of linkTypes) { const linkType = new FatLinkType({ id, label: label.values }); this.graph.addLinkType(linkType); types.push(linkType); } return types; } private setLinkSettings(settings: readonly LinkTypeOptions[]) { if (!settings) { return; } for (const setting of settings) { const { visible = true, showLabel = true } = setting; const linkTypeId = setting.property as LinkTypeIri; this.linkSettings[linkTypeId] = { "@type": "LinkTypeOptions", property: linkTypeId, visible, showLabel, }; const linkType = this.getLinkType(linkTypeId); if (linkType) { linkType.setVisibility({ visible, showLabel }); } } } private loadAndRenderLayout(params: { layoutData?: LayoutData; preloadedElements?: Dictionary<ElementModel>; markLinksAsLayoutOnly: boolean; allLinkTypes: readonly FatLinkType[]; hideUnusedLinkTypes?: boolean; }) { const { layoutData = emptyLayoutData(), preloadedElements = {}, markLinksAsLayoutOnly, hideUnusedLinkTypes, } = params; const elementIrisToRequestData: ElementIri[] = []; const usedLinkTypes: Record<string, FatLinkType> = {}; for (const layoutElement of layoutData.elements) { const { "@id": id, iri, position, size, isExpanded, group, elementState, } = layoutElement; const template = preloadedElements[iri]; const data = template || placeholderDataFromIri(iri); const element = new Element({ id, data, position, size, expanded: isExpanded, group, elementState, }); this.graph.addElement(element); if (!template) { elementIrisToRequestData.push(element.iri); } } for (const layoutLink of layoutData.links) { const { "@id": id, property, source, target, vertices, linkState, } = layoutLink; const linkType = this.createLinkType(property); usedLinkTypes[linkType.id] = linkType; const link = this.addLink( new Link({ id, typeId: linkType.id, sourceId: source["@id"], targetId: target["@id"], vertices, linkState, }) ); if (link) { link.setLayoutOnly(markLinksAsLayoutOnly); } } this.subscribeGraph(); const requestingModels = this.requestElementData(elementIrisToRequestData); if (hideUnusedLinkTypes && params.allLinkTypes) { this.hideUnusedLinkTypes(params.allLinkTypes, usedLinkTypes); } return requestingModels; } private hideUnusedLinkTypes( allTypes: readonly FatLinkType[], usedTypes: Record<string, FatLinkType> ) { for (const linkType of allTypes) { if (!usedTypes[linkType.id]) { linkType.setVisibility({ visible: false, showLabel: linkType.showLabel, }); } } } requestElementData(elementIris: readonly ElementIri[]): Promise<void> { return this.fetcher.fetchElementData(elementIris); } requestLinksOfType(linkTypeIds?: LinkTypeIri[]): Promise<void> { const linkTypes = linkTypeIds || this.graph .getLinkTypes() .filter((type) => type.visible) .map((type) => type.id); return this.dataProvider .linksInfo({ elementIds: this.graph.getElements().map((element) => element.iri), linkTypeIds: linkTypes, }) .then((links) => this.onLinkInfoLoaded(links)); } createClass(classId: ElementTypeIri): FatClassModel { if (this.graph.getClass(classId)) { return super.getClass(classId); } const classModel = super.createClass(classId); this.fetcher.fetchClass(classModel); return classModel; } createLinkType(linkTypeId: LinkTypeIri): FatLinkType { if (this.graph.getLinkType(linkTypeId)) { return super.createLinkType(linkTypeId); } const linkType = super.createLinkType(linkTypeId); const setting = this.linkSettings[linkType.id]; if (setting) { const { visible, showLabel } = setting; linkType.setVisibility({ visible, showLabel, preventLoading: true }); } this.fetcher.fetchLinkType(linkType); return linkType; } createProperty(propertyIri: PropertyTypeIri): RichProperty { if (this.graph.getProperty(propertyIri)) { return super.createProperty(propertyIri); } const property = super.createProperty(propertyIri); this.fetcher.fetchPropertyType(property); return property; } private onLinkTypeVisibilityChanged: Listener< FatLinkTypeEvents, "changeVisibility" > = (e) => { if (e.source.visible) { if (!e.preventLoading) { this.requestLinksOfType([e.source.id]); } } else { for (const link of this.linksOfType(e.source.id)) { this.graph.removeLink(link.id); } } }; private onLinkInfoLoaded(links: LinkModel[]) { let allowToCreate: boolean; const cancel = () => { allowToCreate = false; }; for (const linkModel of links) { this.createLinkType(linkModel.linkTypeId); allowToCreate = true; this.asyncSource.trigger("createLoadedLink", { source: this, model: linkModel, cancel, }); if (allowToCreate) { this.createLinks(linkModel); } } } createLinks(data: LinkModel) { const sources = this.graph .getElements() .filter((el) => el.iri === data.sourceId); const targets = this.graph .getElements() .filter((el) => el.iri === data.targetId); const typeId = data.linkTypeId; for (const source of sources) { for (const target of targets) { this.addLink( new Link({ typeId, sourceId: source.id, targetId: target.id, data }) ); } } } loadEmbeddedElements( elementIri: ElementIri ): Promise<Dictionary<ElementModel>> { const elements = this.groupByProperties.map((groupBy) => this.dataProvider.linkElements({ elementId: elementIri, linkId: groupBy.linkType as LinkTypeIri, offset: 0, direction: groupBy.linkDirection, }) ); return Promise.all(elements).then((res) => res.reduce((memo, current) => Object.assign(memo, current), {}) ); } } export function requestElementData( model: AsyncModel, elementIris: readonly ElementIri[] ): Command { return Command.effect("Fetch element data", () => { model.requestElementData(elementIris); }); } export function restoreLinksBetweenElements(model: AsyncModel): Command { return Command.effect("Restore links between elements", () => { model.requestLinksOfType(); }); }