import Map from "../map"; import Node, {Colors, Coordinates, ExportNodeProperties, Font, Image, NodeProperties} from "../models/node"; import {Event} from "./events"; import Log from "../../utils/log"; import Utils from "../../utils/utils"; /** * Manage map history, for each change save a snapshot. */ export default class History { private map: Map; private index: number; private snapshots: MapSnapshot[]; /** * Get the associated map instance, initialize index and snapshots. * @param {Map} map */ constructor(map: Map) { this.map = map; this.index = -1; this.snapshots = []; } /** * Return last snapshot of the current map. * @return {MapSnapshot} [snapshot] - Last snapshot of the map. */ public current = (): MapSnapshot => { return this.snapshots[this.index]; }; /** * Replace old map with a new one or create a new empty map. * @param {MapSnapshot} snapshot */ public new = (snapshot?: MapSnapshot) => { if (snapshot === undefined) { let oldRootCoordinates = Utils.cloneObject(this.map.nodes.getRoot().coordinates); this.map.nodes.setCounter(0); this.map.nodes.clear(); this.map.draw.clear(); this.map.draw.update(); this.map.nodes.addRootNode(oldRootCoordinates); this.map.zoom.center(null, 0); this.save(); this.map.events.call(Event.create); } else if (this.checkSnapshotStructure(snapshot)) { this.redraw(snapshot); this.map.zoom.center("position", 0); this.save(); this.map.events.call(Event.create); } else { Log.error("The snapshot is not correct"); } }; /** * Undo last changes. */ public undo = () => { if (this.index > 0) { this.redraw(this.snapshots[--this.index]); this.map.events.call(Event.undo); } }; /** * Redo one change which was undone. */ public redo = () => { if (this.index < this.snapshots.length - 1) { this.redraw(this.snapshots[++this.index]); this.map.events.call(Event.redo); } }; /** * Save the current snapshot of the mind map. */ public save() { if (this.index < this.snapshots.length - 1) { this.snapshots.splice(this.index + 1); } this.snapshots.push(this.getSnapshot()); this.index++; } /** * Return all history of map with all snapshots. * @returns {MapSnapshot[]} */ public getHistory = (): ExportHistory => { return { snapshots: this.snapshots.slice(0), index: this.index }; }; /** * Redraw the map with a new snapshot. * @param {MapSnapshot} snapshot */ private redraw(snapshot: MapSnapshot) { this.map.nodes.clear(); snapshot.forEach((property: ExportNodeProperties) => { let properties: NodeProperties = { id: this.sanitizeNodeId(property.id), parent: this.map.nodes.getNode(this.sanitizeNodeId(property.parent)), k: property.k, name: property.name, coordinates: Utils.cloneObject(property.coordinates) as Coordinates, image: Utils.cloneObject(property.image) as Image, colors: Utils.cloneObject(property.colors) as Colors, font: Utils.cloneObject(property.font) as Font, locked: property.locked }; let node: Node = new Node(properties); this.map.nodes.setNode(node.id, node); }); this.map.draw.clear(); this.map.draw.update(); this.map.nodes.selectRootNode(); this.setCounter(); } /** * Return a copy of all fundamental node properties. * @return {MapSnapshot} properties */ private getSnapshot(): MapSnapshot { return this.map.nodes.getNodes().map((node: Node) => { return this.map.nodes.getNodeProperties(node, false); }).slice(); } /** * Set the right counter value of the nodes. */ private setCounter() { let id = this.map.nodes.getNodes().map((node: Node) => { let words = node.id.split("_"); return parseInt(words[words.length - 1]) }); this.map.nodes.setCounter(Math.max(...id) + 1); } /** * Sanitize an old map node id with a new. * @param {string} oldId * @returns {string} newId */ private sanitizeNodeId(oldId: string) { if (typeof oldId === "string") { let words = oldId.split("_"); return this.map.id + "_" + words[words.length - 2] + "_" + words[words.length - 1]; } } /** * Check the snapshot structure and return true if it is authentic. * @param {MapSnapshot} snapshot * @return {boolean} result */ private checkSnapshotStructure(snapshot: MapSnapshot): boolean { if (!Array.isArray(snapshot)) { return false; } if (((snapshot[0]).key && (snapshot[0]).value)) { this.convertOldMmp(snapshot); } for (let node of snapshot) { if (!this.checkNodeProperties(node)) { return false; } } this.translateNodePositions(snapshot); return true; } /** * Check the snapshot node properties and return true if they are authentic. * @param {ExportNodeProperties} node * @return {boolean} result */ private checkNodeProperties(node: ExportNodeProperties) { let conditions: boolean[] = [ typeof node.id === "string", typeof node.parent === "string", typeof node.k === "number", typeof node.name === "string", typeof node.locked === "boolean", node.coordinates && typeof node.coordinates.x === "number" && typeof node.coordinates.y === "number", node.image && typeof node.image.size === "number" && typeof node.image.src === "string", node.colors && typeof node.colors.background === "string" && typeof node.colors.branch === "string" && typeof node.colors.name === "string", node.font && typeof node.font.size === "number" && typeof node.font.weight === "string" && typeof node.font.style === "string" ]; return conditions.every(condition => condition); } /** * Convert the old mmp (version: 0.1.7) snapshot to new. * @param {Array} snapshot */ private convertOldMmp(snapshot: Array) { for (let node of snapshot) { let oldNode = Utils.cloneObject(node); Utils.clearObject(node); node.id = "map_node_" + oldNode.key.substr(4); node.parent = oldNode.value.parent ? "map_node_" + oldNode.value.parent.substr(4) : ""; node.k = oldNode.value.k; node.name = oldNode.value.name; node.locked = oldNode.value.fixed; node.coordinates = { x: oldNode.value.x, y: oldNode.value.y }; node.image = { size: parseInt(oldNode.value["image-size"]), src: oldNode.value["image-src"] }; node.colors = { background: oldNode.value["background-color"], branch: oldNode.value["branch-color"] || "", name: oldNode.value["text-color"] }; node.font = { size: parseInt(oldNode.value["font-size"]), weight: oldNode.value.bold ? "bold" : "normal", style: oldNode.value.italic ? "italic" : "normal" }; } } /** * Adapt the coordinates to the old map. * @param {MapSnapshot} snapshot */ private translateNodePositions(snapshot: MapSnapshot) { let oldRootNode = this.map.nodes.getRoot(), newRootNode = (snapshot).find((node: ExportNodeProperties) => { let words = node.id.split("_"); return words[words.length - 1] === "0"; }), dx = newRootNode.coordinates.x - oldRootNode.coordinates.x, dy = newRootNode.coordinates.y - oldRootNode.coordinates.y; for (let node of snapshot) { node.coordinates.x -= dx; node.coordinates.y -= dy; } } } export interface ExportHistory { snapshots: Array, index: number } export interface MapSnapshot extends Array { }