/* * Copyright 2003-2010 Tufts University Licensed under the * Educational Community License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. You may * obtain a copy of the License at * * http://www.osedu.org/licenses/ECL-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an "AS IS" * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing * permissions and limitations under the License. */ package tufts.vue; import java.awt.*; import java.awt.event.*; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; /** * Implements a panel for displaying a map overview, including * the currently visible viewport, and moving (panning) the currently * visible viewport. * * @version $Revision: 1.70 $ / $Date: 2010-02-03 19:17:41 $ / $Author: mike $ * @author Scott Fraize * */ // TODO: fix our aspect to that of canvas (if that's what we tracking, the whole map otherwise); public class MapPanner extends javax.swing.JPanel implements VueConstants, MapViewer.Listener, LWComponent.Listener, MouseListener, MouseMotionListener, MouseWheelListener { private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(MapPanner.class); private MapViewer mapViewer; // active MapViewer private double zoomFactor; // zoomFactor that will fit entire map in the panner private Point dragStart; // where mouse was at mouse press private Point lastDrag; // where mouse was at last drag private Point2D mapStart; // where map origin was at mouse press private LWMap map; // active map // Enable this to keep viewport always visible in panner: (it causes while-you-drag // zoom adjusting tho, which can be a bit disorienting) private static final boolean ViewerViewportAlwaysVisible = true; // If false, map in panner will constantly resize to fit // as large a visible area as possible, tho not fully supported yet, // as dragging is funky if ViewerViewportAlwaysVisible is true, // and the viewport is dragged outside the total LWMap bounds (onto empty canvas). private static final boolean FullScrollCanvasAlwaysVisible = false; // false not fully supported yet (dragging wierd once hit edges: not absolute based dragged) // Also: at bottom and right, MapViewer jitters. What really want is ability to allow // this, but say, not leave the at least a corner on the existing LWMap bounds. private static final boolean AutomaticallyGrowScrollRegions = true; private static final int MapMargin = 0; //private static final int MapMargin = ViewerViewportAlwaysVisible ? 5 : 50; /** * Get's global (thru AWT hierarchy) MapViewerEvent's * to know what to display & when to update. It's * intended that there only be one MapPanner in the * application at a time. */ public MapPanner() { //setPreferredSize(new Dimension(150,100)); addMouseListener(this); addMouseMotionListener(this); addMouseWheelListener(this); setMinimumSize(new Dimension(200,125)); VUE.addActiveListener(MapViewer.class, this); EventHandler.addListener(MapViewer.Event.class, this); } public void addNotify() { super.addNotify(); if (getParent() instanceof Window) ((Window)getParent()).setFocusableWindowState(false); } public void activeChanged(ActiveEvent e, MapViewer viewer) { setViewer(viewer); } /** * Instances of MapViewer raise MapViewer.Event's * as they act, and the MapPanner hears them all * here. */ public void eventRaised(MapViewer.Event e) { if (VUE.inNativeFullScreen()) return; if (e.isActivationEvent() && mapViewer == null) { setViewer(e.viewer); } else if (e.viewer == this.mapViewer) { if (e.id == MapViewer.Event.PAN) repaint(); else if (e.id == MapViewer.Event.ZOOM) updateZoomTitle(); } } private void updateZoomTitle() { String titleInfo = null; if (this.mapViewer != null) titleInfo = ZoomTool.prettyZoomPercent(this.mapViewer.getZoomFactor()); putClientProperty("TITLE-INFO", titleInfo); } private void setViewer(MapViewer mapViewer) { if (VUE.inNativeFullScreen()) return; if (DEBUG.FOCUS) out("setViewer " + mapViewer); if (this.mapViewer != mapViewer) { this.mapViewer = mapViewer; if (mapViewer != null) setMap(mapViewer.getMap()); repaint(); updateZoomTitle(); } } private void setMap(LWMap map) { if (DEBUG.FOCUS) out("setMap " + map); if (this.map != map) { if (this.map != null) this.map.removeLWCListener(this); this.map = map; // if (DEBUG.Enabled) this.map.addLWCListener(this); else // GAA -- very confusing... this.map.addLWCListener(this, LWKey.UserActionCompleted, LWKey.Repaint); } } public void LWCChanged(LWCEvent e) { repaint(); // //if (DEBUG.Enabled) Log.debug("LWCChanged: " + e); // Log.info("LWCChanged: " + e); // if (DEBUG.DYNAMIC_UPDATE || e.key == LWKey.UserActionCompleted || e.key == LWKey.RepaintAsync) // repaint(); } public void mousePressed(MouseEvent e) { if (DEBUG.MOUSE) out(e); dragStart = lastDrag = e.getPoint(); mapStart = mapViewer.getOriginLocation(); repaint(); } public void mouseReleased(MouseEvent e) { if (DEBUG.MOUSE) out(e); dragStart = lastDrag = null; } // TODO: What should remain constant is the absolute PANNING AMOUNT across the MAP: // nothing to do with the panner viewport itself: then we can have constant mouse // response on dragging, even if panner display is changing zoom... You're really // dragging the MAP, not the panner reticle... Also, this could allow for making // the map drag-unit much less chunky... Shit, tho this may FEEL right, it would // look funny, as the mouse would still drift from it's location relative to the // reticle... But at least it would REDUCE this drift, and get rid of the crazy // accelleration experiences as you drag reticle further off map, creating huge // canvas, which makes each mouse move look bigger... // Also: best guess constraint to address Melanie's concern about creating more // map canvas: the corners of the map can't go further out than the center // of the reticle... // Or if wanted to do SIMPLE: Panner uses it's own virtual canvas, which // is min canvas size (so union with actual scroll-pane generated canvas), // which is defined my map bounds plus 1/2 viewport dimensions, then // we never have dynamic scaling, and mouse response can be perfect. // Tho when zooming in/out, that means not only reticle changes, but // the panner displayed map gets bigger/smaller... So maybe something // else we can bas it on: how about the MapViewer viewport itself: // half of that? Crap, same problem: maybe half a viewport at 100% zoom? public void mouseDragged(MouseEvent e) { if (DEBUG.MOUSE) out(e); if (dragStart == null) return; /* Rectangle viewerBounds = new Rectangle(mapViewer.getWidth()-1, mapViewer.getHeight()-1); if (viewerBounds.isEmpty()) return; Rectangle2D mapViewerRect = mapViewer.screenToMapRect(viewerBounds); boolean keepx = false; if (mapViewerRect.getX() <= pannerMinX) keepx = true; // No good -- need to pre-compute this! // // It's surprisingly complex to try and figure out in advance if repositioning the map // change the panner zoom offset... (And we still need to support being "off the grid" // in any case because the user can always manually drag the main view into outer-space) */ if (DEBUG.SCROLL) out("mouse " + e.getPoint()); int x = e.getX(); int y = e.getY(); if (mapViewer.inScrollPane()) { /* if (x < 0 || x > getWidth() || y < 0 || y > getHeight()) { lastDrag = e.getPoint(); return; } */ // TODO: panScrollRegion needs dx,dy to work, but then we give up absolute // mouse delta tracking from drag start, which makes for poor correlation // between mouse movements and panner movements if the scale starts changing // (e.g., we go outside the LWMap bounds, and start auto-growing the // canvas). int dx = x - lastDrag.x; int dy = y - lastDrag.y; double factor = mapViewer.getZoomFactor() / this.zoomFactor; dx = (int) (dx * factor + 0.5); dy = (int) (dy * factor + 0.5); if (DEBUG.SCROLL) out("dx="+dx + " dy="+dy); mapViewer.panScrollRegion(dx, dy, AutomaticallyGrowScrollRegions); lastDrag = e.getPoint(); } else { if (ViewerViewportAlwaysVisible) { // hack till we disallow the maprect from going beyond edge if (x < 0) x = 0; else if (x > getWidth()-2) x = getWidth()-2; if (y < 0) y = 0; else if (y > getHeight()-2) y = getHeight()-2; } double factor = this.zoomFactor / mapViewer.getZoomFactor(); double dragOffsetX = (x - dragStart.getX()) / factor; double dragOffsetY = (y - dragStart.getY()) / factor; mapViewer.setMapOriginOffset(mapStart.getX() + dragOffsetX, mapStart.getY() + dragOffsetY); //if (mapViewer.inScrollPane()) // mapViewer.adjustExtent(); mapViewer.repaint(); repaint(); } } public void mouseWheelMoved(MouseWheelEvent e) { // if (mapViewer != null) // mapViewer.getMouseWheelListener().mouseWheelMoved(e); final int rotation = e.getWheelRotation(); final double zoomChangeFactor = 1.0 + -rotation * 0.01; // each rotation does +/- 1.0% on current zoom factor tufts.vue.ZoomTool.setZoom(mapViewer.getZoomFactor() * zoomChangeFactor); // if (rotation > 0) // tufts.vue.ZoomTool.setZoomSmaller(null); // else if (rotation < 0) // tufts.vue.ZoomTool.setZoomBigger(null); } public void paintComponent(Graphics g) { if (VUE.inNativeFullScreen()) return; if (DEBUG.PAINT) System.out.println("\nPANNER PAINTING"); super.paintComponent(g); if (mapViewer == null) { setViewer(VUE.getActiveViewer());//todo: remove // problem is at startup, somehow we no longer get an active viewer event // -- something got broke if (mapViewer == null) return; } final Rectangle pannerSize = new Rectangle(getSize()); pannerSize.width -= 1; pannerSize.height -= 1; paintViewerIntoRectangle(this, g, this.mapViewer, pannerSize, true); } public static DrawContext paintViewerIntoRectangle(Graphics g, final MapViewer viewer, final Rectangle pannerSize) { return paintViewerIntoRectangle(null, g, viewer, pannerSize, true); } // TODO: take a DrawContext, not a viewer /** * @return the DrawContext that was used to draw the viewer contents into the given paintRect (to provide zoom/offset for picking) * Note that the x/y location of paintRect is ignored. */ static DrawContext paintViewerIntoRectangle(final MapPanner panner, final Graphics g, final MapViewer viewer, final Rectangle paintRect, final boolean drawViewerReticle) { if (viewer.getVisibleWidth() < 1 || viewer.getVisibleHeight() < 1) { if (DEBUG.Enabled) System.out.println("MapPanner: paintViewerIntoRectangle: nothing to paint; visible size=" + viewer.getVisibleSize() + " in " + viewer); return null; } final LWMap map = viewer.getMap(); if (map == null) { Log.error("null map for viewer " + viewer); return null; } final Rectangle2D allComponentBounds = map.getBounds(); final Rectangle2D canvasRect = viewer.getCanvasMapBounds(); final Rectangle2D viewerRect = viewer.getVisibleMapBounds(); final Rectangle2D pannerRect; if (ViewerViewportAlwaysVisible && viewer.inScrollPane()) { if (FullScrollCanvasAlwaysVisible) // the fudgey margins go away with show full canvas -- which indicates // the problem w/out the canvas is obviously because we can *drag* to // edge of full canvas, but if not computing zoom with it, we'll // get zoomed out when we go off edge of map bounds to edge of canvas bounds. pannerRect = canvasRect.createUnion(allComponentBounds); else pannerRect = viewerRect.createUnion(allComponentBounds); } else pannerRect = allComponentBounds; //if (DEBUG.WORK) Log.debug("pannerRect: " + tufts.Util.fmt(pannerRect)); /* * Compute the zoom required to fit everything in the size of the * current panner tool window. */ final Point2D.Float offset = new Point2D.Float(); double zoomFactor = ZoomTool.computeZoomFit(paintRect.getSize(), DEBUG.MARGINS ? 0 : MapMargin, pannerRect, offset); if (panner != null) panner.zoomFactor = zoomFactor; /* * Construct a DrawContext to use in painting the entire * map on the panner window. */ final DrawContext dc = new DrawContext(g, zoomFactor, -offset.x, -offset.y, null, map, false); dc.g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, viewer.AA_ON);//pickup MapViewer AA state for testing dc.setDraftQuality(); // okay to skimp in rendering of panner image -- it's usually so tiny /* * Fill the background representing the currently active canvas region. * If the viewer is in a scroll-region, this will be the total area * it's scrolling over -- a large canvas. If not, it will simply be * the visible viewer canvas, which virtually "pan's" over the infinite * coordinate space the map lies in. */ // need to offset fill, so can't just use existing canvasRect //final Rectangle2D fillCanvas = viewer.screenToMapRect(new Rectangle(1,1, viewer.getWidth(), viewer.getHeight())); if (drawViewerReticle) { dc.g.setColor(map.getFillColor()); // round size of canvas down... //dc.g.fill(canvas); // now we only fill visible on-screen area: dc.g.fill(viewerRect); } /* * Now tell the active LWMap to draw itself here on the panner. */ map.draw(dc); if (drawViewerReticle) { /* * Show where the edge of the *visible* viewer region overlaps the map */ dc.setAntiAlias(false); if (panner == null) dc.setAbsoluteStroke(3); else dc.setAbsoluteStroke(1); dc.g.setColor(Color.red); dc.g.draw(viewerRect); } return dc; } public void mouseClicked(MouseEvent e) { if (DEBUG.MOUSE) out(e); } public void mouseEntered(MouseEvent e) { if (DEBUG.MOUSE) out(e); } public void mouseExited(MouseEvent e) { if (DEBUG.MOUSE) out(e); } public void mouseMoved(MouseEvent e) {} private void out(Object o) { System.out.println("\t*** " + this + " " + (o==null?"null":o.toString())); } public String toString() { return "MapPanner[" + mapViewer + "]"; } }