import xmlFormatter from 'xml-formatter'; import Importer from './Importer'; import Mindmap from '../model/Mindmap'; import RelationshipModel from '../model/RelationshipModel'; import NodeModel from '../model/NodeModel'; import { TopicShape } from '../model/INodeModel'; import LinkModel from '../model/LinkModel'; import IconModel from '../model/IconModel'; import FreemindConstant from '../export/freemind/FreemindConstant'; import FreemindMap from '../export/freemind/Map'; import FreemindNode, { Choise } from '../export/freemind/Node'; import FreemindFont from '../export/freemind/Font'; import FreemindEdge from '../export/freemind/Edge'; import FreemindIcon from '../export/freemind/Icon'; import FreemindHook from '../export/freemind/Hook'; import FreemindRichcontent from '../export/freemind/Richcontent'; import FreemindArrowLink from '../export/freemind/Arrowlink'; import VersionNumber from '../export/freemind/importer/VersionNumber'; import FreemindIconConverter from './FreemindIconConverter'; import NoteModel from '../model/NoteModel'; import FeatureModelFactory from '../model/FeatureModelFactory'; import FeatureModel from '../model/FeatureModel'; import XMLSerializerFactory from '../persistence/XMLSerializerFactory'; export default class FreemindImporter extends Importer { private mindmap: Mindmap; private freemindInput: string; private freemindMap: FreemindMap; private nodesmap: Map; private relationship: Array; constructor(map: string) { super(); this.freemindInput = map; } import(nameMap: string, description: string): Promise { this.mindmap = new Mindmap(nameMap); this.nodesmap = new Map(); this.relationship = new Array(); let wiseTopicId = 0; const parser = new DOMParser(); const freemindDoc = parser.parseFromString(this.freemindInput, 'application/xml'); this.freemindMap = new FreemindMap().loadFromDom(freemindDoc); const version: string = this.freemindMap.getVersion(); if (!version || version.startsWith('freeplane')) { throw new Error('You seems to be be trying to import a Freeplane map. FreePlane is not supported format.'); } else { const mapVersion: VersionNumber = new VersionNumber(version); if (mapVersion.isGreaterThan(FreemindConstant.SUPPORTED_FREEMIND_VERSION)) { throw new Error(`FreeMind version ${mapVersion.getVersion()} is not supported.`); } } const freeNode: FreemindNode = this.freemindMap.getNode(); this.mindmap.setVersion(FreemindConstant.CODE_VERSION); wiseTopicId++; const wiseTopic = this.mindmap.createNode('CentralTopic'); wiseTopic.setPosition(0, 0); wiseTopic.setId(wiseTopicId); this.convertNodeProperties(freeNode, wiseTopic, true); this.nodesmap.set(freeNode.getId(), wiseTopic); this.convertChildNodes(freeNode, wiseTopic, this.mindmap, 1); this.addRelationship(this.mindmap); this.mindmap.setDescription(description); this.mindmap.addBranch(wiseTopic); const serialize = XMLSerializerFactory.createInstanceFromMindmap(this.mindmap); const domMindmap = serialize.toXML(this.mindmap); const xmlToString = new XMLSerializer().serializeToString(domMindmap); const formatXml = xmlFormatter(xmlToString, { indentation: ' ', collapseContent: true, lineSeparator: '\n', }); return Promise.resolve(formatXml); } private addRelationship(mindmap: Mindmap): void { const mapRelaitonship: Array = mindmap.getRelationships(); mapRelaitonship.forEach((relationship: RelationshipModel) => { this.fixRelationshipControlPoints(relationship); // Fix dest ID const destId: string = relationship.getDestCtrlPoint(); const destTopic: NodeModel = this.nodesmap.get(destId); relationship.setDestCtrlPoint(destTopic.getId()); // Fix src ID const srcId: string = relationship.getSrcCtrlPoint(); const srcTopic: NodeModel = this.nodesmap.get(srcId); relationship.setSrcCtrlPoint(srcTopic.getId()); mapRelaitonship.push(relationship); }); } private fixRelationshipControlPoints(relationship: RelationshipModel): void { const srcTopic: NodeModel = this.nodesmap.get(relationship.getToNode().toString()); const destNode: NodeModel = this.nodesmap.get(relationship.getFromNode().toString()); // Fix x coord const srcCtrlPoint: string = relationship.getSrcCtrlPoint(); if (srcCtrlPoint) { const coords = srcTopic.getPosition(); if (coords.x < 0) { const x = coords.x * -1; relationship.setSrcCtrlPoint(`${x},${coords.y}`); // Fix coord if (srcTopic.getOrder() && srcTopic.getOrder() % 2 !== 0) { const y = coords.y * -1; relationship.setSrcCtrlPoint(`${coords.x},${y}`); } } } const destCtrlPoint: string = relationship.getDestCtrlPoint(); if (destCtrlPoint) { const coords = destNode.getPosition(); if (coords.x < 0) { const x = coords.x * -1; relationship.setDestCtrlPoint(`${x},${coords.y}`); } if (destNode.getOrder() && destNode.getOrder() % 2 !== 0) { const y = coords.y * -1; relationship.setDestCtrlPoint(`${coords.x},${y}`); } } } private convertNodeProperties(freeNode: FreemindNode, wiseTopic: NodeModel, centralTopic: boolean): void { const text: string = freeNode.getText(); if (text) wiseTopic.setText(text); const bgColor: string = freeNode.getBackgorundColor(); if (bgColor) wiseTopic.setBackgroundColor(bgColor); if (centralTopic === false) { const shape = this.getShapeFromFreeNode(freeNode); if (shape && shape !== 'fork') wiseTopic.setShapeType(shape); } // Check for style... const fontStyle = this.generateFontStyle(freeNode, null); if (fontStyle && fontStyle !== ';;;;') wiseTopic.setFontStyle(fontStyle); // Is there any link... const url: string = freeNode.getLink(); if (url) { const link: LinkModel = new LinkModel({ url }); wiseTopic.addFeature(link); } const folded = Boolean(freeNode.getFolded()); if (folded) wiseTopic.setChildrenShrunken(folded); } private convertChildNodes(freeParent: FreemindNode, wiseParent: NodeModel, mindmap: Mindmap, depth: number): void { const freeChilden = freeParent.getArrowlinkOrCloudOrEdge(); let currentWiseTopic: NodeModel = wiseParent; let order = 0; let firstLevelRightOrder = 0; let firstLevelLeftOrder = 1; freeChilden.forEach((child) => { if (child instanceof FreemindNode) { const wiseId = parseInt(child.getId().split('_')[1], 10); const wiseChild = mindmap.createNode('MainTopic', wiseId); this.nodesmap.set(child.getId(), wiseChild); let norder: number; if (depth !== 1) { norder = order++; } else if (child.getPosition() && child.getPosition() === FreemindConstant.POSITION_LEFT) { norder = firstLevelLeftOrder; firstLevelLeftOrder += 2; } else { norder = firstLevelRightOrder; firstLevelRightOrder += 2; } wiseChild.setOrder(norder); // Convert node position... const childrenCountSameSide = this.getChildrenCountSameSide(freeChilden, child); const position: {x: number, y: number} = this.convertPosition(wiseParent, child, depth, norder, childrenCountSameSide); wiseChild.setPosition(position.x, position.y); // Convert the rest of the node properties... this.convertNodeProperties(child, wiseChild, false); this.convertChildNodes(child, wiseChild, mindmap, depth++); if (wiseChild !== wiseParent) { wiseParent.append(wiseChild); } currentWiseTopic = wiseChild; } if (child instanceof FreemindFont) { const font: FreemindFont = child as FreemindFont; const fontStyle: string = this.generateFontStyle(freeParent, font); if (fontStyle) { currentWiseTopic.setFontStyle(fontStyle); } } if (child instanceof FreemindEdge) { const edge: FreemindEdge = child as FreemindEdge; currentWiseTopic.setBackgroundColor(edge.getColor()); } if (child instanceof FreemindIcon) { const freeIcon: FreemindIcon = child as FreemindIcon; const iconId: string = freeIcon.getBuiltin(); const wiseIconId = FreemindIconConverter.toWiseId(iconId); if (wiseIconId) { const mindmapIcon: IconModel = new IconModel({ id: wiseIconId }); currentWiseTopic.addFeature(mindmapIcon); } } if (child instanceof FreemindHook) { const hook: FreemindHook = child as FreemindHook; const mindmapNote: NoteModel = new NoteModel({ text: '' }); let textNote: string = hook.getText(); if (!textNote) { textNote = FreemindConstant.EMPTY_NOTE; mindmapNote.setText(textNote); currentWiseTopic.addFeature(mindmapNote); } } if (child instanceof FreemindRichcontent) { const type = child.getType(); if (type === 'NOTE') { // Formating text const text = this.html2Text(child.getHtml()); const noteModel: FeatureModel = FeatureModelFactory.createModel('note', { text: text || FreemindConstant.EMPTY_NOTE }); noteModel.setId(2); currentWiseTopic.addFeature(noteModel); } } if (child instanceof FreemindArrowLink) { const arrow: FreemindArrowLink = child as FreemindArrowLink; const relationship: RelationshipModel = new RelationshipModel(0, 0); const destId: string = arrow.getDestination(); relationship.setSrcCtrlPoint(destId); relationship.setDestCtrlPoint(freeParent.getId()); const endinclination: string = arrow.getEndInclination(); if (endinclination) { const inclination: Array = endinclination.split(';'); relationship.setDestCtrlPoint(`${inclination[0]},${inclination[1]}`); } const startinclination: string = arrow.getStartinclination(); if (startinclination) { const inclination: Array = startinclination.split(';'); relationship.setSrcCtrlPoint(`${inclination[0]},${inclination[1]}`); } const endarrow: string = arrow.getEndarrow(); if (endarrow) { relationship.setEndArrow(endarrow.toLowerCase() !== 'none'); } const startarrow: string = arrow.getStartarrow(); if (startarrow) { relationship.setStartArrow(startarrow.toLowerCase() !== 'none'); } relationship.setLineType(3); this.relationship.push(relationship); } }); } private getChildrenCountSameSide(freeChilden: Array, freeChild: FreemindNode): number { let result = 0; let childSide: string = freeChild.getPosition(); if (!childSide) { childSide = FreemindConstant.POSITION_RIGHT; } freeChilden.forEach((child) => { if (child instanceof FreemindNode) { let side = child.getPosition(); if (!side) { side = FreemindConstant.POSITION_RIGHT; } if (childSide === side) { result++; } } }); return result; } private getShapeFromFreeNode(node: FreemindNode): string { let result: string = node.getStyle(); if (result === 'bubble') { result = TopicShape.ROUNDED_RECT; } else if (node.getBackgorundColor()) { result = TopicShape.RECTANGLE; } else { result = TopicShape.LINE; } return result; } private generateFontStyle(node: FreemindNode, font: FreemindFont | undefined): string { const fontStyle: Array = []; // Font family if (font) { fontStyle.push(font.getName()); } fontStyle.push(';'); // Font Size if (font) { const fontSize: number = ((!font.getSize() || parseInt(font.getSize(), 10) < 8) ? FreemindConstant.FONT_SIZE_NORMAL : parseInt(font.getSize(), 10)); let wiseFontSize: number = FreemindConstant.FONT_SIZE_SMALL; if (fontSize >= 24) { wiseFontSize = FreemindConstant.FONT_SIZE_HUGE; } if (fontSize >= 16) { wiseFontSize = FreemindConstant.FONT_SIZE_LARGE; } if (fontSize >= 8) { wiseFontSize = FreemindConstant.FONT_SIZE_NORMAL; } fontStyle.push(wiseFontSize.toString()); } fontStyle.push(';'); // Font Color const color: string = node.getColor(); if (color && color !== '') { fontStyle.push(color); } fontStyle.push(';'); // Font Italic if (font) { const hasItalic = Boolean(font.getItalic()); fontStyle.push(hasItalic ? FreemindConstant.ITALIC : ''); } fontStyle.push(';'); const result: string = fontStyle.join(''); return result; } private convertPosition(wiseParent: NodeModel, freeChild: FreemindNode, depth: number, order: number, childrenCount: number): {x: number, y: number} { let x: number = FreemindConstant.CENTRAL_TO_TOPIC_DISTANCE + ((depth - 1) * FreemindConstant.TOPIC_TO_TOPIC_DISTANCE); if (depth === 1) { const side: string = freeChild.getPosition(); x *= (side && FreemindConstant.POSITION_LEFT === side ? -1 : 1); } else { const position = wiseParent.getPosition(); x *= position.x < 0 ? 1 : -1; } let y: number; if (depth === 1) { if (order % 2 === 0) { const multiplier = ((order + 1) - childrenCount) * 2; y = multiplier * FreemindConstant.ROOT_LEVEL_TOPIC_HEIGHT; } else { const multiplier = (order - childrenCount) * 2; y = multiplier * FreemindConstant.ROOT_LEVEL_TOPIC_HEIGHT; } } else { const position = wiseParent.getPosition(); y = Math.round(position.y - ((childrenCount / 2) * FreemindConstant.SECOND_LEVEL_TOPIC_HEIGHT - (order * FreemindConstant.SECOND_LEVEL_TOPIC_HEIGHT))); } return { x, y, }; } private html2Text(content: string): string { const contentConvert = content.replace(/(<([^>]+)>)/gi, ''); return contentConvert.trim(); } }