Mercurial > hg > accesspd
comparison java/src/uk/ac/qmul/eecs/ccmi/gui/Edge.java @ 0:78b7fc5391a2
first import, outcome of NIME 2014 hackaton
author | Fiore Martin <f.martin@qmul.ac.uk> |
---|---|
date | Tue, 08 Jul 2014 16:28:59 +0100 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:78b7fc5391a2 |
---|---|
1 /* | |
2 CCmI Editor - A Collaborative Cross-Modal Diagram Editing Tool | |
3 | |
4 Copyright (C) 2002 Cay S. Horstmann (http://horstmann.com) | |
5 Copyright (C) 2011 Queen Mary University of London (http://ccmi.eecs.qmul.ac.uk/) | |
6 | |
7 This program is free software: you can redistribute it and/or modify | |
8 it under the terms of the GNU General Public License as published by | |
9 the Free Software Foundation, either version 3 of the License, or | |
10 (at your option) any later version. | |
11 | |
12 This program is distributed in the hope that it will be useful, | |
13 but WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
15 GNU General Public License for more details. | |
16 | |
17 You should have received a copy of the GNU General Public License | |
18 along with this program. If not, see <http://www.gnu.org/licenses/>. | |
19 */ | |
20 | |
21 package uk.ac.qmul.eecs.ccmi.gui; | |
22 | |
23 import java.awt.BasicStroke; | |
24 import java.awt.Color; | |
25 import java.awt.Graphics2D; | |
26 import java.awt.Shape; | |
27 import java.awt.geom.Line2D; | |
28 import java.awt.geom.Point2D; | |
29 import java.awt.geom.Rectangle2D; | |
30 import java.io.IOException; | |
31 import java.util.ArrayList; | |
32 import java.util.BitSet; | |
33 import java.util.LinkedHashMap; | |
34 import java.util.LinkedHashSet; | |
35 import java.util.LinkedList; | |
36 import java.util.List; | |
37 import java.util.ListIterator; | |
38 import java.util.Map; | |
39 import java.util.ResourceBundle; | |
40 | |
41 import org.w3c.dom.Document; | |
42 import org.w3c.dom.Element; | |
43 import org.w3c.dom.NodeList; | |
44 | |
45 import uk.ac.qmul.eecs.ccmi.diagrammodel.ConnectNodesException; | |
46 import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramEdge; | |
47 import uk.ac.qmul.eecs.ccmi.diagrammodel.DiagramNode; | |
48 import uk.ac.qmul.eecs.ccmi.diagrammodel.ElementChangedEvent; | |
49 import uk.ac.qmul.eecs.ccmi.gui.persistence.PersistenceManager; | |
50 | |
51 /** | |
52 * An edge in a graph. Edge objects are used in a GraphPanel to render a diagram edge visually. | |
53 * Edge objects are used in the tree representation of the diagram as well, as they're | |
54 * subclasses of {@link DiagramEdge} | |
55 * | |
56 */ | |
57 @SuppressWarnings("serial") | |
58 public abstract class Edge extends DiagramEdge implements GraphElement{ | |
59 | |
60 /** | |
61 * Creates a new Edge | |
62 * | |
63 * @param type the type of the edge. The type is just a string that the user assign to the edge. | |
64 * All the edges created via clonation from this edge will have the same type. | |
65 * @param availableEndDescriptions all the possible end description ends of this edge can be | |
66 * associated to. An end description is a text associated to a possible arrow head of an edge end. | |
67 * It's used to give awareness of arrow heads via speech. | |
68 * @param minAttachedNodes the minimum number of nodes that can be attached to this edge | |
69 * @param maxAttachedNodes the minimum number of nodes that can be attached to this edge | |
70 * @param style the style of the edge: whether it's solid, dotted or dashed | |
71 */ | |
72 public Edge(String type, String[] availableEndDescriptions, int minAttachedNodes, int maxAttachedNodes,LineStyle style){ | |
73 super(type,availableEndDescriptions); | |
74 this.minAttachedNodes = minAttachedNodes; | |
75 this.maxAttachedNodes = maxAttachedNodes; | |
76 this.style = style; | |
77 nodes = new ArrayList<Node>(); | |
78 } | |
79 | |
80 /* --- Methods inherited from DiagramEdge --- */ | |
81 @Override | |
82 public Node getNodeAt(int index){ | |
83 return nodes.get(index); | |
84 } | |
85 | |
86 @Override | |
87 public int getNodesNum(){ | |
88 return nodes.size(); | |
89 } | |
90 | |
91 @Override | |
92 public boolean removeNode(DiagramNode diagramNode, Object source){ | |
93 Node n = (Node)diagramNode; | |
94 if(nodes.size() == 2) | |
95 throw new RuntimeException("Cannot remove a node from a two ends edge"); | |
96 else{ | |
97 for(InnerPoint p : points) | |
98 if(p.hasNeighbour(n)){ | |
99 p.neighbours.remove(n); | |
100 if(p.neighbours.size() == 1) | |
101 removePoint(p); | |
102 break; | |
103 } | |
104 boolean nodeRemoved = nodes.remove(n); | |
105 /* for update in the haptic device */ | |
106 notifyChange(new ElementChangedEvent(this,n,"remove_node",source)); | |
107 return nodeRemoved; | |
108 } | |
109 } | |
110 | |
111 @Override | |
112 public void connect(List<DiagramNode> nodes) throws ConnectNodesException{ | |
113 assert(getNodesNum() == 0); | |
114 /* this is to eliminate duplicates */ | |
115 LinkedHashSet<Node> nodeSet = new LinkedHashSet<Node>(); | |
116 for(DiagramNode n : nodes) | |
117 nodeSet.add((Node)n); | |
118 | |
119 /* checks on connection consistency */ | |
120 if((nodeSet == null)||(nodeSet.size() < minAttachedNodes)) | |
121 throw new ConnectNodesException("You must select at least "+ minAttachedNodes + "nodes"); | |
122 if((nodeSet.size() > maxAttachedNodes)) | |
123 throw new ConnectNodesException("You must select at most " + maxAttachedNodes +" nodes"); | |
124 | |
125 points = new ArrayList<InnerPoint>(); | |
126 if(nodeSet.size() > 2){ | |
127 /* there are more than three nodes. compute the central inner point * | |
128 * which will connect all the nodes, as the middle points of the edge bound */ | |
129 Rectangle2D bounds = new Rectangle2D.Double(); | |
130 for(Node n : nodeSet){ | |
131 bounds.add(n.getBounds()); | |
132 } | |
133 InnerPoint p = new InnerPoint(); | |
134 p.translate(new Point2D.Double(0,0), bounds.getCenterX(), bounds.getCenterY(),DiagramEventSource.NONE); | |
135 p.neighbours.addAll(nodeSet); | |
136 points.add(p); | |
137 } | |
138 this.nodes.addAll(nodeSet); | |
139 | |
140 if(!points.isEmpty())// points is empty when the edge has two nodes only | |
141 masterInnerPoint = points.get(0); | |
142 } | |
143 | |
144 @Override | |
145 public abstract void draw(Graphics2D g2); | |
146 | |
147 @Override | |
148 public void translate(Point2D p, double dx, double dy, Object source){ | |
149 for(InnerPoint ip : points) | |
150 ip.translate(p, dx, dy,source); | |
151 } | |
152 | |
153 /** | |
154 * To be called before {@code bend}, determines from {@code downPoint} whether | |
155 * a line is going to be break into two lines (with the creation of a new inner point) | |
156 * or if the bending is determined by an already existing inner point being translated | |
157 */ | |
158 @Override | |
159 public void startMove(Point2D downPoint,Object source){ | |
160 this.downPoint = downPoint; | |
161 newInnerPoint = null; | |
162 for(InnerPoint itrPoint : points) | |
163 if(itrPoint.contains(downPoint)){ | |
164 /* clicked on an already existing EdgePoint */ | |
165 newInnerPoint = itrPoint; | |
166 newPointCreated = false; | |
167 } | |
168 if(newInnerPoint == null){ | |
169 /* no point under the click, create a new one */ | |
170 newInnerPoint = new InnerPoint(); | |
171 newInnerPoint.translate(downPoint, downPoint.getX() - newInnerPoint.getBounds().getCenterX(), | |
172 downPoint.getY() - newInnerPoint.getBounds().getCenterY(),DiagramEventSource.NONE); | |
173 newPointCreated = true; | |
174 /* this methods checks for segments of the edge which are aligned and makes a unique edge out of them */ | |
175 } | |
176 } | |
177 | |
178 /** | |
179 * If this edge is made out of several lines and two or more of them becomes | |
180 * aligned then they are blended in one single line and the inner point that was | |
181 * at the joint is removed. | |
182 * | |
183 * @param source the source of the {@code stopMove} action | |
184 */ | |
185 @Override | |
186 public void stopMove(Object source){ | |
187 for(ListIterator<InnerPoint> pItr = points.listIterator(); pItr.hasNext(); ){ | |
188 InnerPoint ePoint = pItr.next(); | |
189 if(ePoint.neighbours.size() > 2) | |
190 continue; | |
191 Rectangle2D startBounds = ePoint.getBounds(); | |
192 Rectangle2D endBounds = ePoint.neighbours.get(0).getBounds(); | |
193 Direction d1 = new Direction(endBounds.getCenterX() - startBounds.getCenterX(), endBounds.getCenterY() - startBounds.getCenterY()); | |
194 endBounds = ePoint.neighbours.get(1).getBounds(); | |
195 Direction d2 = new Direction(endBounds.getCenterX() - startBounds.getCenterX(), endBounds.getCenterY() - startBounds.getCenterY()); | |
196 if(d1.isParallel(d2)){ | |
197 InnerPoint p = null; | |
198 GraphElement q = null; | |
199 if(ePoint.neighbours.get(0) instanceof InnerPoint){ | |
200 p = (InnerPoint)ePoint.neighbours.get(0); | |
201 q = ePoint.neighbours.get(1); | |
202 p.neighbours.add(q); | |
203 p.neighbours.remove(ePoint); | |
204 } | |
205 if(ePoint.neighbours.get(1) instanceof InnerPoint){ | |
206 p = (InnerPoint)ePoint.neighbours.get(1); | |
207 q = ePoint.neighbours.get(0); | |
208 p.neighbours.add(q); | |
209 p.neighbours.remove(ePoint); | |
210 } | |
211 pItr.remove(); | |
212 } | |
213 } | |
214 notifyChange(new ElementChangedEvent(this,this,"stop_move",source)); | |
215 } | |
216 | |
217 @Override | |
218 public abstract Rectangle2D getBounds(); | |
219 | |
220 @Override | |
221 public Point2D getConnectionPoint(Direction d){return null;} | |
222 | |
223 @Override | |
224 public boolean contains(Point2D aPoint){ | |
225 if(points.isEmpty()){ | |
226 return fatStrokeContains (nodes.get(0), nodes.get(1), aPoint); | |
227 } | |
228 for(InnerPoint p : points){ | |
229 for(GraphElement ge : p.neighbours){ | |
230 if(fatStrokeContains(p,ge,aPoint)) | |
231 return true; | |
232 } | |
233 } | |
234 return false; | |
235 } | |
236 | |
237 /** | |
238 * Look for the node attached to this edge which lays at the minimum distance | |
239 * from the point passed as argument. The distance cannot be lower than the | |
240 * value passed as argument. | |
241 * | |
242 * @param aPoint the point the distance is measured from | |
243 * @param distanceLimit the limit from the distance between the nodes and the point | |
244 * @return the closest node or null if the node lays at an higher distance than distanceLimit | |
245 */ | |
246 public Node getClosestNode(Point2D aPoint, double distanceLimit){ | |
247 Node closestNode = null; | |
248 double minDist = distanceLimit; | |
249 | |
250 if(points.isEmpty()){ | |
251 Line2D line = getSegment(nodes.get(0),nodes.get(1)); | |
252 if(line.getP1().distance(aPoint) < minDist){ | |
253 minDist = line.getP1().distance(aPoint); | |
254 closestNode = nodes.get(0); | |
255 } | |
256 if(line.getP2().distance(aPoint) < minDist){ | |
257 minDist = line.getP2().distance(aPoint); | |
258 closestNode = nodes.get(1); | |
259 } | |
260 return closestNode; | |
261 }else{ | |
262 for(InnerPoint p : points){ | |
263 for(GraphElement ge : p.getNeighbours()) | |
264 if(ge instanceof Node){ | |
265 Node n = (Node)ge; | |
266 Direction d = new Direction(p.getBounds().getCenterX() - n.getBounds().getCenterX(),p.getBounds().getCenterY() - n.getBounds().getCenterY()); | |
267 if(n.getConnectionPoint(d).distance(aPoint) < minDist){ | |
268 minDist = n.getConnectionPoint(d).distance(aPoint); | |
269 closestNode = n; | |
270 } | |
271 } | |
272 } | |
273 return closestNode; | |
274 } | |
275 } | |
276 | |
277 private void removePoint(InnerPoint p){ | |
278 /* we assume at this moment p has one neighbour only */ | |
279 InnerPoint neighbour = (InnerPoint)p.neighbours.get(0); | |
280 points.remove(p); | |
281 neighbour.neighbours.remove(p); | |
282 if(neighbour.neighbours.size() == 1) | |
283 removePoint(neighbour); | |
284 } | |
285 | |
286 /* checks if a point belongs to a shape with a margin of MAX_DIST*/ | |
287 private boolean fatStrokeContains(GraphElement ge1, GraphElement ge2, Point2D p){ | |
288 BasicStroke fatStroke = new BasicStroke((float) (2 * MAX_DIST)); | |
289 Line2D line = new Line2D.Double( | |
290 ge1.getBounds().getCenterX(), | |
291 ge1.getBounds().getCenterY(), | |
292 ge2.getBounds().getCenterX(), | |
293 ge2.getBounds().getCenterY()); | |
294 Shape fatPath = fatStroke.createStrokedShape(line); | |
295 return fatPath.contains(p); | |
296 } | |
297 | |
298 /** | |
299 * Returns a line connecting the centre of the graph elements passed as argument. | |
300 * | |
301 * @param start the first graph element | |
302 * @param end the second graph element | |
303 * | |
304 * @return a line connecting {@code start} and {@code end} | |
305 */ | |
306 protected Line2D.Double getSegment(GraphElement start, GraphElement end){ | |
307 Rectangle2D startBounds = start.getBounds(); | |
308 Rectangle2D endBounds = end.getBounds(); | |
309 Direction d = new Direction(endBounds.getCenterX() - startBounds.getCenterX(), endBounds.getCenterY() - startBounds.getCenterY()); | |
310 return new Line2D.Double(start.getConnectionPoint(d), end.getConnectionPoint(d.turn(180))); | |
311 } | |
312 | |
313 /** | |
314 * Returns a list of the points where this edge and the nodes it connects come to a contact | |
315 * | |
316 * @return a list of points | |
317 */ | |
318 public List<Point2D> getConnectionPoints(){ | |
319 List<Point2D> list = new LinkedList<Point2D>(); | |
320 if(points.isEmpty()){ | |
321 Line2D line = getSegment(nodes.get(0),nodes.get(1)); | |
322 list.add(line.getP1()); | |
323 list.add(line.getP2()); | |
324 }else{ | |
325 for(InnerPoint p : points){ | |
326 for(GraphElement ge : p.neighbours) | |
327 if(ge instanceof Node){ | |
328 Direction d = new Direction(p.getBounds().getCenterX() - ge.getBounds().getCenterX(),p.getBounds().getCenterY() - ge.getBounds().getCenterY()); | |
329 list.add(((Node)ge).getConnectionPoint(d)); | |
330 } | |
331 } | |
332 } | |
333 return list; | |
334 } | |
335 | |
336 /** | |
337 * Gets the stipple pattern of this edge line style | |
338 * | |
339 * @see LineStyle#getStipplePattern() | |
340 * | |
341 * @return an int representing the stipple pattern of this edge | |
342 */ | |
343 public int getStipplePattern(){ | |
344 return getStyle().getStipplePattern(); | |
345 } | |
346 | |
347 /** | |
348 * Bends one of the lines forming this edge. | |
349 * | |
350 * When an line is bent, if the location where the bending happens is a line then a new | |
351 * inner point is created and the line is broken into two sub lines. If the location is an | |
352 * already existing inner point, then the point is translated. | |
353 * | |
354 * @param p the starting point of the bending | |
355 * @param source the source of the bending action | |
356 */ | |
357 public void bend(Point2D p,Object source) { | |
358 boolean found = false; | |
359 if(points.isEmpty()){ | |
360 newInnerPoint.neighbours.addAll(nodes); | |
361 points.add(newInnerPoint); | |
362 newPointCreated = false; | |
363 }else if(newPointCreated){ | |
364 /* find the segment closest to where the new point lays */ | |
365 InnerPoint closestP1 = null; | |
366 GraphElement closestP2 = null; | |
367 double minDist = 0; | |
368 for(ListIterator<InnerPoint> pItr = points.listIterator(); pItr.hasNext(); ){ | |
369 InnerPoint ePoint = pItr.next(); | |
370 for(ListIterator<GraphElement> geItr = ePoint.neighbours.listIterator(); geItr.hasNext() && !found;){ | |
371 /* find the neighbour of the current edge point whose line the new point lays on */ | |
372 GraphElement next = geItr.next(); | |
373 double dist = Line2D.ptSegDist( | |
374 ePoint.getBounds().getCenterX(), | |
375 ePoint.getBounds().getCenterY(), | |
376 next.getBounds().getCenterX(), | |
377 next.getBounds().getCenterY(), | |
378 downPoint.getX(), | |
379 downPoint.getY() | |
380 ); | |
381 | |
382 if(closestP1 == null || dist < minDist){ | |
383 closestP1 = ePoint; | |
384 closestP2 = next; | |
385 minDist = dist; | |
386 continue; | |
387 } | |
388 } | |
389 } | |
390 | |
391 if(closestP2 instanceof InnerPoint ){ | |
392 /* remove current edge point from the neighbour's neighbours */ | |
393 ((InnerPoint)closestP2).neighbours.remove(closestP1); | |
394 /* add the new inner point */ | |
395 ((InnerPoint)closestP2).neighbours.add(newInnerPoint); | |
396 } | |
397 /*remove old neighbour from edge inner point neighbours */ | |
398 closestP1.neighbours.remove(closestP2); | |
399 newInnerPoint.neighbours.add(closestP1); | |
400 newInnerPoint.neighbours.add(closestP2); | |
401 /* add the new node to the list of EdgeNodes of this edge */ | |
402 points.add(newInnerPoint); | |
403 closestP1.neighbours.add(newInnerPoint); | |
404 found = true; | |
405 newPointCreated = false; | |
406 } | |
407 newInnerPoint.translate(p, p.getX() - newInnerPoint.getBounds().getCenterX(), | |
408 p.getY() - newInnerPoint.getBounds().getCenterY(),DiagramEventSource.NONE); | |
409 notifyChange(new ElementChangedEvent(this,this,"bend",source)); | |
410 } | |
411 | |
412 /** | |
413 * Returns the line where the edge name will be painted. If the edge is only made out | |
414 * of a straight line then this will be returned. | |
415 * | |
416 * If the edge has been broken into several segments (by bending it) then the central | |
417 * line is returned. If the edge connects more than two nodes then a line (not necessarily | |
418 * matching the edge) that has the central point in its centre is returned. Note that a | |
419 * edge connecting more than two nodes is painted as a central point connected to all the nodes. | |
420 * | |
421 * @return the line where the name will be painted | |
422 */ | |
423 public Line2D getNameLine(){ | |
424 if(points.isEmpty()){ // straight line | |
425 return getSegment(nodes.get(0),nodes.get(1)); | |
426 }else{ | |
427 if(masterInnerPoint != null){/* multiended edge */ | |
428 Rectangle2D bounds = masterInnerPoint.getBounds(); | |
429 Point2D p = new Point2D.Double(bounds.getCenterX() - 1,bounds.getCenterY()); | |
430 Point2D q = new Point2D.Double(bounds.getCenterX() + 1,bounds.getCenterY()); | |
431 return new Line2D.Double(p, q); | |
432 }else{ | |
433 GraphElement ge1 = nodes.get(0); | |
434 GraphElement ge2 = nodes.get(1); | |
435 InnerPoint c1 = null; | |
436 InnerPoint c2 = null; | |
437 | |
438 for(InnerPoint innp : points){ | |
439 if(innp.getNeighbours().contains(ge1)){ | |
440 c1 = innp; | |
441 } | |
442 if(innp.getNeighbours().contains(ge2)){ | |
443 c2 = innp; | |
444 } | |
445 } | |
446 | |
447 /* we only have two nodes but the edge has been bended */ | |
448 while((c1 != c2)&&(!c2.getNeighbours().contains(c1))){ | |
449 if(c1.getNeighbours().get(0) == ge1){ | |
450 ge1 = c1; | |
451 c1 = (InnerPoint)c1.getNeighbours().get(1); | |
452 } | |
453 else{ | |
454 ge1 = c1; | |
455 c1 = (InnerPoint)c1.getNeighbours().get(0); | |
456 } | |
457 if(c2.getNeighbours().get(0) == ge2){ | |
458 ge2 = c2; | |
459 c2 = (InnerPoint)c2.getNeighbours().get(1); | |
460 } | |
461 else{ | |
462 ge2 = c2; | |
463 c2 = (InnerPoint)c2.getNeighbours().get(0); | |
464 } | |
465 } | |
466 | |
467 Point2D p = new Point2D.Double(); | |
468 Point2D q = new Point2D.Double(); | |
469 if(c1 == c2){ | |
470 Rectangle2D bounds = c1.getBounds(); | |
471 p.setLocation( bounds.getCenterX() - 1,bounds.getCenterY()); | |
472 q.setLocation( bounds.getCenterX() + 1,bounds.getCenterY()); | |
473 }else{ | |
474 Rectangle2D bounds = c1.getBounds(); | |
475 p.setLocation( bounds.getCenterX(),bounds.getCenterY()); | |
476 bounds = c2.getBounds(); | |
477 q.setLocation(bounds.getCenterX(),bounds.getCenterY()); | |
478 | |
479 } | |
480 return new Line2D.Double(p,q); | |
481 } | |
482 } | |
483 } | |
484 | |
485 /** | |
486 * Encodes all the relevant data of this object in XML format. | |
487 * | |
488 * @param doc an XML document | |
489 * @param parent the parent XML element, where tag about this edge will be inserted | |
490 * @param nodes a list of all nodes of the diagram | |
491 */ | |
492 public void encode(Document doc, Element parent, List<Node> nodes){ | |
493 parent.setAttribute(PersistenceManager.TYPE,getType()); | |
494 parent.setAttribute(PersistenceManager.NAME, getName()); | |
495 parent.setAttribute(PersistenceManager.ID, String.valueOf(getId())); | |
496 | |
497 int numNodes = getNodesNum(); | |
498 if(numNodes > 0){ | |
499 Element nodesTag = doc.createElement(PersistenceManager.NODES); | |
500 parent.appendChild(nodesTag); | |
501 for(int i=0; i<numNodes;i++){ | |
502 Element nodeTag = doc.createElement(PersistenceManager.NODE); | |
503 nodeTag.setAttribute(PersistenceManager.ID, String.valueOf(getNodeAt(i).getId())); | |
504 nodeTag.setAttribute(PersistenceManager.LABEL, getEndLabel(getNodeAt(i))); | |
505 nodesTag.appendChild(nodeTag); | |
506 } | |
507 } | |
508 | |
509 if(!points.isEmpty()){ | |
510 Element pointsTag = doc.createElement(PersistenceManager.POINTS); | |
511 parent.appendChild(pointsTag); | |
512 for(InnerPoint point : points){ | |
513 Element pointTag = doc.createElement(PersistenceManager.POINT); | |
514 pointsTag.appendChild(pointTag); | |
515 pointTag.setAttribute(PersistenceManager.ID, String.valueOf(-(points.indexOf(point)+1))); | |
516 | |
517 Element positionTag = doc.createElement(PersistenceManager.POSITION); | |
518 pointTag.appendChild(positionTag); | |
519 Rectangle2D bounds = point.getBounds(); | |
520 positionTag.setAttribute(PersistenceManager.X,String.valueOf(bounds.getX())); | |
521 positionTag.setAttribute(PersistenceManager.Y,String.valueOf(bounds.getY())); | |
522 | |
523 Element neighboursTag = doc.createElement(PersistenceManager.NEIGHBOURS); | |
524 pointTag.appendChild(neighboursTag); | |
525 StringBuilder builder = new StringBuilder(); | |
526 for(GraphElement ge : point.getNeighbours()){ | |
527 if(ge instanceof Node){ | |
528 builder.append(((Node)ge).getId()); | |
529 }else{ | |
530 builder.append(-(points.indexOf(ge)+1)); | |
531 } | |
532 builder.append(" "); | |
533 } | |
534 builder.deleteCharAt(builder.length()-1); | |
535 neighboursTag.setTextContent(builder.toString()); | |
536 } | |
537 } | |
538 } | |
539 | |
540 | |
541 /** | |
542 * Decodes an edge from the XML representation. | |
543 * | |
544 * @see #encode(Document, Element, List) | |
545 * | |
546 * @param doc an XML document | |
547 * @param edgeTag the tag in the XML file related to this edge | |
548 * @param nodesId a map linking node ids in the XML file to the {@code Node} objects they represent | |
549 * @throws IOException if something goes wrong while reading the XML file | |
550 */ | |
551 public void decode(Document doc, Element edgeTag, Map<String,Node> nodesId) throws IOException{ | |
552 setName(edgeTag.getAttribute(PersistenceManager.NAME),DiagramEventSource.PERS); | |
553 if(getName().isEmpty()) | |
554 throw new IOException(); | |
555 try{ | |
556 setId(Integer.parseInt(edgeTag.getAttribute(PersistenceManager.ID))); | |
557 }catch(NumberFormatException nfe){ | |
558 throw new IOException(nfe); | |
559 } | |
560 | |
561 NodeList nodeList = edgeTag.getElementsByTagName(PersistenceManager.NODE); | |
562 List<DiagramNode> attachedNodes = new ArrayList<DiagramNode>(nodeList.getLength()); | |
563 List<String> labels = new ArrayList<String>(nodeList.getLength()); | |
564 for(int i=0; i<nodeList.getLength();i++){ | |
565 String id = ((Element)nodeList.item(i)).getAttribute(PersistenceManager.ID); | |
566 if(!nodesId.containsKey(id)) | |
567 throw new IOException(); | |
568 attachedNodes.add(nodesId.get(id)); | |
569 labels.add(((Element)nodeList.item(i)).getAttribute(PersistenceManager.LABEL)); | |
570 } | |
571 | |
572 try { | |
573 connect(attachedNodes); | |
574 } catch (ConnectNodesException e) { | |
575 throw new IOException(e); | |
576 } | |
577 | |
578 for(int i=0; i < labels.size(); i++){ | |
579 setEndLabel(attachedNodes.get(i), labels.get(i),DiagramEventSource.PERS); | |
580 } | |
581 | |
582 Map<String, InnerPoint> pointsId = new LinkedHashMap<String, InnerPoint>(); | |
583 NodeList pointTagList = edgeTag.getElementsByTagName(PersistenceManager.POINT); | |
584 | |
585 for(int i=0; i<pointTagList.getLength(); i++){ | |
586 InnerPoint point = new InnerPoint(); | |
587 Element pointTag = (Element)pointTagList.item(i); | |
588 String id = pointTag.getAttribute(PersistenceManager.ID); | |
589 /* id of property nodes must be a negative value */ | |
590 try{ | |
591 if(Integer.parseInt(id) >= 0) | |
592 throw new IOException(); | |
593 }catch(NumberFormatException nfe){ | |
594 throw new IOException(nfe); | |
595 } | |
596 | |
597 pointsId.put(id, point); | |
598 | |
599 if(pointTag.getElementsByTagName(PersistenceManager.POSITION).item(0) == null) | |
600 throw new IOException(); | |
601 Element pointPositionTag = (Element)pointTag.getElementsByTagName(PersistenceManager.POSITION).item(0); | |
602 double dx = 0,dy = 0; | |
603 try{ | |
604 dx = Double.parseDouble(pointPositionTag.getAttribute(PersistenceManager.X)); | |
605 dy = Double.parseDouble(pointPositionTag.getAttribute(PersistenceManager.Y)); | |
606 }catch(NumberFormatException nfe){ | |
607 throw new IOException(); | |
608 } | |
609 point.translate(new Point2D.Double(), dx, dy,DiagramEventSource.PERS); | |
610 } | |
611 | |
612 /* remove the master inner point eventually created by connect */ | |
613 /* we're going to replace it with the one in the XML file */ | |
614 points.clear(); | |
615 /* re do the cycle when all the points id have been Map-ped */ | |
616 for(int i=0; i<pointTagList.getLength(); i++){ | |
617 Element pointTag = (Element)pointTagList.item(i); | |
618 InnerPoint point = pointsId.get(pointTag.getAttribute(PersistenceManager.ID)); | |
619 | |
620 if(pointTag.getElementsByTagName(PersistenceManager.NEIGHBOURS).item(0) == null) | |
621 throw new IOException(); | |
622 Element pointNeighboursTag = (Element)pointTag.getElementsByTagName(PersistenceManager.NEIGHBOURS).item(0); | |
623 String pointNeighboursTagContent = pointNeighboursTag.getTextContent(); | |
624 String[] neighboursId = pointNeighboursTagContent.split(" "); | |
625 | |
626 for(String neighbourId : neighboursId){ | |
627 GraphElement ge = nodesId.get(neighbourId); | |
628 if(ge == null) // it ain't a node | |
629 ge = pointsId.get(neighbourId); | |
630 if(ge == null) | |
631 throw new IOException(); | |
632 point.neighbours.add(ge); | |
633 } | |
634 points.add(point); | |
635 if(i==0) | |
636 masterInnerPoint = point; | |
637 } | |
638 } | |
639 | |
640 /** | |
641 * Returns the minimum number of nodes that edge of this type can connect | |
642 * | |
643 * @return the minimum nodes for edges of this type | |
644 */ | |
645 public int getMinAttachedNodes(){ | |
646 return minAttachedNodes; | |
647 } | |
648 | |
649 /** | |
650 * Returns the maximum number of nodes that edge of this type can connect | |
651 * | |
652 * @return the maximum nodes for edges of this type | |
653 */ | |
654 public int getMaxAttachedNodes(){ | |
655 return maxAttachedNodes; | |
656 } | |
657 | |
658 /** | |
659 * Return the line style of this edge | |
660 * | |
661 * @return the line style of this edge | |
662 */ | |
663 public LineStyle getStyle(){ | |
664 return style; | |
665 } | |
666 | |
667 protected Point2D downPoint; | |
668 private List<Node> nodes; | |
669 | |
670 /* list containing the vertex of the edge which are not nodes */ | |
671 /** | |
672 * The list of the inner points of this edge | |
673 */ | |
674 protected List<InnerPoint> points; | |
675 /** | |
676 * For edges connecting more than two nodes, this is the central inner point where | |
677 * all the lines from the nodes join | |
678 */ | |
679 protected InnerPoint masterInnerPoint; | |
680 | |
681 private boolean newPointCreated; | |
682 private InnerPoint newInnerPoint; | |
683 private int minAttachedNodes; | |
684 private int maxAttachedNodes; | |
685 | |
686 private LineStyle style; | |
687 | |
688 | |
689 private static final double MAX_DIST = 5; | |
690 private static final Color POINT_COLOR = Color.GRAY; | |
691 /** | |
692 * The end description for an end that has no hand description set by the user | |
693 */ | |
694 public static final String NO_ENDDESCRIPTION_STRING = ResourceBundle.getBundle(EditorFrame.class.getName()).getString("no_arrow_string"); | |
695 | |
696 | |
697 /** | |
698 * When an edge's (straight) line is bent it breaks into two sub lines. At the point where this two | |
699 * sub lines join a square shaped point is painted. This class represent that point. Objects of this class | |
700 * are graph elements and the user can click on them and translate them along the graph. | |
701 * | |
702 */ | |
703 protected static class InnerPoint implements GraphElement{ | |
704 /** | |
705 * Creates a new inner point | |
706 */ | |
707 public InnerPoint(){ | |
708 bounds = new Rectangle2D.Double(0,0,DIM,DIM); | |
709 neighbours = new LinkedList<GraphElement>(); | |
710 } | |
711 | |
712 @Override | |
713 public void startMove(Point2D p, Object source){} | |
714 | |
715 @Override | |
716 public void stopMove(Object source){} | |
717 | |
718 @Override | |
719 public void draw(Graphics2D g2){ | |
720 Color oldColor = g2.getColor(); | |
721 g2.setColor(POINT_COLOR); | |
722 g2.fill(bounds); | |
723 g2.setColor(oldColor); | |
724 } | |
725 | |
726 @Override | |
727 public boolean contains(Point2D p){ | |
728 return bounds.contains(p); | |
729 } | |
730 | |
731 @Override | |
732 public Point2D getConnectionPoint(Direction d){ | |
733 return new Point2D.Double(bounds.getCenterX(),bounds.getCenterY()); | |
734 } | |
735 | |
736 @Override | |
737 public void translate(Point2D p, double dx, double dy, Object source){ | |
738 bounds.setFrame(bounds.getX() + dx, | |
739 bounds.getY() + dy, | |
740 bounds.getWidth(), | |
741 bounds.getHeight()); | |
742 } | |
743 | |
744 @Override | |
745 public Rectangle2D getBounds(){ | |
746 return (Rectangle2D)bounds.clone(); | |
747 } | |
748 | |
749 /** | |
750 * Neighbours are the graph elements (either nodes or other inner points) this inner point is | |
751 * directly connected to. Directly connected means there is a straight line from this node | |
752 * and the neighbour. | |
753 * | |
754 * @return a list of neighbours of this inner node | |
755 */ | |
756 public List<GraphElement> getNeighbours(){ | |
757 return neighbours; | |
758 } | |
759 | |
760 /** | |
761 * Returns true if this inner node and {@code ge} are neighbours. | |
762 * | |
763 * @see #getNeighbours() | |
764 * | |
765 * @param ge the graph element to be tested | |
766 * @return {@code true} if {@code ge} is a neighbour of this graph element, {@code false} otherwise | |
767 * | |
768 */ | |
769 public boolean hasNeighbour(GraphElement ge){ | |
770 return neighbours.contains(ge); | |
771 } | |
772 | |
773 @Override | |
774 public String toString(){ | |
775 return "EdgePoint: "+bounds.getCenterX()+"-"+bounds.getCenterY(); | |
776 } | |
777 | |
778 private Rectangle2D bounds; | |
779 private List<GraphElement> neighbours; | |
780 private static final int DIM = 7; | |
781 } | |
782 | |
783 /** | |
784 * A representation of this edge as a set of 2D points. | |
785 * Every node the edge connects and every inner point are represented as a pair with coordinates | |
786 * of their centre. Furthermore an adjacency matrix holds the information | |
787 * about which point is connected to which is. This representation of the edge is | |
788 * used in the haptic space, being more suitable for haptic devices. | |
789 * | |
790 */ | |
791 public static class PointRepresentation { | |
792 public PointRepresentation(int size){ | |
793 xs = new double[size]; | |
794 ys = new double[size]; | |
795 adjMatrix = new BitSet[size]; | |
796 for(int i=0; i<size; i++){ | |
797 adjMatrix[i] = new BitSet(size); | |
798 } | |
799 } | |
800 /** | |
801 * An array with all the x coordinate of the edge's points (nodes and inner points) | |
802 */ | |
803 public double xs[]; | |
804 /** | |
805 * An array with all the y coordinate of the edge's points (nodes and inner points) | |
806 */ | |
807 public double ys[]; | |
808 /** | |
809 * The adjacency matrix. If the i-th bit of {@code adjMatrix[j]} is set to {@code true} | |
810 * it means there is a direct line connecting the i-th point (coordinates {@code (xs[i],ys[i])} | |
811 * to the j-th point (coordinates {@code (xs[j],ys[j])}. Note that connection are represented only | |
812 * once to avoid double paintings by the haptic engine. So if the i-th bit of {@code adjMatrix[j]} | |
813 * is set to {@code true}, the j-th bit of {@code adjMatrix[i]} and information redundancy is avoided. | |
814 */ | |
815 public BitSet adjMatrix[]; | |
816 /** | |
817 * The index of the beginning of the nodes (after inner points) in {@code adjMatrix}. | |
818 * So if the edge has three nodes and two inner points. The inner points | |
819 * will be at {@code adjMatrix[0]} and {@code adjMatrix[1]} and the nodes at the following indexes. | |
820 * In this case {@code nodeStart} is equal to 2. | |
821 */ | |
822 public int nodeStart; | |
823 } | |
824 | |
825 /** | |
826 * Returns a new {@code PointRepresentation} of this edge | |
827 * | |
828 * @return a new {@code PointRepresentation} of this edge | |
829 */ | |
830 public PointRepresentation getPointRepresentation(){ | |
831 PointRepresentation pr = new PointRepresentation(points.size()+nodes.size()); | |
832 if(points.isEmpty()){ // two ended edge | |
833 pr.xs[0] = nodes.get(0).getBounds().getCenterX(); | |
834 pr.ys[0] = nodes.get(0).getBounds().getCenterY(); | |
835 pr.xs[1] = nodes.get(1).getBounds().getCenterX(); | |
836 pr.ys[1] = nodes.get(1).getBounds().getCenterY(); | |
837 // we only need one edge, else it would be painted twice | |
838 pr.adjMatrix[0].set(1); | |
839 pr.nodeStart = 0; | |
840 }else{ | |
841 //[ point 1, point 2, point 3, ... , point n, node, 1 node 2, ... , node n ] | |
842 int pSize = points.size(); | |
843 pr.nodeStart = pSize; // the first node starts after the points | |
844 for(int i=0; i<pSize;i++){ | |
845 pr.xs[i] = points.get(i).getBounds().getCenterX(); | |
846 pr.ys[i] = points.get(i).getBounds().getCenterY(); | |
847 for(GraphElement ge : points.get(i).neighbours){ | |
848 if(ge instanceof InnerPoint) | |
849 pr.adjMatrix[i].set(points.indexOf(ge)); | |
850 else //Node | |
851 pr.adjMatrix[i].set(pSize+nodes.indexOf(ge)); | |
852 } | |
853 } | |
854 /* set the coordinates of the nodes, no adj matrix needed as the inner points are enough */ | |
855 for(int i=0 ; i<nodes.size(); i++){ | |
856 pr.xs[pSize+i] = nodes.get(i).getBounds().getCenterX(); | |
857 pr.ys[pSize+i] = nodes.get(i).getBounds().getCenterY(); | |
858 } | |
859 } | |
860 return pr; | |
861 } | |
862 | |
863 } | |
864 |