UNPKG

graph-explorer

Version:

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

291 lines (261 loc) 8.21 kB
import { ElementModel, LinkModel, ElementIri, sameLink, hashLink, } from "../data/model"; import { HashMap, ReadonlyHashMap, cloneMap } from "../viewUtils/collections"; export interface AuthoringState { readonly elements: ReadonlyMap<ElementIri, ElementChange>; readonly links: ReadonlyHashMap<LinkModel, LinkChange>; } export type AuthoringEvent = ElementChange | LinkChange; export enum AuthoringKind { ChangeElement = "changeElement", ChangeLink = "changeLink", } export interface ElementChange { readonly type: AuthoringKind.ChangeElement; readonly before?: ElementModel; readonly after: ElementModel; readonly newIri?: ElementIri; readonly deleted: boolean; } export interface LinkChange { readonly type: AuthoringKind.ChangeLink; readonly before?: LinkModel; readonly after: LinkModel; readonly deleted: boolean; } interface MutableAuthoringState extends AuthoringState { readonly elements: Map<ElementIri, ElementChange>; readonly links: HashMap<LinkModel, LinkChange>; } export const AuthoringState = { empty: { elements: new Map<ElementIri, ElementChange>(), links: new HashMap<LinkModel, LinkChange>(hashLink, sameLink), } as AuthoringState, isEmpty(state: AuthoringState) { return state.elements.size === 0 && state.links.size === 0; }, clone(index: AuthoringState): MutableAuthoringState { return { elements: cloneMap(index.elements), links: index.links.clone(), }; }, has(state: AuthoringState, event: AuthoringEvent): boolean { return event.type === AuthoringKind.ChangeElement ? state.elements.get(event.after.id) === event : state.links.get(event.after) === event; }, discard(state: AuthoringState, discarded: AuthoringEvent): AuthoringState { if (!this.has(state, discarded)) { return state; } const newState = this.clone(state); if (discarded.type === AuthoringKind.ChangeElement) { newState.elements.delete(discarded.after.id); if (!discarded.before) { state.links.forEach((e) => { if (isLinkConnectedToElement(e.after, discarded.after.id)) { newState.links.delete(e.after); } }); } } else { newState.links.delete(discarded.after); } return newState; }, addElement(state: AuthoringState, item: ElementModel): AuthoringState { const event: ElementChange = { type: AuthoringKind.ChangeElement, after: item, deleted: false, }; const newState = this.clone(state); newState.elements.set(event.after.id, event); return newState; }, addLink(state: AuthoringState, item: LinkModel): AuthoringState { const event: LinkChange = { type: AuthoringKind.ChangeLink, after: item, deleted: false, }; const newState = this.clone(state); newState.links.set(event.after, event); return newState; }, changeElement( state: AuthoringState, before: ElementModel, after: ElementModel ): AuthoringState { const newState = this.clone(state); // delete previous state for an entity newState.elements.delete(before.id); const previous = state.elements.get(before.id); if (previous && !previous.before) { // adding or changing new entity newState.elements.set(after.id, { type: AuthoringKind.ChangeElement, after, deleted: false, }); if (before.id !== after.id) { state.links.forEach((e) => { if (!e.before && isLinkConnectedToElement(e.after, before.id)) { const updatedLink = updateLinkToReferByNewIri( e.after, before.id, after.id ); newState.links.delete(e.after); newState.links.set(updatedLink, { type: AuthoringKind.ChangeLink, after: updatedLink, deleted: false, }); } }); } } else { // changing existing entity const iriChanged = after.id !== before.id; const previousBefore = previous ? previous.before : undefined; newState.elements.set(before.id, { type: AuthoringKind.ChangeElement, // always initialize 'before', otherwise entity will be considered new before: previousBefore || before, after: iriChanged ? { ...after, id: before.id } : after, newIri: iriChanged ? after.id : undefined, deleted: false, }); } return newState; }, changeLink( state: AuthoringState, before: LinkModel, after: LinkModel ): AuthoringState { if (!sameLink(before, after)) { throw new Error("Cannot move link to another element or change its type"); } const newState = this.clone(state); const previous = state.links.get(before); newState.links.set(before, { type: AuthoringKind.ChangeLink, before: previous ? previous.before : undefined, after: after, deleted: false, }); return newState; }, deleteElement(state: AuthoringState, model: ElementModel): AuthoringState { const newState = this.clone(state); newState.elements.delete(model.id); state.links.forEach((e) => { if (isLinkConnectedToElement(e.after, model.id)) { newState.links.delete(e.after); } }); if (!this.isNewElement(state, model.id)) { newState.elements.set(model.id, { type: AuthoringKind.ChangeElement, before: model, after: model, deleted: true, }); } return newState; }, deleteLink(state: AuthoringState, target: LinkModel): AuthoringState { const newState = this.clone(state); newState.links.delete(target); if (!this.isNewLink(state, target)) { newState.links.set(target, { type: AuthoringKind.ChangeLink, before: target, after: target, deleted: true, }); } return newState; }, deleteNewLinksConnectedToElements( state: AuthoringState, elementIris: Set<ElementIri> ): AuthoringState { const newState = this.clone(state); state.links.forEach((e) => { if (!e.before) { const target = e.after; if ( elementIris.has(target.sourceId) || elementIris.has(target.targetId) ) { newState.links.delete(target); } } }); return newState; }, isNewElement(state: AuthoringState, target: ElementIri): boolean { const event = state.elements.get(target); return event && event.type === AuthoringKind.ChangeElement && !event.before; }, isDeletedElement(state: AuthoringState, target: ElementIri): boolean { const event = state.elements.get(target); return event && event.deleted; }, isElementWithModifiedIri(state: AuthoringState, target: ElementIri): boolean { const event = state.elements.get(target); return ( event && event.type === AuthoringKind.ChangeElement && event.before && Boolean(event.newIri) ); }, isNewLink(state: AuthoringState, linkModel: LinkModel): boolean { const event = state.links.get(linkModel); return event && !event.before; }, isDeletedLink(state: AuthoringState, linkModel: LinkModel): boolean { const event = state.links.get(linkModel); return ( (event && event.deleted) || this.isDeletedElement(state, linkModel.sourceId) || this.isDeletedElement(state, linkModel.targetId) ); }, isUncertainLink(state: AuthoringState, linkModel: LinkModel): boolean { return ( !this.isDeletedLink(state, linkModel) && (this.isElementWithModifiedIri(state, linkModel.sourceId) || this.isElementWithModifiedIri(state, linkModel.targetId)) ); }, }; export function isLinkConnectedToElement( link: LinkModel, elementIri: ElementIri ) { return link.sourceId === elementIri || link.targetId === elementIri; } function updateLinkToReferByNewIri( link: LinkModel, oldIri: ElementIri, newIri: ElementIri ): LinkModel { return { ...link, sourceId: link.sourceId === oldIri ? newIri : link.sourceId, targetId: link.targetId === oldIri ? newIri : link.targetId, }; }