UNPKG

graph-explorer

Version:

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

276 lines (240 loc) 7.39 kB
import { ElementTypeIri, LinkTypeIri, PropertyTypeIri } from "../data/model"; import { OrderedMap } from "../viewUtils/collections"; import { EventSource, Events, AnyEvent, AnyListener, } from "../viewUtils/events"; import { Element as DiagramElement, ElementEvents, Link as DiagramLink, LinkEvents, FatLinkType, FatLinkTypeEvents, FatClassModel, FatClassModelEvents, RichProperty, } from "./elements"; export interface GraphEvents { changeCells: CellsChangedEvent; elementEvent: AnyEvent<ElementEvents>; linkEvent: AnyEvent<LinkEvents>; linkTypeEvent: AnyEvent<FatLinkTypeEvents>; classEvent: AnyEvent<FatClassModelEvents>; } export interface CellsChangedEvent { readonly updateAll: boolean; readonly changedElement?: DiagramElement; readonly changedLinks?: readonly DiagramLink[]; } export class Graph { private readonly source = new EventSource<GraphEvents>(); readonly events: Events<GraphEvents> = this.source; private elements = new OrderedMap<DiagramElement>(); private links = new OrderedMap<DiagramLink>(); private classesById = new Map<ElementTypeIri, FatClassModel>(); private propertiesById = new Map<PropertyTypeIri, RichProperty>(); private linkTypes = new Map<LinkTypeIri, FatLinkType>(); private static nextLinkTypeIndex = 0; getElements() { return this.elements.items; } getLinks() { return this.links.items; } getLink(linkId: string): DiagramLink | undefined { return this.links.get(linkId); } findLink( linkTypeId: LinkTypeIri, sourceId: string, targetId: string ): DiagramLink | undefined { const source = this.getElement(sourceId); if (!source) { return undefined; } const index = findLinkIndex(source.links, linkTypeId, sourceId, targetId); return index >= 0 ? source.links[index] : undefined; } sourceOf(link: DiagramLink) { return this.getElement(link.sourceId); } targetOf(link: DiagramLink) { return this.getElement(link.targetId); } reorderElements(compare: (a: DiagramElement, b: DiagramElement) => number) { this.elements.reorder(compare); } getElement(elementId: string): DiagramElement | undefined { return this.elements.get(elementId); } addElement(element: DiagramElement): void { if (this.getElement(element.id)) { throw new Error(`Element '${element.id}' already exists.`); } element.events.onAny(this.onElementEvent); this.elements.push(element.id, element); this.source.trigger("changeCells", { updateAll: false, changedElement: element, }); } private onElementEvent: AnyListener<ElementEvents> = (data, key) => { this.source.trigger("elementEvent", { key, data }); }; removeElement(elementId: string): void { const element = this.elements.get(elementId); if (element) { const options = { silent: true }; // clone links to prevent modifications during iteration const changedLinks = [...element.links]; for (const link of changedLinks) { this.removeLink(link.id, options); } this.elements.delete(elementId); element.events.offAny(this.onElementEvent); this.source.trigger("changeCells", { updateAll: false, changedElement: element, changedLinks, }); } } addLink(link: DiagramLink): void { if (this.getLink(link.id)) { throw new Error(`Link '${link.id}' already exists.`); } const linkType = this.getLinkType(link.typeId); if (!linkType) { throw new Error(`Link type '${link.typeId}' not found.`); } this.registerLink(link); } private registerLink(link: DiagramLink) { this.sourceOf(link).links.push(link); if (link.sourceId !== link.targetId) { this.targetOf(link).links.push(link); } link.events.onAny(this.onLinkEvent); this.links.push(link.id, link); this.source.trigger("changeCells", { updateAll: false, changedLinks: [link], }); } private onLinkEvent: AnyListener<LinkEvents> = (data, key) => { this.source.trigger("linkEvent", { key, data }); }; removeLink(linkId: string, options?: { silent?: boolean }) { const link = this.links.delete(linkId); if (link) { const { typeId, sourceId, targetId } = link; link.events.offAny(this.onLinkEvent); this.removeLinkReferences(typeId, sourceId, targetId); if (!(options && options.silent)) { this.source.trigger("changeCells", { updateAll: false, changedLinks: [link], }); } } } private removeLinkReferences( linkTypeId: LinkTypeIri, sourceId: string, targetId: string ) { const source = this.getElement(sourceId); if (source) { removeLinkFrom(source.links, linkTypeId, sourceId, targetId); } const target = this.getElement(targetId); if (target) { removeLinkFrom(target.links, linkTypeId, sourceId, targetId); } } getLinkTypes(): FatLinkType[] { const result: FatLinkType[] = []; this.linkTypes.forEach((type) => result.push(type)); return result; } getLinkType(linkTypeId: LinkTypeIri): FatLinkType | undefined { return this.linkTypes.get(linkTypeId); } addLinkType(linkType: FatLinkType): void { if (this.getLinkType(linkType.id)) { throw new Error(`Link type '${linkType.id}' already exists.`); } linkType.setIndex(Graph.nextLinkTypeIndex++); linkType.events.onAny(this.onLinkTypeEvent); this.linkTypes.set(linkType.id, linkType); } private onLinkTypeEvent: AnyListener<FatLinkTypeEvents> = (data, key) => { this.source.trigger("linkTypeEvent", { key, data }); }; getProperty(propertyId: PropertyTypeIri): RichProperty | undefined { return this.propertiesById.get(propertyId); } addProperty(property: RichProperty): void { if (this.getProperty(property.id)) { throw new Error(`Property '${property.id}' already exists.`); } this.propertiesById.set(property.id, property); } getClass(classId: ElementTypeIri): FatClassModel | undefined { return this.classesById.get(classId); } getClasses(): FatClassModel[] { const classes: FatClassModel[] = []; this.classesById.forEach((richClass) => classes.push(richClass)); return classes; } addClass(classModel: FatClassModel): void { if (this.getClass(classModel.id)) { throw new Error(`Class '${classModel.id}' already exists.`); } classModel.events.onAny(this.onClassEvent); this.classesById.set(classModel.id, classModel); } private onClassEvent: AnyListener<FatClassModelEvents> = (data, key) => { this.source.trigger("classEvent", { key, data }); }; } function removeLinkFrom( links: DiagramLink[], linkTypeId: LinkTypeIri, sourceId: string, targetId: string ) { if (!links) { return; } while (true) { const index = findLinkIndex(links, linkTypeId, sourceId, targetId); if (index < 0) { break; } links.splice(index, 1); } } function findLinkIndex( haystack: DiagramLink[], linkTypeId: LinkTypeIri, sourceId: string, targetId: string ) { for (let i = 0; i < haystack.length; i++) { const link = haystack[i]; if ( link.sourceId === sourceId && link.targetId === targetId && link.typeId === linkTypeId ) { return i; } } return -1; }