root/trunk/JGraphViewer.java

Revision 3, 27.5 kB (checked in by dpaola2, 1 month ago)

old pathways project added

Line 
1 // JGraphViewer.java
2 //
3 // This is a JComponent that shows a Graph, including its nodes, edges, and
4 // maps, on the screen in a customizable manner, and informs interested parties
5 // when MouseEvents happen on it.
6 //
7 // To be informed when mouse events happen, implement PointListener and use
8 // addPointListener() to register yourself.  When mouse events occur, their
9 // coordinates will be translated to a real-space point and sent to you.
10 //
11 // The scale is how many pixels are used to represent one unit in the real
12 // coordinate system, and it can be changed with the associated functions.
13 // Space can also be added on the sides, allowing users to scroll outside the
14 // actual bounds of the graph so they can, for example, add nodes there.
15 //
16 // The way nodes, edges, and maps appear is dictated by ObjectPainters, which
17 // are specific to a type.  To change the appearance of an object or set of
18 // objects, the first step is to create an ObjectPainter of the appropriate
19 // type representing the attributes you want.  You can make this ObjectPainter
20 // be used for objects that don't have a more specific one with
21 // setDefault*Painter().  setPath*Painter() sets the painter for objects on
22 // the path, and setPath() sets this path.  setSelected*Painter() sets the
23 // painter for the selected object, and the selected object can be set with
24 // setSelectedObject().
25 //
26 // Currently maps are implemented outside the ObjectPainter system because they
27 // are not associated with Graphs, and because they have no parameters that
28 // affect how they're drawn.  This could be changed in the future, especially
29 // if maps are to be serializable and owned by graphs.  The reason maps
30 // currently aren't serializable is because half of what they are (how they
31 // name their files) is embodied in a special class, one per map.
32
33 import java.awt.*;
34 import java.awt.event.*;
35 import java.awt.geom.*;
36 import java.awt.image.*;
37 import java.util.*;
38 import javax.swing.*;
39 import java.math.*;
40
41 class JGraphViewer extends JPanel implements GraphChangeListener {
42
43         public JGraphViewer(Graph graph, double scaleX, double scaleY, BackgroundMap backgroundMap) {
44                 // Note - most of our members get initialized where they're
45                 // declared - cool, huh?  Yay Java.
46
47                 // we must use a borderlayout, or else the scrollPane doesn't
48                 // get resized when we do.  Alternately, we could derive from
49                 // the scrollpane instead of containing it, but that might let
50                 // people screw with us too much.  (Maybe worth it anyway?)
51                 // (Just s/scrollPane/this/, change above to JScrollPane.)
52
53                 super(new BorderLayout());
54
55                 add(scrollPane);
56
57                 scale = AffineTransform.getScaleInstance(scaleX, scaleY);
58                 setGraph(graph);
59                 setBackgroundMap(backgroundMap);
60
61
62                 // This is a workaround for what I can only conclude is a Swing
63                 // bug.  It makes scrolling slower, because it redraws the
64                 // entire visible area, not just the newly exposed area.
65
66                 // The bug is, when scrolling exposes a new map tile, when the
67                 // map tile loads, the parts of the tile that were exposed
68                 // during the scroll but before the tile's image was loaded
69                 // are sometimes not repainted, leaving black areas.
70
71                 // I have confirmed that the imageUpdate events are received by
72                 // the drawingPane, and I have attempted calling repaint() and
73                 // paintImmediately(0,0,inf,inf) when they are received, as well
74                 // as turning off double buffering, but this is the only way
75                 // the problem doesn't appear.
76
77                 // It has appeared in with Sun 1.5, Blackdown 1.4, and something
78                 // on Windows.
79
80                 // The bug seems mostly worked around with my new imageobserver,
81                 // so i'll comment out the complete workaround for now...
82
83                 //scrollPane.getViewport().setScrollMode(JViewport.SIMPLE_SCROLL_MODE);
84         }
85
86         /**
87          * This must be called whenever a change is made to our graph,
88          * especially if the area could have changed.  (It isn't necessary to
89          * call this after modifying painters, though.)
90          */
91         public void graphChanged() {
92                 graphRectangle = null;
93                 objectsToPainters = null;
94                 paintersToObjectLists = null;
95
96                 drawingPane.repaint();
97         }
98
99         // the space functions control how much blank space is left around the
100         // graph.  This is so you can add points that are outside the current
101         // bounding rectangle.
102
103         public void addSpace(double top, double bottom, double left, double right) {
104                 // calls setSpace()
105                 // todo: implement
106
107         }
108         public void setSpace(double top, double bottom, double left, double right) {
109                 // makes us have this much space, in screen coordinates, on our
110                 // sides, so that someone can add stuff by clicking in that
111                 // area.
112
113                 //todo: implement
114         }
115
116         // these let you get or set the graph we use.
117         public Graph getGraph() { return graph; }
118         public void setGraph(Graph graph) {
119                 // this triggers an unnecessary immediate repaint, which I
120                 // unfortunately can't avoid.
121                 scrollPane.getViewport().setViewPosition(new Point(0, 0));
122
123                 clearAllSpecialNodes();
124                 this.graph = graph;
125                 if(graph != null) graph.addGraphChangeListener(this);
126                 graphChanged();
127                 computeAndChangePreferredSize();
128                 drawingPane.repaint();
129         }
130
131         // these let you manipulate the scale, in px/m.
132         public double getScaleX() { return scale.getScaleX(); }
133         public double getScaleY() { return scale.getScaleY(); }
134         public void setScale(double scaleX, double scaleY) {
135                 Point2D.Double center = getVisibleRegionCenter();
136                 scale = AffineTransform.getScaleInstance(scaleX, scaleY);
137                 realToScreen = null;
138                 setVisibleRegionCenter(center);
139         drawingPane.repaint();
140         }
141
142         // these let you register your interest in translated mouse events.
143         public void addPointListener(PointListener pointListener) {
144                 pointListeners.add(pointListener);
145         }
146
147         public Collection getPointListeners() { return pointListeners; }
148         public void removePointListener(PointListener pointListener) {
149                 pointListeners.remove(pointListener);
150         }
151
152
153         // not sure if overriding this instead of making my own function is best
154         public void setBackground(Color bg) {
155                 if(drawingPane != null) drawingPane.setBackground(bg);
156         }
157
158
159         public Path getPath() { return path; }
160         public void setPath(Path path) {
161                 // Case of NULL should do just fine.
162                 this.path = path;
163                 objectsToPainters = null;
164                 drawingPane.repaint();
165         }
166
167         public Object getSelectedObject() { return selectedObject; }
168         public void setSelectedObject(Object object) {
169                 selectedObject = object;
170                 objectsToPainters = null;
171                 drawingPane.repaint();
172         }
173
174         public BackgroundMap getBackgroundMap() { return backgroundMap; }
175         // null map is ok
176         public void setBackgroundMap(BackgroundMap backgroundMap) {
177                 Point2D.Double center = getVisibleRegionCenter();
178                 this.backgroundMap = backgroundMap;
179                 graphRectangle = null;
180                 setVisibleRegionCenter(center);
181                 drawingPane.repaint();
182         }
183
184         // zOrder is 0 for on top, 1 for under selection but over path, and
185         // 2 for under everything.
186         public void addCustomObjectPainter(Object object, int zOrder, ObjectPainter painter) {
187                 objectsToCustomPainters[zOrder].put(object, painter);
188                 objectsToPainters = null;
189                 drawingPane.repaint();
190         }
191
192         public void clearCustomObjectPainters() {
193                 for(int i = 0; i < objectsToCustomPainters.length; i++) {
194                         objectsToCustomPainters[i].clear();
195                 }
196                 objectsToPainters = null;
197                 drawingPane.repaint();
198         }
199         public void clearAllSpecialNodes() {
200                 setPath(null);
201                 setSelectedObject(null);
202                 clearCustomObjectPainters();
203         }
204
205         public PhysicalNodePainter getDefaultNodePainter() { return defaultNodePainter; }
206         public void setDefaultNodePainter(PhysicalNodePainter painter) {
207                 defaultNodePainter = painter;
208                 objectsToPainters = null;
209                 drawingPane.repaint();
210         }
211
212         public PhysicalEdgePainter getDefaultEdgePainter() { return defaultEdgePainter; }
213         public void setDefaultEdgePainter(PhysicalEdgePainter painter) {
214                 defaultEdgePainter = painter;
215                 objectsToPainters = null;
216                 drawingPane.repaint();
217         }
218
219         public void setPathNodePainter(PhysicalNodePainter painter) {
220                 pathNodePainter = painter;
221                 objectsToPainters = null;
222                 drawingPane.repaint();
223         }
224
225         public void setPathEdgePainter(PhysicalEdgePainter painter) {
226                 pathEdgePainter = painter;
227                 objectsToPainters = null;
228                 drawingPane.repaint();
229         }
230
231         public void setSelectedNodePainter(PhysicalNodePainter painter) {
232                 selectedNodePainter = painter;
233                 objectsToPainters = null;
234                 drawingPane.repaint();
235         }
236
237         public void setSelectedEdgePainter(PhysicalEdgePainter painter) {
238                 selectedEdgePainter = painter;
239                 objectsToPainters = null;
240                 drawingPane.repaint();
241         }
242
243
244
245
246         public Object closestMatch(Point2D.Double realPoint, double maxDistance) {
247                 updateRealToScreen();
248                 updateObjectsToPainters();
249
250                 Object closestMatch = null;
251
252                 Iterator iterator = objectsToPainters.entrySet().iterator();
253                 while(iterator.hasNext()) {
254                         Map.Entry entry = (Map.Entry)iterator.next();
255                         ObjectPainter painter = (ObjectPainter)entry.getValue();
256                         Object object = entry.getKey();
257
258                         double distance = painter.visualDistanceTo(object, realPoint, backgroundMap, realToScreen);
259
260                         if(distance < maxDistance) {
261                                 closestMatch = object;
262                         }
263                 }
264                 return closestMatch;
265         }
266
267         /*
268          * The following functions return true if their objects aren't null
269          * and the update functions for all the things they depend on return
270          * true.  In this way, the functions ensure their objects are up-to-date
271          * when the call returns.  To mark an object as not up-to-date, set it
272          * to null.  You need not set objects that depend on it to null, because
273          * the dependency graph in implicit in the update*() functions.  Just
274          * call the appropriate update*() function when you need that object
275          * to be up-to-date.
276          */
277
278         /**
279          * This function computes, if necessary, objectsToPainters, which is a
280          * map that tells us, for every object we need to paint, which
281          * ObjecPainter to use.  It returns true and does nothing if
282          * objectsToPainters was already up-to-date, otherwise, it returns
283          * false.
284          */
285         private boolean updateObjectsToPainters() {
286
287                 // return early if we're up-to-date.
288                 if(objectsToPainters != null)
289                         return true;
290
291                 // a LinkedHashMap will be iterated through in the order keys
292                 // were inserted, so we can ensure that all edges are drawn
293                 // before all nodes.
294
295                 if(graph == null) {
296                         objectsToPainters = new LinkedHashMap();
297                         return false;
298                 }
299
300                 objectsToPainters = new LinkedHashMap(graph.getNodes().size() + graph.getEdges().size());
301
302                 Iterator iterator;
303
304                 // add all the edges with the default painter
305                 if(defaultEdgePainter != null) {
306                         iterator = graph.getEdges().iterator();
307                         while(iterator.hasNext()) {
308                                 Edge edge = (Edge)iterator.next();
309                                 if(!(edge instanceof PhysicalEdge)) continue;
310                                 objectsToPainters.put(edge, defaultEdgePainter);
311                         }
312                 }
313
314                 // add all the nodes with the default painter
315                 if(defaultNodePainter != null) {
316                         iterator = graph.getNodes().iterator();
317                         while(iterator.hasNext()) {
318                                 Node node = (Node)iterator.next();
319                                 if(!(node instanceof PhysicalNode)) continue;
320                                 objectsToPainters.put(node, defaultNodePainter);
321                         }
322                 }
323
324                 objectsToPainters.putAll(objectsToCustomPainters[2]);
325
326                 // add all the nodes in the path (this doesn't affect
327                 // LinkedHashSet order, so this is ok assuming the path only
328                 // has nodes in the graph.)
329                 if(path != null) {
330                         // add all the edges with the path painter
331                         if(pathEdgePainter != null) {
332                                 iterator = path.edges.iterator();
333                                 while(iterator.hasNext()) {
334                                         Edge edge = (Edge)iterator.next();
335                                         if(!(edge instanceof PhysicalEdge)) continue;
336                                         objectsToPainters.put(edge, pathEdgePainter);
337                                 }
338                         }
339
340                         // add all the nodes with the path painter
341                         if(pathNodePainter != null) {
342                                 iterator = path.nodes.iterator();
343                                 while(iterator.hasNext()) {
344                                         Node node = (Node)iterator.next();
345                                         if(!(node instanceof PhysicalNode)) continue;
346                                         objectsToPainters.put(node, pathNodePainter);
347                                 }
348                         }
349                 }
350
351                 objectsToPainters.putAll(objectsToCustomPainters[1]);
352
353
354                 // add in the selected object with the proper painter
355                 if(selectedObject instanceof PhysicalEdge) {
356                         objectsToPainters.put(selectedObject, selectedEdgePainter);
357                 } else if(selectedObject instanceof PhysicalNode) {
358                         objectsToPainters.put(selectedObject, selectedNodePainter);
359                 }
360
361                 objectsToPainters.putAll(objectsToCustomPainters[0]);
362
363                 paintersToObjectLists = null;
364                 //assert objectsToPainters != null;
365
366                 return false;
367         }
368
369         /**
370          * This function computes, if necessary, paintersToObjectLists, which
371          * maps each painter to a list of the objects that should be painted by
372          * it.  It's more efficient to draw groups of objects that use the same
373          * ObjectPainter at the same time, which is why we use such a map. It
374          * returns true and does nothing if paintersToObjectLists was already
375          * up-to-date, otherwise, it returns false.
376          */
377         private boolean updatePaintersToObjectLists() {
378                 // return early if we're up-to-date
379                 boolean upToDate = true;
380                 upToDate &= updateObjectsToPainters();
381                 upToDate &= (paintersToObjectLists != null);
382                 if(upToDate) return true;
383
384                // assert(objectsToPainters != null);
385
386                 // a LinkedHashSet preserves the order
387                 paintersToObjectLists = new LinkedHashMap();
388
389                 // for each key-value pair in objectsToPainters...
390                 Iterator iterator = objectsToPainters.entrySet().iterator();
391                 while(iterator.hasNext()) {
392                         Map.Entry entry = (Map.Entry)iterator.next();
393                         // get the painter for this key-value pair
394                         ObjectPainter painter = (ObjectPainter)entry.getValue();
395                         // get the associated list, creating it if necessary
396                         LinkedList list = (LinkedList)paintersToObjectLists.get(painter);
397                         if(list == null) {
398                                 list = new LinkedList();
399                                 paintersToObjectLists.put(painter, list);
400                         }
401                         // add the paintable object to the list
402                         list.add(entry.getKey());
403                 }
404
405                 // assert(paintersToObjectLists != null);
406                 return false;
407         }
408
409         /**
410          * This computes graphRectangle if necessary.  It returns true and does
411          * nothing if graphRectangle was already up-to-date, otherwise, it
412          * returns false.
413          */
414         private boolean updateGraphRectangle() {
415
416                 // return early if we're up-to-date
417                 boolean upToDate = true;
418                 upToDate &= (graphRectangle != null);
419                 if(upToDate) return true;
420
421                 graphRectangle = new Rectangle2D.Double();
422                 if(backgroundMap != null)
423                         graphRectangle.setRect(backgroundMap.getRealBoundingRectangle());
424
425                 // assert(graphRectangle != null);
426                 return false; // because we weren't already up-to-date.
427         }
428
429         /**
430          * This function computes realToScreen, if necessary.  It returns true
431          * and does nothing if realToScreen was already up-to-date, otherwise it
432          * returns false.
433          */
434         private boolean updateRealToScreen() {
435                 // return early if we're up-to-date
436                 boolean upToDate = true;
437                 upToDate &= updateGraphRectangle();
438                 upToDate &= (realToScreen != null);
439
440                 if(upToDate) return true;
441
442                 // assert(graphRectangle != null);
443
444                 computeAndChangePreferredSize();
445
446                 // here's 2 defining corners of that rectangle
447                 Point2D.Double corners[] = { new Point2D.Double(graphRectangle.x, graphRectangle.y),
448                                              new Point2D.Double(graphRectangle.x + graphRectangle.width,
449                                                                 graphRectangle.y + graphRectangle.height)};
450
451                 // transform the corners to screen coordinates
452                 scale.transform(corners, 0, corners, 0, 2);
453
454                 // if we are supposed to have extra space, we should add it now
455
456                 // find the lowest coordinates
457                 double minX = Math.min(corners[0].x, corners[1].x);
458                 double minY = Math.min(corners[0].y, corners[1].y);
459
460                 // make a slide transform so everything onscreen is positive.
461                 // we round it off because non-integer translations will make
462                 // the map look fuzzy even at 1:1 scale.
463                 realToScreen = AffineTransform.getTranslateInstance((double)(int)-minX, (double)(int)-minY);
464                 realToScreen.concatenate(scale);
465
466                 // assert(realToScreen != null);
467                 return false;
468         }
469
470         private void computeAndChangePreferredSize() {
471                 if(dontComputeAndChangePreferredSize) return;
472
473                 updateGraphRectangle();
474
475                 // we can set our preferred size here
476                 drawingPane.setPreferredSize(new Dimension(
477                         (int)(graphRectangle.width * scale.getScaleX() + 1),
478                         (int)(graphRectangle.height * scale.getScaleY() + 1)
479                 ));
480
481                 // update our scrollbars's unit scroll
482                 // the size of our visible area, I think.
483                 Dimension viewportSize = ((JViewport)drawingPane.getParent()).getSize();
484                 // our size
485                 Dimension preferredSize = drawingPane.getPreferredSize();
486
487                 // make there be only 30 scroll-stops, unless that would
488                 // reqiure scrollying more than 1/15 of what we can.
489                 scrollPane.getHorizontalScrollBar().setUnitIncrement(
490                         Math.min(
491                                 Math.max(1, (preferredSize.width - viewportSize.width) / 30),
492                                 Math.max(1, viewportSize.width / 15)
493                         )
494                 );
495                 scrollPane.getVerticalScrollBar().setUnitIncrement(
496                         Math.min(
497                                 Math.max(1, (preferredSize.height - viewportSize.height) / 30),
498                                 Math.max(1, viewportSize.height / 15)
499                         )
500                 );
501
502
503
504         }
505
506