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
text/typescript
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();
});
}