diff --git a/src/org/openstreetmap/josm/gui/MapSlider.java b/src/org/openstreetmap/josm/gui/MapSlider.java
index fab953d..2b3f726 100644
a
|
b
|
import javax.swing.event.ChangeListener;
|
12 | 12 | |
13 | 13 | import org.openstreetmap.josm.data.ProjectionBounds; |
14 | 14 | import org.openstreetmap.josm.gui.help.Helpful; |
| 15 | import static org.openstreetmap.josm.gui.MapView.snapZoomMode; |
15 | 16 | |
16 | 17 | class MapSlider extends JSlider implements PropertyChangeListener, ChangeListener, Helpful { |
17 | 18 | |
18 | 19 | private final MapView mv; |
19 | 20 | private boolean preventChange; |
| 21 | private static final double zoomStep = Math.pow(2, 0.2); |
20 | 22 | |
21 | 23 | MapSlider(MapView mv) { |
22 | | super(35, 150); |
| 24 | super(0, 150); |
23 | 25 | setOpaque(false); |
24 | 26 | this.mv = mv; |
25 | 27 | mv.addPropertyChangeListener("scale", this); |
… |
… |
class MapSlider extends JSlider implements PropertyChangeListener, ChangeListene
|
30 | 32 | |
31 | 33 | @Override |
32 | 34 | public void propertyChange(PropertyChangeEvent evt) { |
33 | | if (getModel().getValueIsAdjusting()) return; |
34 | | |
35 | | ProjectionBounds world = this.mv.getMaxProjectionBounds(); |
36 | | ProjectionBounds current = this.mv.getProjectionBounds(); |
37 | | |
38 | | double cur_e = current.maxEast-current.minEast; |
39 | | double cur_n = current.maxNorth-current.minNorth; |
40 | | double e = world.maxEast-world.minEast; |
41 | | double n = world.maxNorth-world.minNorth; |
42 | | int zoom = 0; |
43 | | |
44 | | while (zoom <= 150) { |
45 | | e /= 1.1; |
46 | | n /= 1.1; |
47 | | if (e < cur_e && n < cur_n) { |
48 | | break; |
49 | | } |
50 | | ++zoom; |
51 | | } |
| 35 | double maxScale = this.mv.getMaxScale(); |
| 36 | int zoom = (int) Math.round(Math.log(maxScale/mv.getScale())/Math.log(zoomStep)); |
52 | 37 | preventChange = true; |
53 | 38 | setValue(zoom); |
54 | 39 | preventChange = false; |
… |
… |
class MapSlider extends JSlider implements PropertyChangeListener, ChangeListene
|
57 | 42 | @Override |
58 | 43 | public void stateChanged(ChangeEvent e) { |
59 | 44 | if (preventChange) return; |
60 | | |
61 | | ProjectionBounds world = this.mv.getMaxProjectionBounds(); |
62 | | double fact = Math.pow(1.1, getValue()); |
63 | | double es = world.maxEast-world.minEast; |
64 | | double n = world.maxNorth-world.minNorth; |
65 | | |
66 | | this.mv.zoomTo(new ProjectionBounds(this.mv.getCenter(), es/fact, n/fact)); |
| 45 | double maxScale = this.mv.getMaxScale(); |
| 46 | double scale = maxScale/Math.pow(zoomStep, getValue()); |
| 47 | boolean isAdjusting = getModel().getValueIsAdjusting(); |
| 48 | snapZoomMode snap = isAdjusting ? snapZoomMode.FLOOR : snapZoomMode.STEP; |
| 49 | double snapped = mv.getSnappedScale(scale, snap); |
| 50 | this.mv.zoomTo(this.mv.getCenter(), snapped); |
| 51 | propertyChange(null); |
67 | 52 | } |
68 | 53 | |
69 | 54 | @Override |
diff --git a/src/org/openstreetmap/josm/gui/MapView.java b/src/org/openstreetmap/josm/gui/MapView.java
index ad767f2..23ef09a 100644
a
|
b
|
implements PropertyChangeListener, PreferenceChangedListener, OsmDataLayer.Layer
|
1103 | 1103 | |
1104 | 1104 | @Override |
1105 | 1105 | public void preferenceChanged(PreferenceChangeEvent e) { |
| 1106 | super.preferenceChanged(e); |
1106 | 1107 | synchronized (this) { |
1107 | 1108 | paintPreferencesChanged = true; |
1108 | 1109 | } |
diff --git a/src/org/openstreetmap/josm/gui/NavigatableComponent.java b/src/org/openstreetmap/josm/gui/NavigatableComponent.java
index feae89f..e82ee85 100644
a
|
b
|
import org.openstreetmap.josm.data.osm.WaySegment;
|
45 | 45 | import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor; |
46 | 46 | import org.openstreetmap.josm.data.osm.visitor.paint.PaintColors; |
47 | 47 | import org.openstreetmap.josm.data.preferences.IntegerProperty; |
| 48 | import org.openstreetmap.josm.data.preferences.BooleanProperty; |
48 | 49 | import org.openstreetmap.josm.data.projection.Projection; |
49 | 50 | import org.openstreetmap.josm.data.projection.Projections; |
| 51 | import org.openstreetmap.josm.data.projection.proj.Mercator; |
50 | 52 | import org.openstreetmap.josm.gui.download.DownloadDialog; |
51 | 53 | import org.openstreetmap.josm.gui.help.Helpful; |
52 | 54 | import org.openstreetmap.josm.gui.mappaint.MapPaintStyles; |
… |
… |
import org.openstreetmap.josm.gui.mappaint.mapcss.MapCSSStyleSource;
|
54 | 56 | import org.openstreetmap.josm.gui.util.CursorManager; |
55 | 57 | import org.openstreetmap.josm.tools.Predicate; |
56 | 58 | import org.openstreetmap.josm.tools.Utils; |
| 59 | import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent; |
| 60 | import org.openstreetmap.josm.data.Preferences.PreferenceChangedListener; |
57 | 61 | |
58 | 62 | /** |
59 | 63 | * A component that can be navigated by a {@link MapMover}. Used as map view and for the |
… |
… |
import org.openstreetmap.josm.tools.Utils;
|
62 | 66 | * @author imi |
63 | 67 | * @since 41 |
64 | 68 | */ |
65 | | public class NavigatableComponent extends JComponent implements Helpful { |
| 69 | public class NavigatableComponent extends JComponent implements Helpful, PreferenceChangedListener { |
66 | 70 | |
67 | 71 | /** |
68 | 72 | * Interface to notify listeners of the change of the zoom area. |
… |
… |
public class NavigatableComponent extends JComponent implements Helpful {
|
89 | 93 | }; |
90 | 94 | |
91 | 95 | public static final IntegerProperty PROP_SNAP_DISTANCE = new IntegerProperty("mappaint.node.snap-distance", 10); |
| 96 | public static final BooleanProperty PROP_SNAP_ZOOM = new BooleanProperty("zoom.snap-scale-to-mercator-zoom-levels", true); |
| 97 | public enum snapZoomMode { FLOOR, STEP, ROUND } |
92 | 98 | |
93 | 99 | public static final String PROPNAME_CENTER = "center"; |
94 | 100 | public static final String PROPNAME_SCALE = "scale"; |
… |
… |
public class NavigatableComponent extends JComponent implements Helpful {
|
143 | 149 | */ |
144 | 150 | public NavigatableComponent() { |
145 | 151 | setLayout(null); |
| 152 | Main.pref.addPreferenceChangeListener(this); |
| 153 | scale = getSnappedScale(scale, snapZoomMode.ROUND); |
146 | 154 | } |
147 | 155 | |
148 | 156 | protected DataSet getCurrentDataSet() { |
… |
… |
public class NavigatableComponent extends JComponent implements Helpful {
|
284 | 292 | getProjection().latlon2eastNorth(b.getMax())); |
285 | 293 | } |
286 | 294 | |
| 295 | // maximum: world in 256 pixels |
| 296 | // getSnappedScale() and also MaxSlider uses this value |
| 297 | // as scale for zoom 0 |
| 298 | public double getMaxScale() { |
| 299 | ProjectionBounds world = getMaxProjectionBounds(); |
| 300 | return Math.max( |
| 301 | world.maxNorth-world.minNorth, |
| 302 | world.maxEast-world.minEast |
| 303 | )/256; |
| 304 | } |
| 305 | |
287 | 306 | /* FIXME: replace with better method - used by Main to reset Bounds when projection changes, don't use otherwise */ |
288 | 307 | public Bounds getRealBounds() { |
289 | 308 | return new Bounds( |
… |
… |
public class NavigatableComponent extends JComponent implements Helpful {
|
409 | 428 | * @param initial true if this call initializes the viewport. |
410 | 429 | */ |
411 | 430 | public void zoomTo(EastNorth newCenter, double newScale, boolean initial) { |
412 | | Bounds b = getProjection().getWorldBoundsLatLon(); |
413 | 431 | ProjectionBounds pb = getProjection().getWorldBoundsBoxEastNorth(); |
414 | | int width = getWidth(); |
415 | | int height = getHeight(); |
416 | 432 | |
417 | 433 | // make sure, the center of the screen is within projection bounds |
418 | 434 | double east = newCenter.east(); |
… |
… |
public class NavigatableComponent extends JComponent implements Helpful {
|
423 | 439 | north = Math.min(north, pb.maxNorth); |
424 | 440 | newCenter = new EastNorth(east, north); |
425 | 441 | |
426 | | // don't zoom out too much, the world bounds should be at least |
427 | | // half the size of the screen |
428 | | double pbHeight = pb.maxNorth - pb.minNorth; |
429 | | if (height > 0 && 2 * pbHeight < height * newScale) { |
430 | | double newScaleH = 2 * pbHeight / height; |
431 | | double pbWidth = pb.maxEast - pb.minEast; |
432 | | if (width > 0 && 2 * pbWidth < width * newScale) { |
433 | | double newScaleW = 2 * pbWidth / width; |
434 | | newScale = Math.max(newScaleH, newScaleW); |
435 | | } |
436 | | } |
| 442 | // don't zoom out too much |
| 443 | newScale = Math.min(newScale, getMaxScale()); |
437 | 444 | |
438 | 445 | // don't zoom in too much, minimum: 100 px = 1 cm |
439 | | LatLon ll1 = getLatLon(width / 2 - 50, height / 2); |
440 | | LatLon ll2 = getLatLon(width / 2 + 50, height / 2); |
441 | | if (ll1.isValid() && ll2.isValid() && b.contains(ll1) && b.contains(ll2)) { |
442 | | double d_m = ll1.greatCircleDistance(ll2); |
443 | | double d_en = 100 * scale; |
444 | | double scaleMin = 0.01 * d_en / d_m / 100; |
445 | | if (!Double.isInfinite(scaleMin) && newScale < scaleMin) { |
446 | | newScale = scaleMin; |
447 | | } |
448 | | } |
| 446 | double d_m = getDist100Pixel(); |
| 447 | double scaleMin = 0.01 * scale / d_m; |
| 448 | if (!Double.isInfinite(scaleMin) && newScale < scaleMin) |
| 449 | newScale = scaleMin; |
449 | 450 | |
| 451 | newScale = getSnappedScale(newScale, snapZoomMode.FLOOR); |
450 | 452 | if (!newCenter.equals(center) || !Utils.equalsEpsilon(scale, newScale)) { |
451 | 453 | if (!initial) { |
452 | 454 | pushZoomUndo(center, scale); |
… |
… |
public class NavigatableComponent extends JComponent implements Helpful {
|
455 | 457 | } |
456 | 458 | } |
457 | 459 | |
| 460 | public double getSnappedScale(double newScale) { |
| 461 | return getSnappedScale(newScale, snapZoomMode.STEP); |
| 462 | } |
| 463 | |
| 464 | public double getSnappedScale(double newScale, snapZoomMode mode) { |
| 465 | if (!(PROP_SNAP_ZOOM.get() && "EPSG:3857".equals(Main.getProjection().toCode()))) return newScale; |
| 466 | double askedScale = newScale; |
| 467 | double tileSizeAtZeroZoom = getMaxScale(); |
| 468 | double tmsZoom = Math.log(tileSizeAtZeroZoom/newScale)/Math.log(2); |
| 469 | int tmsZoomLevel = (int) (mode == snapZoomMode.FLOOR ? Math.floor(tmsZoom) : Math.round(tmsZoom)); |
| 470 | newScale = tileSizeAtZeroZoom/Math.pow(2, tmsZoomLevel); |
| 471 | double diff = askedScale/this.scale-1; |
| 472 | if (mode == snapZoomMode.STEP && newScale == this.scale && Math.abs(diff) > 0.0001) { |
| 473 | if (diff > 0) newScale *= 2; |
| 474 | if (diff < 0) newScale /= 2; |
| 475 | } |
| 476 | return newScale; |
| 477 | } |
| 478 | |
458 | 479 | /** |
459 | 480 | * Zoom to the given coordinate without adding to the zoom undo buffer. |
460 | 481 | * |
… |
… |
public class NavigatableComponent extends JComponent implements Helpful {
|
484 | 505 | } |
485 | 506 | } |
486 | 507 | |
| 508 | public void zoomTo(double newScale) { |
| 509 | zoomTo(center, newScale); |
| 510 | } |
| 511 | |
487 | 512 | public void zoomTo(EastNorth newCenter) { |
488 | 513 | zoomTo(newCenter, scale); |
489 | 514 | } |
… |
… |
public class NavigatableComponent extends JComponent implements Helpful {
|
529 | 554 | } |
530 | 555 | |
531 | 556 | public void zoomToFactor(double x, double y, double factor) { |
532 | | double newScale = scale*factor; |
| 557 | double newScale = getSnappedScale(scale*factor); |
533 | 558 | // New center position so that point under the mouse pointer stays the same place as it was before zooming |
534 | 559 | // You will get the formula by simplifying this expression: newCenter = oldCenter + mouseCoordinatesInNewZoom - mouseCoordinatesInOldZoom |
535 | 560 | zoomTo(new EastNorth( |
… |
… |
public class NavigatableComponent extends JComponent implements Helpful {
|
539 | 564 | } |
540 | 565 | |
541 | 566 | public void zoomToFactor(EastNorth newCenter, double factor) { |
542 | | zoomTo(newCenter, scale*factor); |
| 567 | zoomTo(newCenter, getSnappedScale(scale*factor)); |
543 | 568 | } |
544 | 569 | |
545 | 570 | public void zoomToFactor(double factor) { |
546 | | zoomTo(center, scale*factor); |
| 571 | zoomTo(center, getSnappedScale(scale*factor)); |
547 | 572 | } |
548 | 573 | |
549 | 574 | public void zoomTo(ProjectionBounds box) { |
… |
… |
public class NavigatableComponent extends JComponent implements Helpful {
|
1516 | 1541 | } |
1517 | 1542 | repaint(); |
1518 | 1543 | } |
| 1544 | |
| 1545 | @Override |
| 1546 | public void preferenceChanged(PreferenceChangeEvent e) { |
| 1547 | if (e != null && e.getKey() == PROP_SNAP_ZOOM.getKey()) { |
| 1548 | zoomTo(getSnappedScale(scale, snapZoomMode.ROUND)); |
| 1549 | } |
| 1550 | } |
1519 | 1551 | } |
diff --git a/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java b/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java
index 926bf61..a17ca3f 100644
a
|
b
|
import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
|
84 | 84 | import org.openstreetmap.josm.gui.progress.ProgressMonitor; |
85 | 85 | import org.openstreetmap.josm.io.WMSLayerImporter; |
86 | 86 | import org.openstreetmap.josm.tools.GBC; |
| 87 | import org.openstreetmap.josm.tools.Utils; |
87 | 88 | |
88 | 89 | /** |
89 | 90 | * Base abstract class that supports displaying images provided by TileSource. It might be TMS source, WMS or WMTS |
… |
… |
public abstract class AbstractTileSourceLayer extends ImageryLayer implements Im
|
278 | 279 | |
279 | 280 | int screenPixels = mv.getWidth()*mv.getHeight(); |
280 | 281 | double tilePixels = Math.abs((t2.getY()-t1.getY())*(t2.getX()-t1.getX())*tileSource.getTileSize()*tileSource.getTileSize()); |
| 282 | tilePixels = Utils.roundToSignificantDigits(tilePixels, 9); |
281 | 283 | if (screenPixels == 0 || tilePixels == 0) return 1; |
282 | 284 | return screenPixels/tilePixels; |
283 | 285 | } |
diff --git a/src/org/openstreetmap/josm/gui/preferences/display/LafPreference.java b/src/org/openstreetmap/josm/gui/preferences/display/LafPreference.java
index 026fbeb..80a1ec2 100644
a
|
b
|
import javax.swing.UIManager.LookAndFeelInfo;
|
24 | 24 | import org.openstreetmap.josm.Main; |
25 | 25 | import org.openstreetmap.josm.actions.ExpertToggleAction; |
26 | 26 | import org.openstreetmap.josm.gui.dialogs.ToggleDialog; |
| 27 | import org.openstreetmap.josm.gui.NavigatableComponent; |
27 | 28 | import org.openstreetmap.josm.gui.preferences.PreferenceSetting; |
28 | 29 | import org.openstreetmap.josm.gui.preferences.PreferenceSettingFactory; |
29 | 30 | import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane; |
… |
… |
public class LafPreference implements SubPreferenceSetting {
|
61 | 62 | private final JCheckBox dynamicButtons = new JCheckBox(tr("Dynamic buttons in side menus")); |
62 | 63 | private final JCheckBox isoDates = new JCheckBox(tr("Display ISO dates")); |
63 | 64 | private final JCheckBox nativeFileChoosers = new JCheckBox(tr("Use native file choosers (nicer, but do not support file filters)")); |
| 65 | private final JCheckBox snapToMercatorZoomLevels = new JCheckBox(tr("Snap zoom to Mercator zoom levels (imagery looks perfect, zoom steps are larger)")); |
64 | 66 | |
65 | 67 | @Override |
66 | 68 | public void addGui(PreferenceTabbedPane gui) { |
… |
… |
public class LafPreference implements SubPreferenceSetting {
|
140 | 142 | nativeFileChoosers.setSelected(FileChooserManager.PROP_USE_NATIVE_FILE_DIALOG.get()); |
141 | 143 | panel.add(nativeFileChoosers, GBC.eop().insets(20, 0, 0, 0)); |
142 | 144 | |
| 145 | snapToMercatorZoomLevels.setToolTipText( |
| 146 | tr("Displays tiles without resizing, similar to most websites. Zoom steps are twice larger than default. Ignored when using different projection.")); |
| 147 | snapToMercatorZoomLevels.setSelected(NavigatableComponent.PROP_SNAP_ZOOM.get()); |
| 148 | panel.add(snapToMercatorZoomLevels, GBC.eop().insets(20, 0, 0, 0)); |
| 149 | |
143 | 150 | panel.add(Box.createVerticalGlue(), GBC.eol().insets(0, 20, 0, 0)); |
144 | 151 | |
145 | 152 | panel.add(new JLabel(tr("Look and Feel")), GBC.std().insets(20, 0, 0, 0)); |
… |
… |
public class LafPreference implements SubPreferenceSetting {
|
161 | 168 | Main.pref.put(ToggleDialog.PROP_DYNAMIC_BUTTONS.getKey(), dynamicButtons.isSelected()); |
162 | 169 | Main.pref.put(DateUtils.PROP_ISO_DATES.getKey(), isoDates.isSelected()); |
163 | 170 | Main.pref.put(FileChooserManager.PROP_USE_NATIVE_FILE_DIALOG.getKey(), nativeFileChoosers.isSelected()); |
| 171 | Main.pref.put(NavigatableComponent.PROP_SNAP_ZOOM.getKey(), snapToMercatorZoomLevels.isSelected()); |
164 | 172 | mod |= Main.pref.put("laf", ((LookAndFeelInfo) lafCombo.getSelectedItem()).getClassName()); |
165 | 173 | return mod; |
166 | 174 | } |
diff --git a/src/org/openstreetmap/josm/tools/Utils.java b/src/org/openstreetmap/josm/tools/Utils.java
index b08d5f9..8ab1f5e 100644
a
|
b
|
public final class Utils {
|
1636 | 1636 | return gvs; |
1637 | 1637 | } |
1638 | 1638 | |
| 1639 | public static double roundToSignificantDigits(double number, int digits) { |
| 1640 | double scale = Math.pow(10, Math.floor(Math.log10(Math.abs(number))) + 1 - digits); |
| 1641 | return scale * Math.round(number / scale); |
| 1642 | } |
| 1643 | |
1639 | 1644 | } |