root/trunk/JGraphViewer.java

Revision 3, 27.5 kB (checked in by dpaola2, 4 years 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         /**
507          * This is a special type of JPanel that knows how to paint itself with
508          * all of our components.  It's an nested class, which means it has
509          * access to all the parent class's fields, which means we don't have
510          * to make a bunch of functions in JGraphViewer that just forward
511          * themselves to the JSimpleGraphViewer.
512          */
513         private class DrawingPane extends JPanel implements MouseListener, MouseMotionListener {
514                 public DrawingPane() {
515                         addMouseListener(this);
516                         addMouseMotionListener(this);
517                 }
518
519                 protected void paintComponent(Graphics graphics) {
520                         super.paintComponent(graphics);
521                         if(dontPaint) {
522                             System.out.println("superfluous paint, please report");
523                         }
524
525                         // we really always have a Graphics2D, Java just uses
526                         // Graphics for backward-compatibility.
527                         Graphics2D g = (Graphics2D)graphics.create();
528
529                         // this prevents line endpoints from being rounded to
530                         // the nearest pixel, which prevents lines from
531                         // "jittering" when the window is resized.
532                         g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
533                                            RenderingHints.VALUE_STROKE_PURE);
534
535
536                         // We draw crappily if we're moving, nicely otherwise.
537                         if(!highQuality ||
538                            scrollPane.getHorizontalScrollBar().getValueIsAdjusting() ||
539                            scrollPane.getVerticalScrollBar().getValueIsAdjusting() ||
540                            temporaryLowQuality) {
541                                 // we are adjusting, so set it for crappy:
542                                 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
543                                                    RenderingHints.VALUE_ANTIALIAS_OFF);
544                                 g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
545                                                    RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
546                                 // and schedule a redraw after we have stopped
547                                 // moving
548                                 if(highQuality)
549                                         goodRepainter.scheduleGoodRepaint();
550                         } else {
551                                 // we aren't adjusting, so we set high-quality
552                                 g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
553                                                    RenderingHints.VALUE_ANTIALIAS_ON);
554                                 g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
555                                                    RenderingHints.VALUE_INTERPOLATION_BICUBIC);
556                         }
557
558                         // make sure everything we use is up-to-date
559                         updatePaintersToObjectLists();
560                         updateRealToScreen();
561
562                         if(backgroundMap != null) {
563                                 if(backgroundMap instanceof TiledMap) {
564                                         // draw the map
565                                         // we need this crappy imageobserver to trigger a redraw
566                                         // manually because java is buggy and otherwise it won't
567                                         // redraw images that have loaded while we're scrolling.                               
568                                         backgroundMap.paint(g, realToScreen, new ImageObserver() {
569                                                 public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
570                                                         if((infoflags & ImageObserver.ALLBITS) != 0) {
571                                                                 System.out.println("the image is loaded");
572                                                                 SwingUtilities.invokeLater(new Runnable() {
573                                                                         public void run() {
574                                                                                 repaint();
575                                                                         }
576                                                                 });
577                                                                 return false;
578                                                         } else {
579                                                                 return true;
580                                                         }
581                                                 }
582                                         });
583                                 } else {
584                                         backgroundMap.paint(g, realToScreen, this);     
585                                 }
586                         }
587
588                         // draw all the objects
589                         Iterator iterator = paintersToObjectLists.entrySet().iterator();
590                         while(iterator.hasNext()) {
591                                 Map.Entry entry = (Map.Entry)iterator.next();
592                                 ((ObjectPainter)entry.getKey()).paintObjects(
593                                         ((Collection)entry.getValue()), realToScreen, backgroundMap, g
594                                 );
595                         }
596                 }
597
598                 // This nested class maintains a single thread that schedules a
599                 // repaint for after the scrollbars have stopped moving.
600                 public class GoodRepainter {
601                         // calling this will schedule a repaint to occur after
602                         // the scrollbars have stopped being adjusted.
603                         public void scheduleGoodRepaint() {
604                                 if(needRepaint == false) {
605                                         needRepaint = true;
606                                         (new Thread(new Runnable() {
607                                                 public void run() {
608                                                         while(scrollPane.getHorizontalScrollBar().getValueIsAdjusting() ||
609                                                                scrollPane.getVerticalScrollBar().getValueIsAdjusting() ||
610                                                                    temporaryLowQuality) {
611                                                                 try {
612                                                                         Thread.sleep(25);
613                                                                 } catch(InterruptedException e) {}
614                                                         }
615                                                         SwingUtilities.invokeLater(new Runnable() {
616                                                                 public void run() {
617                                                                         repaint();
618                                                                         needRepaint = false;
619                                                                 }
620                                                         });
621                                                 }
622                                         }, "GoodRepainter")).start();
623                                 }
624                         }
625                         private boolean needRepaint;
626                 }
627
628                 public void setPreferredSize(Dimension preferredSize) {
629                         super.setPreferredSize(preferredSize);
630                         // this causes the first paint to usually be
631                         // unnecessary, but I don't know how to avoid that.
632                         revalidate();
633                 }
634
635                 /*
636                  * Here's all the mouse handling stuff.  It's here instead of
637                  * in the parent so it doesn't interfere with the parent's
638                  * possible attempts to do "hand-scrolling".
639                  */
640
641                 // these events get translated and forwarded to our PointListeners.
642                 // MouseListener:
643                 public void mouseClicked (MouseEvent event) { dispatchMouseEvent(event); }
644                 public void mouseEntered (MouseEvent event) { dispatchMouseEvent(event); }
645                 public void mouseExited  (MouseEvent event) { dispatchMouseEvent(event); }
646                 public void mousePressed (MouseEvent event) { dispatchMouseEvent(event); }
647                 public void mouseReleased(MouseEvent event) { dispatchMouseEvent(event); }
648                 // MouseMotionListener:
649                 public void mouseDragged (MouseEvent event) { dispatchMouseEvent(event); }
650                 public void mouseMoved   (MouseEvent event) { dispatchMouseEvent(event); }
651
652
653                 /**
654                 * This translates the event's point to real space, then forwards
655                 * it and the translated point to our PointListeners
656                 */
657                 private void dispatchMouseEvent(MouseEvent event) {
658                         // we assume realToScreen is the same transform that
659                         // was used to draw the currently-visible points on the screen,
660                         // which will almost always be the case.  Things could get weird
661                         // if the user resizes the window and then clicks a point before
662                         // the window is fully redrawn.  Oh well.
663
664                         if(realToScreen == null) return;
665                         try {
666                                 // transform from screen to real coordinates
667                                 Point2D.Double screenPoint = new Point2D.Double(event.getX(), event.getY());
668                                 Point2D.Double realPoint = new Point2D.Double();
669                                 realToScreen.inverseTransform(screenPoint, realPoint);
670
671                                 // notify each pointlistener
672                                 Iterator iterator = pointListeners.iterator();
673                                 while(iterator.hasNext()) {
674                                         PointListener pointListener = (PointListener)iterator.next();
675                                         pointListener.pointReceived(event, realPoint);
676                                 }
677                         } catch(NoninvertibleTransformException exception) {
678                                 // This should never happen, but we need to tell Java it won't.
679                                 System.out.println("Couldn't inverse transform the point");
680                         }
681                 }
682
683                 // allows us to schedule "good" repaints for later.
684                 private GoodRepainter goodRepainter = new GoodRepainter();
685         };
686
687         // Between 0 (showing whole map) and 1 (pixel for pixel), can be above 1
688         public void scaleRelative(double zoom) {
689                 System.out.println("scaleRelative: " + zoom + " " + temporaryLowQuality);
690                 if(backgroundMap == null) return;
691                 double fudge = 1; // almost 1
692                 
693                 // todo - make this 1 px less than the size it could be if there were no scrollbars
694                 Dimension currentVisibleSize = scrollPane.getViewport().getExtentSize();//getSize();
695                 
696                 updateGraphRectangle();
697                
698                 double zeroScale = Math.min(
699                         currentVisibleSize.width * fudge / graphRectangle.width,
700                         currentVisibleSize.height * fudge / graphRectangle.height
701                 );
702                
703                
704                 double oneScale = backgroundMap.getPixelForPixelScale();
705                 double scale;
706                 if(zoom <= 1)
707                         scale = (oneScale - zeroScale) * zoom + zeroScale;
708                 else
709                         scale = oneScale * zoom;               
710                
711                 setScale(scale, scale);
712         }
713
714         public Point2D.Double getVisibleRegionCenter() {
715                 updateRealToScreen();
716
717                 Rectangle rect = scrollPane.getViewport().getViewRect();
718                 Point2D.Double point = new Point2D.Double(
719                         rect.x + rect.width / 2,
720                         rect.y + rect.height / 2
721                 );
722
723                 try {
724                         realToScreen.inverseTransform(point, point);
725                 } catch(NoninvertibleTransformException e) {
726                         System.out.println("Couldn't invert the transform.");
727                 }
728
729                 return point;
730         }
731
732         public void setVisibleRegionCenter(Point2D.Double newCenter) {
733                 dontComputeAndChangePreferredSize = true;
734                 updateRealToScreen();
735                 dontComputeAndChangePreferredSize = false;
736
737
738                 Point screenPoint = new Point();
739                 realToScreen.transform(newCenter, screenPoint);
740
741                 // there really should be no paints before this point, as we shouldn't
742                 // have done anything
743
744                 JViewport viewport = scrollPane.getViewport();
745
746
747                 Rectangle rect = viewport.getViewRect();
748
749                 int oldScrollMode = viewport.getScrollMode();
750                 viewport.setScrollMode(JViewport.SIMPLE_SCROLL_MODE);
751
752                 // these two actions really ought to be atomic
753                 dontPaint = true;
754                 computeAndChangePreferredSize();
755                 dontPaint = false;
756                 viewport.setViewPosition(new Point(
757                         (int)(screenPoint.x - rect.width / 2),
758                         (int)(screenPoint.y - rect.height / 2)
759                 ));
760
761                 viewport.setScrollMode(oldScrollMode);
762         }
763
764         // whether we should try to render the map at highest quality
765         public void setHighQuality(boolean b) { highQuality = b; }
766
767         // if someone's dragging a slider or something, set this
768         // make darn sure to unset it at some point
769         public void setTemporaryLowQuality(boolean b) { temporaryLowQuality = b; }
770
771         /// here's the graph we refer to
772         private Graph graph;
773
774         /// a rectangle, in real coordinates, that contains all visible things.
775         private Rectangle2D.Double graphRectangle;
776         /// tells us how to scale points from real to screen, but does not
777         /// slide the coordinates so they're all positive
778         private AffineTransform scale;
779         /// tells us how to translate stuff to the screen so it's positive
780         private AffineTransform realToScreen;
781
782         /// this is the drawing pane
783         private DrawingPane drawingPane = new DrawingPane();
784         /// this is the scrollbars
785         private JScrollPane scrollPane = new JScrollPane(drawingPane);
786
787         /**
788          * These are cached because they're slow to recompute
789          * Set objectsToPainters to null when they must be updated.
790          */
791         private Map objectsToPainters;
792         private Map paintersToObjectLists;
793
794         /// default painters
795         private PhysicalNodePainter defaultNodePainter = new PhysicalNodePainter(Color.BLACK, 3);
796         private PhysicalEdgePainter defaultEdgePainter = new PhysicalEdgePainter(Color.BLACK, (float)1.5);
797
798         /// The path we're showing
799         private Path path;
800         /// How to draw nodes and edges on that path
801         private PhysicalNodePainter pathNodePainter = new PhysicalNodePainter(Color.BLUE, 5);
802         private PhysicalEdgePainter pathEdgePainter = new PhysicalEdgePainter(Color.BLUE, 5);
803
804         /// The currently selected object
805         private Object selectedObject;
806         /// How to draw it, depending on what type of thing it is
807         private PhysicalNodePainter selectedNodePainter = new PhysicalNodePainter(Color.RED, 7);
808         private PhysicalEdgePainter selectedEdgePainter = new PhysicalEdgePainter(Color.RED, 7);
809
810         /// Custom drawing commands
811         private Map objectsToCustomPainters [] = {new HashMap(), new HashMap(), new HashMap()};
812
813         /// Allows us to inform others about mouse events we receive
814         private Collection pointListeners = new LinkedList();
815
816         /// the backgroundmaps we draw
817         private BackgroundMap backgroundMap;
818
819         private boolean dontComputeAndChangePreferredSize = false;
820         private boolean dontPaint = false;
821
822         private boolean highQuality = true;
823         private boolean temporaryLowQuality = false;
824 }
Note: See TracBrowser for help on using the browser.