/* * 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.Util; import java.awt.Container; import java.awt.Point; import java.awt.Dimension; import java.awt.Rectangle; import java.awt.Color; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.awt.geom.Dimension2D; import javax.swing.*; /** * * Zoom tool handler for MapViewer, and static code for computing * zoom needed to display an arbitraty map region into an arbitrary * pixel region. * * @version $Revision: 1.92 $ / $Date: 2010-02-03 19:17:41 $ / $Author: mike $ * @author Scott Fraize * */ public class ZoomTool extends VueTool implements VueConstants { private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(ZoomTool.class); static private final int ZOOM_MANUAL = -1; static private final double[] ZoomDefaults = { // 1.0/100, 1.0/64, 1.0/48, 1.0/32, 1.0/24, 1.0/16, 1.0/12, 1.0/8, 1.0/6, 1.0/5, 1.0/4, 1.0/3, 1.0/2, 2.0/3, 0.75, 1.0/64, 1.0/32, 1.0/16, 1.0/8, 1.0/4, 1.0/2, 0.75, 1.0, 1.25, 1.5, 2, 3, 4, 6, 8, 12, 16, 24, 32, 48, 64, 128 // 1.5, 2, 4, 8, 16, 32, 64 //, 96, 128, 256, 384, 512 }; static private final int ZOOM_FIT_PAD = 20; // make this is > SelectionStrokeWidth & SelectionHandleSize static private final double MAX_ZOOM = ZoomDefaults[ZoomDefaults.length - 1]; public ZoomTool() { super(); setActiveWhileDownKeyCode(java.awt.event.KeyEvent.VK_BACK_QUOTE); } public JPanel getContextualPanel() { return VueToolbarController.getController().getSuggestedContextualPanel(); } private static final Color SelectorColor = Color.red; private static final Color SelectorColorInverted = new Color(0,255,255); // inverse of red public void drawSelector(DrawContext dc, java.awt.Rectangle r) { /* if (VueUtil.isMacPlatform()) g.setXORMode(SelectorColorInverted); else g.setXORMode(SelectorColor); */ dc.g.setColor(Color.red); super.drawSelector(dc, r); } @Override public boolean usesRightClick() { return true; } /* public boolean isZoomOutToMapMode() { return getSelectedSubTool().getID().equals("zoomTool.zoomOutToMap"); } */ public boolean isZoomFullScreenMode() { return getSelectedSubTool().getID().equals("zoomTool.zoomFullScreen"); } public boolean isZoomOutMode() { return getSelectedSubTool().getID().equals("zoomTool.zoomOut"); } public boolean isZoomInMode() { return getSelectedSubTool().getID().equals("zoomTool.zoomIn"); } @Override public boolean supportsSelection() { return false; } @Override public boolean supportsDraggedSelector(MapMouseEvent e) { if (false && e.getPicked() != null) // is causing a pick traversal for ever mouse drag event: too slow return false; // todo: if zoom level on viewer == MaxZoom, return false // This is so that if they RIGHT click, the dragged selector doesn't appear -- // because right click in zoom does a zoom out, and it makes less sense to // zoom out on a particular region. // Need to recognize button 1 on a drag, where getButton=0, or a release, where modifiers 0 but getButton=1 return !isZoomOutMode() && (e.getButton() == MouseEvent.BUTTON1 || (e.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK) != 0); } private LWComponent zoomedTo; private LWComponent zoomedToFull; private LWComponent oldFocal; private LWSlide editingFocal; // for now, should only ever be an LWSlide private boolean ignoreRelease = false; // /** Enabled rollover indication highlighting */ // @Override // public boolean handleMouseMoved(MapMouseEvent e) { // if (isZoomFullScreenMode()) { // if (e.getPicked() != null) { // e.getViewer().setIndicated(e.getPicked()); // } else { // e.getViewer().setIndicated(null); // } // } // return false; // } public LWComponent getZoomedTo() { return zoomedTo; } /** called by SelectionTool */ void setEditingFocal(LWSlide editFocal, LWComponent oldFocal) { this.editingFocal = editFocal; //this.oldFocal = oldFocal; } public void handleToolSelection(boolean selected, VueTool otherTool) { super.handleToolSelection(selected, otherTool); // for debug // TODO: this is by no means perfect yet, and has confusing cases // if you switch to full screen and back, etc. if (selected && editingFocal != null) { final MapViewer viewer = VUE.getActiveViewer(); if (viewer == null) return; viewer.popFocal(); zoomToSlide(viewer, editingFocal, false); } editingFocal = null; } // Classic simple version: private boolean disableToggleZoom = false; @Override public boolean handleMousePressed(MapMouseEvent e) { super.handleMousePressed(e); // System.out.println("isZoomInMode:" + isZoomInMode() // +" isZoomOutMode():"+isZoomOutMode() // +" isZoomFullScreen:"+isZoomFullScreenMode() // +" isZoomOutToMap:"+isZoomOutToMapMode()); if (isZoomInMode() || isZoomOutMode()) { if (zoomedTo != null) zoomedTo = null; return false; } //if (isZoomOutToMapMode()) // return false; final LWComponent picked = e.getPicked(); final MapViewer viewer = e.getViewer(); if (disableToggleZoom && !e.isShiftDown()) { disableToggleZoom = false; } if (disableToggleZoom) return false; if (zoomedTo != null && !e.isShiftDown()) { // already zoomed into something: un-zoom us if (oldFocal != null) { // we were focused into a slide: pop the focal viewer.loadFocal(oldFocal); // ZoomTool.setZoom(tempZoom); if (tufts.vue.gui.FullScreen.inWorkingFullScreen()) ZoomTool.setZoomFitRegion(tufts.vue.gui.FullScreen.getObscuredViewer(),VUE.getActiveMap().getTempBounds()); if (VUE.getActiveMap().getTempBounds() != null) ZoomTool.setZoomFitRegion(VUE.getActiveMap().getTempBounds()); oldFocal = null; } else { // zoom out to the whole map //setZoomFit(viewer, true); // setZoomFitRegion(viewer, viewer.getDisplayableMapBounds(), DEBUG.MARGINS ? 0 : ZOOM_FIT_PAD, false,true); //Point2D.Float originOffset = VUE.getActiveMap().getTempUserOrigin(); //VUE.getActiveViewer().setMapOriginOffset(originOffset.getX(), originOffset.getY()); //VUE.getActiveViewer().loadFocal(VUE.getActiveMap()); //VUE.setActive(LWMap.class, this, VUE.getActiveMap()); //double tempZoom = VUE.getActiveMap().getTempZoom(); //System.out.println("temp #s : " +originOffset + " " + tempZoom); //ZoomTool.setZoom(tempZoom); if (VUE.getActiveMap().getTempBounds() != null) { if (tufts.vue.gui.FullScreen.inWorkingFullScreen()) ZoomTool.setZoomFitRegion(tufts.vue.gui.FullScreen.getObscuredViewer(),VUE.getActiveMap().getTempBounds()); ZoomTool.setZoomFitRegion(VUE.getActiveMap().getTempBounds()); } else { if (tufts.vue.gui.FullScreen.inWorkingFullScreen()) setZoomFit(tufts.vue.gui.FullScreen.getObscuredViewer(),true); setZoomFit(viewer, true); } //if (originOffset != null) //VUE.getActiveViewer().setMapOriginOffset(originOffset.getX(), originOffset.getY()); } zoomedTo = null; ignoreRelease = true; return true; } else if (picked != null) { // nothing zoomed into, or shift was down, meaning keep zooming in: // zoomFactor = VUE.getActiveViewer().getZoomFactor(); //originOffset=VUE.getActiveMap().getUserOrigin(); if (picked instanceof LWSlide) { if (zoomedTo == picked) { if (VUE.getActiveViewer().getFocal() instanceof LWSlide) return false; // we're already zoomed to this slide: now load it as an editable focal // (slides only editable if they're the focal: not on map as slide icons) oldFocal = viewer.getFocal(); viewer.loadFocal((LWSlide) zoomedTo); } else { //System.out.println("zoomTo : " + zoomedTo); //System.out.println("picked : " + picked); if (zoomedTo == null) { VUE.getActiveMap().setTempZoom(VUE.getActiveViewer().getZoomFactor()); VUE.getActiveMap().setTempUserOrigin(VUE.getActiveViewer().getOriginLocation()); VUE.getActiveMap().setTempBounds(VUE.getActiveViewer().getVisibleMapBounds()); } if (tufts.vue.gui.FullScreen.inWorkingFullScreen()) zoomToSlide(tufts.vue.gui.FullScreen.getObscuredViewer(),(LWSlide)picked); zoomToSlide(viewer, (LWSlide) picked); } // System.out.println("SLIDE"); } else { final Rectangle2D zoomBounds; if (e.isControlDown()) zoomBounds = picked.getFanBounds(); else { if (!e.isShiftDown()) { VUE.getActiveMap().setTempZoom(VUE.getActiveViewer().getZoomFactor()); VUE.getActiveMap().setTempUserOrigin(VUE.getActiveViewer().getOriginLocation()); VUE.getActiveMap().setTempBounds(VUE.getActiveViewer().getVisibleMapBounds()); } zoomBounds = picked.getBounds(); } Point2D.Double offset = new Point2D.Double(); double newZoom = computeZoomFit(viewer.getVisibleSize(), 0, zoomBounds, offset); double curZoom = viewer.getZoomFactor(); // System.out.println("New Zoom : " + newZoom); // System.out.println("Cur ZOOM : " + curZoom); if (newZoom == curZoom) { disableToggleZoom = true; return false; } // System.out.println(e.getPicked().toString()); if (tufts.vue.gui.FullScreen.inWorkingFullScreen()) setZoomFitRegion(tufts.vue.gui.FullScreen.getObscuredViewer(), zoomBounds,0,true); setZoomFitRegion(viewer, zoomBounds, 0, true); //System.out.println("NODE"); } zoomedTo = picked; ignoreRelease = true; return true; } else return false; } /* // Experimental fancy version: @Override public boolean handleMousePressed(MapMouseEvent e) { super.handleMousePressed(e); if (isZoomInMode() || isZoomOutMode()) return false; if (isZoomOutToMapMode()) return false; LWComponent picked = e.getPicked(); final MapViewer viewer = e.getViewer(); // TODO: now that we've got this tweaked the way we want, // refactor so this code is actually clear //if (zoomedTo != null && !e.isShiftDown() && !e.isMetaDown()) { //if (picked == null || (picked == zoomedToFull && !picked.hasLinks())) { if (picked == null || (picked == zoomedToFull && e.isShiftDown())) { // already zoomed into something: un-zoom us if (oldFocal != null) { // we were focused into a slide: pop the focal viewer.loadFocal(oldFocal); oldFocal = null; } else { // zoom out to the whole map setZoomFit(viewer, true); } zoomedTo = null; zoomedToFull = null; ignoreRelease = true; return true; } else if (picked != null) { // nothing zoomed into, or shift was down, meaning keep zooming in: if (picked instanceof LWSlide) { if (zoomedTo == picked) { // we're already zoomed to this slide: now load it as an editable focal // (slides only editable if they're the focal: not on map as slide icons) out("LOADING FOCAL"); oldFocal = viewer.getFocal(); viewer.loadFocal((LWSlide) zoomedTo); } else { zoomToSlide(viewer, (LWSlide) picked); } } else { final Rectangle2D bounds; final int margin; if (picked instanceof LWLink) { bounds = picked.getFanBounds(); margin = 30; zoomedToFull = null; //} else if (e.isMetaDown()) { } else if (e.isShiftDown() || (!picked.hasLinks() && zoomedToFull != picked) || (zoomedTo == picked && zoomedToFull != picked)) { // if no links, no fan, so default to this with a margin bounds = picked.getBounds(); margin = picked.getFocalMargin(); zoomedToFull = picked; } else if (picked == zoomedToFull && !picked.hasLinks()) { LWComponent parent = picked.getParent(); bounds = parent.getBounds(); margin = parent.getFocalMargin(); if (parent instanceof LWMap) zoomedTo = zoomedToFull = null; else zoomedTo = zoomedToFull = parent; picked = null; // so zoomed-to stays null } else { zoomedToFull = null; if (VUE.inFullScreen()) { // this means we can animate bounds = picked.getCenteredFanBounds(); } else { // when can't animate, more likely to get lost with centering bounds = picked.getFanBounds(); } margin = 0; } setZoomFitRegion(viewer, bounds, margin, true); } zoomedTo = picked; ignoreRelease = true; return true; } else return false; } */ private void zoomToSlide(MapViewer viewer, final LWSlide slide) { // animated zoom-to: only works in full-screen zoomToSlide(viewer, slide, VUE.inFullScreen()); } private void zoomToSlide(MapViewer viewer, final LWSlide slide, boolean animate) { // zoom-to for the map region of the slide icon: setZoomFitRegion(viewer, //slide.getSourceNode().getMapSlideIconBounds(), slide.getBounds(), 0, animate); } // private void loadSlideFocal(final MapViewer viewer, LWSlide slide) // { // tufts.vue.gui.GUI.invokeAfterAWT(new Runnable() { // public void run() { // viewer.loadFocal(slide); // //setZoomFitRegion(viewer, slide.getBounds(), 0, false); // }}); // } @Override public DrawContext getDrawContext(DrawContext dc) { if (zoomedTo instanceof LWSlide) ; else dc.skipDraw = zoomedTo; // don't draw now: force on top later return dc; } @Override public void handlePostDraw(DrawContext dc, MapViewer viewer) { if (zoomedTo instanceof LWSlide) { ; } else if (zoomedTo != null) { zoomedTo.draw(dc); // force zoomed-to on-top } } @Override public PickContext initPick(PickContext pc, float x, float y) { if (zoomedTo != null) pc.pickDepth = zoomedTo.getPickLevel() + 1; return pc; } @Override public boolean handleMouseReleased(MapMouseEvent e) { if (DEBUG.TOOL) System.out.println(this + " handleMouseReleased " + e); if (ignoreRelease) { ignoreRelease = false; return true; } //Point p = e.getPoint(); Point2D p = e.getMapPoint(); if (e.isShiftDown() || e.getButton() != MouseEvent.BUTTON1 //|| toolKeyEvent != null && toolKeyEvent.isShiftDown() ) { if (isZoomOutMode() || disableToggleZoom) setZoomBigger(p); // else if (isZoomOutToMapMode()) // setZoom(1.0); else setZoomSmaller(p); } else { Rectangle box = e.getSelectorBox(); if (box != null && box.width > 10 && box.height > 10) { setZoomFitRegion(e.getMapSelectorBox()); // TODO: ensure a zoom-cap } else { if (isZoomOutMode()) setZoomSmaller(p); // else if (isZoomOutToMapMode()) // setZoom(1.0); else setZoomBigger(p); } } return true; } public boolean handleKeyPressed(KeyEvent e){return false;} public static boolean setZoomBigger(Point2D focus) { final double curZoom = VUE.getActiveViewer().getZoomFactor(); for (int i = 0; i < ZoomDefaults.length; i++) { if (ZoomDefaults[i] > curZoom) { setZoom(ZoomDefaults[i], focus); return true; } } return false; } public static boolean setZoomSmaller(Point2D focus) { final double curZoom = VUE.getActiveViewer().getZoomFactor(); for (int i = ZoomDefaults.length - 1; i >= 0; i--) { if (ZoomDefaults[i] < curZoom) { setZoom(ZoomDefaults[i], focus); return true; } } return false; } /** special zoom focus marker: use center of view as zoom anchor */ private static final Point2D CENTER_FOCUS = new Point2D.Float(); /** special zoom focus marker: don't adjust around a focal point: just adjust the zoom */ private static final Point2D DONT_FOCUS = new Point2D.Float(); public static void setZoom(double zoomFactor) { setZoom(VUE.getActiveViewer(), zoomFactor, true, CENTER_FOCUS, false); } public static void setZoom(MapViewer viewer, double zoomFactor) { setZoom(viewer, zoomFactor, true, CENTER_FOCUS, false); } public static void setZoom(double zoomFactor, Point2D focus) { setZoom(VUE.getActiveViewer(), zoomFactor, true, focus, false); } /** * @param focus - map location to anchor the zoom at (keep at same screen location) */ private static void setZoom(MapViewer viewer, double newZoomFactor, boolean adjustViewport, Point2D focus, boolean reset) { if (newZoomFactor < 0.001) newZoomFactor = 0.001; else if (newZoomFactor > 100000) newZoomFactor = 100000; // this is much simpler as the viewer now handles adjusting for the focal point if (focus == DONT_FOCUS) focus = null; //else if (focus instanceof Point) { // if a Point and not just a Point2D, it was a screen coordinate from setZoomBigger/Smaller //focus = viewer.screenToMapPoint((Point)focus); //} else if (adjustViewport && (focus == null || focus == CENTER_FOCUS)) { // If no user selected zoom focus point, zoom in to // towards the map location at the center of the // viewport. if (DEBUG.SCROLL) System.out.println("VISIBLE CENTER " + viewer.getVisibleCenter()); focus = viewer.screenToMapPoint2D(viewer.getVisibleCenter()); } /* // If zooming in, anchor to the click point. If zooming out, always // zoom out from the center. if (newZoomFactor > viewer.mZoomFactor) viewer.setZoomFactor(newZoomFactor, reset, focus, false); else viewer.setZoomFactor(newZoomFactor, reset, null, true); */ // Changed from the above to always zoom in OR out from the cursor. viewer.setZoomFactor(newZoomFactor, reset, focus, false); } public static void setZoomFitRegion(MapViewer viewer, Rectangle2D mapRegion, float borderGap, boolean animate) { setZoomFitRegion(viewer,mapRegion,borderGap,animate,false); } /** @param currently only works if NOT in a scroll pane */ public static void setZoomFitRegion (final MapViewer viewer, final Rectangle2D mapRegion, final float borderGap, final boolean animate, final boolean fitAndCenterMap) { if (mapRegion == null) { new Throwable("setZoomFitRegion: mapRegion is null for " + viewer).printStackTrace(); return; } if (viewer == null) throw new NullPointerException("null viewer in setZoomFitRegion"); final Point2D.Float offset = new Point2D.Float(); final double newZoom = computeZoomFit(viewer.getVisibleSize(), borderGap, mapRegion, offset); if (!viewer.canAnimate()) { Point2D center = new Point2D.Double(mapRegion.getCenterX(), mapRegion.getCenterY()); // if (newZoom > MaxZoom) // newZoom = MaxZoom; // TODO: this code should probably be referencing viewer, not VUE.getActiveViewer! Mike added this Feb-08? // What condition is this trying to handle? if ( (newZoom > 1 && ((VUE.getActiveViewer() == null) || (VUE.getActiveViewer() != null && (VUE.getActiveViewer().getFocal() == null || VUE.getActiveViewer().getFocal() instanceof LWMap ))) && fitAndCenterMap)) viewer.setZoomFactor(1, false, center, true); else viewer.setZoomFactor(newZoom, false, center, true); } else { // if (newZoom > MaxZoom) { // setZoom(viewer, MaxZoom, true, CENTER_FOCUS, true); // Point2D mapAnchor = new Point2D.Double(mapRegion.getCenterX(), mapRegion.getCenterY()); // Point focus = new Point(viewer.getVisibleWidth()/2, viewer.getVisibleHeight()/2); // double offsetX = (mapAnchor.getX() * MaxZoom) - focus.getX(); // double offsetY = (mapAnchor.getY() * MaxZoom) - focus.getY(); // viewer.setMapOriginOffset(offsetX, offsetY); // //viewer.resetScrollRegion(); // } else { if (animate) animatedZoomTo(viewer, newZoom, offset.x, offset.y); setZoom(viewer, newZoom, false, DONT_FOCUS, true); viewer.setMapOriginOffset(offset.x, offset.y); //} } } // may make sense to move a bunch of this code to MapViewer (e.g., animatedZoomTo, setZoom) public static void animatedZoomTo (final MapViewer viewer, final double newZoom, final float offsetX, final float offsetY) { if (!viewer.canAnimate()) return; viewer.setAnimating(true); try { doAnimatedZoomTo(viewer, newZoom, offsetX, offsetY); } catch (Throwable t) { Log.error("animatedZoomTo", t); } finally { viewer.setAnimating(false); } } /** Animate all but the last step of a zoom to the given given zoom and offset. Caller must provide the final calls. */ private static void doAnimatedZoomTo (final MapViewer viewer, final double newZoom, final float offsetX, final float offsetY) { // This will currenly only work on a viewer that's NOT // in a scroll-pane (so ony full-screen windows for now) // as the repaint does nothing to adjust the scrolling // viewport. if (!viewer.canAnimate()) return; final int frames = 4; final int framesOut = frames / 2; final int framesIn = framesOut; // will paint (frame - 1) intermediate frames: the last frame is left so the caller // can establish the exact final display coordinate final double zoomApex = .3; final double cz = viewer.getZoomFactor(); final double cx = viewer.getOriginX(); final double cy = viewer.getOriginY(); final double dz = newZoom - cz; //final double dzOut = zoomApex - cz; //final double dzIn = newZoom - zoomApex; final double dx = offsetX - cx; final double dy = offsetY - cy; final double iz = dz/frames; //final double izOut = dzOut/framesOut; //final double izIn = dzIn/framesIn; final double ix = dx/frames; final double iy = dy/frames; double zoom; // TODO: adjust zoom based on a curve (quadradic/parabola/sine wave) instead of // linearly, and will need to get much fancier to get this to work at all (even // with linear zoom adjustment): the offset also needs to be plotted to center // on the path tracked from the start point to the end point. for (int i = 1; i < frames; i++) { zoom = cz + iz*i; // if (i <= framesOut) // zoom = cz + izOut*i; // else // zoom = zoomApex - izIn*i; if (DEBUG.VIEWER|| DEBUG.PAINT) Log.debug(String.format("zoomAnimate frame %2d/%d %.1f%%", i, frames, zoom*100)); setZoom(viewer, zoom, false, DONT_FOCUS, true); viewer.setMapOriginOffset(cx + ix*i, cy + iy*i, false); viewer.paintImmediately(); } } /* private static void animatedZoomTo(MapViewer viewer, double newZoom, Point2D offset) { // This will currenly only work on a viewer that's NOT // in a scroll-pane (so ony full-screen windows for now) // as the repaint does nothing to adjust the scrolling // viewport. if (viewer.inScrollPane()) return; //final int frames = 20; //final int frames = 16; final int frames = DEBUG.Enabled ? 17 : 4; // will paint (frame - 1) intermediate frames: the last frame is left so the caller // can establish the exact final display coordinate final double cz = viewer.getZoomFactor(); final double cx = viewer.getOriginX(); final double cy = viewer.getOriginY(); final double dz = newZoom - cz; final double dx = offset.getX() - cx; final double dy = offset.getY() - cy; final double iz = dz/frames; final double ix = dx/frames; final double iy = dy/frames; double zoom; for (int i = 1; i < frames; i++) { zoom = cz + iz*i; setZoom(viewer, zoom, false, DONT_FOCUS, true); viewer.setMapOriginOffset(cx + ix*i, cy + iy*i); viewer.paintImmediately(); //if (DEBUG.Enabled) System.out.println("zoomAnimate " + zoom); } } */ public static void setZoomFitRegion(MapViewer viewer,Rectangle2D mapRegion) { setZoomFitRegion(viewer,mapRegion,0,false); } public static void setZoomFitRegion(Rectangle2D mapRegion, int edgePadding) { setZoomFitRegion(VUE.getActiveViewer(), mapRegion, edgePadding, false); } public static void setZoomFitRegion(Rectangle2D mapRegion) { setZoomFitRegion(mapRegion, 0); } /** fit all of the map contents for the given viewer to be visible */ public static void setZoomFit(MapViewer viewer) { setZoomFit(viewer, false); } /** fit all of the map contents for the given viewer to be visible */ public static void setZoomFit(MapViewer viewer, boolean animate) { // if don't want this to vertically center map in viewport, will need // to tell setZoomFitRegion above to compute center using mapRegion.getY() // instead of mapRegion.getCenterY() setZoomFitRegion(viewer, viewer.getDisplayableMapBounds(), DEBUG.MARGINS ? 0 : ZOOM_FIT_PAD, animate); //setZoomFitRegion(viewer, viewer.getMap().getBounds(), DEBUG.MARGINS ? 0 : ZOOM_FIT_PAD, false); // while it would be nice to call getActiveViewer().getContentBounds() // as a way to get bounds with max selection edges, etc, it computes some // of it's size based on current zoom, which we're about to change, so // we can't use it as our zoom fit becomes a circular, cycling computation. } /** fit everything in the current map into the current viewport */ public static void setZoomFit() { setZoomFit(VUE.getActiveViewer()); } /** Does exactly like zoom fit but never zooms in */ public static void setZoomOutFit() { final Point2D.Float offset = new Point2D.Float(); double zoomFactor = computeZoomFit(VUE.getActiveViewer().getVisibleSize(),0f,VUE.getActiveViewer().getDisplayableMapBounds(),offset); if(zoomFactor < 1) { setZoomFit(); } } public static double computeZoomFit(Dimension2D viewport, float borderGap, Rectangle2D bounds, Point2D outgoingOffset) { return computeZoomFit(viewport, borderGap, bounds, outgoingOffset, true, MAX_ZOOM); } public static double computeZoomFit(Dimension2D viewport, float borderGap, Rectangle2D bounds, Point2D outgoingOffset, double maxZoom ) { return computeZoomFit(viewport, borderGap, bounds, outgoingOffset, true, maxZoom); } public static double computeZoomFit(Dimension2D viewport, float borderGap, Rectangle2D bounds, Point2D outgoingOffset, boolean center) { return computeZoomFit(viewport, borderGap, bounds, outgoingOffset, center, MAX_ZOOM); } /** * Compute two items: the zoom factor that will fit everything * within the given map bounds into the given viewport, and put * into @param offset the offset to place the viewport at. Used to * figure out how to fit everything within a map on the screen and * where to pan to so you can see it all. * * @param outgoingOffset may be null (not interested in result) */ public static double computeZoomFit(Dimension2D viewport, float borderGap, Rectangle2D bounds, Point2D outgoingOffset, boolean centerSmallerDimensionInViewport, double maxZoom) { if (viewport.getWidth() <= 0 || viewport.getHeight() <= 0 || bounds.isEmpty()) { return 1.0; } else { return computeZoomFitImpl(viewport, borderGap, bounds, outgoingOffset, centerSmallerDimensionInViewport, maxZoom); } // if (DEBUG.PRESENT) { // Log.debug(String.format("computed zoom of %7.2f%% for map bounds %s in viewport %s", // newZoom * 100, Util.fmt(bounds), viewport)); // } } private static double computeZoomFitImpl(final Dimension2D viewport, final float borderGap, Rectangle2D bounds, final Point2D outgoingOffset, final boolean centerSmallerDimensionInViewport, final double maxZoom) { final double viewWidth, viewHeight; final double marginX, marginY; if (borderGap < 0) { // A borderGap that is negative indicates special semantics // for how to interpret the magnitude of it's value. if (true) { //----------------------------------------------------------------------------- // // 2008-04-23 SMF: change in default semantics: < 0 borderGap no // longer means "at scale" -- it now means a standard percent of the viewport // dimensions. E.g., -10 means create a minimum 10% of height vertical margin, // and 10% of width horizontal margin. // //----------------------------------------------------------------------------- final float percent = -borderGap; //Log.debug("viewport: " + Util.fmt(viewport) + "; gap=" + borderGap + "; pct=" + percent + "; bounds=" + Util.fmt(bounds)); // now claim that the viewport is smaller than it is before computing // the actual fit: final double w = viewport.getWidth(); final double h = viewport.getHeight(); marginX = (w * percent) / 100; marginY = (h * percent) / 100; //Log.debug(" marginX=" + marginX); //Log.debug(" marginY=" + marginY); viewWidth = w - marginX * 2; viewHeight = h - marginY * 2; //percentOfViewportMargin = true; //Log.debug("simulatedViewport=" + viewWidth + "x" + viewHeight); } else { // [ OLD SEMANTICS: ] < 0 borderGap means using compute the gap at scale // To do this we just add it to the bounds of the region we want to zoom // to. So if the region is small in pixel magnitude, this will produce // a big relative gap, but if the region is of large pixel magnitude, // this will produce a small relative gap. E.g., 10 pixels at the edge // of of 20x20 region produces a much larger gap when zoomed to than 10 // pixels at the edge of a 200x200 region. // One possible advantage to this method is that as we focal in on nodes // at smaller scales, the net margin grows -- so the size of the node, // while larger as focal, gives some subtle indication that it is deeper // in a hierarchy. final Rectangle2D.Float grownBounds = new Rectangle2D.Float(); grownBounds.setRect(bounds); grownBounds.x -= -borderGap; grownBounds.y -= -borderGap; grownBounds.width += -borderGap * 2; grownBounds.height += -borderGap * 2; bounds = grownBounds; viewWidth = viewport.getWidth(); viewHeight = viewport.getHeight(); marginX = 0; marginY = 0; } } else { marginX = borderGap; marginY = borderGap; viewWidth = (viewport.getWidth() - marginX * 2); viewHeight = (viewport.getHeight() - marginY * 2); } final double vertZoom = viewHeight / bounds.getHeight(); final double horzZoom = viewWidth / bounds.getWidth(); //final double vertZoom = Math.min(maxZoom, viewHeight / bounds.getHeight()); //final double horzZoom = Math.min(maxZoom, viewWidth / bounds.getWidth()); double newZoom; final boolean centerVertical; if (horzZoom < vertZoom) { newZoom = horzZoom; centerVertical = true; } else { newZoom = vertZoom; centerVertical = false; } // TODO: when we exceed max-zoom, centering in the smaller // dimension no longer works properly if (newZoom > maxZoom) newZoom = maxZoom; // Now center the components within the dimension // that had extra room to scale in. if (outgoingOffset != null) { double offsetX = bounds.getX() * newZoom - marginX; double offsetY = bounds.getY() * newZoom - marginY; if (centerSmallerDimensionInViewport) { if (centerVertical) offsetY -= (viewHeight - bounds.getHeight()*newZoom) / 2; else // center horizontal offsetX -= (viewWidth - bounds.getWidth()*newZoom) / 2; } outgoingOffset.setLocation(offsetX, offsetY); } //return newZoom < ZoomDefaults[0] ? ZoomDefaults[0] : newZoom; // never let us be less than min-zoom return newZoom < ZoomDefaults[0] ? ZoomDefaults[0] : newZoom; // never let us be less than min-zoom } public static String prettyZoomPercent(double zoom) { double zoomPct = zoom * 100; if (zoomPct < 10) { // if < 10% zoom, show with 1 digit of decimal value if it would be non-zero return String.format("%.1f%%", zoomPct); //return VueUtil.oneDigitDecimal(zoomPct) + "%"; } else if (zoom >= 100) { return Integer.valueOf((int) (zoom + 0.5)) + "x"; } else if (zoom >= 10) { return String.format("%.1fx", zoom); } else { //title += (int) Math.round(zoomPct); return ((int)Math.floor(zoomPct + 0.49)) + "%"; } //title += //return ((int)Math.floor(zoomPct + 0.49)) + "%"; } }