/* * 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 tufts.vue.gui.GUI; import java.awt.*; import java.awt.geom.*; import javax.swing.JViewport; /** * MapViewport -- a viewport that handles a dynamically sized extent. * * This would be better as a static inner class in MapViewer.java, but * it's here just to keep the code better organized. * * The viewport code is complicated to deal with the fact that we * operate on an infinite canvas and need to guess at something * reasonable to do in a bunch of different cases, and because * JScrollPane's/JViewport weren't designed to handle components that * may grow up/left as opposed to just down/right. * * In JViewport, The EXTENT is the physical, visible JPanel, through which * the contents of the VIEW (the MapViewer) are scrolled. Here, we * call the view the CANVAS. * * @version $Revision: 1.17 $ / $Date: 2007/08/28 17:35:02 $ / $Author: sfraize $ */ public class MapViewport extends JViewport implements VueConstants { private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(MapViewport.class); private /*final*/ MapViewer viewer; // should be final, but can't due to JScrollPane init code private final javax.swing.JScrollPane scrollPane; //private Rectangle2D lastMapBounds = new Rectangle2D.Float(); private Dimension lastCanvas = new Dimension(); private Point2D lastMapLocationAtCanvasOrigin = new Point2D.Float(); public MapViewport(javax.swing.JScrollPane scrollPane) { // if (viewer == null) // throw new NullPointerException("viewer is null"); // this.viewer = viewer; this.scrollPane = scrollPane; } // public MapViewport(MapViewer viewer) { // setView(viewer); // } // public MapViewport() {} @Override public void setView(Component view) { viewer = (MapViewer) view; // if (view != viewer) // throw new Error("view != viewer; " + view + " != " + viewer); super.setView(view); } private LWMap getMap() { return viewer.getMap(); } /** * Configures the viewer to display the given map coordinate in the * 0,0 location of the panel. Note that if we're in a scroll * region, this results in setting what displays in the 0,0 of the * extent -- not what's actually on screen, unelss user happens to * be scrolled all the way up and to the left. * * E.g. -- to have map location 10,10 display in the upper left * hand corner of the extent (panel location 0,0) we use * setMapOriginoffset to position the 0,0 map offset position * at 10,10, thus when we draw, location 10,10 will be at * 0,0. This method is here to compensate for the zoom factor: * E.g., at a zoom of 200%, we actually have to set the map offset * to 20,20, as each map coordinate unit now takes up two pixels. * */ private void placeMapLocationAtCanvasOrigin(float mapX, float mapY) { viewer.setMapOriginOffset((float) (mapX * viewer.mZoomFactor), (float) (mapY * viewer.mZoomFactor), false); } private void placeMapLocationAtCanvasOrigin(Point2D.Float p) { placeMapLocationAtCanvasOrigin(p.x, p.y); } private Point2D.Float getMapLocationAtCanvasOrigin() { return new Point2D.Float ((float) (viewer.mOffset.x * viewer.mZoomInverse), (float) (viewer.mOffset.y * viewer.mZoomInverse)); } /** width of the "view" region we're scrolling over in a scroll pane. */ private int getCanvasWidth() { return viewer.getPreferredSize().width; //return viewer.getWidth(); } /** height of the extent region we're scrolling over in a scroll pane */ private int getCanvasHeight() { return viewer.getPreferredSize().height; //return viewer.getHeight(); } /** equivalent to JViewport.getViewSize() */ private Dimension getCanvasSize() { return viewer.getPreferredSize(); //return viewer.getSize(); } void setCanvasSize(Dimension d) { if (DEBUG.SCROLL) out(" setCanvasSize " + out(d)); setViewSize(d); viewer.setPreferredSize(d); //viewer.setSize(d); } @Override public Dimension getViewSize() { return getCanvasSize(); } private Point lastPosition; private boolean isAdjusting; @Override public void setViewPosition(Point p) { if (DEBUG.SCROLL) out("setViewPosition " + out(p)); if (!p.equals(lastPosition)) { // for when scroll-bars are dragged viewer.fireViewerEvent(MapViewer.Event.PAN, "MapViewport:setViewPosition0"); } lastPosition = p; super.setViewPosition(p); if (!isAdjusting()) viewer.trackViewChanges("MapViewport:setViewPosition1"); } public void setAdjusting(boolean b) { isAdjusting = b; //scrollPane.getHorizontalScrollBar().setValueIsAdjusting(b); //scrollPane.getVerticalScrollBar().setValueIsAdjusting(b); } public boolean isAdjusting() { if (DEBUG.SCROLL) Log.debug("isAdjusting=" + isAdjusting + ", horzAdjusting=" + scrollPane.getHorizontalScrollBar().getValueIsAdjusting() + ", vertAdjusting=" + scrollPane.getVerticalScrollBar().getValueIsAdjusting()); return isAdjusting || scrollPane.getHorizontalScrollBar().getValueIsAdjusting() || scrollPane.getVerticalScrollBar().getValueIsAdjusting(); } public void setVisibleCanvasCorner(Point2D p) { setCanvasPosition(new Point2D.Double(-p.getX(), -p.getY())); } public void zoomAdjust(Point2D mapAnchor, Point2D screenPositionOfMapAnchor) { final boolean centerZoomStyle = false; adjustCanvasSize(false, true, true, false, true); if (centerZoomStyle || screenPositionOfMapAnchor == null) placeMapLocationAtViewCenter(mapAnchor); else placeMapLocationAtViewLocation(mapAnchor, screenPositionOfMapAnchor); } public void placeMapLocationAtViewLocation(Point2D mapAnchor, Point2D viewLocation) { Point2D canvasAnchor = viewer.mapToScreenPoint2D(mapAnchor); if (DEBUG.SCROLL) System.out.println(" ZOOM CANVAS ANCHOR: " + out(canvasAnchor)); Point2D.Double canvasOffset = new Point2D.Double(); canvasOffset.x = canvasAnchor.getX() - viewLocation.getX(); canvasOffset.y = canvasAnchor.getY() - viewLocation.getY(); if (DEBUG.SCROLL) System.out.println(" ZOOM CANVAS OFFSET: " + out(canvasOffset)); setVisibleCanvasCorner(canvasOffset); } public void placeMapLocationAtViewCenter(Point2D mapAnchor) { placeMapLocationAtViewLocation(mapAnchor, new Point2D.Double(getWidth() / 2, getHeight() / 2)); /* Point2D canvasAnchor = viewer.mapToScreenPoint2D(mapAnchor); if (DEBUG.SCROLL) System.out.println(" ZOOM CANVAS ANCHOR: " + out(canvasAnchor)); Point2D.Double canvasOffset = new Point2D.Double(); canvasOffset.x = canvasAnchor.getX() - getWidth() / 2.0; canvasOffset.y = canvasAnchor.getY() - getHeight() / 2.0; if (DEBUG.SCROLL) System.out.println(" ZOOM CANVAS OFFSET: " + out(canvasOffset)); setVisibleCanvasCorner(canvasOffset); */ } void adjustSize() { adjustCanvasSize(false, true, true, true, false); } void adjustSize(boolean expand, boolean trimNorthWest, boolean trimSouthEast) { adjustCanvasSize(expand, trimNorthWest, trimSouthEast, true, false); } /** * adjustSize -- adjust the size of the MapViewer canvas - the viewport view * * Called after changes to map bounds (drag or resize operations). * * Adjust the size of the canvas -- the size of the region being scrolled over. This * changes as the size of the map bounds changes, and as we zoom in and out. E.g., zooming * in from 100% to 200% will generally double the size of the canvs.. Zooming to less than * 100% will generally set the canvas to the same size as the viewport. The canvas will never * be less than the size of the viewport. What happens exactly on each adjustment depends * on where the user is currently panned to -- e.g., on zooms, we want to center on the * viewport, which means that besides resizing the canvas for the soom factor, if we're * zooming in on, say, the upper left of the canvas, and the soom would result in the upper * left of the canvas now being in the middle of the viewport, we have to grow the canvas * and reset the offset so that the upper left of the canvas is in the upper left of the * viewport so the focused region is in the actual center of the screen. This is because * the canvas (the MapViewer JComponent), can never have a positive location -- it can never * be > 0,0, although if we scroll over it, it can take on negative location values as we scroll. * * @param expand -- automatically expand the canvas to cover the current map origin offset (mOffset) & viewport size * @param trimNorthWest -- trim north west corner of canvas to map bounds, and place map at upper left of display * @param trimSouthEst -- trim south west corner of canvas to the current on-screen viewport display size * @param validate - call validate at end (e.g., for intermediate adjustments) * @param intermediate - this is an intermediate adjustment, and thus we don't * need to ensure that the canvas isn't smaller than the view (e.g., we're about * to call setCanvasPosition, which will then adjust our minimum size). * * todo: Expand is incompatable with the trims -- reorganize arguments. * todo: expand not used */ private void adjustCanvasSize (boolean expand, boolean trimNorthWest, boolean trimSouthEast, boolean validate, boolean intermediate) { if (DEBUG.SCROLL && DEBUG.META) new Throwable("adjustSize").printStackTrace(); //------------------------------------------------------------------ // Compute the canvas, which is going to be the new total size // of the region we're going to have available to scroll over. // We always include the bounds of every object, as well as // the current map origin -- so grows up & to the left are // "permanent" until a an adjustExtend with both trims sis called (currently // only via ZoomFit). //------------------------------------------------------------------ final Rectangle2D mapBounds = getMap().getPaintBounds(); if (DEBUG.SCROLL) out("---MAP BOUNDS: " + out(mapBounds) + " expand="+expand + " trimNorthWest="+trimNorthWest + " trimSouthEast="+trimSouthEast + " validate="+validate + " intermediate="+intermediate ); if (DEBUG.SCROLL) out(" view position: " + out(getViewPosition())); // compute the size of the minumum canvas that can contain everything in the map Rectangle2D.Float mapCanvas = viewer.getContentBounds(); if (DEBUG.SCROLL) out(" map canvas: " + out(mapCanvas)); Point2D.Float mapLocationAtCanvasOrigin = getMapLocationAtCanvasOrigin(); if (trimNorthWest) { // If we're collapsing, compress the canvas by moving the // origin to the upper left hand corner of all the // component bounds. We "trim" the canvas of usused map // "whitespace" when we trimNorthWest. if (DEBUG.SCROLL) out(" old origin: " + out(viewer.mOffset)); placeMapLocationAtCanvasOrigin(mapCanvas.x, mapCanvas.y); if (DEBUG.SCROLL) out(" reset origin: " + out(viewer.mOffset)); } else { // add the current origin, otherwise everything would // always be jamming itself up against the upper left hand // corner. This has no effect unless they've moved the // component with the smallest x/y (the farthest to the upper // left). if (DEBUG.SCROLL) out(" add offset: " + out(viewer.mOffset)); if (DEBUG.SCROLL) out(" is map loc: " + out(mapLocationAtCanvasOrigin)); mapCanvas.add(mapLocationAtCanvasOrigin); if (DEBUG.SCROLL) out(" +plusOrigin: " + out(mapCanvas)); } // okay to call this mapToScreen while adjusting origin as we're // only interested in the zoom conversion for the size. final Dimension minCanvas = viewer.mapToScreenDim(mapCanvas); final Dimension curCanvas = getCanvasSize(); final Dimension newCanvas = new Dimension(minCanvas); final Dimension curView = getSize(); // current view size //if (!trimNorthWest && lastCanvas.equals(canvas) && lastMapLocationAtCanvasOrigin.equals(mapLocationAtCanvasOrigin)) // return; lastCanvas = minCanvas; lastMapLocationAtCanvasOrigin = mapLocationAtCanvasOrigin; if (!trimNorthWest) { // If canvas is outside the the current map origin (that is, // something's been dragged off the left or top of the screen), // reset the origin to include the region where the components // were moved to. boolean originGrew = false; // mOffset is what? //float ox = mOffset.x; //float oy = mOffset.y; float ox = mapLocationAtCanvasOrigin.x; float oy = mapLocationAtCanvasOrigin.y; if (mapCanvas.x < mapLocationAtCanvasOrigin.x) { ox = mapCanvas.x; originGrew = true; } if (mapCanvas.y < mapLocationAtCanvasOrigin.y) { oy = mapCanvas.y; originGrew = true; } if (originGrew) placeMapLocationAtCanvasOrigin(ox, oy); } //if (curView.equals(newCanvas)) return; // unless this is an intermediate adjustment, never let new size be less than current view if (!intermediate) { if (newCanvas.width < curView.width) newCanvas.width = curView.width; if (newCanvas.height < curView.height) newCanvas.height = curView.height; } if (!trimSouthEast) { // don't let new size be less than current canvas if (newCanvas.width < curCanvas.width) newCanvas.width = curCanvas.width; if (newCanvas.height < curCanvas.height) newCanvas.height = curCanvas.height; } if (DEBUG.SCROLL) { Dimension vp = getSize(); out("currnt viewport: " + out(vp)); if (!vp.equals(curView)) out("!!cur view size: " + out(curView)); // same as above out(" current canvas: " + out(curCanvas)); if (!curCanvas.equals(viewer.getSize())) out("!!!!actual size: " + out(viewer.getSize())); // same as above out(" minimum canvas: " + out(minCanvas)); out(" new canvas: " + out(newCanvas)); } setCanvasSize(newCanvas); if (validate) { // until call validate, calls to setLocation were triggering reshape with old size // but now we call setViewSize manually in setCanvasSize, which get's it set // in JViewport w/out the revalidate. if (DEBUG.SCROLL) out("calling revalidate"); revalidate(); } } @Override public void reshape(int x, int y, int w, int h) { if (DEBUG.SCROLL||DEBUG.PAINT||DEBUG.EVENTS||DEBUG.FOCUS) { boolean ignore = getX() == x && getY() == y && getWidth() == w && getHeight() == h; Log.debug("reshape: " + (ignore?"(no change) ":"") + w + " x " + h + " " + x + "," + y + " " + this); } super.reshape(x, y, w, h); //viewer.setFreshPaint(); // This is a workaround for repaint bugs in TextBox/JTextPane when it get's taller // (when you insert newlines). Why we get reshapes on the parent (ignoreable ones) // every time you type a key (or is it when the TextBox resizes?) I don't know, // or why we *must* do the repaint here -- e.g., detecting "enter" keypress // and repainting doesn't help in textbox. Maybe resize/reshape override there would? if (viewer.isEditingLabel()) repaint(); } // If canvas //if (expand) { //Point vPos = viewport.getViewPosition(); /* if (panning) { Point vPos = viewport.getViewPosition(); System.out.println("SCROLL: vp="+vPos); //canvas.add(vPos.x, vPos.y); //System.out.println(getMap().getLabel() + "plusViewerPos: " + canvas); canvas.add(vPos.x + viewport.getWidth(), vPos.y + viewport.getHeight()); System.out.println(getMap().getLabel() + " plusCorner: " + canvas); } */ void pan(int dx, int dy, boolean allowGrowth) { //Point location = viewport.getViewPosition(); Point location = viewer.getLocation(); // both x/y should always be <= 0 if (DEBUG.SCROLL) { out("-----------------------------------------------------------------------------"); out("PAN: dx=" + dx + ", dy=" + dy + " allowGrowth="+allowGrowth); out("PAN: viewport start: " + out(location)); } location.translate(-dx, -dy); if (DEBUG.SCROLL) out("PAN: viewport end: " + out(location)); /* if (!allowGrowth) { // If drag would take us beyond width or height of existing canvas, // clip to existing canvas. if (location.x + getWidth() > getCanvasWidth()) location.x = getCanvasWidth() - getWidth(); if (location.y + getHeight() > getCanvasHeight()) location.y = getCanvasHeight() - getHeight(); } */ if (DEBUG.SCROLL) out("PAN: setViewPosition " + out(location)); setCanvasPosition(location, allowGrowth); revalidate(); //viewer.fireViewerEvent(MapViewer.Event.PAN, "MapViewport::pan"); } private void setCanvasPosition(Point2D p) { setCanvasPosition(p, true); } private void setCanvasPosition(Point2D p, boolean allowGrowth) { if (DEBUG.SCROLL) out("setCanvasPosition " + out(p) + " allowGrowth=" + allowGrowth); Dimension canvas = getCanvasSize(); Dimension view = getSize(); if (DEBUG.SCROLL) { out("setCanvasPosition canvas: " + out(canvas)); out("setCanvasPosition view: " + out(view)); } boolean grew = false; boolean moved = false; double ox = viewer.mOffset.x; double oy = viewer.mOffset.y; double grow; if (p.getX() > 0) { grow = p.getX(); if (DEBUG.SCROLL) out("GROW LEFT " + grow); p.setLocation(0, p.getY()); ox -= grow; moved = true; canvas.width += grow; grew = true; } if (p.getY() > 0) { grow = p.getY(); if (DEBUG.SCROLL) out("GROW UP " + grow); p.setLocation(p.getX(), 0); oy -= grow; moved = true; canvas.height += grow; grew = true; } if (canvas.width + p.getX() < view.width) { grow = view.width - (canvas.width + p.getX()); if (DEBUG.SCROLL) out("GROW RIGHT " + grow); canvas.width += grow; grew = true; } if (canvas.height + p.getY() < view.height) { grow = view.height - (canvas.height + p.getY()); if (DEBUG.SCROLL) out("GROW DOWN " + grow); canvas.height += grow; grew = true; } double px = p.getX(); double py = p.getY(); int floorx = (int) px; int floory = (int) py; if (floorx != px || floory != py) { Point2D.Double decimal = new Point2D.Double(); decimal.x = px - floorx; decimal.y = py - floory; if (DEBUG.SCROLL) out("compensating " + out(decimal)); ox -= decimal.x; oy -= decimal.y; moved = true; if (!allowGrowth) out("setCanvasPosition: growth disallowed, losing compensation " + out(decimal)); } if (allowGrowth) { if (grew) setCanvasSize(canvas); if (moved) viewer.setMapOriginOffset(ox, oy); } setViewPosition(new Point(-floorx, -floory)); if (DEBUG.SCROLL) out("setCanvasPosition completed"); } /* if (p.x > 0 || p.y > 0) { out("GROW ORIGIN"); //placeMapLocationAtCanvasOrigin(mapCanvas.x, mapCanvas.y); //viewport.setViewPosition(p); //panScrollRegion(-p.x, -p.y, true); } */ /* if (location.x < 0) { if (allowGrowth) { if (DEBUG.SCROLL) out("PAN: GROW X " + location.x); ox += location.x; originMoved = true; location.x = 0; } else { // if drag would take us to left of existing canvas, clip location.x = 0; } } if (location.y < 0) { if (allowGrowth) { if (DEBUG.SCROLL) out("PAN: GROW Y " + location.y); oy += location.y; originMoved = true; location.y = 0; } else { // if drag would take us above existing canvas, clip location.y = 0; } } if (originMoved) { // not working -- adjustCanvas should // handle setPreferredSize? //setMapOriginOffset(ox, oy); Dimension s = getPreferredSize(); s.width += dx; s.height += dy; setCanvasSize(s); } out("setCanvasPosition, ASR:"); try { Thread.sleep(1000); } catch (Exception e) {} adjustCanvas(false, false); */ /* from from bottom of old pan: if (false) { Rectangle2D.Float canvas = viewer.getContentBounds(); //Point vPos = viewport.getViewPosition(); Point vPos = location; Rectangle2D.union(canvas, viewer.getVisibleMapBounds(), canvas); if (DEBUG.SCROLL) System.out.println(getMap().getLabel() + " plusVISMAP: " + canvas); //canvas.add(mOffset); //System.out.println(getMap().getLabel() + " plusOrigin: " + canvas); //System.out.println("SCROLL: vp="+vPos); // NOTE: Canvas is current a bunch of map coords... //canvas.add(vPos.x, vPos.y); //System.out.println(getMap().getLabel() + "plusViewerPos: " + canvas); //canvas.add(vPos.x + viewport.getWidth(), vPos.y + viewport.getHeight()); //System.out.println(getMap().getLabel() + " plusCorner: " + canvas); Dimension curSize = getSize(); int newWidth = curSize.width; int newHeight = curSize.height; Rectangle canvasSize = mapToScreenRect(canvas); if (canvasSize.width > newWidth) newWidth = canvasSize.width; if (canvasSize.height > newHeight) newHeight = canvasSize.height; Dimension newSize = new Dimension(newWidth, newHeight); System.out.println("PAN: size to " + newSize); setPreferredSize(newSize); viewer.setPreferredSize(viewer.mapToScreenDim(canvas)); revalidate(); } */ /* public void paint(Graphics g) { super.paint(g); out("paint"); Point center = new Point(getWidth() / 2, getHeight() / 2); g.setColor(Color.blue); g.drawLine(-99999, center.y, 99999, center.y); g.drawLine(center.x, -99999, center.x, 99999); } public Dimension getViewSize() { //new Throwable("getViewSize").printStackTrace(); return viewer.mapToScreenDim(viewer.getContentBounds()); } void update() { fireStateChanged(); } */ public String toString() { return "MapViewport[" + this.viewer + "]"; } private void out(Object o) { Log.debug(viewer.getDiagName() + " " + (o==null?"null":o.toString())); //System.out.println(this + " " + (o==null?"null":o.toString())); } private String out(Point2D p) { return (float)p.getX() + ", " + (float)p.getY(); } private String out(Rectangle2D r) { return "" + (float)r.getX() + ", " + (float)r.getY() + " " + (float)r.getWidth() + " x " + (float)r.getHeight() ; } private String out(Dimension d) { return d.width + " x " + d.height; } }