// License: GPL. See LICENSE file for details. package org.openstreetmap.josm.gui; import java.beans.PropertyChangeSupport; import java.util.Date; import java.util.Stack; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.beboj.CanvasView; import org.openstreetmap.josm.data.Bounds; import org.openstreetmap.josm.data.ProjectionBounds; import org.openstreetmap.josm.data.coor.CachedLatLon; import org.openstreetmap.josm.data.coor.EastNorth; import org.openstreetmap.josm.data.coor.LatLon; /** * GWT * * This class is not in the JOSM code base. */ /** * Add support for slippy map style zoom level (in contrast to JOSM's smooth zoom). */ public class DiscreteZoomNavigationSupport extends AbstractNavigationSupport { public PropertyChangeSupport propertyChangeManager = new PropertyChangeSupport(this); protected Integer zoom = 12; protected final static double FAC = 2.0 * Math.PI * 6378137.0 / 256.0; /** * The scale factor in x or y-units per pixel. This means, if scale = 10, * every physical pixel on screen are 10 x or 10 y units in the * northing/easting space of the projection. */ public double getScale() { return zoomToScale(zoom); } protected double zoomToScale(int zoom) { if (zoom < 0) throw new AssertionError(); return FAC / (1 << zoom); } protected final static double logFAC = Math.log(FAC); protected final static double log2 = Math.log(2.0); protected Integer scaleToZoom(Double s) { if (s == null) return null; int z = (int) Math.floor((logFAC - Math.log(s)) / log2); return Math.max(0, z); } public DiscreteZoomNavigationSupport(CanvasView view) { super(view); } @Override public boolean isReady() { return center != null && zoom >= 0; } public void zoomIn() { zoomTo(getCenter(), zoom + 1); } public void zoomOut() { if (zoom == 0) return; zoomTo(getCenter(), zoom - 1); } public int getZoom() { return zoom; } public void setZoom(int newZoom) { if (zoom != newZoom) { zoom = newZoom; pushZoomUndo(center, zoom); zoomNoUndoTo(center, newZoom); } } /** * Zoom to the given coordinate. * @param newCenter The center x-value (easting) to zoom to. * @param scale The scale to use. */ private void zoomTo(EastNorth newCenter, int newZoom) { Bounds b = getProjection().getWorldBoundsLatLon(); CachedLatLon cl = new CachedLatLon(newCenter); boolean changed = false; double lat = cl.lat(); double lon = cl.lon(); if(lat < b.getMin().lat()) {changed = true; lat = b.getMin().lat(); } else if(lat > b.getMax().lat()) {changed = true; lat = b.getMax().lat(); } if(lon < b.getMin().lon()) {changed = true; lon = b.getMin().lon(); } else if(lon > b.getMax().lon()) {changed = true; lon = b.getMax().lon(); } if(changed) { newCenter = new CachedLatLon(lat, lon).getEastNorth(); } int width = view.getWidth()/2; int height = view.getHeight()/2; LatLon l1 = new LatLon(b.getMin().lat(), lon); LatLon l2 = new LatLon(b.getMax().lat(), lon); EastNorth e1 = getProjection().latlon2eastNorth(l1); EastNorth e2 = getProjection().latlon2eastNorth(l2); double d = e2.north() - e1.north(); double newScale = zoomToScale(newZoom); if(d < height*newScale) { double newScaleH = d/height; e1 = getProjection().latlon2eastNorth(new LatLon(lat, b.getMin().lon())); e2 = getProjection().latlon2eastNorth(new LatLon(lat, b.getMax().lon())); d = e2.east() - e1.east(); if(d < width*newScale) { newZoom = scaleToZoom(Math.max(newScaleH, d/width)); } } else { d = d/(l1.greatCircleDistance(l2)*height*10); if(newScale < d) { newZoom = scaleToZoom(d); } } if (!newCenter.equals(center) || (zoom != newZoom)) { pushZoomUndo(center, zoom); zoomNoUndoTo(newCenter, newZoom); } } /** * Zoom to the given coordinate without adding to the zoom undo buffer. * @param newCenter The center x-value (easting) to zoom to. * @param scale The scale to use. */ private void zoomNoUndoTo(EastNorth newCenter, int newZoom) { if (!newCenter.equals(center)) { EastNorth oldCenter = center; center = newCenter; propertyChangeManager.firePropertyChange("center", oldCenter, newCenter); } if (zoom != newZoom) { double oldZoom = zoom; zoom = newZoom; propertyChangeManager.firePropertyChange("scale", oldZoom, newZoom); } view.repaint(); fireZoomChanged(); } @Override public void zoomTo(EastNorth newCenter) { zoomTo(newCenter, zoom); } @Override public void zoomTo(LatLon newCenter) { if(newCenter instanceof CachedLatLon) { zoomTo(((CachedLatLon)newCenter).getEastNorth(), zoom); } else { zoomTo(getProjection().latlon2eastNorth(newCenter), zoom); } } // public void zoomToFactor(double x, double y, double factor) { // double newScale = getScale()*factor; // // New center position so that point under the mouse pointer stays the same place as it was before zooming // // You will get the formula by simplifying this expression: newCenter = oldCenter + mouseCoordinatesInNewZoom - mouseCoordinatesInOldZoom // zoomTo(new EastNorth( // center.east() - (x - view.getWidth()/2.0) * (newScale - getScale()), // center.north() + (y - view.getHeight()/2.0) * (newScale - getScale())), // newScale); // } // public void zoomToFactor(EastNorth newCenter, double factor) { // zoomTo(newCenter, scale*factor); // } // // public void zoomToFactor(double factor) { // zoomTo(center, scale*factor); // } @Override public void zoomTo(ProjectionBounds box) { // -20 to leave some border int w = view.getWidth()-20; if (w < 20) { w = 20; } int h = view.getHeight()-20; if (h < 20) { h = 20; } double scaleX = (box.maxEast-box.minEast)/w; double scaleY = (box.maxNorth-box.minNorth)/h; int newZoom = scaleToZoom(Math.max(scaleX, scaleY)); zoomTo(box.getCenter(), newZoom); } public void zoomTo(Bounds box) { zoomTo(new ProjectionBounds(getProjection().latlon2eastNorth(box.getMin()), getProjection().latlon2eastNorth(box.getMax()))); } private class ZoomData { LatLon center; int zoom; public ZoomData(EastNorth center, int zoom) { this.center = new CachedLatLon(center); this.zoom = zoom; } public EastNorth getCenterEastNorth() { return getProjection().latlon2eastNorth(center); } public int getZoom() { return zoom; } } private Stack<ZoomData> zoomUndoBuffer = new Stack<ZoomData>(); private Stack<ZoomData> zoomRedoBuffer = new Stack<ZoomData>(); private Date zoomTimestamp = new Date(); private void pushZoomUndo(EastNorth center, int zoom) { Date now = new Date(); if ((now.getTime() - zoomTimestamp.getTime()) > (Main.pref.getDouble("zoom.undo.delay", 1.0) * 1000)) { zoomUndoBuffer.push(new ZoomData(center, zoom)); if (zoomUndoBuffer.size() > Main.pref.getInteger("zoom.undo.max", 50)) { zoomUndoBuffer.remove(0); } zoomRedoBuffer.clear(); } zoomTimestamp = now; } public void zoomPrevious() { if (!zoomUndoBuffer.isEmpty()) { ZoomData zd = zoomUndoBuffer.pop(); zoomRedoBuffer.push(new ZoomData(center, zoom)); zoomNoUndoTo(zd.getCenterEastNorth(), zoom); } } public void zoomNext() { if (!zoomRedoBuffer.isEmpty()) { ZoomData zd = zoomRedoBuffer.pop(); zoomUndoBuffer.push(new ZoomData(center, zoom)); zoomNoUndoTo(zd.getCenterEastNorth(), zd.getZoom()); } } public boolean hasZoomUndoEntries() { return !zoomUndoBuffer.isEmpty(); } public boolean hasZoomRedoEntries() { return !zoomRedoBuffer.isEmpty(); } }