graph-explorer
Version:
Graph Explorer can be used to explore and RDF graphs in SPARQL endpoints or on the web.
295 lines (254 loc) • 7.62 kB
text/typescript
import {
ElementModel,
ElementIri,
ElementTypeIri,
LinkTypeIri,
PropertyTypeIri,
} from "../data/model";
import { GenerateID } from "../data/schema";
import {
EventSource,
Events,
EventObserver,
AnyEvent,
} from "../viewUtils/events";
import {
Element,
ElementEvents,
Link,
LinkEvents,
FatLinkType,
FatLinkTypeEvents,
FatClassModel,
FatClassModelEvents,
RichProperty,
} from "./elements";
import { Graph, CellsChangedEvent } from "./graph";
import { CommandHistory, Command } from "./history";
export interface DiagramModelEvents {
changeCells: CellsChangedEvent;
elementEvent: AnyEvent<ElementEvents>;
linkEvent: AnyEvent<LinkEvents>;
linkTypeEvent: AnyEvent<FatLinkTypeEvents>;
classEvent: AnyEvent<FatClassModelEvents>;
changeGroupContent: { group: string };
}
/**
* Model of diagram.
*/
export class DiagramModel {
protected readonly source = new EventSource<DiagramModelEvents>();
readonly events: Events<DiagramModelEvents> = this.source;
protected graph = new Graph();
protected graphListener = new EventObserver();
constructor(readonly history: CommandHistory) {}
get elements() {
return this.graph.getElements();
}
get links() {
return this.graph.getLinks();
}
getElement(elementId: string): Element | undefined {
return this.graph.getElement(elementId);
}
getLinkById(linkId: string): Link | undefined {
return this.graph.getLink(linkId);
}
linksOfType(linkTypeId: LinkTypeIri): readonly Link[] {
return this.graph.getLinks().filter((link) => link.typeId === linkTypeId);
}
findLink(
linkTypeId: LinkTypeIri,
sourceId: string,
targetId: string
): Link | undefined {
return this.graph.findLink(linkTypeId, sourceId, targetId);
}
sourceOf(link: Link) {
return this.getElement(link.sourceId);
}
targetOf(link: Link) {
return this.getElement(link.targetId);
}
isSourceAndTargetVisible(link: Link): boolean {
return Boolean(this.sourceOf(link) && this.targetOf(link));
}
resetGraph() {
if (this.graphListener) {
this.graphListener.stopListening();
this.graphListener = new EventObserver();
}
this.graph = new Graph();
}
subscribeGraph() {
this.graphListener.listen(this.graph.events, "changeCells", (e) => {
this.source.trigger("changeCells", e);
});
this.graphListener.listen(this.graph.events, "elementEvent", (e) => {
this.source.trigger("elementEvent", e);
});
this.graphListener.listen(this.graph.events, "linkEvent", (e) => {
this.source.trigger("linkEvent", e);
});
this.graphListener.listen(this.graph.events, "linkTypeEvent", (e) => {
this.source.trigger("linkTypeEvent", e);
});
this.graphListener.listen(this.graph.events, "classEvent", (e) => {
this.source.trigger("classEvent", e);
});
this.source.trigger("changeCells", { updateAll: true });
}
reorderElements(compare: (a: Element, b: Element) => number) {
this.graph.reorderElements(compare);
}
createElement(
elementIriOrModel: ElementIri | ElementModel,
group?: string
): Element {
const elementIri =
typeof elementIriOrModel === "string"
? elementIriOrModel
: (elementIriOrModel as ElementModel).id;
const elements = this.elements.filter(
(el) => el.iri === elementIri && el.group === group
);
if (elements.length > 0) {
// usually there should be only one element
return elements[0];
}
let data =
typeof elementIriOrModel === "string"
? placeholderDataFromIri(elementIri)
: (elementIriOrModel as ElementModel);
data = { ...data, id: data.id };
const element = new Element({ id: GenerateID.forElement(), data, group });
this.addElement(element);
return element;
}
addElement(element: Element): void {
this.history.execute(addElement(this.graph, element, []));
}
removeElement(elementId: string) {
const element = this.getElement(elementId);
if (element) {
this.history.execute(removeElement(this.graph, element));
}
}
addLink(link: Link): Link {
const { typeId, sourceId, targetId, data } = link;
if (data && data.linkTypeId !== typeId) {
throw new Error("linkTypeId must match linkType.id");
}
const existingLink = this.findLink(typeId, sourceId, targetId);
if (existingLink) {
if (link.data) {
existingLink.setLayoutOnly(false);
existingLink.setData(data);
}
return existingLink;
}
const linkType = this.createLinkType(link.typeId);
const source = this.getElement(sourceId);
const target = this.getElement(targetId);
const shouldBeVisible = linkType.visible && source && target;
if (!shouldBeVisible) {
return undefined;
}
if (!link.data) {
link.setData({
linkTypeId: typeId,
sourceId: source.iri,
targetId: target.iri,
});
}
this.graph.addLink(link);
return link;
}
removeLink(linkId: string) {
this.graph.removeLink(linkId);
}
getClass(classIri: ElementTypeIri): FatClassModel {
return this.graph.getClass(classIri);
}
createClass(classIri: ElementTypeIri): FatClassModel {
const existing = this.graph.getClass(classIri);
if (existing) {
return existing;
}
const classModel = new FatClassModel({ id: classIri });
this.addClass(classModel);
return classModel;
}
addClass(model: FatClassModel) {
this.graph.addClass(model);
}
getLinkType(linkTypeIri: LinkTypeIri): FatLinkType | undefined {
return this.graph.getLinkType(linkTypeIri);
}
createLinkType(linkTypeIri: LinkTypeIri): FatLinkType {
const existing = this.graph.getLinkType(linkTypeIri);
if (existing) {
return existing;
}
const linkType = new FatLinkType({ id: linkTypeIri });
this.graph.addLinkType(linkType);
return linkType;
}
getProperty(propertyTypeIri: PropertyTypeIri): RichProperty {
return this.graph.getProperty(propertyTypeIri);
}
createProperty(propertyIri: PropertyTypeIri): RichProperty {
const existing = this.graph.getProperty(propertyIri);
if (existing) {
return existing;
}
const property = new RichProperty({ id: propertyIri });
this.graph.addProperty(property);
return property;
}
triggerChangeGroupContent(group: string) {
this.source.trigger("changeGroupContent", { group });
}
createTemporaryElement(): Element {
const target = new Element({
id: GenerateID.forElement(),
data: placeholderDataFromIri("" as ElementIri),
temporary: true,
});
this.graph.addElement(target);
return target;
}
}
export function placeholderDataFromIri(iri: ElementIri): ElementModel {
return {
id: iri,
types: [],
label: { values: [] },
properties: {},
};
}
function addElement(
graph: Graph,
element: Element,
connectedLinks: readonly Link[]
): Command {
return Command.create("Add element", () => {
graph.addElement(element);
for (const link of connectedLinks) {
const existing =
graph.getLink(link.id) ||
graph.findLink(link.typeId, link.sourceId, link.targetId);
if (!existing) {
graph.addLink(link);
}
}
return removeElement(graph, element);
});
}
function removeElement(graph: Graph, element: Element): Command {
return Command.create("Remove element", () => {
const connectedLinks = [...element.links];
graph.removeElement(element.id);
return addElement(graph, element, connectedLinks);
});
}