mirror of
https://bitbucket.org/wisemapping/wisemapping-frontend.git
synced 2024-11-25 23:54:55 +01:00
Added class freemind import
This commit is contained in:
parent
0fcbf6d111
commit
a2d655e0b9
@ -26,7 +26,7 @@
|
|||||||
"lint": "eslint src --ext js,ts",
|
"lint": "eslint src --ext js,ts",
|
||||||
"playground": "webpack serve --config webpack.playground.js",
|
"playground": "webpack serve --config webpack.playground.js",
|
||||||
"cy:run": "cypress run",
|
"cy:run": "cypress run",
|
||||||
"test:unit": "jest ./test/unit/export/*.ts ./test/unit/layout/*.js",
|
"test:unit": "jest ./test/unit/export/*.ts ./test/unit/import/*.ts ./test/unit/layout/*.js",
|
||||||
"test:integration": "start-server-and-test playground http-get://localhost:8083 cy:run",
|
"test:integration": "start-server-and-test playground http-get://localhost:8083 cy:run",
|
||||||
"test": "yarn test:unit && yarn test:integration"
|
"test": "yarn test:unit && yarn test:integration"
|
||||||
},
|
},
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { createDocument } from '@wisemapping/core-js';
|
import { createDocument, $assert } from '@wisemapping/core-js';
|
||||||
import Arrowlink from './Arrowlink';
|
import Arrowlink from './Arrowlink';
|
||||||
import Cloud from './Cloud';
|
import Cloud from './Cloud';
|
||||||
import Edge from './Edge';
|
import Edge from './Edge';
|
||||||
@ -7,7 +7,7 @@ import Icon from './Icon';
|
|||||||
import Node, { Choise } from './Node';
|
import Node, { Choise } from './Node';
|
||||||
import Richcontent from './Richcontent';
|
import Richcontent from './Richcontent';
|
||||||
|
|
||||||
export default class Map {
|
export default class Freemap {
|
||||||
protected node: Node;
|
protected node: Node;
|
||||||
|
|
||||||
protected version: string;
|
protected version: string;
|
||||||
@ -52,6 +52,115 @@ export default class Map {
|
|||||||
return document;
|
return document;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadFromDom(dom: Document): Freemap {
|
||||||
|
$assert(dom, 'dom can not be null');
|
||||||
|
|
||||||
|
const rootElem = dom.documentElement;
|
||||||
|
|
||||||
|
// Is a freemap?
|
||||||
|
$assert(
|
||||||
|
rootElem.tagName === 'map',
|
||||||
|
`This seem not to be a map document. Found tag: ${rootElem.tagName}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Start the loading process...
|
||||||
|
const version = rootElem.getAttribute('version') || '1.0.1';
|
||||||
|
const freemap: Freemap = new Freemap();
|
||||||
|
freemap.setVesion(version);
|
||||||
|
|
||||||
|
const mainTopicElement = rootElem.firstElementChild;
|
||||||
|
const mainTopic: Node = this.domToNode(mainTopicElement) as Node;
|
||||||
|
freemap.setNode(mainTopic);
|
||||||
|
|
||||||
|
// Add all the topics nodes...
|
||||||
|
const childNodes = Array.from(mainTopicElement.childNodes);
|
||||||
|
const topicsNodes = childNodes
|
||||||
|
.filter(
|
||||||
|
(child: ChildNode) => child.nodeType === 1 && (child as Element).tagName === 'node',
|
||||||
|
)
|
||||||
|
.map((c) => c as Element);
|
||||||
|
|
||||||
|
topicsNodes.forEach((child) => {
|
||||||
|
const childNode = this.domToNode(child);
|
||||||
|
mainTopic.setArrowlinkOrCloudOrEdge(childNode);
|
||||||
|
});
|
||||||
|
|
||||||
|
return freemap;
|
||||||
|
}
|
||||||
|
|
||||||
|
private domToNode(nodeElem: Element): Choise {
|
||||||
|
if (nodeElem.tagName === 'node') {
|
||||||
|
const node: Node = new Node().loadFromElement(nodeElem);
|
||||||
|
|
||||||
|
if (nodeElem.childNodes.length > 0) {
|
||||||
|
const childElement = Array.from(nodeElem.childNodes)
|
||||||
|
.filter(
|
||||||
|
(child: ChildNode) => child.nodeType === 1 && (child as Element).tagName === 'node',
|
||||||
|
)
|
||||||
|
.map((c) => c as Element);
|
||||||
|
|
||||||
|
childElement.forEach((child) => {
|
||||||
|
const childNode = new Node().loadFromElement(child);
|
||||||
|
node.setArrowlinkOrCloudOrEdge(childNode);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodeElem.tagName === 'font') {
|
||||||
|
const font: Font = new Font();
|
||||||
|
if (nodeElem.getAttribute('NAME')) font.setName(nodeElem.getAttribute('NAME'));
|
||||||
|
if (nodeElem.getAttribute('BOLD')) font.setBold(nodeElem.getAttribute('BOLD'));
|
||||||
|
if (nodeElem.getAttribute('ITALIC')) font.setItalic(nodeElem.getAttribute('ITALIC'));
|
||||||
|
if (nodeElem.getAttribute('SIZE')) font.setSize(nodeElem.getAttribute('SIZE'));
|
||||||
|
|
||||||
|
return font;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodeElem.tagName === 'edge') {
|
||||||
|
const edge = new Edge();
|
||||||
|
if (nodeElem.getAttribute('COLOR')) edge.setColor(nodeElem.getAttribute('COLOR'));
|
||||||
|
if (nodeElem.getAttribute('STYLE')) edge.setStyle(nodeElem.getAttribute('STYLE'));
|
||||||
|
if (nodeElem.getAttribute('WIDTH')) edge.setWidth(nodeElem.getAttribute('WIDTH'));
|
||||||
|
|
||||||
|
return edge;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodeElem.tagName === 'arrowlink') {
|
||||||
|
const arrowlink = new Arrowlink();
|
||||||
|
if (nodeElem.getAttribute('COLOR')) arrowlink.setColor(nodeElem.getAttribute('COLOR'));
|
||||||
|
if (nodeElem.getAttribute('DESTINATION')) arrowlink.setDestination(nodeElem.getAttribute('DESTINATION'));
|
||||||
|
if (nodeElem.getAttribute('ENDARROW')) arrowlink.setEndarrow(nodeElem.getAttribute('ENDARROW'));
|
||||||
|
if (nodeElem.getAttribute('ENDINCLINATION')) arrowlink.setEndinclination(nodeElem.getAttribute('ENDINCLINATION'));
|
||||||
|
if (nodeElem.getAttribute('ID')) arrowlink.setId(nodeElem.getAttribute('ID'));
|
||||||
|
if (nodeElem.getAttribute('STARTARROW')) arrowlink.setStartarrow(nodeElem.getAttribute('STARTARROW'));
|
||||||
|
if (nodeElem.getAttribute('STARTINCLINATION')) arrowlink.setStartinclination(nodeElem.getAttribute('STARTINCLINATION'));
|
||||||
|
|
||||||
|
return arrowlink;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodeElem.tagName === 'cloud') {
|
||||||
|
const cloud = new Cloud();
|
||||||
|
if (nodeElem.getAttribute('COLOR')) cloud.setColor(nodeElem.getAttribute('COLOR'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodeElem.tagName === 'icon') {
|
||||||
|
const icon = new Icon();
|
||||||
|
if (nodeElem.getAttribute('BUILTIN')) icon.setBuiltin(nodeElem.getAttribute('BUILTIN'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nodeElem.tagName === 'richcontent') {
|
||||||
|
const richcontent = new Richcontent();
|
||||||
|
|
||||||
|
if (nodeElem.getAttribute('TYPE')) richcontent.setType(nodeElem.getAttribute('TYPE'));
|
||||||
|
if (nodeElem.lastElementChild) richcontent.setHtml(String(nodeElem.lastElementChild));
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeDefault = new Node();
|
||||||
|
return nodeDefault;
|
||||||
|
}
|
||||||
|
|
||||||
private nodeToXml(childNode: Choise, parentNode: HTMLElement, document: Document): HTMLElement {
|
private nodeToXml(childNode: Choise, parentNode: HTMLElement, document: Document): HTMLElement {
|
||||||
if (childNode instanceof Node) {
|
if (childNode instanceof Node) {
|
||||||
childNode.setCentralTopic(false);
|
childNode.setCentralTopic(false);
|
||||||
|
@ -226,6 +226,46 @@ class Node {
|
|||||||
|
|
||||||
return nodeElem;
|
return nodeElem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadFromElement(element: Element): Node {
|
||||||
|
const node = new Node();
|
||||||
|
|
||||||
|
const nodeId = element.getAttribute('ID');
|
||||||
|
const nodePosition = element.getAttribute('POSITION');
|
||||||
|
const nodeStyle = element.getAttribute('STYLE');
|
||||||
|
const nodeBGColor = element.getAttribute('BACKGROUND_COLOR');
|
||||||
|
const nodeColor = element.getAttribute('COLOR');
|
||||||
|
const nodeText = element.getAttribute('TEXT');
|
||||||
|
const nodeLink = element.getAttribute('LINK');
|
||||||
|
const nodeFolded = element.getAttribute('FOLDED');
|
||||||
|
const nodeCreated = element.getAttribute('CREATED');
|
||||||
|
const nodeModified = element.getAttribute('MODIFIED');
|
||||||
|
const nodeHgap = element.getAttribute('HGAP');
|
||||||
|
const nodeVgap = element.getAttribute('VGAP');
|
||||||
|
const nodeWcoords = element.getAttribute('WCOORDS');
|
||||||
|
const nodeWorder = element.getAttribute('WORDER');
|
||||||
|
const nodeVshift = element.getAttribute('VSHIFT');
|
||||||
|
const nodeEncryptedContent = element.getAttribute('ENCRYPTED_CONTENT');
|
||||||
|
|
||||||
|
if (nodeId) node.setId(nodeId);
|
||||||
|
if (nodePosition) node.setPosition(nodePosition);
|
||||||
|
if (nodeStyle) node.setStyle(nodeStyle);
|
||||||
|
if (nodeBGColor) node.setBackgorundColor(nodeBGColor);
|
||||||
|
if (nodeColor) node.setColor(nodeColor);
|
||||||
|
if (nodeText) node.setText(nodeText);
|
||||||
|
if (nodeLink) node.setLink(nodeLink);
|
||||||
|
if (nodeFolded) node.setFolded(nodeFolded);
|
||||||
|
if (nodeCreated) node.setCreated(nodeCreated);
|
||||||
|
if (nodeModified) node.setModified(nodeModified);
|
||||||
|
if (nodeHgap) node.setHgap(nodeHgap);
|
||||||
|
if (nodeVgap) node.setVgap(nodeVgap);
|
||||||
|
if (nodeWcoords) node.setWcoords(nodeWcoords);
|
||||||
|
if (nodeWorder) node.setWorder(nodeWorder);
|
||||||
|
if (nodeVshift) node.setVshift(nodeVshift);
|
||||||
|
if (nodeEncryptedContent) node.setEncryptedContent(nodeEncryptedContent);
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Choise = Arrowlink | Cloud | Edge | Font | Hook | Icon | Richcontent | Node
|
export type Choise = Arrowlink | Cloud | Edge | Font | Hook | Icon | Richcontent | Node
|
||||||
|
@ -8,4 +8,56 @@ export default class VersionNumber {
|
|||||||
public getVersion(): string {
|
public getVersion(): string {
|
||||||
return this.version;
|
return this.version;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public isGreaterThan(versionNumber: VersionNumber): boolean {
|
||||||
|
return this.compareTo(versionNumber) < 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public compareTo(otherObject: VersionNumber): number {
|
||||||
|
if (this.equals<VersionNumber>(otherObject)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ownTokinizer = this.getTokinizer();
|
||||||
|
const otherTokinizer = otherObject.getTokinizer();
|
||||||
|
|
||||||
|
for (let i = 0; i < ownTokinizer.length; i++) {
|
||||||
|
let ownNumber: number;
|
||||||
|
let ohterNumber: number;
|
||||||
|
|
||||||
|
try {
|
||||||
|
ownNumber = parseInt(ownTokinizer[i], 10);
|
||||||
|
ohterNumber = parseInt(otherTokinizer[i], 10);
|
||||||
|
} catch (e) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ownNumber > ohterNumber) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (ownNumber < ohterNumber) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public equals<T>(o: T): boolean {
|
||||||
|
if (o instanceof VersionNumber) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(o instanceof VersionNumber)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const versionNumber: VersionNumber = o as VersionNumber;
|
||||||
|
|
||||||
|
return !(this.version ? this.version !== versionNumber.version : versionNumber.version);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTokinizer(): Array<string> {
|
||||||
|
return this.getVersion().split('.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,10 @@
|
|||||||
|
import IconModel from '../model/IconModel';
|
||||||
|
|
||||||
|
export default class FreemindIconConverter {
|
||||||
|
private static freeIdToIcon: Map<string, IconModel> = new Map<string, IconModel>();
|
||||||
|
|
||||||
|
public static toWiseId(iconId: string): number | null {
|
||||||
|
const result: IconModel = this.freeIdToIcon.get(iconId);
|
||||||
|
return result ? result.getId() : null;
|
||||||
|
}
|
||||||
|
}
|
406
packages/mindplot/src/components/import/FreemindImporter.ts
Normal file
406
packages/mindplot/src/components/import/FreemindImporter.ts
Normal file
@ -0,0 +1,406 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
export default class FreemindImporter extends Importer {
|
||||||
|
private mindmap: Mindmap;
|
||||||
|
|
||||||
|
private freemindMap: FreemindMap;
|
||||||
|
|
||||||
|
private nodesmap: Map<string, NodeModel>;
|
||||||
|
|
||||||
|
private relationship: Array<RelationshipModel>;
|
||||||
|
|
||||||
|
private currentId: number;
|
||||||
|
|
||||||
|
constructor(map: FreemindMap) {
|
||||||
|
super();
|
||||||
|
this.freemindMap = map;
|
||||||
|
}
|
||||||
|
|
||||||
|
import(nameMap: string, description: string): Promise<Mindmap> {
|
||||||
|
this.mindmap = new Mindmap(nameMap);
|
||||||
|
this.nodesmap = new Map<string, NodeModel>();
|
||||||
|
this.relationship = new Array<RelationshipModel>();
|
||||||
|
|
||||||
|
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);
|
||||||
|
const wiseTopic = this.mindmap.createNode('CentralTopic', 0);
|
||||||
|
wiseTopic.setId(this.currentId++);
|
||||||
|
wiseTopic.setPosition(0, 0);
|
||||||
|
|
||||||
|
this.convertNodeProperties(freeNode, wiseTopic);
|
||||||
|
|
||||||
|
wiseTopic.setShapeType(TopicShape.ROUNDED_RECT);
|
||||||
|
|
||||||
|
this.nodesmap.set(freeNode.getId(), wiseTopic);
|
||||||
|
|
||||||
|
this.convertChildNodes(freeNode, wiseTopic, this.mindmap, 1);
|
||||||
|
this.addRelationship(this.mindmap);
|
||||||
|
|
||||||
|
this.mindmap.setDescription(description);
|
||||||
|
|
||||||
|
return Promise.resolve(this.mindmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
private addRelationship(mindmap: Mindmap): void {
|
||||||
|
const mapRelaitonship: Array<RelationshipModel> = 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): void {
|
||||||
|
const text: string = freeNode.getText();
|
||||||
|
wiseTopic.setText(text);
|
||||||
|
|
||||||
|
const bgColor: string = freeNode.getBackgorundColor();
|
||||||
|
wiseTopic.setBackgroundColor(bgColor);
|
||||||
|
|
||||||
|
const shape = this.getShapeFromFreeNode(freeNode);
|
||||||
|
wiseTopic.setShapeType(shape);
|
||||||
|
|
||||||
|
// Check for style...
|
||||||
|
const fontStyle = this.generateFontStyle(freeNode, null);
|
||||||
|
if (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());
|
||||||
|
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 freeChild: FreemindNode = child as FreemindNode;
|
||||||
|
const wiseChild = mindmap.createNode('MainTopic', this.currentId++);
|
||||||
|
|
||||||
|
this.nodesmap.set(freeChild.getId(), wiseChild);
|
||||||
|
|
||||||
|
let norder: number;
|
||||||
|
if (depth !== 1) {
|
||||||
|
norder = order++;
|
||||||
|
} else if (freeChild.getPosition() && freeChild.getPosition() === FreemindConstant.POSITION_LEFT) {
|
||||||
|
norder = firstLevelLeftOrder;
|
||||||
|
firstLevelLeftOrder += 2;
|
||||||
|
} else {
|
||||||
|
norder = firstLevelRightOrder;
|
||||||
|
firstLevelRightOrder += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
wiseChild.setOrder(norder);
|
||||||
|
|
||||||
|
// Convert node position...
|
||||||
|
const childrenCountSameSide = this.getChildrenCountSameSide(freeChilden, freeChild);
|
||||||
|
const position: {x: number, y: number} = this.convertPosition(wiseParent, freeChild, depth, norder, childrenCountSameSide);
|
||||||
|
wiseChild.setPosition(position.x, position.y);
|
||||||
|
|
||||||
|
// Convert the rest of the node properties...
|
||||||
|
this.convertNodeProperties(freeChild, wiseChild);
|
||||||
|
|
||||||
|
this.convertChildNodes(freeChild, wiseChild, mindmap, depth + 1);
|
||||||
|
|
||||||
|
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 content: FreemindRichcontent = child as FreemindRichcontent;
|
||||||
|
const type: string = content.getType();
|
||||||
|
|
||||||
|
if (type === FreemindConstant.NODE_TYPE) {
|
||||||
|
const text = this.html2text(content);
|
||||||
|
currentWiseTopic.setText(text);
|
||||||
|
} else {
|
||||||
|
let text = this.html2text(content);
|
||||||
|
const mindmapNote: NoteModel = new NoteModel({ text: '' });
|
||||||
|
text = text || FreemindConstant.EMPTY_NOTE;
|
||||||
|
mindmapNote.setText(text);
|
||||||
|
currentWiseTopic.addFeature(mindmapNote);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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<string> = endinclination.split(';');
|
||||||
|
relationship.setDestCtrlPoint(`${inclination[0]},${inclination[1]}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const startinclination: string = arrow.getStartinclination();
|
||||||
|
if (startinclination) {
|
||||||
|
const inclination: Array<string> = 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<Choise>, freeChild: FreemindNode): number {
|
||||||
|
let result = 0;
|
||||||
|
let childSide: string = freeChild.getPosition();
|
||||||
|
|
||||||
|
if (!childSide) {
|
||||||
|
childSide = FreemindConstant.POSITION_RIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
|
freeChilden.forEach((child) => {
|
||||||
|
if (child instanceof FreemindNode) {
|
||||||
|
const node: FreemindNode = child as FreemindNode;
|
||||||
|
|
||||||
|
let side = node.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<string> = [];
|
||||||
|
|
||||||
|
// 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 = position.y - ((childrenCount / 2) * FreemindConstant.SECOND_LEVEL_TOPIC_HEIGHT - (order * FreemindConstant.SECOND_LEVEL_TOPIC_HEIGHT));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private html2text(content: FreemindRichcontent): string {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(content.getHtml(), 'text/html');
|
||||||
|
doc.querySelector('br').append('\\n');
|
||||||
|
doc.querySelector('p').append('\\n');
|
||||||
|
doc.querySelector('div').append('\\n');
|
||||||
|
return doc.textContent.replace('\\\\n', '\\n').trim();
|
||||||
|
}
|
||||||
|
}
|
5
packages/mindplot/src/components/import/Importer.ts
Normal file
5
packages/mindplot/src/components/import/Importer.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import Mindmap from '../model/Mindmap';
|
||||||
|
|
||||||
|
export default abstract class Importer {
|
||||||
|
abstract import(nameMap: string, description: string): Promise<Mindmap>;
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
import FreemindImporter from './FreemindImporter';
|
||||||
|
import FreemindMap from '../export/freemind/Map';
|
||||||
|
import Importer from './Importer';
|
||||||
|
|
||||||
|
type textType = 'mm';
|
||||||
|
type mapType = FreemindMap
|
||||||
|
|
||||||
|
export default class TextImporterFactory {
|
||||||
|
static create(type: textType, map: mapType): Importer {
|
||||||
|
let result: Importer;
|
||||||
|
switch (type) {
|
||||||
|
case 'mm':
|
||||||
|
result = new FreemindImporter(map);
|
||||||
|
return result;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported type ${type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
|
import { test } from '@jest/globals';
|
||||||
|
import { parseXMLFile } from '../export/Helper';
|
||||||
|
import FreemindMap from '../../../src/components/export/freemind/Map';
|
||||||
|
import TextImporterFactory from '../../../src/components/import/TextImporterFactory';
|
||||||
|
import XMLSerializerFactory from '../../../src/components/persistence/XMLSerializerFactory';
|
||||||
|
import Mindmap from '../../../src/components/model/Mindmap';
|
||||||
|
|
||||||
|
const testNames = fs
|
||||||
|
.readdirSync(path.resolve(__dirname, './input/'))
|
||||||
|
.map((filename: string) => filename.split('.')[0]);
|
||||||
|
|
||||||
|
describe('MMa import test execution', () => {
|
||||||
|
test.each(testNames)('Importing %p suite', async (testName: string) => {
|
||||||
|
// load freemap...
|
||||||
|
const freemapPath = path.resolve(__dirname, `./input/${testName}.mm`);
|
||||||
|
const mapDocument = parseXMLFile(freemapPath, 'text/xml');
|
||||||
|
|
||||||
|
const freemap: FreemindMap = new FreemindMap().loadFromDom(mapDocument);
|
||||||
|
|
||||||
|
// Load mindmap DOM..
|
||||||
|
const mindmapPath = path.resolve(__dirname, `./expected/${testName}.wxml`);
|
||||||
|
const mindmapMapDocument = parseXMLFile(mindmapPath, 'text/xml');
|
||||||
|
|
||||||
|
// Convert to mindmap...
|
||||||
|
const serializer = XMLSerializerFactory.createInstanceFromDocument(mindmapMapDocument);
|
||||||
|
const mindmap: Mindmap = serializer.loadFromDom(mapDocument, testName);
|
||||||
|
|
||||||
|
const importer = TextImporterFactory.create('mm', freemap);
|
||||||
|
const mindmapImport: Mindmap = await importer.import(testName, '');
|
||||||
|
|
||||||
|
expect(mindmapImport).toStrictEqual(mindmap);
|
||||||
|
});
|
||||||
|
});
|
49
packages/mindplot/test/unit/import/expected/bug2.wxml
Normal file
49
packages/mindplot/test/unit/import/expected/bug2.wxml
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<map name="58984" version="tango">
|
||||||
|
<topic central="true" text="SaberMás" id="1">
|
||||||
|
<topic position="271,-39" order="8" text="Utilización de medios de expresión artística, digitales y analógicos"
|
||||||
|
id="5"/>
|
||||||
|
<topic position="-181,-17" order="5" text="Precio también limitado: 100-120?" id="9"/>
|
||||||
|
<topic position="132,165" order="14" text="Talleres temáticos" id="2">
|
||||||
|
<topic position="242,57" order="0" text="Naturaleza" id="13">
|
||||||
|
<topic position="362,57" order="0" text="Animales, Plantas, Piedras" id="17"/>
|
||||||
|
</topic>
|
||||||
|
<topic position="245,84" order="1" text="Arqueología" id="21"/>
|
||||||
|
<topic position="236,138" order="3" text="Energía" id="18"/>
|
||||||
|
<topic position="244,192" order="5" text="Astronomía" id="16"/>
|
||||||
|
<topic position="245,219" order="6" text="Arquitectura" id="20"/>
|
||||||
|
<topic position="234,246" order="7" text="Cocina" id="11"/>
|
||||||
|
<topic position="234,273" order="8" text="Poesía" id="24"/>
|
||||||
|
<topic position="256,111" order="2" text="Culturas Antiguas" id="25">
|
||||||
|
<topic position="378,111" order="0" text="Egipto, Grecia, China..." id="26"/>
|
||||||
|
</topic>
|
||||||
|
<topic position="248,165" order="4" text="Paleontología" id="38"/>
|
||||||
|
</topic>
|
||||||
|
<topic position="-168,-49" order="3" text="Duración limitada: 5-6 semanas" id="6"/>
|
||||||
|
<topic position="-181,16" order="7" text="Niños y niñas que quieren saber más" id="7"/>
|
||||||
|
<topic position="-184,-81" order="1" text="Alternativa a otras actividades de ocio" id="8"/>
|
||||||
|
<topic position="255,-6" order="10" text="Uso de la tecnología durante todo el proceso de aprendizaje" id="23"/>
|
||||||
|
<topic position="336,-137" order="2"
|
||||||
|
text="Estructura PBL: aprendemos cuando buscamos respuestas a nuestras propias preguntas " id="3"/>
|
||||||
|
<topic position="238,-105" order="4" text="Trabajo basado en la experimentación y en la investigación" id="4"/>
|
||||||
|
<topic position="-201,48" order="9" text="De 8 a 12 años, sin separación por edades" id="10"/>
|
||||||
|
<topic position="-146,81" order="11" text="Máximo 10/1 por taller" id="19"/>
|
||||||
|
<topic position="211,-72" order="6" text="Actividades centradas en el contexto cercano" id="37"/>
|
||||||
|
<topic position="303,27" order="12"
|
||||||
|
text="Flexibilidad en el uso de las lenguas de trabajo (inglés, castellano, esukara?)" id="22"/>
|
||||||
|
<topic position="206,-220" order="0" text="Complementamos el trabajo de la escuela" shape="rounded rectagle"
|
||||||
|
id="27">
|
||||||
|
<note><![CDATA[Todos los contenidos de los talleres están relacionados con el currículo de la enseñanza básica.
|
||||||
|
A diferencia de la práctica tradicional, pretendemos ahondar en el conocimiento partiendo de lo que realmente interesa al niño o niña,
|
||||||
|
ayudándole a que encuentre respuesta a las preguntas que él o ella se plantea.
|
||||||
|
|
||||||
|
Por ese motivo, SaberMás proyecta estar al lado de los niños que necesitan una motivación extra para entender la escuela y fluir en ella,
|
||||||
|
y también al lado de aquellos a quienes la curiosidad y las ganas de saber les lleva más allá.]]></note>
|
||||||
|
<topic position="477,-220" order="2" text="Cada uno va a su ritmo, y cada cual pone sus límites" id="30"/>
|
||||||
|
<topic position="425,-193" order="3" text="Aprendemos todos de todos" id="31"/>
|
||||||
|
<topic position="440,-167" order="4" text="Valoramos lo que hemos aprendido" id="33"/>
|
||||||
|
<topic position="468,-273" order="0" text="SaberMás trabaja con, desde y para la motivación" shape="line"
|
||||||
|
id="28"/>
|
||||||
|
<topic position="458,-247" order="1" text="Trabajamos en equipo en nuestros proyectos " id="32"/>
|
||||||
|
</topic>
|
||||||
|
</topic>
|
||||||
|
</map>
|
51
packages/mindplot/test/unit/import/input/bug2.mm
Normal file
51
packages/mindplot/test/unit/import/input/bug2.mm
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<map version="1.0.1">
|
||||||
|
<node ID="ID_1" TEXT="SaberMás">
|
||||||
|
<node ID="ID_5" POSITION="right" STYLE="fork" TEXT="Utilización de medios de expresión artística, digitales y analógicos"/>
|
||||||
|
<node ID="ID_9" POSITION="left" STYLE="fork" TEXT="Precio también limitado: 100-120?"/>
|
||||||
|
<node ID="ID_2" POSITION="right" STYLE="fork" TEXT="Talleres temáticos">
|
||||||
|
<node ID="ID_13" POSITION="right" STYLE="fork" TEXT="Naturaleza">
|
||||||
|
<node ID="ID_17" POSITION="right" STYLE="fork" TEXT="Animales, Plantas, Piedras"/>
|
||||||
|
</node>
|
||||||
|
<node ID="ID_21" POSITION="right" STYLE="fork" TEXT="Arqueología"/>
|
||||||
|
<node ID="ID_18" POSITION="right" STYLE="fork" TEXT="Energía"/>
|
||||||
|
<node ID="ID_16" POSITION="right" STYLE="fork" TEXT="Astronomía"/>
|
||||||
|
<node ID="ID_20" POSITION="right" STYLE="fork" TEXT="Arquitectura"/>
|
||||||
|
<node ID="ID_11" POSITION="right" STYLE="fork" TEXT="Cocina"/>
|
||||||
|
<node ID="ID_24" POSITION="right" STYLE="fork" TEXT="Poesía"/>
|
||||||
|
<node ID="ID_25" POSITION="right" STYLE="fork" TEXT="Culturas Antiguas">
|
||||||
|
<node ID="ID_26" POSITION="right" STYLE="fork" TEXT="Egipto, Grecia, China..."/>
|
||||||
|
</node>
|
||||||
|
<node ID="ID_38" POSITION="right" STYLE="fork" TEXT="Paleontología"/>
|
||||||
|
</node>
|
||||||
|
<node ID="ID_6" POSITION="left" STYLE="fork" TEXT="Duración limitada: 5-6 semanas"/>
|
||||||
|
<node ID="ID_7" POSITION="left" STYLE="fork" TEXT="Niños y niñas que quieren saber más"/>
|
||||||
|
<node ID="ID_8" POSITION="left" STYLE="fork" TEXT="Alternativa a otras actividades de ocio"/>
|
||||||
|
<node ID="ID_23" POSITION="right" STYLE="fork" TEXT="Uso de la tecnología durante todo el proceso de aprendizaje"/>
|
||||||
|
<node ID="ID_3" POSITION="right" STYLE="fork" TEXT="Estructura PBL: aprendemos cuando buscamos respuestas a nuestras propias preguntas "/>
|
||||||
|
<node ID="ID_4" POSITION="right" STYLE="fork" TEXT="Trabajo basado en la experimentación y en la investigación"/>
|
||||||
|
<node ID="ID_10" POSITION="left" STYLE="fork" TEXT="De 8 a 12 años, sin separación por edades"/>
|
||||||
|
<node ID="ID_19" POSITION="left" STYLE="fork" TEXT="Máximo 10/1 por taller"/>
|
||||||
|
<node ID="ID_37" POSITION="right" STYLE="fork" TEXT="Actividades centradas en el contexto cercano"/>
|
||||||
|
<node ID="ID_22" POSITION="right" STYLE="fork" TEXT="Flexibilidad en el uso de las lenguas de trabajo (inglés, castellano, esukara?)"/>
|
||||||
|
<node ID="ID_27" POSITION="right" STYLE="bubble" TEXT="Complementamos el trabajo de la escuela">
|
||||||
|
<richcontent TYPE="NOTE">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||||
|
<head></head>
|
||||||
|
<body>
|
||||||
|
<p>Todos los contenidos de los talleres están relacionados con el currículo de la enseñanza básica.</p>
|
||||||
|
<p>A diferencia de la práctica tradicional, pretendemos ahondar en el conocimiento partiendo de lo que realmente interesa al niño o niña,</p>
|
||||||
|
<p>ayudándole a que encuentre respuesta a las preguntas que él o ella se plantea.</p>
|
||||||
|
<p></p>
|
||||||
|
<p>Por ese motivo, SaberMás proyecta estar al lado de los niños que necesitan una motivación extra para entender la escuela y fluir en ella,</p>
|
||||||
|
<p>y también al lado de aquellos a quienes la curiosidad y las ganas de saber les lleva más allá.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
</richcontent>
|
||||||
|
<node ID="ID_30" POSITION="right" STYLE="fork" TEXT="Cada uno va a su ritmo, y cada cual pone sus límites"/>
|
||||||
|
<node ID="ID_31" POSITION="right" STYLE="fork" TEXT="Aprendemos todos de todos"/>
|
||||||
|
<node ID="ID_33" POSITION="right" STYLE="fork" TEXT="Valoramos lo que hemos aprendido"/>
|
||||||
|
<node ID="ID_28" POSITION="right" TEXT="SaberMás trabaja con, desde y para la motivación"/>
|
||||||
|
<node ID="ID_32" POSITION="right" STYLE="fork" TEXT="Trabajamos en equipo en nuestros proyectos "/>
|
||||||
|
</node>
|
||||||
|
</node>
|
||||||
|
</map>
|
Loading…
Reference in New Issue
Block a user