Ticket #13386: patch-mapview-extract-imagery-rendering.patch
| File patch-mapview-extract-imagery-rendering.patch, 175.1 KB (added by , 10 years ago) |
|---|
-
src/org/openstreetmap/josm/data/Bounds.java
diff --git a/src/org/openstreetmap/josm/data/Bounds.java b/src/org/openstreetmap/josm/data/Bounds.java index 5cdbd0e..8108239 100644
a b public class Bounds { 396 396 } 397 397 398 398 /** 399 * Compute the intersection of this with an other bounds object. 400 * @param other The other bounds 401 * @return The intersection area or <code>null</code> if they do not intersect. 402 */ 403 public Bounds intersect(Bounds other) { 404 if (crosses180thMeridian() || other.crosses180thMeridian()) { 405 throw new UnsupportedOperationException(); 406 } else { 407 Bounds bounds = new Bounds( 408 Math.max(minLat, other.minLat), 409 Math.max(minLon, other.minLon), 410 Math.min(maxLat, other.maxLat), 411 Math.min(maxLon, other.maxLon), false); 412 if (bounds.minLat >= bounds.maxLat || bounds.minLon >= bounds.maxLon) { 413 return null; 414 } else { 415 return bounds; 416 } 417 } 418 } 419 420 /** 399 421 * Converts the lat/lon bounding box to an object of type Rectangle2D.Double 400 422 * @return the bounding box to Rectangle2D.Double 401 423 */ -
src/org/openstreetmap/josm/gui/MapViewState.java
diff --git a/src/org/openstreetmap/josm/gui/MapViewState.java b/src/org/openstreetmap/josm/gui/MapViewState.java index 6466eef..ebe4f44 100644
a b package org.openstreetmap.josm.gui; 4 4 import java.awt.Container; 5 5 import java.awt.Point; 6 6 import java.awt.Rectangle; 7 import java.awt.Shape; 7 8 import java.awt.geom.AffineTransform; 8 9 import java.awt.geom.Area; 9 10 import java.awt.geom.Path2D; … … import org.openstreetmap.josm.data.coor.EastNorth; 20 21 import org.openstreetmap.josm.data.coor.LatLon; 21 22 import org.openstreetmap.josm.data.projection.Projecting; 22 23 import org.openstreetmap.josm.data.projection.Projection; 24 import org.openstreetmap.josm.data.projection.ShiftedProjecting; 23 25 import org.openstreetmap.josm.gui.download.DownloadDialog; 24 26 import org.openstreetmap.josm.tools.bugreport.BugReport; 25 27 … … public final class MapViewState { 138 140 } 139 141 140 142 /** 143 * Gets the MapViewPoint representation for a position in view coordinates. 144 * @param point The point in view space. 145 * @return The MapViewPoint. 146 */ 147 public MapViewPoint getForView(Point2D point) { 148 return new MapViewViewPoint(point.getX(), point.getY()); 149 } 150 151 /** 141 152 * Gets the {@link MapViewPoint} for the given {@link EastNorth} coordinate. 142 153 * @param eastNorth the position. 143 154 * @return The point for that position. … … public final class MapViewState { 217 228 } 218 229 219 230 public Area getArea(Bounds bounds) { 231 Path2D area = getPath(bounds); 232 return new Area(area); 233 } 234 235 private Path2D getPath(Bounds bounds) { 220 236 Path2D area = new Path2D.Double(); 221 237 bounds.visitEdge(getProjection(), latlon -> { 222 238 MapViewPoint point = getPointFor(latlon); … … public final class MapViewState { 227 243 } 228 244 }); 229 245 area.closePath(); 230 return new Area(area);246 return area; 231 247 } 232 248 233 249 /** … … public final class MapViewState { 293 309 } 294 310 295 311 /** 312 * Creates a shifted mapviewstate. 313 * @param offset The delta to apply in east/north space 314 * @return The shifted MapViewState 315 */ 316 public MapViewState shifted(EastNorth offset) { 317 return new MapViewState(new ShiftedProjecting(projecting, offset), this); 318 } 319 320 /** 296 321 * Create the default {@link MapViewState} object for the given map view. The screen position won't be set so that this method can be used 297 322 * before the view was added to the hirarchy. 298 323 * @param width The view width … … public final class MapViewState { 390 415 } 391 416 392 417 /** 418 * Gets a rectangle from this point to an other point in lat/lon space, clamped to the world bounds. 419 * @param p2 The other point 420 * @return The rectangle 421 */ 422 public MapViewLatLonRectangle latLonRectTo(MapViewPoint p2) { 423 return new MapViewLatLonRectangle(getLatLonClamped(), p2.getLatLonClamped()); 424 } 425 426 /** 393 427 * Add the given offset to this point 394 428 * @param en The offset in east/north space. 395 429 * @return The new point … … public final class MapViewState { 455 489 } 456 490 457 491 /** 492 * This is a shape on the map view area. 493 * @author Michael Zangl 494 * @since xxx 495 */ 496 public interface MapViewArea { 497 498 /** 499 * Gets the shape in view space. 500 * @return The area in view coordinates 501 */ 502 public Shape getInView(); 503 504 /** 505 * Gets the real bounds that enclose this rectangle. 506 * This is computed respecting that the borders of this rectangle may not be a straignt line in latlon coordinates. 507 * @return The bounds. 508 * @since 10458 509 */ 510 public Bounds getLatLonBoundsBox(); 511 512 /** 513 * Gets the projection bounds for this rectangle. 514 * @return The projection bounds. 515 */ 516 public ProjectionBounds getProjectionBounds(); 517 518 /** 519 * Check if the given point is contained in this rectangle. 520 * @param point The position 521 * @return true if the point is contained in this shape. 522 */ 523 public boolean contains(MapViewPoint point); 524 } 525 526 /** 458 527 * A rectangle on the MapView. It is rectangular in screen / EastNorth space. 459 528 * @author Michael Zangl 460 529 */ 461 public class MapViewRectangle {530 public class MapViewRectangle implements MapViewArea { 462 531 private final MapViewPoint p1; 463 532 private final MapViewPoint p2; 464 533 … … public final class MapViewState { 472 541 this.p2 = p2; 473 542 } 474 543 475 /** 476 * Gets the projection bounds for this rectangle. 477 * @return The projection bounds. 478 */ 544 @Override 479 545 public ProjectionBounds getProjectionBounds() { 480 546 ProjectionBounds b = new ProjectionBounds(p1.getEastNorth()); 481 547 b.extend(p2.getEastNorth()); … … public final class MapViewState { 493 559 return b; 494 560 } 495 561 496 /** 497 * Gets the real bounds that enclose this rectangle. 498 * This is computed respecting that the borders of this rectangle may not be a straignt line in latlon coordinates. 499 * @return The bounds. 500 * @since 10458 501 */ 562 @Override 502 563 public Bounds getLatLonBoundsBox() { 503 564 // TODO @michael2402: Use hillclimb. 504 565 return projecting.getBaseProjection().getLatLonBoundsBox(getProjectionBounds()); … … public final class MapViewState { 509 570 * @return The rectangle. 510 571 * @since 10651 511 572 */ 573 @Override 512 574 public Rectangle2D getInView() { 513 575 double x1 = p1.getInViewX(); 514 576 double y1 = p1.getInViewY(); … … public final class MapViewState { 516 578 double y2 = p2.getInViewY(); 517 579 return new Rectangle2D.Double(Math.min(x1, x2), Math.min(y1, y2), Math.abs(x1 - x2), Math.abs(y1 - y2)); 518 580 } 581 582 @Override 583 public boolean contains(MapViewPoint point) { 584 return getInView().contains(point.getInView()); 585 } 519 586 } 520 587 588 /** 589 * A rectangle in lat/lon space 590 * @author Michael Zangl 591 * @since xxx 592 */ 593 public class MapViewLatLonRectangle implements MapViewArea { 594 595 private final Bounds bounds; 596 597 MapViewLatLonRectangle(LatLon l1, LatLon l2) { 598 bounds = new Bounds(l1); 599 bounds.extend(l2); 600 } 601 602 @Override 603 public Shape getInView() { 604 return getPath(bounds); 605 } 606 607 @Override 608 public boolean contains(MapViewPoint point) { 609 return bounds.contains(point.getLatLon()); 610 } 611 612 @Override 613 public Bounds getLatLonBoundsBox() { 614 return new Bounds(bounds); 615 } 616 617 @Override 618 public ProjectionBounds getProjectionBounds() { 619 return new ProjectionBounds(getProjection().latlon2eastNorth(bounds.getMin()), 620 getProjection().latlon2eastNorth(bounds.getMax())); 621 } 622 } 521 623 } -
src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java
diff --git a/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java b/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java index 13e0392..a82dd1e 100644
a b package org.openstreetmap.josm.gui.layer; 3 3 4 4 import static org.openstreetmap.josm.tools.I18n.tr; 5 5 6 import java.awt.Color;7 6 import java.awt.Component; 8 import java.awt.Dimension;9 import java.awt.Font;10 import java.awt.Graphics;11 7 import java.awt.Graphics2D; 12 import java.awt.GridBagLayout;13 8 import java.awt.Image; 14 import java.awt.Point;15 import java.awt.Toolkit;16 9 import java.awt.event.ActionEvent; 17 import java.awt.event.MouseAdapter;18 import java.awt.event.MouseEvent;19 import java.awt.geom.Point2D;20 import java.awt.geom.Rectangle2D;21 import java.awt.image.BufferedImage;22 10 import java.awt.image.ImageObserver; 23 11 import java.io.File; 24 import java.io.IOException;25 12 import java.net.MalformedURLException; 26 13 import java.net.URL; 27 import java.text.SimpleDateFormat;28 14 import java.util.ArrayList; 29 15 import java.util.Arrays; 30 import java.util.Collections; 31 import java.util.Comparator; 32 import java.util.Date; 33 import java.util.LinkedList; 16 import java.util.HashMap; 34 17 import java.util.List; 35 18 import java.util.Map; 36 import java.util.Map.Entry;37 import java.util.Objects;38 19 import java.util.Set; 39 20 import java.util.concurrent.ConcurrentSkipListSet; 40 21 import java.util.concurrent.atomic.AtomicInteger; 41 import java.util.function.Consumer;42 import java.util.function.Function;43 import java.util.stream.Collectors;44 import java.util.stream.IntStream;45 import java.util.stream.Stream;46 22 47 23 import javax.swing.AbstractAction; 48 24 import javax.swing.Action; 49 import javax.swing.BorderFactory;50 25 import javax.swing.JCheckBoxMenuItem; 51 26 import javax.swing.JLabel; 52 27 import javax.swing.JMenuItem; 53 import javax.swing.JOptionPane;54 28 import javax.swing.JPanel; 55 29 import javax.swing.JPopupMenu; 56 import javax.swing.JSeparator;57 import javax.swing.JTextField;58 30 import javax.swing.Timer; 59 31 60 import org.openstreetmap.gui.jmapviewer.AttributionSupport;61 import org.openstreetmap.gui.jmapviewer.MemoryTileCache;62 32 import org.openstreetmap.gui.jmapviewer.OsmTileLoader; 63 33 import org.openstreetmap.gui.jmapviewer.Tile; 64 34 import org.openstreetmap.gui.jmapviewer.TileXY; 65 import org.openstreetmap.gui.jmapviewer.interfaces.CachedTileLoader;66 import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;67 35 import org.openstreetmap.gui.jmapviewer.interfaces.TemplatedTileSource; 68 import org.openstreetmap.gui.jmapviewer.interfaces.TileCache;69 36 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader; 70 37 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener; 71 38 import org.openstreetmap.gui.jmapviewer.interfaces.TileSource; … … import org.openstreetmap.josm.actions.ImageryAdjustAction; 75 42 import org.openstreetmap.josm.actions.RenameLayerAction; 76 43 import org.openstreetmap.josm.actions.SaveActionBase; 77 44 import org.openstreetmap.josm.data.Bounds; 78 import org.openstreetmap.josm.data.ProjectionBounds;79 45 import org.openstreetmap.josm.data.coor.EastNorth; 80 46 import org.openstreetmap.josm.data.coor.LatLon; 81 47 import org.openstreetmap.josm.data.imagery.ImageryInfo; … … import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader; 83 49 import org.openstreetmap.josm.data.imagery.TileLoaderFactory; 84 50 import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor; 85 51 import org.openstreetmap.josm.data.preferences.IntegerProperty; 86 import org.openstreetmap.josm.gui.ExtendedDialog;87 52 import org.openstreetmap.josm.gui.MapFrame; 88 53 import org.openstreetmap.josm.gui.MapView; 89 import org.openstreetmap.josm.gui.MapViewState.MapViewRectangle;90 54 import org.openstreetmap.josm.gui.NavigatableComponent.ZoomChangeListener; 91 import org.openstreetmap.josm.gui.PleaseWaitRunnable;92 55 import org.openstreetmap.josm.gui.dialogs.LayerListDialog; 93 56 import org.openstreetmap.josm.gui.dialogs.LayerListPopup; 94 57 import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings.FilterChangeListener; 95 import org.openstreetmap.josm.gui.layer.imagery.TileCoordinateConverter;96 58 import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings; 97 59 import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings.DisplaySettingsChangeEvent; 98 60 import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings.DisplaySettingsChangeListener; 61 import org.openstreetmap.josm.gui.layer.imagery.TileSourcePainter; 99 62 import org.openstreetmap.josm.gui.progress.ProgressMonitor; 100 63 import org.openstreetmap.josm.gui.util.GuiHelper; 101 64 import org.openstreetmap.josm.io.WMSLayerImporter; 102 65 import org.openstreetmap.josm.tools.GBC; 103 import org.openstreetmap.josm.tools.MemoryManager;104 import org.openstreetmap.josm.tools.MemoryManager.MemoryHandle;105 import org.openstreetmap.josm.tools.MemoryManager.NotEnoughMemoryException;106 66 107 67 /** 108 68 * Base abstract class that supports displaying images provided by TileSource. It might be TMS source, WMS or WMTS … … import org.openstreetmap.josm.tools.MemoryManager.NotEnoughMemoryException; 115 75 * @since 3715 116 76 * @since 8526 (copied from TMSLayer) 117 77 */ 118 public abstract class AbstractTileSourceLayer<T extends AbstractTMSTileSource> extends ImageryLayer 119 implementsImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeListener, DisplaySettingsChangeListener {78 public abstract class AbstractTileSourceLayer<T extends AbstractTMSTileSource> extends ImageryLayer implements 79 ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeListener, DisplaySettingsChangeListener { 120 80 private static final String PREFERENCE_PREFIX = "imagery.generic"; 81 121 82 /** 122 83 * Registers all setting properties 123 84 */ … … implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi 129 90 public static final int MAX_ZOOM = 30; 130 91 /** minium zoom level supported */ 131 92 public static final int MIN_ZOOM = 2; 132 private static final Font InfoFont = new Font("sansserif", Font.BOLD, 13);133 93 134 94 /** minimum zoom level to show to user */ 135 95 public static final IntegerProperty PROP_MIN_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".min_zoom_lvl", 2); 136 96 /** maximum zoom level to show to user */ 137 public static final IntegerProperty PROP_MAX_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".max_zoom_lvl", 20); 97 public static final IntegerProperty PROP_MAX_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".max_zoom_lvl", 98 20); 138 99 139 100 //public static final BooleanProperty PROP_DRAW_DEBUG = new BooleanProperty(PREFERENCE_PREFIX + ".draw_debug", false); 140 /**141 * Zoomlevel at which tiles is currently downloaded.142 * Initial zoom lvl is set to bestZoom143 */144 public int currentZoomLevel;145 146 private final AttributionSupport attribution = new AttributionSupport();147 private final TileHolder clickedTileHolder = new TileHolder();148 101 149 102 /** 150 103 * Offset between calculated zoom level and zoom level used to download and show tiles. Negative values will result in … … implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi 152 105 */ 153 106 public static final IntegerProperty ZOOM_OFFSET = new IntegerProperty(PREFERENCE_PREFIX + ".zoom_offset", 0); 154 107 155 /*156 * use MemoryTileCache instead of tileLoader JCS cache, as tileLoader caches only content (byte[] of image)157 * and MemoryTileCache caches whole Tile. This gives huge performance improvement when a lot of tiles are visible158 * in MapView (for example - when limiting min zoom in imagery)159 *160 * Use per-layer tileCache instance, as the more layers there are, the more tiles needs to be cached161 */162 protected TileCache tileCache; // initialized together with tileSource163 protected T tileSource;164 protected TileLoader tileLoader;165 166 108 /** 167 109 * A timer that is used to delay invalidation events if required. 168 110 */ 169 111 private final Timer invalidateLaterTimer = new Timer(100, e -> this.invalidate()); 170 112 171 private final MouseAdapter adapter = new MouseAdapter() {172 @Override173 public void mouseClicked(MouseEvent e) {174 if (!isVisible()) return;175 if (e.getButton() == MouseEvent.BUTTON3) {176 clickedTileHolder.setTile(getTileForPixelpos(e.getX(), e.getY()));177 new TileSourceLayerPopup().show(e.getComponent(), e.getX(), e.getY());178 } else if (e.getButton() == MouseEvent.BUTTON1) {179 attribution.handleAttribution(e.getPoint(), true);180 }181 }182 };183 184 113 private final TileSourceDisplaySettings displaySettings = createDisplaySettings(); 185 114 186 115 private final ImageryAdjustAction adjustAction = new ImageryAdjustAction(this); 187 // prepared to be moved to the painter 188 private TileCoordinateConverter coordinateConverter;116 117 private HashMap<MapView, TileSourcePainter<T>> painters = new HashMap<>(); 189 118 190 119 /** 191 120 * Creates Tile Source based Imagery Layer based on Imagery Info … … implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi 222 151 invalidate(); 223 152 } 224 153 225 protected abstract TileLoaderFactory getTileLoaderFactory();226 227 154 /** 228 * 229 * @param info imagery info 230 * @return TileSource for specified ImageryInfo 231 * @throws IllegalArgumentException when Imagery is not supported by layer 155 * Generate the tile loader 156 * @param tileSource A tile source that is already generated. 157 * @return The tile loader. 232 158 */ 233 protected abstract T getTileSource(ImageryInfo info); 234 235 protected Map<String, String> getHeaders(T tileSource) { 236 if (tileSource instanceof TemplatedTileSource) { 237 return ((TemplatedTileSource) tileSource).getHeaders(); 238 } 239 return null; 240 } 241 242 protected void initTileSource(T tileSource) { 243 coordinateConverter = new TileCoordinateConverter(Main.map.mapView, tileSource, getDisplaySettings()); 244 attribution.initialize(tileSource); 245 246 currentZoomLevel = getBestZoom(); 247 159 public TileLoader generateTileLoader(T tileSource) { 248 160 Map<String, String> headers = getHeaders(tileSource); 249 161 250 tileLoader = getTileLoaderFactory().makeTileLoader(this, headers); 162 TileLoader loader = getTileLoaderFactory().makeTileLoader(this, headers); 163 if (loader != null) { 164 return loader; 165 } 251 166 252 167 try { 253 168 if ("file".equalsIgnoreCase(new URL(tileSource.getBaseUrl()).getProtocol())) { 254 tileLoader =new OsmTileLoader(this);169 return new OsmTileLoader(this); 255 170 } 256 171 } catch (MalformedURLException e) { 257 172 // ignore, assume that this is not a file … … implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi 260 175 } 261 176 } 262 177 263 if (tileLoader == null) 264 tileLoader = new OsmTileLoader(this, headers); 178 return new OsmTileLoader(this, headers); 179 } 180 181 /** 182 * Generates the tile source for this layer. 183 * @return The tile source 184 */ 185 public T getTileSource() { 186 return getTileSource(getInfo()); 187 } 188 189 protected abstract TileLoaderFactory getTileLoaderFactory(); 190 191 /** 192 * Used by the default {@link TileSourcePainter} to create the tile source. 193 * @param info imagery info 194 * @return TileSource for specified ImageryInfo 195 * @throws IllegalArgumentException when Imagery is not supported by layer 196 */ 197 protected T getTileSource(ImageryInfo info) { 198 throw new UnsupportedOperationException(); 199 } 265 200 266 tileCache = new MemoryTileCache(estimateTileCacheSize()); 201 protected Map<String, String> getHeaders(T tileSource) { 202 if (tileSource instanceof TemplatedTileSource) { 203 return ((TemplatedTileSource) tileSource).getHeaders(); 204 } 205 return null; 267 206 } 268 207 269 208 @Override … … implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi 280 219 } 281 220 282 221 /** 283 * Clears the tile cache.284 *285 * If the current tileLoader is an instance of OsmTileLoader, a new286 * TmsTileClearController is created and passed to the according clearCache287 * method.288 *289 * @param monitor not used in this implementation - as cache clear is instaneus290 */291 public void clearTileCache(ProgressMonitor monitor) {292 if (tileLoader instanceof CachedTileLoader) {293 ((CachedTileLoader) tileLoader).clearCache(tileSource);294 }295 tileCache.clear();296 }297 298 /**299 222 * Initiates a repaint of Main.map 300 223 * 301 224 * @see Main#map … … implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi 361 284 return adjustAction; 362 285 } 363 286 364 /**365 * Returns average number of screen pixels per tile pixel for current mapview366 * @param zoom zoom level367 * @return average number of screen pixels per tile pixel368 */369 private double getScaleFactor(int zoom) {370 if (coordinateConverter != null) {371 return coordinateConverter.getScaleFactor(zoom);372 } else {373 return 1;374 }375 }376 377 protected int getBestZoom() {378 double factor = getScaleFactor(1); // check the ratio between area of tilesize at zoom 1 to current view379 double result = Math.log(factor)/Math.log(2)/2;380 /*381 * Math.log(factor)/Math.log(2) - gives log base 2 of factor382 * We divide result by 2, as factor contains ratio between areas. We could do Math.sqrt before log, or just divide log by 2383 *384 * ZOOM_OFFSET controls, whether we work with overzoomed or underzoomed tiles. Positive ZOOM_OFFSET385 * is for working with underzoomed tiles (higher quality when working with aerial imagery), negative ZOOM_OFFSET386 * is for working with overzoomed tiles (big, pixelated), which is good when working with high-dpi screens and/or387 * maps as a imagery layer388 */389 390 int intResult = (int) Math.round(result + 1 + ZOOM_OFFSET.get() / 1.9);391 392 intResult = Math.min(intResult, getMaxZoomLvl());393 intResult = Math.max(intResult, getMinZoomLvl());394 return intResult;395 }396 397 287 private static boolean actionSupportLayers(List<Layer> layers) { 398 288 return layers.size() == 1 && layers.get(0) instanceof TMSLayer; 399 289 } 400 290 401 private final class ShowTileInfoAction extends AbstractAction {402 403 private ShowTileInfoAction() {404 super(tr("Show tile info"));405 }406 407 private String getSizeString(int size) {408 StringBuilder ret = new StringBuilder();409 return ret.append(size).append('x').append(size).toString();410 }411 412 private JTextField createTextField(String text) {413 JTextField ret = new JTextField(text);414 ret.setEditable(false);415 ret.setBorder(BorderFactory.createEmptyBorder());416 return ret;417 }418 419 @Override420 public void actionPerformed(ActionEvent ae) {421 Tile clickedTile = clickedTileHolder.getTile();422 if (clickedTile != null) {423 ExtendedDialog ed = new ExtendedDialog(Main.parent, tr("Tile Info"), new String[]{tr("OK")});424 JPanel panel = new JPanel(new GridBagLayout());425 Rectangle2D displaySize = coordinateConverter.getRectangleForTile(clickedTile);426 String url = "";427 try {428 url = clickedTile.getUrl();429 } catch (IOException e) {430 // silence exceptions431 Main.trace(e);432 }433 434 String[][] content = {435 {"Tile name", clickedTile.getKey()},436 {"Tile url", url},437 {"Tile size", getSizeString(clickedTile.getTileSource().getTileSize()) },438 {"Tile display size", new StringBuilder().append(displaySize.getWidth())439 .append('x')440 .append(displaySize.getHeight()).toString()},441 };442 443 for (String[] entry: content) {444 panel.add(new JLabel(tr(entry[0]) + ':'), GBC.std());445 panel.add(GBC.glue(5, 0), GBC.std());446 panel.add(createTextField(entry[1]), GBC.eol().fill(GBC.HORIZONTAL));447 }448 449 for (Entry<String, String> e: clickedTile.getMetadata().entrySet()) {450 panel.add(new JLabel(tr("Metadata ") + tr(e.getKey()) + ':'), GBC.std());451 panel.add(GBC.glue(5, 0), GBC.std());452 String value = e.getValue();453 if ("lastModification".equals(e.getKey()) || "expirationTime".equals(e.getKey())) {454 value = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(Long.parseLong(value)));455 }456 panel.add(createTextField(value), GBC.eol().fill(GBC.HORIZONTAL));457 458 }459 ed.setIcon(JOptionPane.INFORMATION_MESSAGE);460 ed.setContent(panel);461 ed.showDialog();462 }463 }464 }465 466 private final class LoadTileAction extends AbstractAction {467 468 private LoadTileAction() {469 super(tr("Load tile"));470 }471 472 @Override473 public void actionPerformed(ActionEvent ae) {474 Tile clickedTile = clickedTileHolder.getTile();475 if (clickedTile != null) {476 loadTile(clickedTile, true);477 invalidate();478 }479 }480 }481 482 291 private class AutoZoomAction extends AbstractAction implements LayerAction { 483 292 AutoZoomAction() { 484 293 super(tr("Auto zoom")); … … implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi 548 357 } 549 358 } 550 359 551 private class LoadAllTilesAction extends AbstractAction {552 LoadAllTilesAction() {553 super(tr("Load all tiles"));554 }555 556 @Override557 public void actionPerformed(ActionEvent ae) {558 loadAllTiles(true);559 }560 }561 562 private class LoadErroneusTilesAction extends AbstractAction {563 LoadErroneusTilesAction() {564 super(tr("Load all error tiles"));565 }566 567 @Override568 public void actionPerformed(ActionEvent ae) {569 loadAllErrorTiles(true);570 }571 }572 573 private class ZoomToNativeLevelAction extends AbstractAction {574 ZoomToNativeLevelAction() {575 super(tr("Zoom to native resolution"));576 }577 578 @Override579 public void actionPerformed(ActionEvent ae) {580 double newFactor = Math.sqrt(getScaleFactor(currentZoomLevel));581 Main.map.mapView.zoomToFactor(newFactor);582 redraw();583 }584 }585 586 private class ZoomToBestAction extends AbstractAction {587 ZoomToBestAction() {588 super(tr("Change resolution"));589 setEnabled(!getDisplaySettings().isAutoZoom() && getBestZoom() != currentZoomLevel);590 }591 592 @Override593 public void actionPerformed(ActionEvent ae) {594 setZoomLevel(getBestZoom());595 }596 }597 598 private class IncreaseZoomAction extends AbstractAction {599 IncreaseZoomAction() {600 super(tr("Increase zoom"));601 setEnabled(!getDisplaySettings().isAutoZoom() && zoomIncreaseAllowed());602 }603 604 @Override605 public void actionPerformed(ActionEvent ae) {606 increaseZoomLevel();607 }608 }609 610 private class DecreaseZoomAction extends AbstractAction {611 DecreaseZoomAction() {612 super(tr("Decrease zoom"));613 setEnabled(!getDisplaySettings().isAutoZoom() && zoomDecreaseAllowed());614 }615 616 @Override617 public void actionPerformed(ActionEvent ae) {618 decreaseZoomLevel();619 }620 }621 622 private class FlushTileCacheAction extends AbstractAction {623 FlushTileCacheAction() {624 super(tr("Flush tile cache"));625 setEnabled(tileLoader instanceof CachedTileLoader);626 }627 628 @Override629 public void actionPerformed(ActionEvent ae) {630 new PleaseWaitRunnable(tr("Flush tile cache")) {631 @Override632 protected void realRun() {633 clearTileCache(getProgressMonitor());634 }635 636 @Override637 protected void finish() {638 // empty - flush is instaneus639 }640 641 @Override642 protected void cancel() {643 // empty - flush is instaneus644 }645 }.run();646 }647 }648 649 /**650 * Simple class to keep clickedTile within hookUpMapView651 */652 private static final class TileHolder {653 private Tile t;654 655 public Tile getTile() {656 return t;657 }658 659 public void setTile(Tile t) {660 this.t = t;661 }662 }663 664 /**665 * Creates popup menu items and binds to mouse actions666 */667 360 @Override 668 361 public void hookUpMapView() { 669 // this needs to be here and not in constructor to allow empty TileSource class construction670 // using SessionWriter671 initializeIfRequired();672 673 super.hookUpMapView();674 362 } 675 363 676 364 @Override 677 365 public LayerPainter attachToMapView(MapViewEvent event) { 678 initializeIfRequired(); 679 680 event.getMapView().addMouseListener(adapter); 366 GuiHelper.assertCallFromEdt(); 681 367 MapView.addZoomChangeListener(this); 682 368 683 369 if (this instanceof NativeScaleLayer) { 684 370 event.getMapView().setNativeScaleLayer((NativeScaleLayer) this); 685 371 } 686 372 687 // FIXME: why do we need this? Without this, if you add a WMS layer and do not move the mouse, sometimes, tiles do not 688 // start loading. 689 // FIXME: Check if this is still required. 690 event.getMapView().repaint(500); 691 692 return super.attachToMapView(event); 693 } 694 695 private void initializeIfRequired() { 696 if (tileSource == null) { 697 tileSource = getTileSource(info); 698 if (tileSource == null) { 699 throw new IllegalArgumentException(tr("Failed to create tile source")); 700 } 701 // check if projection is supported 702 projectionChanged(null, Main.getProjection()); 703 initTileSource(this.tileSource); 704 } 373 TileSourcePainter<T> painter = createMapViewPainter(event); 374 painters.put(event.getMapView(), painter); 375 return painter; 705 376 } 706 377 707 378 @Override 708 protected LayerPaintercreateMapViewPainter(MapViewEvent event) {709 return new TileSourcePainter ();379 protected TileSourcePainter<T> createMapViewPainter(MapViewEvent event) { 380 return new TileSourcePainter<>(this, event.getMapView()); 710 381 } 711 382 712 383 /** … … implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi 715 386 public class TileSourceLayerPopup extends JPopupMenu { 716 387 /** 717 388 * Constructs a new {@code TileSourceLayerPopup}. 389 * @param mv 718 390 */ 719 public TileSourceLayerPopup( ) {391 public TileSourceLayerPopup(MapView mv) { 720 392 for (Action a : getCommonEntries()) { 721 if (a instanceof LayerAction) { 722 add(((LayerAction) a).createMenuComponent()); 723 } else { 724 add(new JMenuItem(a)); 725 } 393 addAction(a); 394 } 395 for (Action a : getMapViewEntries(mv)) { 396 addAction(a); 726 397 } 727 add(new JSeparator());728 add(new JMenuItem(new LoadTileAction()));729 add(new JMenuItem(new ShowTileInfoAction()));730 398 } 731 }732 399 733 protected int estimateTileCacheSize() { 734 Dimension screenSize = GuiHelper.getMaximumScreenSize(); 735 int height = screenSize.height; 736 int width = screenSize.width; 737 int tileSize = 256; // default tile size 738 if (tileSource != null) { 739 tileSize = tileSource.getTileSize(); 400 private void addAction(Action a) { 401 if (a instanceof LayerAction) { 402 add(((LayerAction) a).createMenuComponent()); 403 } else { 404 add(new JMenuItem(a)); 405 } 740 406 } 741 // as we can see part of the tile at the top and at the bottom, use Math.ceil(...) + 1 to accommodate for that742 int visibileTiles = (int) (Math.ceil((double) height / tileSize + 1) * Math.ceil((double) width / tileSize + 1));743 // add 10% for tiles from different zoom levels744 int ret = (int) Math.ceil(745 Math.pow(2d, ZOOM_OFFSET.get()) * visibileTiles // use offset to decide, how many tiles are visible746 * 4);747 Main.info("AbstractTileSourceLayer: estimated visible tiles: {0}, estimated cache size: {1}", visibileTiles, ret);748 return ret;749 407 } 750 408 751 409 @Override 752 410 public void displaySettingsChanged(DisplaySettingsChangeEvent e) { 753 if (tileSource == null) {754 return;755 }756 411 switch (e.getChangedSetting()) { 757 case TileSourceDisplaySettings.AUTO_ZOOM:758 if (getDisplaySettings().isAutoZoom() && getBestZoom() != currentZoomLevel) {759 setZoomLevel(getBestZoom());760 invalidate();761 }762 break;763 412 case TileSourceDisplaySettings.AUTO_LOAD: 764 413 if (getDisplaySettings().isAutoLoad()) { 765 414 invalidate(); … … implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi 847 496 */ 848 497 @Override 849 498 public void zoomChanged() { 850 if (Main.isDebugEnabled()) {851 Main.debug("zoomChanged(): " + currentZoomLevel);852 }853 if (tileLoader instanceof TMSCachedTileLoader) {854 ((TMSCachedTileLoader) tileLoader).cancelOutstandingTasks();855 }856 invalidate();857 }858 859 protected int getMaxZoomLvl() {860 if (info.getMaxZoom() != 0)861 return checkMaxZoomLvl(info.getMaxZoom(), tileSource);862 else863 return getMaxZoomLvl(tileSource);864 }865 866 protected int getMinZoomLvl() {867 if (info.getMinZoom() != 0)868 return checkMinZoomLvl(info.getMinZoom(), tileSource);869 else870 return getMinZoomLvl(tileSource);871 }872 873 /**874 *875 * @return if its allowed to zoom in876 */877 public boolean zoomIncreaseAllowed() {878 boolean zia = currentZoomLevel < this.getMaxZoomLvl();879 if (Main.isDebugEnabled()) {880 Main.debug("zoomIncreaseAllowed(): " + zia + ' ' + currentZoomLevel + " vs. " + this.getMaxZoomLvl());881 }882 return zia;883 }884 885 /**886 * Zoom in, go closer to map.887 *888 * @return true, if zoom increasing was successful, false otherwise889 */890 public boolean increaseZoomLevel() {891 if (zoomIncreaseAllowed()) {892 currentZoomLevel++;893 if (Main.isDebugEnabled()) {894 Main.debug("increasing zoom level to: " + currentZoomLevel);895 }896 zoomChanged();897 } else {898 Main.warn("Current zoom level ("+currentZoomLevel+") could not be increased. "+899 "Max.zZoom Level "+this.getMaxZoomLvl()+" reached.");900 return false;901 }902 return true;903 }904 905 /**906 * Sets the zoom level of the layer907 * @param zoom zoom level908 * @return true, when zoom has changed to desired value, false if it was outside supported zoom levels909 */910 public boolean setZoomLevel(int zoom) {911 if (zoom == currentZoomLevel) return true;912 if (zoom > this.getMaxZoomLvl()) return false;913 if (zoom < this.getMinZoomLvl()) return false;914 currentZoomLevel = zoom;915 zoomChanged();916 return true;917 }918 919 /**920 * Check if zooming out is allowed921 *922 * @return true, if zooming out is allowed (currentZoomLevel > minZoomLevel)923 */924 public boolean zoomDecreaseAllowed() {925 boolean zda = currentZoomLevel > this.getMinZoomLvl();926 if (Main.isDebugEnabled()) {927 Main.debug("zoomDecreaseAllowed(): " + zda + ' ' + currentZoomLevel + " vs. " + this.getMinZoomLvl());928 }929 return zda;930 }931 932 /**933 * Zoom out from map.934 *935 * @return true, if zoom increasing was successfull, false othervise936 */937 public boolean decreaseZoomLevel() {938 if (zoomDecreaseAllowed()) {939 if (Main.isDebugEnabled()) {940 Main.debug("decreasing zoom level to: " + currentZoomLevel);941 }942 currentZoomLevel--;943 zoomChanged();944 } else {945 return false;946 }947 return true;948 }949 950 /*951 * We use these for quick, hackish calculations. They952 * are temporary only and intentionally not inserted953 * into the tileCache.954 */955 private Tile tempCornerTile(Tile t) {956 int x = t.getXtile() + 1;957 int y = t.getYtile() + 1;958 int zoom = t.getZoom();959 Tile tile = getTile(x, y, zoom);960 if (tile != null)961 return tile;962 return new Tile(tileSource, x, y, zoom);963 }964 965 private Tile getOrCreateTile(TilePosition tilePosition) {966 return getOrCreateTile(tilePosition.getX(), tilePosition.getY(), tilePosition.getZoom());967 }968 969 private Tile getOrCreateTile(int x, int y, int zoom) {970 Tile tile = getTile(x, y, zoom);971 if (tile == null) {972 tile = new Tile(tileSource, x, y, zoom);973 tileCache.addTile(tile);974 }975 976 if (!tile.isLoaded()) {977 tile.loadPlaceholderFromCache(tileCache);978 }979 return tile;980 }981 982 private Tile getTile(TilePosition tilePosition) {983 return getTile(tilePosition.getX(), tilePosition.getY(), tilePosition.getZoom());984 }985 986 /**987 * Returns tile at given position.988 * This can and will return null for tiles that are not already in the cache.989 * @param x tile number on the x axis of the tile to be retrieved990 * @param y tile number on the y axis of the tile to be retrieved991 * @param zoom zoom level of the tile to be retrieved992 * @return tile at given position993 */994 private Tile getTile(int x, int y, int zoom) {995 if (x < tileSource.getTileXMin(zoom) || x > tileSource.getTileXMax(zoom)996 || y < tileSource.getTileYMin(zoom) || y > tileSource.getTileYMax(zoom))997 return null;998 return tileCache.getTile(tileSource, x, y, zoom);999 }1000 1001 private boolean loadTile(Tile tile, boolean force) {1002 if (tile == null)1003 return false;1004 if (!force && (tile.isLoaded() || tile.hasError()))1005 return false;1006 if (tile.isLoading())1007 return false;1008 tileLoader.createTileLoaderJob(tile).submit(force);1009 return true;1010 }1011 1012 private TileSet getVisibleTileSet() {1013 MapView mv = Main.map.mapView;1014 MapViewRectangle area = mv.getState().getViewArea();1015 ProjectionBounds bounds = area.getProjectionBounds();1016 return getTileSet(bounds.getMin(), bounds.getMax(), currentZoomLevel);1017 }1018 1019 protected void loadAllTiles(boolean force) {1020 TileSet ts = getVisibleTileSet();1021 1022 // if there is more than 18 tiles on screen in any direction, do not load all tiles!1023 if (ts.tooLarge()) {1024 Main.warn("Not downloading all tiles because there is more than 18 tiles on an axis!");1025 return;1026 }1027 ts.loadAllTiles(force);1028 invalidate();1029 }1030 1031 protected void loadAllErrorTiles(boolean force) {1032 TileSet ts = getVisibleTileSet();1033 ts.loadAllErrorTiles(force);1034 499 invalidate(); 1035 500 } 1036 501 … … implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi 1061 526 }); 1062 527 } 1063 528 1064 private boolean imageLoaded(Image i) {1065 if (i == null)1066 return false;1067 int status = Toolkit.getDefaultToolkit().checkImage(i, -1, -1, this);1068 if ((status & ALLBITS) != 0)1069 return true;1070 return false;1071 }1072 1073 /**1074 * Returns the image for the given tile image is loaded.1075 * Otherwise returns null.1076 *1077 * @param tile the Tile for which the image should be returned1078 * @return the image of the tile or null.1079 */1080 private Image getLoadedTileImage(Tile tile) {1081 Image img = tile.getImage();1082 if (!imageLoaded(img))1083 return null;1084 return img;1085 }1086 1087 // 'source' is the pixel coordinates for the area that1088 // the img is capable of filling in. However, we probably1089 // only want a portion of it.1090 //1091 // 'border' is the screen cordinates that need to be drawn.1092 // We must not draw outside of it.1093 private void drawImageInside(Graphics g, Image sourceImg, Rectangle2D source, Rectangle2D border) {1094 Rectangle2D target = source;1095 1096 // If a border is specified, only draw the intersection1097 // if what we have combined with what we are supposed to draw.1098 if (border != null) {1099 target = source.createIntersection(border);1100 if (Main.isDebugEnabled()) {1101 Main.debug("source: " + source + "\nborder: " + border + "\nintersection: " + target);1102 }1103 }1104 1105 // All of the rectangles are in screen coordinates. We need1106 // to how these correlate to the sourceImg pixels. We could1107 // avoid doing this by scaling the image up to the 'source' size,1108 // but this should be cheaper.1109 //1110 // In some projections, x any y are scaled differently enough to1111 // cause a pixel or two of fudge. Calculate them separately.1112 double imageYScaling = sourceImg.getHeight(this) / source.getHeight();1113 double imageXScaling = sourceImg.getWidth(this) / source.getWidth();1114 1115 // How many pixels into the 'source' rectangle are we drawing?1116 double screenXoffset = target.getX() - source.getX();1117 double screenYoffset = target.getY() - source.getY();1118 // And how many pixels into the image itself does that correlate to?1119 int imgXoffset = (int) (screenXoffset * imageXScaling + 0.5);1120 int imgYoffset = (int) (screenYoffset * imageYScaling + 0.5);1121 // Now calculate the other corner of the image that we need1122 // by scaling the 'target' rectangle's dimensions.1123 int imgXend = imgXoffset + (int) (target.getWidth() * imageXScaling + 0.5);1124 int imgYend = imgYoffset + (int) (target.getHeight() * imageYScaling + 0.5);1125 1126 if (Main.isDebugEnabled()) {1127 Main.debug("drawing image into target rect: " + target);1128 }1129 g.drawImage(sourceImg,1130 (int) target.getX(), (int) target.getY(),1131 (int) target.getMaxX(), (int) target.getMaxY(),1132 imgXoffset, imgYoffset,1133 imgXend, imgYend,1134 this);1135 if (PROP_FADE_AMOUNT.get() != 0) {1136 // dimm by painting opaque rect...1137 g.setColor(getFadeColorWithAlpha());1138 ((Graphics2D) g).fill(target);1139 }1140 }1141 1142 private List<Tile> paintTileImages(Graphics g, TileSet ts) {1143 Object paintMutex = new Object();1144 List<TilePosition> missed = Collections.synchronizedList(new ArrayList<>());1145 ts.visitTiles(tile -> {1146 Image img = getLoadedTileImage(tile);1147 if (img == null) {1148 missed.add(new TilePosition(tile));1149 }1150 img = applyImageProcessors((BufferedImage) img);1151 Rectangle2D sourceRect = coordinateConverter.getRectangleForTile(tile);1152 synchronized (paintMutex) {1153 //cannot paint in parallel1154 drawImageInside(g, img, sourceRect, null);1155 }1156 }, missed::add);1157 1158 return missed.stream().map(this::getOrCreateTile).collect(Collectors.toList());1159 }1160 1161 // This function is called for several zoom levels, not just1162 // the current one. It should not trigger any tiles to be1163 // downloaded. It should also avoid polluting the tile cache1164 // with any tiles since these tiles are not mandatory.1165 //1166 // The "border" tile tells us the boundaries of where we may1167 // draw. It will not be from the zoom level that is being1168 // drawn currently. If drawing the displayZoomLevel,1169 // border is null and we draw the entire tile set.1170 private List<Tile> paintTileImages(Graphics g, TileSet ts, int zoom, Tile border) {1171 if (zoom <= 0) return Collections.emptyList();1172 Rectangle2D borderRect = coordinateConverter.getRectangleForTile(border);1173 List<Tile> missedTiles = new LinkedList<>();1174 // The callers of this code *require* that we return any tiles1175 // that we do not draw in missedTiles. ts.allExistingTiles() by1176 // default will only return already-existing tiles. However, we1177 // need to return *all* tiles to the callers, so force creation here.1178 for (Tile tile : ts.allTilesCreate()) {1179 Image img = getLoadedTileImage(tile);1180 if (img == null || tile.hasError()) {1181 if (Main.isDebugEnabled()) {1182 Main.debug("missed tile: " + tile);1183 }1184 missedTiles.add(tile);1185 continue;1186 }1187 1188 // applying all filters to this layer1189 img = applyImageProcessors((BufferedImage) img);1190 1191 Rectangle2D sourceRect = coordinateConverter.getRectangleForTile(tile);1192 if (borderRect != null && !sourceRect.intersects(borderRect)) {1193 continue;1194 }1195 drawImageInside(g, img, sourceRect, borderRect);1196 }1197 return missedTiles;1198 }1199 1200 private void myDrawString(Graphics g, String text, int x, int y) {1201 Color oldColor = g.getColor();1202 String textToDraw = text;1203 if (g.getFontMetrics().stringWidth(text) > tileSource.getTileSize()) {1204 // text longer than tile size, split it1205 StringBuilder line = new StringBuilder();1206 StringBuilder ret = new StringBuilder();1207 for (String s: text.split(" ")) {1208 if (g.getFontMetrics().stringWidth(line.toString() + s) > tileSource.getTileSize()) {1209 ret.append(line).append('\n');1210 line.setLength(0);1211 }1212 line.append(s).append(' ');1213 }1214 ret.append(line);1215 textToDraw = ret.toString();1216 }1217 int offset = 0;1218 for (String s: textToDraw.split("\n")) {1219 g.setColor(Color.black);1220 g.drawString(s, x + 1, y + offset + 1);1221 g.setColor(oldColor);1222 g.drawString(s, x, y + offset);1223 offset += g.getFontMetrics().getHeight() + 3;1224 }1225 }1226 1227 private void paintTileText(TileSet ts, Tile tile, Graphics g, MapView mv, int zoom, Tile t) {1228 if (tile == null) {1229 return;1230 }1231 Point2D p = coordinateConverter.getPixelForTile(t);1232 int fontHeight = g.getFontMetrics().getHeight();1233 int x = (int) p.getX();1234 int y = (int) p.getY();1235 int texty = y + 2 + fontHeight;1236 1237 /*if (PROP_DRAW_DEBUG.get()) {1238 myDrawString(g, "x=" + t.getXtile() + " y=" + t.getYtile() + " z=" + zoom + "", p.x + 2, texty);1239 texty += 1 + fontHeight;1240 if ((t.getXtile() % 32 == 0) && (t.getYtile() % 32 == 0)) {1241 myDrawString(g, "x=" + t.getXtile() / 32 + " y=" + t.getYtile() / 32 + " z=7", p.x + 2, texty);1242 texty += 1 + fontHeight;1243 }1244 }*/1245 1246 /*String tileStatus = tile.getStatus();1247 if (!tile.isLoaded() && PROP_DRAW_DEBUG.get()) {1248 myDrawString(g, tr("image " + tileStatus), p.x + 2, texty);1249 texty += 1 + fontHeight;1250 }*/1251 1252 if (tile.hasError() && getDisplaySettings().isShowErrors()) {1253 myDrawString(g, tr("Error") + ": " + tr(tile.getErrorMessage()), x + 2, texty);1254 //texty += 1 + fontHeight;1255 }1256 1257 int xCursor = -1;1258 int yCursor = -1;1259 if (Main.isDebugEnabled()) {1260 if (yCursor < t.getYtile()) {1261 if (t.getYtile() % 32 == 31) {1262 g.fillRect(0, y - 1, mv.getWidth(), 3);1263 } else {1264 g.drawLine(0, y, mv.getWidth(), y);1265 }1266 //yCursor = t.getYtile();1267 }1268 // This draws the vertical lines for the entire column. Only draw them for the top tile in the column.1269 if (xCursor < t.getXtile()) {1270 if (t.getXtile() % 32 == 0) {1271 // level 7 tile boundary1272 g.fillRect(x - 1, 0, 3, mv.getHeight());1273 } else {1274 g.drawLine(x, 0, x, mv.getHeight());1275 }1276 //xCursor = t.getXtile();1277 }1278 }1279 }1280 1281 private LatLon getShiftedLatLon(EastNorth en) {1282 return coordinateConverter.getProjecting().eastNorth2latlonClamped(en);1283 }1284 1285 private ICoordinate getShiftedCoord(EastNorth en) {1286 return getShiftedLatLon(en).toCoordinate();1287 }1288 1289 private LatLon getShiftedLatLon(ICoordinate latLon) {1290 return getShiftedLatLon(Main.getProjection().latlon2eastNorth(new LatLon(latLon)));1291 }1292 1293 1294 private final TileSet nullTileSet = new TileSet();1295 1296 /**1297 * This is a rectangular range of tiles.1298 */1299 private static class TileRange {1300 int minX;1301 int maxX;1302 int minY;1303 int maxY;1304 int zoom;1305 1306 private TileRange() {1307 }1308 1309 protected TileRange(TileXY t1, TileXY t2, int zoom) {1310 minX = (int) Math.floor(Math.min(t1.getX(), t2.getX()));1311 minY = (int) Math.floor(Math.min(t1.getY(), t2.getY()));1312 maxX = (int) Math.ceil(Math.max(t1.getX(), t2.getX()));1313 maxY = (int) Math.ceil(Math.max(t1.getY(), t2.getY()));1314 this.zoom = zoom;1315 }1316 1317 protected double tilesSpanned() {1318 return Math.sqrt(1.0 * this.size());1319 }1320 1321 protected int size() {1322 int xSpan = maxX - minX + 1;1323 int ySpan = maxY - minY + 1;1324 return xSpan * ySpan;1325 }1326 1327 /**1328 * Gets a stream of all tile positions in this set1329 * @return A stream of all positions1330 */1331 public Stream<TilePosition> tilePositions() {1332 if (zoom == 0) {1333 return Stream.empty();1334 } else {1335 return IntStream.rangeClosed(minX, maxX).mapToObj(1336 x -> IntStream.rangeClosed(minY, maxY).mapToObj(y -> new TilePosition(x, y, zoom))1337 ).flatMap(Function.identity());1338 }1339 }1340 }1341 1342 /**1343 * The position of a single tile.1344 * @author Michael Zangl1345 * @since xxx1346 */1347 private static class TilePosition {1348 private final int x;1349 private final int y;1350 private final int zoom;1351 TilePosition(int x, int y, int zoom) {1352 super();1353 this.x = x;1354 this.y = y;1355 this.zoom = zoom;1356 }1357 1358 TilePosition(Tile tile) {1359 this(tile.getXtile(), tile.getYtile(), tile.getZoom());1360 }1361 1362 /**1363 * @return the x position1364 */1365 public int getX() {1366 return x;1367 }1368 1369 /**1370 * @return the y position1371 */1372 public int getY() {1373 return y;1374 }1375 1376 /**1377 * @return the zoom1378 */1379 public int getZoom() {1380 return zoom;1381 }1382 1383 @Override1384 public String toString() {1385 return "TilePosition [x=" + x + ", y=" + y + ", zoom=" + zoom + "]";1386 }1387 }1388 1389 private class TileSet extends TileRange {1390 1391 protected TileSet(TileXY t1, TileXY t2, int zoom) {1392 super(t1, t2, zoom);1393 sanitize();1394 }1395 1396 /**1397 * null tile set1398 */1399 private TileSet() {1400 // default1401 }1402 1403 protected void sanitize() {1404 if (minX < tileSource.getTileXMin(zoom)) {1405 minX = tileSource.getTileXMin(zoom);1406 }1407 if (minY < tileSource.getTileYMin(zoom)) {1408 minY = tileSource.getTileYMin(zoom);1409 }1410 if (maxX > tileSource.getTileXMax(zoom)) {1411 maxX = tileSource.getTileXMax(zoom);1412 }1413 if (maxY > tileSource.getTileYMax(zoom)) {1414 maxY = tileSource.getTileYMax(zoom);1415 }1416 }1417 1418 private boolean tooSmall() {1419 return this.tilesSpanned() < 2.1;1420 }1421 1422 private boolean tooLarge() {1423 return insane() || this.tilesSpanned() > 20;1424 }1425 1426 private boolean insane() {1427 return tileCache == null || size() > tileCache.getCacheSize();1428 }1429 1430 /**1431 * Get all tiles represented by this TileSet that are already in the tileCache.1432 */1433 private List<Tile> allExistingTiles() {1434 return allTiles(p -> getTile(p));1435 }1436 1437 private List<Tile> allTilesCreate() {1438 return allTiles(p -> getOrCreateTile(p));1439 }1440 1441 private List<Tile> allTiles(Function<TilePosition, Tile> mapper) {1442 return tilePositions().map(mapper).filter(Objects::nonNull).collect(Collectors.toList());1443 }1444 1445 @Override1446 public Stream<TilePosition> tilePositions() {1447 if (this.insane()) {1448 // Tileset is either empty or too large1449 return Stream.empty();1450 } else {1451 return super.tilePositions();1452 }1453 }1454 1455 private List<Tile> allLoadedTiles() {1456 return allExistingTiles().stream().filter(Tile::isLoaded).collect(Collectors.toList());1457 }1458 1459 /**1460 * @return comparator, that sorts the tiles from the center to the edge of the current screen1461 */1462 private Comparator<Tile> getTileDistanceComparator() {1463 final int centerX = (int) Math.ceil((minX + maxX) / 2d);1464 final int centerY = (int) Math.ceil((minY + maxY) / 2d);1465 return Comparator.comparingInt(t -> Math.abs(t.getXtile() - centerX) + Math.abs(t.getYtile() - centerY));1466 }1467 1468 private void loadAllTiles(boolean force) {1469 if (!getDisplaySettings().isAutoLoad() && !force)1470 return;1471 List<Tile> allTiles = allTilesCreate();1472 allTiles.sort(getTileDistanceComparator());1473 for (Tile t : allTiles) {1474 loadTile(t, force);1475 }1476 }1477 1478 private void loadAllErrorTiles(boolean force) {1479 if (!getDisplaySettings().isAutoLoad() && !force)1480 return;1481 for (Tile t : this.allTilesCreate()) {1482 if (t.hasError()) {1483 tileLoader.createTileLoaderJob(t).submit(force);1484 }1485 }1486 }1487 1488 /**1489 * Call the given paint method for all tiles in this tile set.1490 * <p>1491 * Uses a parallel stream.1492 * @param visitor A visitor to call for each tile.1493 * @param missed a consumer to call for each missed tile.1494 */1495 public void visitTiles(Consumer<Tile> visitor, Consumer<TilePosition> missed) {1496 tilePositions().parallel().forEach(tp -> visitTilePosition(visitor, tp, missed));1497 }1498 1499 private void visitTilePosition(Consumer<Tile> visitor, TilePosition tp, Consumer<TilePosition> missed) {1500 Tile tile = getTile(tp);1501 if (tile == null) {1502 missed.accept(tp);1503 } else {1504 visitor.accept(tile);1505 }1506 }1507 1508 @Override1509 public String toString() {1510 return getClass().getName() + ": zoom: " + zoom + " X(" + minX + ", " + maxX + ") Y(" + minY + ", " + maxY + ") size: " + size();1511 }1512 }1513 1514 /**1515 * Create a TileSet by EastNorth bbox taking a layer shift in account1516 * @param topLeft top-left lat/lon1517 * @param botRight bottom-right lat/lon1518 * @param zoom zoom level1519 * @return the tile set1520 * @since 106511521 */1522 protected TileSet getTileSet(EastNorth topLeft, EastNorth botRight, int zoom) {1523 return getTileSet(getShiftedLatLon(topLeft), getShiftedLatLon(botRight), zoom);1524 }1525 1526 /**1527 * Create a TileSet by known LatLon bbox without layer shift correction1528 * @param topLeft top-left lat/lon1529 * @param botRight bottom-right lat/lon1530 * @param zoom zoom level1531 * @return the tile set1532 * @since 106511533 */1534 protected TileSet getTileSet(LatLon topLeft, LatLon botRight, int zoom) {1535 if (zoom == 0)1536 return new TileSet();1537 1538 TileXY t1 = tileSource.latLonToTileXY(topLeft.toCoordinate(), zoom);1539 TileXY t2 = tileSource.latLonToTileXY(botRight.toCoordinate(), zoom);1540 return new TileSet(t1, t2, zoom);1541 }1542 1543 private static class TileSetInfo {1544 public boolean hasVisibleTiles;1545 public boolean hasOverzoomedTiles;1546 public boolean hasLoadingTiles;1547 }1548 1549 private static <S extends AbstractTMSTileSource> TileSetInfo getTileSetInfo(AbstractTileSourceLayer<S>.TileSet ts) {1550 List<Tile> allTiles = ts.allExistingTiles();1551 TileSetInfo result = new TileSetInfo();1552 result.hasLoadingTiles = allTiles.size() < ts.size();1553 for (Tile t : allTiles) {1554 if ("no-tile".equals(t.getValue("tile-info"))) {1555 result.hasOverzoomedTiles = true;1556 }1557 1558 if (t.isLoaded()) {1559 if (!t.hasError()) {1560 result.hasVisibleTiles = true;1561 }1562 } else if (t.isLoading()) {1563 result.hasLoadingTiles = true;1564 }1565 }1566 return result;1567 }1568 1569 private class DeepTileSet {1570 private final ProjectionBounds bounds;1571 private final int minZoom, maxZoom;1572 private final TileSet[] tileSets;1573 private final TileSetInfo[] tileSetInfos;1574 1575 @SuppressWarnings("unchecked")1576 DeepTileSet(ProjectionBounds bounds, int minZoom, int maxZoom) {1577 this.bounds = bounds;1578 this.minZoom = minZoom;1579 this.maxZoom = maxZoom;1580 this.tileSets = new AbstractTileSourceLayer.TileSet[maxZoom - minZoom + 1];1581 this.tileSetInfos = new TileSetInfo[maxZoom - minZoom + 1];1582 }1583 1584 public TileSet getTileSet(int zoom) {1585 if (zoom < minZoom)1586 return nullTileSet;1587 synchronized (tileSets) {1588 TileSet ts = tileSets[zoom-minZoom];1589 if (ts == null) {1590 ts = AbstractTileSourceLayer.this.getTileSet(bounds.getMin(), bounds.getMax(), zoom);1591 tileSets[zoom-minZoom] = ts;1592 }1593 return ts;1594 }1595 }1596 1597 public TileSetInfo getTileSetInfo(int zoom) {1598 if (zoom < minZoom)1599 return new TileSetInfo();1600 synchronized (tileSetInfos) {1601 TileSetInfo tsi = tileSetInfos[zoom-minZoom];1602 if (tsi == null) {1603 tsi = AbstractTileSourceLayer.getTileSetInfo(getTileSet(zoom));1604 tileSetInfos[zoom-minZoom] = tsi;1605 }1606 return tsi;1607 }1608 }1609 }1610 1611 @Override1612 public void paint(Graphics2D g, MapView mv, Bounds bounds) {1613 // old and unused.1614 }1615 1616 private void drawInViewArea(Graphics2D g, MapView mv, ProjectionBounds pb) {1617 int zoom = currentZoomLevel;1618 if (getDisplaySettings().isAutoZoom()) {1619 zoom = getBestZoom();1620 }1621 1622 DeepTileSet dts = new DeepTileSet(pb, getMinZoomLvl(), zoom);1623 TileSet ts = dts.getTileSet(zoom);1624 1625 int displayZoomLevel = zoom;1626 1627 boolean noTilesAtZoom = false;1628 if (getDisplaySettings().isAutoZoom() && getDisplaySettings().isAutoLoad()) {1629 // Auto-detection of tilesource maxzoom (currently fully works only for Bing)1630 TileSetInfo tsi = dts.getTileSetInfo(zoom);1631 if (!tsi.hasVisibleTiles && (!tsi.hasLoadingTiles || tsi.hasOverzoomedTiles)) {1632 noTilesAtZoom = true;1633 }1634 // Find highest zoom level with at least one visible tile1635 for (int tmpZoom = zoom; tmpZoom > dts.minZoom; tmpZoom--) {1636 if (dts.getTileSetInfo(tmpZoom).hasVisibleTiles) {1637 displayZoomLevel = tmpZoom;1638 break;1639 }1640 }1641 // Do binary search between currentZoomLevel and displayZoomLevel1642 while (zoom > displayZoomLevel && !tsi.hasVisibleTiles && tsi.hasOverzoomedTiles) {1643 zoom = (zoom + displayZoomLevel)/2;1644 tsi = dts.getTileSetInfo(zoom);1645 }1646 1647 setZoomLevel(zoom);1648 1649 // If all tiles at displayZoomLevel is loaded, load all tiles at next zoom level1650 // to make sure there're really no more zoom levels1651 // loading is done in the next if section1652 if (zoom == displayZoomLevel && !tsi.hasLoadingTiles && zoom < dts.maxZoom) {1653 zoom++;1654 tsi = dts.getTileSetInfo(zoom);1655 }1656 // When we have overzoomed tiles and all tiles at current zoomlevel is loaded,1657 // load tiles at previovus zoomlevels until we have all tiles on screen is loaded.1658 // loading is done in the next if section1659 while (zoom > dts.minZoom && tsi.hasOverzoomedTiles && !tsi.hasLoadingTiles) {1660 zoom--;1661 tsi = dts.getTileSetInfo(zoom);1662 }1663 ts = dts.getTileSet(zoom);1664 } else if (getDisplaySettings().isAutoZoom()) {1665 setZoomLevel(zoom);1666 }1667 1668 // Too many tiles... refuse to download1669 if (!ts.tooLarge()) {1670 //Main.debug("size: " + ts.size() + " spanned: " + ts.tilesSpanned());1671 ts.loadAllTiles(false);1672 }1673 1674 if (displayZoomLevel != zoom) {1675 ts = dts.getTileSet(displayZoomLevel);1676 }1677 1678 g.setColor(Color.DARK_GRAY);1679 1680 List<Tile> missedTiles = this.paintTileImages(g, ts);1681 int[] otherZooms = {-1, 1, -2, 2, -3, -4, -5};1682 for (int zoomOffset : otherZooms) {1683 if (!getDisplaySettings().isAutoZoom()) {1684 break;1685 }1686 int newzoom = displayZoomLevel + zoomOffset;1687 if (newzoom < getMinZoomLvl() || newzoom > getMaxZoomLvl()) {1688 continue;1689 }1690 if (missedTiles.isEmpty()) {1691 break;1692 }1693 List<Tile> newlyMissedTiles = new LinkedList<>();1694 for (Tile missed : missedTiles) {1695 if ("no-tile".equals(missed.getValue("tile-info")) && zoomOffset > 0) {1696 // Don't try to paint from higher zoom levels when tile is overzoomed1697 newlyMissedTiles.add(missed);1698 continue;1699 }1700 Tile t2 = tempCornerTile(missed);1701 TileSet ts2 = getTileSet(1702 getShiftedLatLon(tileSource.tileXYToLatLon(missed)),1703 getShiftedLatLon(tileSource.tileXYToLatLon(t2)),1704 newzoom);1705 // Instantiating large TileSets is expensive. If there1706 // are no loaded tiles, don't bother even trying.1707 if (ts2.allLoadedTiles().isEmpty()) {1708 newlyMissedTiles.add(missed);1709 continue;1710 }1711 if (ts2.tooLarge()) {1712 continue;1713 }1714 newlyMissedTiles.addAll(this.paintTileImages(g, ts2, newzoom, missed));1715 }1716 missedTiles = newlyMissedTiles;1717 }1718 if (Main.isDebugEnabled() && !missedTiles.isEmpty()) {1719 Main.debug("still missed "+missedTiles.size()+" in the end");1720 }1721 g.setColor(Color.red);1722 g.setFont(InfoFont);1723 1724 // The current zoom tileset should have all of its tiles due to the loadAllTiles(), unless it to tooLarge()1725 for (Tile t : ts.allExistingTiles()) {1726 this.paintTileText(ts, t, g, mv, displayZoomLevel, t);1727 }1728 1729 EastNorth min = pb.getMin();1730 EastNorth max = pb.getMax();1731 attribution.paintAttribution(g, mv.getWidth(), mv.getHeight(), getShiftedCoord(min), getShiftedCoord(max),1732 displayZoomLevel, this);1733 1734 //g.drawString("currentZoomLevel=" + currentZoomLevel, 120, 120);1735 g.setColor(Color.lightGray);1736 1737 if (ts.insane()) {1738 myDrawString(g, tr("zoom in to load any tiles"), 120, 120);1739 } else if (ts.tooLarge()) {1740 myDrawString(g, tr("zoom in to load more tiles"), 120, 120);1741 } else if (!getDisplaySettings().isAutoZoom() && ts.tooSmall()) {1742 myDrawString(g, tr("increase tiles zoom level (change resolution) to see more detail"), 120, 120);1743 }1744 1745 if (noTilesAtZoom) {1746 myDrawString(g, tr("No tiles at this zoom level"), 120, 120);1747 }1748 if (Main.isDebugEnabled()) {1749 myDrawString(g, tr("Current zoom: {0}", currentZoomLevel), 50, 140);1750 myDrawString(g, tr("Display zoom: {0}", displayZoomLevel), 50, 155);1751 myDrawString(g, tr("Pixel scale: {0}", getScaleFactor(currentZoomLevel)), 50, 170);1752 myDrawString(g, tr("Best zoom: {0}", getBestZoom()), 50, 185);1753 myDrawString(g, tr("Estimated cache size: {0}", estimateTileCacheSize()), 50, 200);1754 if (tileLoader instanceof TMSCachedTileLoader) {1755 TMSCachedTileLoader cachedTileLoader = (TMSCachedTileLoader) tileLoader;1756 int offset = 200;1757 for (String part: cachedTileLoader.getStats().split("\n")) {1758 offset += 15;1759 myDrawString(g, tr("Cache stats: {0}", part), 50, offset);1760 }1761 }1762 }1763 }1764 1765 /**1766 * Returns tile for a pixel position.<p>1767 * This isn't very efficient, but it is only used when the user right-clicks on the map.1768 * @param px pixel X coordinate1769 * @param py pixel Y coordinate1770 * @return Tile at pixel position1771 */1772 private Tile getTileForPixelpos(int px, int py) {1773 if (Main.isDebugEnabled()) {1774 Main.debug("getTileForPixelpos("+px+", "+py+')');1775 }1776 MapView mv = Main.map.mapView;1777 Point clicked = new Point(px, py);1778 EastNorth topLeft = mv.getEastNorth(0, 0);1779 EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());1780 int z = currentZoomLevel;1781 TileSet ts = getTileSet(topLeft, botRight, z);1782 1783 if (!ts.tooLarge()) {1784 ts.loadAllTiles(false); // make sure there are tile objects for all tiles1785 }1786 Stream<Tile> clickedTiles = ts.allExistingTiles().stream()1787 .filter(t -> coordinateConverter.getRectangleForTile(t).contains(clicked));1788 if (Main.isTraceEnabled()) {1789 clickedTiles = clickedTiles.peek(t -> Main.trace("Clicked on tile: " + t.getXtile() + ' ' + t.getYtile() +1790 " currentZoomLevel: " + currentZoomLevel));1791 }1792 return clickedTiles.findAny().orElse(null);1793 }1794 1795 529 @Override 1796 530 public Action[] getMenuEntries() { 1797 531 ArrayList<Action> actions = new ArrayList<>(); … … implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi 1803 537 } 1804 538 1805 539 public Action[] getLayerListEntries() { 1806 return new Action[] { 1807 LayerListDialog.getInstance().createActivateLayerAction(this), 1808 LayerListDialog.getInstance().createShowHideLayerAction(), 1809 LayerListDialog.getInstance().createDeleteLayerAction(), 1810 SeparatorLayerAction.INSTANCE, 1811 // color, 1812 new OffsetAction(), 1813 new RenameLayerAction(this.getAssociatedFile(), this), 1814 SeparatorLayerAction.INSTANCE 1815 }; 540 return new Action[] { LayerListDialog.getInstance().createActivateLayerAction(this), 541 LayerListDialog.getInstance().createShowHideLayerAction(), 542 LayerListDialog.getInstance().createDeleteLayerAction(), SeparatorLayerAction.INSTANCE, 543 // color, 544 new OffsetAction(), new RenameLayerAction(this.getAssociatedFile(), this), 545 SeparatorLayerAction.INSTANCE }; 1816 546 } 1817 547 1818 548 /** … … implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi 1821 551 */ 1822 552 public Action[] getCommonEntries() { 1823 553 return new Action[] { 1824 new AutoLoadTilesAction(), 1825 new AutoZoomAction(), 1826 new ShowErrorsAction(), 1827 new IncreaseZoomAction(), 1828 new DecreaseZoomAction(), 1829 new ZoomToBestAction(), 1830 new ZoomToNativeLevelAction(), 1831 new FlushTileCacheAction(), 1832 new LoadErroneusTilesAction(), 1833 new LoadAllTilesAction() 1834 }; 554 new AutoLoadTilesAction(), 555 new AutoZoomAction(), 556 new ShowErrorsAction() }; 557 } 558 559 private List<Action> getMapViewEntries(MapView mv) { 560 TileSourcePainter<T> painter = painters.get(mv); 561 return painter.getMenuEntries(); 1835 562 } 1836 563 1837 564 @Override 1838 565 public String getToolTipText() { 566 String currentZoomLevel = painters.values().stream().findAny().map(TileSourcePainter::getZoomString).orElse("?"); 1839 567 if (getDisplaySettings().isAutoLoad()) { 1840 return tr("{0} ({1}), automatically downloading in zoom {2}", this.getClass().getSimpleName(), getName(), currentZoomLevel); 568 return tr("{0} ({1}), automatically downloading in zoom {2}", this.getClass().getSimpleName(), getName(), 569 currentZoomLevel); 1841 570 } else { 1842 return tr("{0} ({1}), downloading in zoom {2}", this.getClass().getSimpleName(), getName(), currentZoomLevel); 571 return tr("{0} ({1}), downloading in zoom {2}", this.getClass().getSimpleName(), getName(), 572 currentZoomLevel); 1843 573 } 1844 574 } 1845 575 … … implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi 1853 583 return false; 1854 584 } 1855 585 586 @Override 587 public void paint(Graphics2D g, MapView mv, Bounds bbox) { 588 // never called, we use a custom painter 589 throw new UnsupportedOperationException(); 590 } 591 1856 592 /** 1857 593 * Task responsible for precaching imagery along the gpx track 1858 594 * … … implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi 1868 604 */ 1869 605 public PrecacheTask(ProgressMonitor progressMonitor) { 1870 606 this.progressMonitor = progressMonitor; 607 // TODO 608 T tileSource = null; 1871 609 this.tileLoader = getTileLoaderFactory().makeTileLoader(this, getHeaders(tileSource)); 1872 610 if (this.tileLoader instanceof TMSCachedTileLoader) { 1873 ((TMSCachedTileLoader) this.tileLoader) .setDownloadExecutor(1874 TMSCachedTileLoader.getNewThreadPoolExecutor("Precache downloader"));611 ((TMSCachedTileLoader) this.tileLoader) 612 .setDownloadExecutor(TMSCachedTileLoader.getNewThreadPoolExecutor("Precache downloader")); 1875 613 } 1876 614 } 1877 615 … … implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi 1929 667 * @param bufferY how many units in current Coordinate Reference System to cover in Y axis in both sides 1930 668 * @return precache task representing download task 1931 669 */ 1932 public AbstractTileSourceLayer<T>.PrecacheTask downloadAreaToCache(final ProgressMonitor progressMonitor, List<LatLon> points,1933 double bufferX, double bufferY) {670 public AbstractTileSourceLayer<T>.PrecacheTask downloadAreaToCache(final ProgressMonitor progressMonitor, 671 List<LatLon> points, double bufferX, double bufferY) { 1934 672 PrecacheTask precacheTask = new PrecacheTask(progressMonitor); 1935 673 final Set<Tile> requestedTiles = new ConcurrentSkipListSet<>( 1936 674 (o1, o2) -> String.CASE_INSENSITIVE_ORDER.compare(o1.getKey(), o2.getKey())); 1937 for (LatLon point: points) { 1938 675 for (LatLon point : points) { 676 //TODO 677 TileSource tileSource = null; 678 int currentZoomLevel = 0; 1939 679 TileXY minTile = tileSource.latLonToTileXY(point.lat() - bufferY, point.lon() - bufferX, currentZoomLevel); 1940 680 TileXY curTile = tileSource.latLonToTileXY(point.toCoordinate(), currentZoomLevel); 1941 681 TileXY maxTile = tileSource.latLonToTileXY(point.lat() + bufferY, point.lon() + bufferX, currentZoomLevel); … … implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi 1957 697 precacheTask.progressMonitor.setTicksCount(requestedTiles.size()); 1958 698 1959 699 TileLoader loader = precacheTask.getTileLoader(); 1960 for (Tile t : requestedTiles) {700 for (Tile t : requestedTiles) { 1961 701 loader.createTileLoaderJob(t).submit(); 1962 702 } 1963 703 return precacheTask; … … implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeLi 1979 719 adjustAction.destroy(); 1980 720 } 1981 721 1982 private class TileSourcePainter extends CompatibilityModeLayerPainter { 1983 /** 1984 * The memory handle that will hold our tile source. 1985 */ 1986 private MemoryHandle<?> memory; 1987 1988 @Override 1989 public void paint(MapViewGraphics graphics) { 1990 allocateCacheMemory(); 1991 if (memory != null) { 1992 doPaint(graphics); 1993 } 1994 } 1995 1996 private void doPaint(MapViewGraphics graphics) { 1997 ProjectionBounds pb = graphics.getClipBounds().getProjectionBounds(); 1998 1999 drawInViewArea(graphics.getDefaultGraphics(), graphics.getMapView(), pb); 2000 } 2001 2002 private void allocateCacheMemory() { 2003 if (memory == null) { 2004 MemoryManager manager = MemoryManager.getInstance(); 2005 if (manager.isAvailable(getEstimatedCacheSize())) { 2006 try { 2007 memory = manager.allocateMemory("tile source layer", getEstimatedCacheSize(), Object::new); 2008 } catch (NotEnoughMemoryException e) { 2009 Main.warn("Could not allocate tile source memory", e); 2010 } 2011 } 2012 } 2013 } 2014 2015 protected long getEstimatedCacheSize() { 2016 return 4L * tileSource.getTileSize() * tileSource.getTileSize() * estimateTileCacheSize(); 2017 } 2018 2019 @Override 2020 public void detachFromMapView(MapViewEvent event) { 2021 event.getMapView().removeMouseListener(adapter); 2022 MapView.removeZoomChangeListener(AbstractTileSourceLayer.this); 2023 super.detachFromMapView(event); 2024 if (memory != null) { 2025 memory.free(); 2026 } 2027 } 722 /** 723 * A {@link TileSourcePainter} notifies us of a dispatch 724 * @param tileSourcePainter The painter. 725 */ 726 public void detach(TileSourcePainter<T> tileSourcePainter) { 727 GuiHelper.assertCallFromEdt(); 728 painters.entrySet().removeIf(e -> e.getValue().equals(tileSourcePainter)); 2028 729 } 2029 730 } -
src/org/openstreetmap/josm/gui/layer/TMSLayer.java
diff --git a/src/org/openstreetmap/josm/gui/layer/TMSLayer.java b/src/org/openstreetmap/josm/gui/layer/TMSLayer.java index bf833ff..e220828 100644
a b import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader; 22 22 import org.openstreetmap.josm.data.preferences.BooleanProperty; 23 23 import org.openstreetmap.josm.data.preferences.IntegerProperty; 24 24 import org.openstreetmap.josm.data.projection.Projection; 25 import org.openstreetmap.josm.gui.layer.NativeScaleLayer.ScaleList; 26 import org.openstreetmap.josm.gui.layer.imagery.TileSourcePainter; 25 27 26 28 /** 27 29 * Class that displays a slippy map layer. … … import org.openstreetmap.josm.data.projection.Projection; 32 34 * @author Upliner <upliner@gmail.com> 33 35 * @since 3715 34 36 */ 35 public class TMSLayer extends AbstractCachedTileSourceLayer<TMSTileSource> implements NativeScaleLayer{37 public class TMSLayer extends AbstractCachedTileSourceLayer<TMSTileSource> { 36 38 private static final String CACHE_REGION_NAME = "TMS"; 37 39 38 40 private static final String PREFERENCE_PREFIX = "imagery.tms"; … … public class TMSLayer extends AbstractCachedTileSourceLayer<TMSTileSource> imple 57 59 super(info); 58 60 } 59 61 60 /**61 * Creates and returns a new TileSource instance depending on the {@link ImageryType}62 * of the passed ImageryInfo object.63 *64 * If no appropriate TileSource is found, null is returned.65 * Currently supported ImageryType are {@link ImageryType#TMS},66 * {@link ImageryType#BING}, {@link ImageryType#SCANEX}.67 *68 *69 * @param info imagery info70 * @return a new TileSource instance or null if no TileSource for the ImageryInfo/ImageryType could be found.71 * @throws IllegalArgumentException if url from imagery info is null or invalid72 */73 @Override74 protected TMSTileSource getTileSource(ImageryInfo info) {75 return getTileSourceStatic(info, () -> {76 Main.debug("Attribution loaded, running loadAllErrorTiles");77 this.loadAllErrorTiles(false);78 });79 }80 81 @Override82 public final boolean isProjectionSupported(Projection proj) {83 return "EPSG:3857".equals(proj.toCode()) || "EPSG:4326".equals(proj.toCode());84 }85 86 62 @Override 87 63 public final String nameSupportedProjections() { 88 64 return tr("EPSG:4326 and Mercator projection are supported"); … … public class TMSLayer extends AbstractCachedTileSourceLayer<TMSTileSource> imple 148 124 return AbstractCachedTileSourceLayer.getCache(CACHE_REGION_NAME); 149 125 } 150 126 151 @Override152 public ScaleList getNativeScales() {153 return nativeScaleList;154 }155 127 156 128 private static ScaleList initNativeScaleList() { 157 129 Collection<Double> scales = new ArrayList<>(AbstractTileSourceLayer.MAX_ZOOM); … … public class TMSLayer extends AbstractCachedTileSourceLayer<TMSTileSource> imple 161 133 } 162 134 return new ScaleList(scales); 163 135 } 136 137 @Override 138 protected TileSourcePainter<TMSTileSource> createMapViewPainter(MapViewEvent event) { 139 return new TileSourcePainter<TMSTileSource>(this, event.getMapView()) { 140 @Override 141 public final boolean isProjectionSupported(Projection proj) { 142 return "EPSG:3857".equals(proj.toCode()) || "EPSG:4326".equals(proj.toCode()); 143 } 144 // TODO @Override 145 // public ScaleList getNativeScales() { 146 // return nativeScaleList; 147 // } 148 149 @Override 150 protected TMSTileSource generateTileSource(AbstractTileSourceLayer<TMSTileSource> layer) { 151 return getTileSourceStatic(info, () -> { 152 Main.debug("Attribution loaded, running loadAllErrorTiles"); 153 loadAllErrorTiles(false); 154 }); 155 } 156 }; 157 } 164 158 } -
src/org/openstreetmap/josm/gui/layer/WMSLayer.java
diff --git a/src/org/openstreetmap/josm/gui/layer/WMSLayer.java b/src/org/openstreetmap/josm/gui/layer/WMSLayer.java index acf27c6..5c8fbf2 100644
a b import static org.openstreetmap.josm.tools.I18n.tr; 6 6 import java.awt.event.ActionEvent; 7 7 import java.util.ArrayList; 8 8 import java.util.Arrays; 9 import java.util.HashSet; 9 10 import java.util.List; 10 11 import java.util.Map; 11 12 import java.util.Set; … … import java.util.TreeSet; 13 14 14 15 import javax.swing.AbstractAction; 15 16 import javax.swing.Action; 16 import javax.swing.JOptionPane;17 17 18 18 import org.apache.commons.jcs.access.CacheAccess; 19 19 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader; … … import org.openstreetmap.josm.data.imagery.WMSCachedTileLoader; 27 27 import org.openstreetmap.josm.data.preferences.BooleanProperty; 28 28 import org.openstreetmap.josm.data.preferences.IntegerProperty; 29 29 import org.openstreetmap.josm.data.projection.Projection; 30 import org.openstreetmap.josm.gui.ExtendedDialog; 30 import org.openstreetmap.josm.data.projection.ProjectionChangeListener; 31 import org.openstreetmap.josm.gui.MapView; 31 32 import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings; 33 import org.openstreetmap.josm.gui.layer.imagery.TileSourcePainter; 32 34 33 35 /** 34 36 * This is a layer that grabs the current screen from an WMS server. The data … … public class WMSLayer extends AbstractCachedTileSourceLayer<TemplatedWMSTileSour 115 117 } 116 118 117 119 @Override 118 public boolean isProjectionSupported(Projection proj) {119 return supportedProjections == null || supportedProjections.isEmpty() || supportedProjections.contains(proj.toCode()) ||120 (info.isEpsg4326To3857Supported() && supportedProjections.contains("EPSG:4326")121 && "EPSG:3857".equals(Main.getProjection().toCode()));122 }123 124 @Override125 120 public String nameSupportedProjections() { 126 121 StringBuilder ret = new StringBuilder(); 127 122 for (String e: supportedProjections) { … … public class WMSLayer extends AbstractCachedTileSourceLayer<TemplatedWMSTileSour 136 131 return ret.substring(0, ret.length()-2) + appendix; 137 132 } 138 133 139 @Override 140 public void projectionChanged(Projection oldValue, Projection newValue) { 141 // do not call super - we need custom warning dialog 142 143 if (!isProjectionSupported(newValue)) { 144 String message = 145 "<html><body><p>" + tr("The layer {0} does not support the new projection {1}.", getName(), newValue.toCode()) + 146 "<p style='width: 450px; position: absolute; margin: 0px;'>" + 147 tr("Supported projections are: {0}", nameSupportedProjections()) + "</p>" + 148 "<p>" + tr("Change the projection again or remove the layer."); 149 150 ExtendedDialog warningDialog = new ExtendedDialog(Main.parent, tr("Warning"), new String[]{tr("OK")}). 151 setContent(message). 152 setIcon(JOptionPane.WARNING_MESSAGE); 153 154 if (isReprojectionPossible()) { 155 warningDialog.toggleEnable("imagery.wms.projectionSupportWarnings." + tileSource.getBaseUrl()); 156 } 157 warningDialog.showDialog(); 158 } 159 160 if (!newValue.equals(oldValue)) { 161 tileSource.initProjection(newValue); 162 } 163 } 134 // @Override 135 // public void projectionChanged(Projection oldValue, Projection newValue) { 136 // // do not call super - we need custom warning dialog 137 // 138 // if (!isProjectionSupported(newValue)) { 139 // String message = 140 // "<html><body><p>" + tr("The layer {0} does not support the new projection {1}.", getName(), newValue.toCode()) + 141 // "<p style='width: 450px; position: absolute; margin: 0px;'>" + 142 // tr("Supported projections are: {0}", nameSupportedProjections()) + "</p>" + 143 // "<p>" + tr("Change the projection again or remove the layer."); 144 // 145 // ExtendedDialog warningDialog = new ExtendedDialog(Main.parent, tr("Warning"), new String[]{tr("OK")}). 146 // setContent(message). 147 // setIcon(JOptionPane.WARNING_MESSAGE); 148 // 149 // if (isReprojectionPossible()) { 150 //// TODO: warningDialog.toggleEnable("imagery.wms.projectionSupportWarnings." + tileSource.getBaseUrl()); 151 // } 152 // warningDialog.showDialog(); 153 // } 154 // } 164 155 165 156 @Override 166 157 protected Class<? extends TileLoader> getTileLoaderClass() { … … public class WMSLayer extends AbstractCachedTileSourceLayer<TemplatedWMSTileSour 182 173 private boolean isReprojectionPossible() { 183 174 return supportedProjections.contains("EPSG:4326") && "EPSG:3857".equals(Main.getProjection().toCode()); 184 175 } 176 177 @Override 178 protected TileSourcePainter<TemplatedWMSTileSource> createMapViewPainter(MapViewEvent event) { 179 return new WMSPainter(this, event.getMapView()); 180 } 181 182 private static class WMSPainter extends TileSourcePainter<TemplatedWMSTileSource> { 183 private final ProjectionChangeListener initOnProjectionChange = (oldValue, newValue) -> tileSource.initProjection(newValue); 184 private HashSet<String> supportedProjections; 185 186 public WMSPainter(AbstractTileSourceLayer<TemplatedWMSTileSource> abstractTileSourceLayer, MapView mapView) { 187 super(abstractTileSourceLayer, mapView); 188 Main.addProjectionChangeListener(initOnProjectionChange); 189 190 ImageryInfo info2 = abstractTileSourceLayer.getInfo(); 191 supportedProjections = new HashSet<>(info2.getServerProjections()); 192 if (info2.isEpsg4326To3857Supported() && supportedProjections.contains("EPSG:4326")) { 193 supportedProjections.add("EPSG:3857"); 194 } 195 196 zoom.setZoomBounds(0, zoom.getMaxZoom()); 197 } 198 199 @Override 200 public void detachFromMapView(MapViewEvent event) { 201 Main.removeProjectionChangeListener(initOnProjectionChange); 202 super.detachFromMapView(event); 203 } 204 205 @Override 206 public boolean isProjectionSupported(Projection proj) { 207 return supportedProjections == null || supportedProjections.isEmpty() || supportedProjections.contains(proj.toCode()); 208 209 } 210 211 } 185 212 } -
src/org/openstreetmap/josm/gui/layer/WMTSLayer.java
diff --git a/src/org/openstreetmap/josm/gui/layer/WMTSLayer.java b/src/org/openstreetmap/josm/gui/layer/WMTSLayer.java index f4cb70c..07ac173 100644
a b 2 2 package org.openstreetmap.josm.gui.layer; 3 3 4 4 import java.io.IOException; 5 import java.util.Set;6 5 7 6 import org.apache.commons.jcs.access.CacheAccess; 8 7 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader; … … import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType; 13 12 import org.openstreetmap.josm.data.imagery.WMSCachedTileLoader; 14 13 import org.openstreetmap.josm.data.imagery.WMTSTileSource; 15 14 import org.openstreetmap.josm.data.projection.Projection; 15 import org.openstreetmap.josm.data.projection.ProjectionChangeListener; 16 import org.openstreetmap.josm.gui.MapView; 16 17 import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings; 18 import org.openstreetmap.josm.gui.layer.imagery.TileSourcePainter; 17 19 18 20 /** 19 21 * WMTS layer based on AbstractTileSourceLayer. Overrides few methods to align WMTS to Tile based computations … … import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings; 25 27 * @author Wiktor NiesiobÄ™dzki 26 28 * 27 29 */ 28 public class WMTSLayer extends AbstractCachedTileSourceLayer<WMTSTileSource> implements NativeScaleLayer{30 public class WMTSLayer extends AbstractCachedTileSourceLayer<WMTSTileSource> { 29 31 private static final String PREFERENCE_PREFIX = "imagery.wmts"; 30 32 31 33 /** … … public class WMTSLayer extends AbstractCachedTileSourceLayer<WMTSTileSource> imp 50 52 return new TileSourceDisplaySettings(PREFERENCE_PREFIX); 51 53 } 52 54 53 @Override 54 protected WMTSTileSource getTileSource(ImageryInfo info) { 55 try { 56 if (info.getImageryType() == ImageryType.WMTS && info.getUrl() != null) { 57 WMTSTileSource.checkUrl(info.getUrl()); 58 WMTSTileSource tileSource = new WMTSTileSource(info); 59 info.setAttribution(tileSource); 60 return tileSource; 61 } 62 return null; 63 } catch (IOException e) { 64 Main.warn(e); 65 throw new IllegalArgumentException(e); 66 } 67 } 68 69 @Override 70 protected int getBestZoom() { 71 if (!Main.isDisplayingMapView()) 72 return 0; 73 ScaleList scaleList = getNativeScales(); 74 if (scaleList == null) { 75 return getMaxZoomLvl(); 76 } 77 Scale snap = scaleList.getSnapScale(Main.map.mapView.getScale(), false); 78 return Math.max( 79 getMinZoomLvl(), 80 Math.min( 81 snap != null ? snap.getIndex() : getMaxZoomLvl(), 82 getMaxZoomLvl() 83 ) 84 ); 85 } 86 87 @Override 88 protected int getMinZoomLvl() { 89 return 0; 90 } 91 92 @Override 93 public boolean isProjectionSupported(Projection proj) { 94 Set<String> supportedProjections = tileSource.getSupportedProjections(); 95 return supportedProjections.contains(proj.toCode()); 96 } 97 98 @Override 99 public String nameSupportedProjections() { 100 StringBuilder ret = new StringBuilder(); 101 for (String e: tileSource.getSupportedProjections()) { 102 ret.append(e).append(", "); 103 } 104 return ret.length() > 2 ? ret.substring(0, ret.length()-2) : ret.toString(); 105 } 106 107 @Override 108 public void projectionChanged(Projection oldValue, Projection newValue) { 109 super.projectionChanged(oldValue, newValue); 110 tileSource.initProjection(newValue); 111 } 55 // TODO @Override 56 // protected int getBestZoom() { 57 // if (!Main.isDisplayingMapView()) 58 // return 0; 59 // ScaleList scaleList = getNativeScales(); 60 // if (scaleList == null) { 61 // return getMaxZoomLvl(); 62 // } 63 // Scale snap = scaleList.getSnapScale(Main.map.mapView.getScale(), false); 64 // return Math.max( 65 // getMinZoomLvl(), 66 // Math.min( 67 // snap != null ? snap.getIndex() : getMaxZoomLvl(), 68 // getMaxZoomLvl() 69 // ) 70 // ); 71 // } 72 73 // 74 // @Override 75 // public String nameSupportedProjections() { 76 // StringBuilder ret = new StringBuilder(); 77 // for (String e: tileSource.getSupportedProjections()) { 78 // ret.append(e).append(", "); 79 // } 80 // return ret.length() > 2 ? ret.substring(0, ret.length()-2) : ret.toString(); 81 // } 112 82 113 83 @Override 114 84 protected Class<? extends TileLoader> getTileLoaderClass() { … … public class WMTSLayer extends AbstractCachedTileSourceLayer<WMTSTileSource> imp 127 97 return AbstractCachedTileSourceLayer.getCache(CACHE_REGION_NAME); 128 98 } 129 99 100 // TODO @Override 101 // public ScaleList getNativeScales() { 102 // return tileSource.getNativeScales(); 103 // } 104 130 105 @Override 131 public ScaleList getNativeScales() { 132 return tileSource.getNativeScales(); 106 protected TileSourcePainter<WMTSTileSource> createMapViewPainter(MapViewEvent event) { 107 return new WMTSPainter(this, event.getMapView()); 108 } 109 110 private static class WMTSPainter extends TileSourcePainter<WMTSTileSource> { 111 private final ProjectionChangeListener initOnProjectionChange = (oldValue, newValue) -> tileSource 112 .initProjection(newValue); 113 114 public WMTSPainter(AbstractTileSourceLayer<WMTSTileSource> abstractTileSourceLayer, MapView mapView) { 115 super(abstractTileSourceLayer, mapView); 116 Main.addProjectionChangeListener(initOnProjectionChange); 117 118 zoom.setZoomBounds(0, zoom.getMaxZoom()); 119 } 120 121 @Override 122 protected WMTSTileSource generateTileSource(AbstractTileSourceLayer<WMTSTileSource> layer) { 123 try { 124 ImageryInfo layerInfo = layer.getInfo(); 125 if (layerInfo.getImageryType() == ImageryType.WMTS && layerInfo.getUrl() != null) { 126 WMTSTileSource.checkUrl(layerInfo.getUrl()); 127 WMTSTileSource tileSource = new WMTSTileSource(layerInfo); 128 layerInfo.setAttribution(tileSource); 129 return tileSource; 130 } 131 return null; 132 } catch (IOException e) { 133 Main.warn(e); 134 throw new IllegalArgumentException(e); 135 } 136 } 137 138 @Override 139 public void detachFromMapView(MapViewEvent event) { 140 Main.removeProjectionChangeListener(initOnProjectionChange); 141 super.detachFromMapView(event); 142 } 143 144 @Override 145 public boolean isProjectionSupported(Projection proj) { 146 return tileSource.getSupportedProjections().contains(proj.toCode()); 147 } 133 148 } 134 149 } -
new file src/org/openstreetmap/josm/gui/layer/imagery/AbstractTileSourceLoader.java
diff --git a/src/org/openstreetmap/josm/gui/layer/imagery/AbstractTileSourceLoader.java b/src/org/openstreetmap/josm/gui/layer/imagery/AbstractTileSourceLoader.java new file mode 100644 index 0000000..248cded
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.layer.imagery; 3 4 import static org.openstreetmap.josm.tools.I18n.tr; 5 6 import java.awt.Dimension; 7 import java.awt.event.ActionEvent; 8 import java.util.function.Predicate; 9 10 import javax.swing.AbstractAction; 11 12 import org.openstreetmap.gui.jmapviewer.AttributionSupport; 13 import org.openstreetmap.gui.jmapviewer.MemoryTileCache; 14 import org.openstreetmap.gui.jmapviewer.Tile; 15 import org.openstreetmap.gui.jmapviewer.interfaces.CachedTileLoader; 16 import org.openstreetmap.gui.jmapviewer.interfaces.TileCache; 17 import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader; 18 import org.openstreetmap.gui.jmapviewer.tilesources.AbstractTMSTileSource; 19 import org.openstreetmap.josm.Main; 20 import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader; 21 import org.openstreetmap.josm.gui.MapView; 22 import org.openstreetmap.josm.gui.NavigatableComponent.ZoomChangeListener; 23 import org.openstreetmap.josm.gui.PleaseWaitRunnable; 24 import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer; 25 import org.openstreetmap.josm.gui.layer.imagery.TileForAreaFinder.TileForAreaGetter; 26 import org.openstreetmap.josm.gui.util.GuiHelper; 27 import org.openstreetmap.josm.tools.MemoryManager; 28 import org.openstreetmap.josm.tools.MemoryManager.MemoryHandle; 29 import org.openstreetmap.josm.tools.MemoryManager.NotEnoughMemoryException; 30 31 /** 32 * This class backs the {@link TileSourcePainter} by handling the loading / acces of the tile images 33 * @author Michael Zangl 34 * @param <T> The imagery type to use 35 * @since xxx 36 */ 37 public abstract class AbstractTileSourceLoader<T extends AbstractTMSTileSource> implements TileForAreaGetter, ZoomChangeListener { 38 39 /* 40 * use MemoryTileCache instead of tileLoader JCS cache, as tileLoader caches only content (byte[] of image) 41 * and MemoryTileCache caches whole Tile. This gives huge performance improvement when a lot of tiles are visible 42 * in MapView (for example - when limiting min zoom in imagery) 43 * 44 * Use per-layer tileCache instance, as the more layers there are, the more tiles needs to be cached 45 */ 46 private final TileCache tileCache; // initialized together with tileSource 47 protected final T tileSource; 48 protected final TileLoader tileLoader; 49 protected final AttributionSupport attribution = new AttributionSupport(); 50 51 /** 52 * The memory handle that will hold our tile source. 53 */ 54 private MemoryHandle<?> memory; 55 56 protected AbstractTileSourceLoader(AbstractTileSourceLayer<T> layer) { 57 tileSource = generateTileSource(layer); 58 if (tileSource == null) { 59 throw new IllegalArgumentException(tr("Failed to create tile source")); 60 } 61 62 attribution.initialize(tileSource); 63 64 tileLoader = layer.generateTileLoader(tileSource); 65 66 tileCache = new MemoryTileCache(estimateTileCacheSize()); 67 MapView.addZoomChangeListener(this); 68 } 69 70 protected T generateTileSource(AbstractTileSourceLayer<T> layer) { 71 return layer.getTileSource(); 72 } 73 /** 74 * Check if there are any matching tiles in the given range 75 * @param range The range to check in 76 * @param pred The predicate the tiles need to match 77 * @return If there are such tiles. 78 */ 79 public boolean hasTiles(TileRange range, Predicate<Tile> pred) { 80 return range.tilePositions().map(this::getTile).anyMatch(pred); 81 } 82 83 protected Tile getOrCreateTile(TilePosition tilePosition) { 84 Tile tile = getTile(tilePosition); 85 if (tile == null) { 86 tile = new Tile(tileSource, tilePosition.getX(), tilePosition.getY(), tilePosition.getZoom()); 87 tileCache.addTile(tile); 88 } 89 90 if (!tile.isLoaded()) { 91 tile.loadPlaceholderFromCache(tileCache); 92 } 93 return tile; 94 } 95 96 /** 97 * Returns tile at given position. 98 * This can and will return null for tiles that are not already in the cache. 99 * @param tilePosition The position 100 * @return tile at given position 101 */ 102 protected Tile getTile(TilePosition tilePosition) { 103 if (!contains(tilePosition)) { 104 return null; 105 } else { 106 return tileCache.getTile(tileSource, tilePosition.getX(), tilePosition.getY(), tilePosition.getZoom()); 107 } 108 } 109 110 /** 111 * Check if this tile source contains the given position. 112 * @param position The position 113 * @return <code>true</code> if that positon is contained. 114 */ 115 private boolean contains(TilePosition position) { 116 return position.getZoom() >= tileSource.getMinZoom() && position.getZoom() <= tileSource.getMaxZoom() 117 && position.getX() >= tileSource.getTileXMin(position.getZoom()) 118 && position.getX() <= tileSource.getTileXMax(position.getZoom()) 119 && position.getY() >= tileSource.getTileYMin(position.getZoom()) 120 && position.getY() <= tileSource.getTileYMax(position.getZoom()); 121 } 122 123 protected void loadTiles(TileRange range, boolean force) { 124 if (force) { 125 if (isTooLarge(range)) { 126 Main.warn("Not downloading all tiles because there are too many tiles on an axis!"); 127 } else { 128 range.tilePositionsSorted().filter(this::contains).forEach(t -> loadTile(t, force)); 129 } 130 } 131 } 132 133 protected static boolean isTooSmall(TileRange range) { 134 return range.tilesSpanned() < 2; 135 } 136 137 protected boolean isTooLarge(TileRange range) { 138 return range.size() > tileCache.getCacheSize() || range.tilesSpanned() > 20; 139 } 140 141 protected boolean loadTile(TilePosition tile, boolean force) { 142 return loadTile(getOrCreateTile(tile), force); 143 } 144 145 private boolean loadTile(Tile tile, boolean force) { 146 if (tile == null) 147 return false; 148 if (!force && (tile.isLoaded() || tile.hasError() || isOverzoomed(tile))) 149 return false; 150 if (tile.isLoading()) 151 return false; 152 tileLoader.createTileLoaderJob(tile).submit(force); 153 return true; 154 } 155 156 @Override 157 public void zoomChanged() { 158 if (tileLoader instanceof TMSCachedTileLoader) { 159 ((TMSCachedTileLoader) tileLoader).cancelOutstandingTasks(); 160 } 161 } 162 163 /** 164 * Test if a tile is visible. 165 * @param t The tile to test 166 * @return <code>true</code> if it is visible 167 */ 168 public static boolean isVisible(Tile t) { 169 return t != null && t.isLoaded() && !t.hasError(); 170 } 171 172 /** 173 * Test if a tile is missing. 174 * @param t The tile to test 175 * @return <code>true</code> if it is loading or not loaded yet. 176 */ 177 public static boolean isMissing(Tile t) { 178 return t == null || t.isLoading(); 179 } 180 181 /** 182 * Test if a tile is marked as loading. 183 * @param t The tile to test 184 * @return <code>true</code> if it is loading 185 */ 186 public static boolean isLoading(Tile t) { 187 return t != null && t.isLoading(); 188 } 189 190 /** 191 * Test if a tile is marked as overzoomed. 192 * @param t The tile to test 193 * @return <code>true</code> if it is overzoomed 194 */ 195 public static boolean isOverzoomed(Tile t) { 196 return t != null && "no-tile".equals(t.getValue("tile-info")); 197 } 198 199 /** 200 * Reserve the memory for the cache 201 * @return <code>true</code> if it is reserved. 202 */ 203 protected boolean allocateCacheMemory() { 204 if (memory == null) { 205 MemoryManager manager = MemoryManager.getInstance(); 206 if (manager.isAvailable(getEstimatedCacheSize())) { 207 try { 208 memory = manager.allocateMemory("tile source layer", getEstimatedCacheSize(), Object::new); 209 } catch (NotEnoughMemoryException e) { 210 Main.warn("Could not allocate tile source memory", e); 211 } 212 } 213 } 214 return memory != null; 215 } 216 217 /** 218 * Free the cache memeory 219 */ 220 protected void freeCacheMemory() { 221 if (memory != null) { 222 memory.free(); 223 } 224 } 225 226 protected long getEstimatedCacheSize() { 227 return 4L * tileSource.getTileSize() * tileSource.getTileSize() * estimateTileCacheSize(); 228 } 229 230 protected int estimateTileCacheSize() { 231 Dimension screenSize = GuiHelper.getMaximumScreenSize(); 232 int height = screenSize.height; 233 int width = screenSize.width; 234 int tileSize = 256; // default tile size 235 if (tileSource != null) { 236 tileSize = tileSource.getTileSize(); 237 } 238 // as we can see part of the tile at the top and at the bottom, use Math.ceil(...) + 1 to accommodate for that 239 int visibileTiles = (int) (Math.ceil((double) height / tileSize + 1) 240 * Math.ceil((double) width / tileSize + 1)); 241 // add 10% for tiles from different zoom levels 242 // use offset to decide, how many tiles are visible 243 int ret = (int) Math.ceil(Math.pow(2d, AbstractTileSourceLayer.ZOOM_OFFSET.get()) * visibileTiles * 4); 244 Main.info("AbstractTileSourceLayer: estimated visible tiles: {0}, estimated cache size: {1}", visibileTiles, 245 ret); 246 return ret; 247 } 248 249 250 protected class FlushTileCacheAction extends AbstractAction { 251 FlushTileCacheAction() { 252 super(tr("Flush tile cache")); 253 setEnabled(tileLoader instanceof CachedTileLoader); 254 } 255 256 @Override 257 public void actionPerformed(ActionEvent ae) { 258 new PleaseWaitRunnable(tr("Flush tile cache")) { 259 @Override 260 protected void realRun() { 261 clearTileCache(); 262 } 263 264 @Override 265 protected void finish() { 266 // empty - flush is instaneus 267 } 268 269 @Override 270 protected void cancel() { 271 // empty - flush is instaneus 272 } 273 274 /** 275 * Clears the tile cache. 276 */ 277 private void clearTileCache() { 278 if (tileLoader instanceof CachedTileLoader) { 279 ((CachedTileLoader) tileLoader).clearCache(tileSource); 280 } 281 tileCache.clear(); 282 } 283 }.run(); 284 } 285 } 286 } -
new file src/org/openstreetmap/josm/gui/layer/imagery/TextPainter.java
diff --git a/src/org/openstreetmap/josm/gui/layer/imagery/TextPainter.java b/src/org/openstreetmap/josm/gui/layer/imagery/TextPainter.java new file mode 100644 index 0000000..580758e
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.layer.imagery; 3 4 import java.awt.Color; 5 import java.awt.Graphics2D; 6 import java.awt.geom.AffineTransform; 7 8 /** 9 * This class handles text painting on the tile source layer. 10 * @author Michael Zangl 11 * @since xxx 12 */ 13 public class TextPainter { 14 private Graphics2D g; 15 private int debugY; 16 private int overlayY; 17 18 /** 19 * Reset internal state and start. 20 * @param g The graphics to paint on 21 */ 22 public void start(Graphics2D g) { 23 this.g = g; 24 debugY = 160; 25 overlayY = 100; 26 } 27 28 /** 29 * Add a debug string 30 * @param debugLine The debug line 31 */ 32 public void addDebug(String debugLine) { 33 debugY += drawString(debugLine, 50, debugY, 500); 34 } 35 36 /** 37 * Draw a string onto a given tile. 38 * @param text The text to draw 39 * @param tile The tile to paint on 40 * @param converter A converter to convert the tile to screen coordinates. 41 */ 42 public void drawTileString(String text, TilePosition tile, TileCoordinateConverter converter) { 43 AffineTransform transform = converter.getTransformForTile(tile, 0, 0, 0, .5, 1, .5); 44 transform.scale(1.0 / 200, 1.0 / 200); 45 AffineTransform oldTransform = g.getTransform(); 46 g.transform(transform); 47 drawString(text, 10, 10, 180); 48 g.setTransform(oldTransform); 49 } 50 51 /** 52 * Add a text overlay for the map. 53 * @param text The text to add. 54 */ 55 public void addTextOverlay(String text) { 56 overlayY += drawString(text, 120, overlayY, 500); 57 } 58 59 private int drawString(String text, int x, int y, int width) { 60 String textToDraw = text; 61 int maxLineWidth = 0; 62 int wholeLineWidth = g.getFontMetrics().stringWidth(text); 63 if (wholeLineWidth > width) { 64 // text longer than tile size, split it 65 StringBuilder line = new StringBuilder(); 66 StringBuilder ret = new StringBuilder(); 67 for (String s: text.split(" ")) { 68 int lineWidth = g.getFontMetrics().stringWidth(line.toString() + s); 69 if (lineWidth > width) { 70 ret.append(line).append('\n'); 71 line.setLength(0); 72 lineWidth = g.getFontMetrics().stringWidth(s); 73 } 74 line.append(s).append(' '); 75 maxLineWidth = Math.max(lineWidth, maxLineWidth); 76 } 77 ret.append(line); 78 textToDraw = ret.toString(); 79 } else { 80 maxLineWidth = wholeLineWidth; 81 } 82 83 return drawLines(x, y, textToDraw.split("\n"), maxLineWidth); 84 } 85 86 private int drawLines(int x, int y, String[] lines, int maxLineWidth) { 87 int height = g.getFontMetrics().getHeight(); 88 89 // background 90 g.setColor(new Color(0, 0, 0, 50)); 91 g.fillRect(x - 3, y - height - 1, maxLineWidth + 6, (3 + height) * lines.length + 2); 92 93 int offset = 0; 94 for (String s: lines) { 95 // shadow 96 g.setColor(Color.black); 97 g.drawString(s, x + 1, y + offset + 1); 98 g.setColor(Color.lightGray); 99 g.drawString(s, x, y + offset); 100 offset += height + 3; 101 } 102 return offset; 103 } 104 105 } -
src/org/openstreetmap/josm/gui/layer/imagery/TileCoordinateConverter.java
diff --git a/src/org/openstreetmap/josm/gui/layer/imagery/TileCoordinateConverter.java b/src/org/openstreetmap/josm/gui/layer/imagery/TileCoordinateConverter.java index ff368e5..fc76dce 100644
a b 1 1 // License: GPL. For details, see LICENSE file. 2 2 package org.openstreetmap.josm.gui.layer.imagery; 3 3 4 import java.awt.Shape; 5 import java.awt.geom.AffineTransform; 4 6 import java.awt.geom.Point2D; 5 import java.awt.geom.Rectangle2D;6 7 7 8 import org.openstreetmap.gui.jmapviewer.Tile; 8 9 import org.openstreetmap.gui.jmapviewer.TileXY; 9 10 import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate; 10 11 import org.openstreetmap.gui.jmapviewer.interfaces.TileSource; 12 import org.openstreetmap.josm.data.Bounds; 11 13 import org.openstreetmap.josm.data.coor.LatLon; 12 import org.openstreetmap.josm.data.projection.Projecting; 13 import org.openstreetmap.josm.data.projection.ShiftedProjecting; 14 import org.openstreetmap.josm.gui.MapView; 14 import org.openstreetmap.josm.gui.MapViewState; 15 import org.openstreetmap.josm.gui.MapViewState.MapViewLatLonRectangle; 15 16 import org.openstreetmap.josm.gui.MapViewState.MapViewPoint; 17 import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer; 18 import org.openstreetmap.josm.tools.Utils; 16 19 17 20 /** 18 21 * This class handles tile coordinate management and computes their position in the map view. … … import org.openstreetmap.josm.gui.MapViewState.MapViewPoint; 20 23 * @since 10651 21 24 */ 22 25 public class TileCoordinateConverter { 23 private MapView mapView; 24 private TileSourceDisplaySettings settings; 26 private MapViewState displacedState; 25 27 private TileSource tileSource; 26 28 27 29 /** … … public class TileCoordinateConverter { 30 32 * @param tileSource The tile source to use when converting coordinates. 31 33 * @param settings displacement settings. 32 34 */ 33 public TileCoordinateConverter(MapView mapView, TileSource tileSource, TileSourceDisplaySettings settings) {34 this. mapView = mapView;35 public TileCoordinateConverter(MapViewState mapView, TileSource tileSource, TileSourceDisplaySettings settings) { 36 this.displacedState = mapView.shifted(settings.getDisplacement()); 35 37 this.tileSource = tileSource; 36 this.settings = settings;37 38 } 38 39 39 private MapViewPoint pos(ICoordinate ll) { 40 return mapView.getState().getPointFor(new LatLon(ll)).add(settings.getDisplacement()); 41 } 42 43 /** 44 * Gets the projecting instance to use to convert between latlon and eastnorth coordinates. 45 * @return The {@link Projecting} instance. 46 */ 47 public Projecting getProjecting() { 48 return new ShiftedProjecting(mapView.getProjection(), settings.getDisplacement()); 40 protected MapViewPoint pos(ICoordinate ll) { 41 return displacedState.getPointFor(new LatLon(ll)); 49 42 } 50 43 51 44 /** … … public class TileCoordinateConverter { 63 56 * @param tile The tile 64 57 * @return The positon. 65 58 */ 66 public Rectangle2D getRectangleForTile(Tiletile) {67 ICoordinate c1 = tile.getTileSource().tileXYToLatLon(tile);68 ICoordinate c2 = tile.getTileSource().tileXYToLatLon(tile.getXtile() + 1, tile.getYtile() + 1, tile.getZoom());59 public MapViewLatLonRectangle getAreaForTile(TilePosition tile) { 60 MapViewPoint p1 = tileUV(tile, 0, 0); 61 MapViewPoint p2 = tileUV(tile, 1, 1); 69 62 70 return pos(c1).rectTo(pos(c2)).getInView(); 63 return p1.latLonRectTo(p2); 64 } 65 66 /** 67 * Gets an affine transform that maps image u/v (0..1) space to east/north space. 68 * <p> 69 * You need to scale it by the image size to draw the buffered image. 70 * @param tile 71 * @param u1 72 * @param v1 73 * @param u2 74 * @param v2 75 * @param u3 76 * @param v3 77 * @return the transform 78 */ 79 public AffineTransform getTransformForTile(TilePosition tile, double u1, double v1, double u2, double v2, double u3, double v3) { 80 MapViewPoint p1 = tileUV(tile, u1, v1); 81 MapViewPoint p2 = tileUV(tile, u2, v2); 82 MapViewPoint p3 = tileUV(tile, u3, v3); 83 84 // We compute the matrix in a way that p_i.inView is mapped to the corresponding image position. 85 // ( u1 ) ( m00 m01 m02 ) (p1.viewX ) 86 // ( v1 ) * ( m10 m11 m12 ) = (p1.viewY ) 87 // ( 1 ) ( 0 0 1 ) (1 ) 88 // ( u2 ) ( m00 m01 m02 ) (p2.viewX ) 89 // ( v2 ) * ( m10 m11 m12 ) = (p2.viewY ) 90 // ( 1 ) ( 0 0 1 ) (1 ) 91 // ( u3 ) ( m00 m01 m02 ) (p3.viewX ) 92 // ( v3 ) * ( m10 m11 m12 ) = (p3.viewY ) 93 // ( 1 ) ( 0 0 1 ) (1 ) 94 95 // u1 * m00 + v1 * m01 + m02 = p1.viewX 96 // u2 * m00 + v2 * m01 + m02 = p2.viewX 97 // u3 * m00 + v3 * m01 + m02 = p3.viewX 98 // u1 * m10 + v1 * m11 + m12 = p1.viewY 99 // u2 * m10 + v2 * m11 + m12 = p2.viewY 100 // u3 * m10 + v3 * m11 + m12 = p3.viewY 101 102 // u1 * m00 + v1 * m01 + m02 = p1.viewX 103 // (u2 - u1) * m00 + (v2 - v1) * m01 = p2.viewX - p1.viewX 104 // (u3 - u1) * m00 + (v3 - v1) * m01 = p3.viewX - p1.viewX 105 106 // if v2 != v1 and v3 != v1 107 // (u2 - u1) / (v2 - v1) * m00 + m01 = (p2.viewX - p1.viewX) / (v2 - v1) 108 // (u3 - u1) / (v3 - v1) * m00 + m01 = (p3.viewX - p1.viewX) / (v3 - v1) 109 110 // m00 = ((p2.viewX - p1.viewX) / (v2 - v1) - (p3.viewX - p1.viewX) / (v3 - v1)) / ((u2 - u1) / (v2 - v1) - (u3 - u1) / (v3 - v1)) 111 // m01 = (p3.viewX - p1.viewX) / (v3 - v1) - (u3 - u1) / (v3 - v1) * m00 112 // m02 = p1.viewX - u1 * m00 + v1 * m01 113 114 // if v2 == v1: 115 // u1 * m00 + v1 * m01 + m02 = p1.viewX 116 // (u2 - u1) * m00 + = p2.viewX - p1.viewX 117 // (u3 - u1) * m00 + (v3 - v1) * m01 = p3.viewX - p1.viewX 118 119 // if v3 == v1 120 // u1 * m00 + v1 * m01 + m02 = p1.viewX 121 // (u2 - u1) * m00 + (v2 - v1) * m01 = p2.viewX - p1.viewX 122 // (u3 - u1) * m00 + = p3.viewX - p1.viewX 123 124 125 double du2 = u2 - u1; 126 double du3 = u3 - u1; 127 double dv2 = v2 - v1; 128 double dv3 = v3 - v1; 129 130 // x space 131 double p1x = p1.getInView().getX(); 132 double p2x = p2.getInView().getX(); 133 double p3x = p3.getInView().getX(); 134 135 double m00; 136 double m01; 137 if (Utils.equalsEpsilon(0, dv2)) { 138 if (Utils.equalsEpsilon(0, du2) || Utils.equalsEpsilon(0, dv3)) { 139 // unsolveable 140 return new AffineTransform(); 141 } 142 m00 = (p2x - p1x) / du2; 143 m01 = (p3x - p1x) / dv3 - du3 / dv3 * m00; 144 } else if (Utils.equalsEpsilon(0, dv3)) { 145 if (Utils.equalsEpsilon(0, du3)) { 146 // unsolveable 147 return new AffineTransform(); 148 } 149 m00 = (p3x - p1x) / du3; 150 m01 = (p2x - p1x) / dv2 - du2 / dv2 * m00; 151 } else { 152 m00 = ((p2x - p1x) / dv2 - (p3x - p1x) / dv3) / (du2 / dv2 - du3 / dv3); 153 m01 = (p3x - p1x) / dv3 - du3 / dv3 * m00; 154 } 155 double m02 = p1x - u1 * m00 + v1 * m01; 156 157 // y space 158 double p1y = p1.getInView().getY(); 159 double p2y = p2.getInView().getY(); 160 double p3y = p3.getInView().getY(); 161 double m10; 162 double m11; 163 if (Utils.equalsEpsilon(0, dv2)) { 164 m10 = (p2y - p1y) / du2; 165 m11 = (p3y - p1y) / dv3 - du3 / dv3 * m10; 166 } else if (Utils.equalsEpsilon(0, dv3)) { 167 m10 = (p3y - p1y) / du3; 168 m11 = (p2y - p1y) / dv2 - du2 / dv2 * m10; 169 } else { 170 m10 = ((p2y - p1y) / dv2 - (p3y - p1y) / dv3) / (du2 / dv2 - du3 / dv3); 171 m11 = (p3y - p1y) / dv3 - du3 / dv3 * m10; 172 } 173 double m12 = p1y - u1 * m10 + v1 * m11; 174 175 return new AffineTransform(new double[] { 176 m00, m10, m01, m11, m02, m12 177 }); 178 } 179 180 private MapViewPoint tileUV(TilePosition tile, double u, double v) { 181 ICoordinate tileLatLon = tileSource.tileXYToLatLon(tile.getX(), tile.getY(), tile.getZoom()); 182 if (Utils.equalsEpsilon(0, u) && Utils.equalsEpsilon(0, v)) { 183 return pos(tileLatLon); 184 } else { 185 ICoordinate nextTile = tileSource.tileXYToLatLon(tile.getX() + 1, tile.getY() + 1, tile.getZoom()); 186 return displacedState.getPointFor(new LatLon( 187 (1 - v) * tileLatLon.getLat() + v * nextTile.getLat(), 188 (1 - u) * tileLatLon.getLon() + u * nextTile.getLon())); 189 } 71 190 } 72 191 73 192 /** … … public class TileCoordinateConverter { 76 195 * @return average number of screen pixels per tile pixel 77 196 */ 78 197 public double getScaleFactor(int zoom) { 79 LatLon topLeft = mapView.getLatLon(0, 0); 80 LatLon botRight = mapView.getLatLon(mapView.getWidth(), mapView.getHeight()); 81 TileXY t1 = tileSource.latLonToTileXY(topLeft.toCoordinate(), zoom); 82 TileXY t2 = tileSource.latLonToTileXY(botRight.toCoordinate(), zoom); 83 84 int screenPixels = mapView.getWidth()*mapView.getHeight(); 85 double tilePixels = Math.abs((t2.getY()-t1.getY())*(t2.getX()-t1.getX())*tileSource.getTileSize()*tileSource.getTileSize()); 86 if (screenPixels == 0 || tilePixels == 0) return 1; 87 return screenPixels/tilePixels; 198 Bounds area = displacedState.getViewArea().getCornerBounds(); 199 TileXY t1 = tileSource.latLonToTileXY(area.getMin().toCoordinate(), zoom); 200 TileXY t2 = tileSource.latLonToTileXY(area.getMax().toCoordinate(), zoom); 201 202 double screenPixels = displacedState.getViewWidth() * displacedState.getViewHeight(); 203 int tileSize = tileSource.getTileSize(); 204 double tilePixels = Math.abs((t2.getY() - t1.getY()) * (t2.getX() - t1.getX()) * tileSize * tileSize); 205 if (screenPixels < 1e-10 || tilePixels < 1e-10) { 206 return 1; 207 } else { 208 return screenPixels / tilePixels; 209 } 210 } 211 212 /** 213 * Get the tiles in view at the given zoom level. 214 * @param zoom The zoom level 215 * @return The tiles that are in the view. 216 */ 217 public TileRange getViewAtZoom(int zoom) { 218 Bounds view = displacedState.getViewArea().getLatLonBoundsBox(); 219 view = view.intersect(displacedState.getProjection().getWorldBoundsLatLon()); 220 if (view == null) { 221 return new TileRange(); 222 } else { 223 TileXY t1 = tileSource.latLonToTileXY(view.getMin().toCoordinate(), zoom); 224 TileXY t2 = tileSource.latLonToTileXY(view.getMax().toCoordinate(), zoom); 225 return new TileRange(t1, t2, zoom); 226 } 227 } 228 229 /** 230 * Gets the mathematically best zoom. May be out of range. 231 * @return The zoom 232 */ 233 public int getBestZoom() { 234 double factor = getScaleFactor(1); // check the ratio between area of tilesize at zoom 1 to current view 235 double result = Math.log(factor) / Math.log(2) / 2; 236 /* 237 * Math.log(factor)/Math.log(2) - gives log base 2 of factor 238 * We divide result by 2, as factor contains ratio between areas. We could do Math.sqrt before log, or just divide log by 2 239 * 240 * ZOOM_OFFSET controls, whether we work with overzoomed or underzoomed tiles. Positive ZOOM_OFFSET 241 * is for working with underzoomed tiles (higher quality when working with aerial imagery), negative ZOOM_OFFSET 242 * is for working with overzoomed tiles (big, pixelated), which is good when working with high-dpi screens and/or 243 * maps as a imagery layer 244 */ 245 246 return (int) Math.round(result + 1 + AbstractTileSourceLayer.ZOOM_OFFSET.get() / 1.9); 247 } 248 249 /** 250 * Gets the clip to use to only paint inside the projection 251 * @return The clip. 252 */ 253 public Shape getProjectionClip() { 254 return displacedState.getArea(displacedState.getProjection().getWorldBoundsLatLon()); 88 255 } 89 256 } -
new file src/org/openstreetmap/josm/gui/layer/imagery/TileForAreaFinder.java
diff --git a/src/org/openstreetmap/josm/gui/layer/imagery/TileForAreaFinder.java b/src/org/openstreetmap/josm/gui/layer/imagery/TileForAreaFinder.java new file mode 100644 index 0000000..3410390
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.layer.imagery; 3 4 import java.util.ArrayList; 5 import java.util.Collections; 6 import java.util.List; 7 import java.util.stream.Collectors; 8 import java.util.stream.Stream; 9 10 import org.openstreetmap.josm.Main; 11 import org.openstreetmap.josm.data.Bounds; 12 13 /** 14 * This class helps finding loaded tiles for a tile area. 15 * @author Michael Zangl 16 * @since xxx 17 */ 18 public final class TileForAreaFinder { 19 20 private TileForAreaFinder() { 21 // hidden 22 } 23 24 /** 25 * Get a stream of all tile positions to paint for the given zoom level. 26 * @param initialRange The range 27 * @param rangeProducer An object that converts between {@link Bounds} and {@link TilePosition} 28 * @return A stream of tiles to paint. 29 */ 30 public static Stream<TilePosition> getAtDefaultZoom(TileRange initialRange, TileForAreaGetter rangeProducer) { 31 return initialRange.tilePositions().filter(rangeProducer::isAvailable); 32 } 33 34 /** 35 * Gets a stream of all positions to be painted taking the fallback zoom levels into account. 36 * <p> 37 * Limiting the resulting stream won't change the performance of this method. It returns a stream so that this may be changed in the future. 38 * 39 * @param initialRange The range 40 * @param rangeProducer An object that converts between {@link Bounds} and {@link TilePosition} 41 * @param zoom The zoom levels to try at. 42 * @return A stream of tiles to paint. 43 */ 44 public static Stream<TilePosition> getWithFallbackZoom(TileRange initialRange, TileForAreaGetter rangeProducer, ZoomLevelManager zoom) { 45 ArrayList<TilePosition> list = new ArrayList<>(); 46 List<List<Bounds>> missedInPreviousRuns = new ArrayList<>(); 47 List<Bounds> missed = initialRange.tilePositions().flatMap(pos -> addPosition(pos, rangeProducer, list)).collect(Collectors.toList()); 48 List<Bounds> missedInLastRun = missed; 49 50 for (int delta : new int[] { -1, 1, -2, 2, -3, -4, -5 }) { 51 int zoomLevel = delta + initialRange.getZoom(); 52 if (zoomLevel >= zoom.getMinZoom() && zoomLevel <= zoom.getMaxZoom()) { 53 missed = missedInLastRun 54 .stream() 55 .flatMap(b -> rangeProducer.toRangeAtZoom(b, zoomLevel).tilePositions()) 56 .distinct() 57 .filter(tile -> missedInPreviousRuns.stream().allMatch(l -> l.stream().anyMatch(rangeProducer.getBounds(tile)::intersects))) 58 .flatMap(pos -> addPosition(pos, rangeProducer, list)) 59 .collect(Collectors.toList()); 60 Main.trace("Still missed {0} tile areas at zoom {1}.", missed.size(), zoomLevel); 61 if (missed.isEmpty()) { 62 break; 63 } 64 65 missedInPreviousRuns.add(missedInLastRun); 66 missedInLastRun = missed; 67 } 68 // no break condition. But missed will be empty, so flatMap should not be costy. 69 } 70 71 Collections.reverse(list); 72 return list.stream().distinct(); 73 } 74 75 private static Stream<Bounds> addPosition(TilePosition pos, TileForAreaGetter rangeProducer, ArrayList<TilePosition> addTo) { 76 if (rangeProducer.isAvailable(pos)) { 77 addTo.add(pos); 78 return Stream.empty(); 79 } else { 80 return Stream.of(rangeProducer.getBounds(pos)); 81 } 82 } 83 84 /** 85 * Classes implementing this interface allow us to convert between a tile range and {@link Bounds}. 86 * @author Michael Zangl 87 * @since xxx 88 */ 89 public interface TileForAreaGetter { 90 /** 91 * Gets a tile range that is enclosing this tile at the given zoom level. 92 * @param bounds The bounds to get the range for 93 * @param zoom The zoom the range should be at 94 * @return The range for the given bounds. 95 */ 96 public TileRange toRangeAtZoom(Bounds bounds, int zoom); 97 98 /** 99 * Gets the bounds for a tile 100 * @param tile The tile 101 * @return The bounds for that tile 102 */ 103 public Bounds getBounds(TilePosition tile); 104 105 /** 106 * Checks if an image is available for the given tile 107 * @param tile The tile to check 108 * @return True if it is available. 109 */ 110 public boolean isAvailable(TilePosition tile); 111 112 } 113 114 } -
new file src/org/openstreetmap/josm/gui/layer/imagery/TilePosition.java
diff --git a/src/org/openstreetmap/josm/gui/layer/imagery/TilePosition.java b/src/org/openstreetmap/josm/gui/layer/imagery/TilePosition.java new file mode 100644 index 0000000..c823341
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.layer.imagery; 3 4 import java.io.Serializable; 5 6 import org.openstreetmap.gui.jmapviewer.Tile; 7 import org.openstreetmap.gui.jmapviewer.TileXY; 8 9 /** 10 * The position of a single tile. In contrast to {@link TileXY}, this stores the position of the whole tile. 11 * @author Michael Zangl 12 * @since xxx 13 */ 14 public class TilePosition implements Serializable { 15 private static final long serialVersionUID = 1; 16 17 private final int x; 18 private final int y; 19 private final int zoom; 20 21 /** 22 * Create a new tile position object 23 * @param x The x coordinate 24 * @param y The y coordinate 25 * @param zoom The zoom at which the tile is. 26 */ 27 TilePosition(int x, int y, int zoom) { 28 super(); 29 this.x = x; 30 this.y = y; 31 this.zoom = zoom; 32 } 33 34 /** 35 * Create a new tile position object 36 * @param tile The tile from wich the position should be copied. 37 */ 38 public TilePosition(Tile tile) { 39 this(tile.getXtile(), tile.getYtile(), tile.getZoom()); 40 } 41 42 /** 43 * @return the x position 44 */ 45 public int getX() { 46 return x; 47 } 48 49 /** 50 * @return the y position 51 */ 52 public int getY() { 53 return y; 54 } 55 56 /** 57 * @return the zoom 58 */ 59 public int getZoom() { 60 return zoom; 61 } 62 63 /** 64 * Gets an x/y coordinate inside this tile 65 * @param du x delta. Range should be 0..1 66 * @param dv y delta. Range should be 0..1 67 * @return The x/y coordinate 68 */ 69 public TileXY uv(double du, double dv) { 70 return new TileXY(getX() + du, getY() + dv); 71 } 72 73 /* (non-Javadoc) 74 * @see java.lang.Object#hashCode() 75 */ 76 @Override 77 public int hashCode() { 78 final int prime = 31; 79 int result = 1; 80 result = prime * result + x; 81 result = prime * result + y; 82 result = prime * result + zoom; 83 return result; 84 } 85 86 /* (non-Javadoc) 87 * @see java.lang.Object#equals(java.lang.Object) 88 */ 89 @Override 90 public boolean equals(Object obj) { 91 if (this == obj) 92 return true; 93 if (obj == null) 94 return false; 95 if (getClass() != obj.getClass()) 96 return false; 97 TilePosition other = (TilePosition) obj; 98 if (x != other.x) 99 return false; 100 if (y != other.y) 101 return false; 102 if (zoom != other.zoom) 103 return false; 104 return true; 105 } 106 107 @Override 108 public String toString() { 109 return "TilePosition [x=" + x + ", y=" + y + ", zoom=" + zoom + "]"; 110 } 111 } -
new file src/org/openstreetmap/josm/gui/layer/imagery/TileRange.java
diff --git a/src/org/openstreetmap/josm/gui/layer/imagery/TileRange.java b/src/org/openstreetmap/josm/gui/layer/imagery/TileRange.java new file mode 100644 index 0000000..901447d
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.layer.imagery; 3 4 import java.util.Comparator; 5 import java.util.function.Function; 6 import java.util.stream.IntStream; 7 import java.util.stream.Stream; 8 9 import org.openstreetmap.gui.jmapviewer.TileXY; 10 11 /** 12 * This is a rectangular range of tiles. 13 */ 14 class TileRange { 15 int minX; 16 int maxX; 17 int minY; 18 int maxY; 19 int zoom; 20 21 TileRange() { 22 } 23 24 protected TileRange(TileXY t1, TileXY t2, int zoom) { 25 minX = (int) Math.floor(Math.min(t1.getX(), t2.getX())); 26 minY = (int) Math.floor(Math.min(t1.getY(), t2.getY())); 27 maxX = (int) Math.ceil(Math.max(t1.getX(), t2.getX())); 28 maxY = (int) Math.ceil(Math.max(t1.getY(), t2.getY())); 29 this.zoom = zoom; 30 } 31 32 protected double tilesSpanned() { 33 return Math.sqrt(1.0 * this.size()); 34 } 35 36 protected int size() { 37 int xSpan = maxX - minX + 1; 38 int ySpan = maxY - minY + 1; 39 return xSpan * ySpan; 40 } 41 42 /** 43 * @return comparator, that sorts the tiles from the center to the edge of the current screen 44 */ 45 private Comparator<TilePosition> getTileDistanceComparator() { 46 final int centerX = (int) Math.ceil((minX + maxX) / 2d); 47 final int centerY = (int) Math.ceil((minY + maxY) / 2d); 48 return Comparator.comparingInt(t -> Math.abs(t.getX() - centerX) + Math.abs(t.getY() - centerY)); 49 } 50 51 /** 52 * Gets a stream of all tile positions in this set 53 * @return A stream of all positions 54 */ 55 public Stream<TilePosition> tilePositions() { 56 if (zoom == 0) { 57 return Stream.empty(); 58 } else { 59 return IntStream.rangeClosed(minX, maxX).mapToObj( 60 x -> IntStream.rangeClosed(minY, maxY).mapToObj(y -> new TilePosition(x, y, zoom)) 61 ).flatMap(Function.identity()); 62 } 63 } 64 65 /** 66 * Gets all tile positions with the ones in the center of the view first. 67 * @return The tile positions 68 * @see #tilePositions() 69 */ 70 public Stream<TilePosition> tilePositionsSorted() { 71 return tilePositions().sorted(getTileDistanceComparator()); 72 } 73 74 /** 75 * Get the zoom level this range is for 76 * @return The zoom. 77 */ 78 public int getZoom() { 79 return zoom; 80 } 81 } -
src/org/openstreetmap/josm/gui/layer/imagery/TileSourceDisplaySettings.java
diff --git a/src/org/openstreetmap/josm/gui/layer/imagery/TileSourceDisplaySettings.java b/src/org/openstreetmap/josm/gui/layer/imagery/TileSourceDisplaySettings.java index 67ab88a..f766ac2 100644
a b public class TileSourceDisplaySettings { 212 212 * @param changedSetting The setting name 213 213 */ 214 214 private void fireSettingsChange(String changedSetting) { 215 DisplaySettingsChangeEvent e = new DisplaySettingsChangeEvent( changedSetting);215 DisplaySettingsChangeEvent e = new DisplaySettingsChangeEvent(this, changedSetting); 216 216 for (DisplaySettingsChangeListener l : listeners) { 217 217 l.displaySettingsChanged(e); 218 218 } … … public class TileSourceDisplaySettings { 332 332 * @author Michael Zangl 333 333 */ 334 334 public static final class DisplaySettingsChangeEvent { 335 private final TileSourceDisplaySettings source; 335 336 private final String changedSetting; 336 337 337 DisplaySettingsChangeEvent(String changedSetting) { 338 DisplaySettingsChangeEvent(TileSourceDisplaySettings source, String changedSetting) { 339 this.source = source; 338 340 this.changedSetting = changedSetting; 339 341 } 340 342 341 343 /** 344 * Gets the display settings that caused this event. 345 * @return The settings. 346 * @since xxx 347 */ 348 public TileSourceDisplaySettings getSource() { 349 return source; 350 } 351 352 /** 342 353 * Gets the setting that was changed 343 354 * @return The name of the changed setting. 344 355 */ -
new file src/org/openstreetmap/josm/gui/layer/imagery/TileSourcePainter.java
diff --git a/src/org/openstreetmap/josm/gui/layer/imagery/TileSourcePainter.java b/src/org/openstreetmap/josm/gui/layer/imagery/TileSourcePainter.java new file mode 100644 index 0000000..9dd7ad7
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.layer.imagery; 3 4 import static org.openstreetmap.josm.tools.I18n.tr; 5 6 import java.awt.Color; 7 import java.awt.Font; 8 import java.awt.Graphics2D; 9 import java.awt.GridBagLayout; 10 import java.awt.Image; 11 import java.awt.Rectangle; 12 import java.awt.Shape; 13 import java.awt.Toolkit; 14 import java.awt.event.ActionEvent; 15 import java.awt.event.MouseAdapter; 16 import java.awt.event.MouseEvent; 17 import java.awt.geom.AffineTransform; 18 import java.awt.geom.Rectangle2D; 19 import java.awt.image.BufferedImage; 20 import java.awt.image.ImageObserver; 21 import java.io.IOException; 22 import java.text.MessageFormat; 23 import java.text.SimpleDateFormat; 24 import java.util.ArrayList; 25 import java.util.Arrays; 26 import java.util.Collections; 27 import java.util.Date; 28 import java.util.List; 29 import java.util.Map.Entry; 30 import java.util.Objects; 31 import java.util.stream.Stream; 32 33 import javax.swing.AbstractAction; 34 import javax.swing.Action; 35 import javax.swing.BorderFactory; 36 import javax.swing.JLabel; 37 import javax.swing.JMenuItem; 38 import javax.swing.JOptionPane; 39 import javax.swing.JPanel; 40 import javax.swing.JPopupMenu; 41 import javax.swing.JSeparator; 42 import javax.swing.JTextField; 43 import javax.swing.event.PopupMenuEvent; 44 import javax.swing.event.PopupMenuListener; 45 46 import org.openstreetmap.gui.jmapviewer.Tile; 47 import org.openstreetmap.gui.jmapviewer.TileXY; 48 import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate; 49 import org.openstreetmap.gui.jmapviewer.tilesources.AbstractTMSTileSource; 50 import org.openstreetmap.josm.Main; 51 import org.openstreetmap.josm.data.Bounds; 52 import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader; 53 import org.openstreetmap.josm.data.projection.Projection; 54 import org.openstreetmap.josm.gui.ExtendedDialog; 55 import org.openstreetmap.josm.gui.MapView; 56 import org.openstreetmap.josm.gui.MapViewState.MapViewLatLonRectangle; 57 import org.openstreetmap.josm.gui.MapViewState.MapViewPoint; 58 import org.openstreetmap.josm.gui.MapViewState.MapViewRectangle; 59 import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer; 60 import org.openstreetmap.josm.gui.layer.ImageryLayer; 61 import org.openstreetmap.josm.gui.layer.MapViewGraphics; 62 import org.openstreetmap.josm.gui.layer.MapViewPaintable.LayerPainter; 63 import org.openstreetmap.josm.gui.layer.MapViewPaintable.MapViewEvent; 64 import org.openstreetmap.josm.tools.GBC; 65 import org.openstreetmap.josm.tools.Pair; 66 67 /** 68 * This class is used to paint a {@link AbstractTileSourceLayer} to a given map view. 69 * @author Michael Zangl 70 * @param <T> The imagery type to use 71 * @since xxx 72 */ 73 public class TileSourcePainter<T extends AbstractTMSTileSource> extends AbstractTileSourceLoader<T> implements LayerPainter { 74 /** 75 * 76 */ 77 protected final AbstractTileSourceLayer<T> layer; 78 private static final Font INFO_FONT = new Font("sansserif", Font.BOLD, 13); 79 /** 80 * Absolute maximum of tiles to paint 81 */ 82 private static final int MAX_TILES = 500; 83 84 protected final ZoomLevelManager zoom; 85 86 private final TextPainter textPainter; 87 88 private TilePosition highlightPosition; 89 90 final MouseAdapter adapter = new TilePainterMouseAdapter(); 91 92 private final MapView mapView; 93 94 /** 95 * Create a new {@link TileSourcePainter} 96 * @param layer The layer to paint 97 * @param mapView The map view to paint for. 98 */ 99 public TileSourcePainter(AbstractTileSourceLayer<T> layer, MapView mapView) { 100 super(layer); 101 this.layer = layer; 102 this.mapView = mapView; 103 mapView.addMouseListener(adapter); 104 105 textPainter = new TextPainter(); 106 zoom = new ZoomLevelManager(getSettings(), tileSource, generateCoordinateConverter()); 107 zoom.setZoomBounds(layer.getInfo()); 108 } 109 110 @Override 111 public void paint(MapViewGraphics graphics) { 112 boolean hasAllocated = allocateCacheMemory(); 113 114 textPainter.start(graphics.getDefaultGraphics()); 115 116 if (hasAllocated) { 117 doPaint(graphics); 118 } else { 119 textPainter.addTextOverlay(tr("There is noth enough memory to display this layer.")); 120 } 121 } 122 123 private void doPaint(MapViewGraphics graphics) { 124 MapViewRectangle pb = graphics.getClipBounds(); 125 126 drawInViewArea(graphics.getDefaultGraphics(), graphics.getMapView(), pb); 127 } 128 129 private void drawInViewArea(Graphics2D g, MapView mapView, MapViewRectangle rect) { 130 g.setFont(INFO_FONT); 131 TileCoordinateConverter converter = generateCoordinateConverter(); 132 zoom.updateZoomLevel(converter, this); 133 loadTilesInView(converter); 134 135 TileRange baseRange = converter.getViewAtZoom(zoom.getCurrentZoomLevel()); 136 137 Shape clip = g.getClip(); 138 g.setClip(converter.getProjectionClip()); 139 Stream<TilePosition> area; 140 if (getSettings().isAutoZoom()) { 141 area = TileForAreaFinder.getWithFallbackZoom(baseRange, this, zoom); 142 } else { 143 area = TileForAreaFinder.getAtDefaultZoom(baseRange, this); 144 } 145 paintTileImages(g, area); 146 g.setClip(clip); 147 148 if (highlightPosition != null) { 149 paintHighlight(g, converter, highlightPosition); 150 } 151 paintStatus(baseRange, mapView.getProjection()); 152 paintAttribution(g, rect); 153 if (Main.isDebugEnabled()) { 154 paintDebug(); 155 } 156 } 157 158 /** 159 * Paints a highlight rectangle around a tile. 160 * @param g 161 * @param converter 162 * @param tile 163 */ 164 private static void paintHighlight(Graphics2D g, TileCoordinateConverter converter, TilePosition tile) { 165 MapViewLatLonRectangle area = converter.getAreaForTile(tile); 166 g.setColor(Color.RED); 167 g.draw(area.getInView()); 168 } 169 170 /** 171 * Paint the filtered images for the given tiles 172 * @param g The graphics to paint on 173 * @param area The tiles to paint. 174 */ 175 private void paintTileImages(Graphics2D g, Stream<TilePosition> area) { 176 TileCoordinateConverter converter = generateCoordinateConverter(); 177 Rectangle b = g.getClipBounds(); 178 int maxTiles = (int) (b.getWidth() * b.getHeight() / tileSource.getTileSize() / tileSource.getTileSize() * 5); 179 List<Tile> errorTiles = Collections.synchronizedList(new ArrayList<>()); 180 Stream<Tile> tiles = area.parallel() 181 .limit(Math.min(maxTiles, MAX_TILES)) 182 .map(this::getTile) 183 .filter(Objects::nonNull); 184 185 if (getSettings().isShowErrors()) { 186 tiles = tiles.peek(t -> { if (t.hasError()) errorTiles.add(t); }); 187 } 188 tiles.map(tile -> new Pair<>(tile, tile.getImage())) 189 .filter(p -> imageLoaded(p.b)) 190 .map(p -> new Pair<>(p.a, layer.applyImageProcessors(p.b))) 191 .forEachOrdered(p -> paintTileImage(g, p.a, p.b, converter)); 192 193 for (Tile error : errorTiles) { 194 textPainter.drawTileString(tr("Error") + ": " + tr(error.getErrorMessage()), 195 new TilePosition(error), converter); 196 } 197 } 198 199 /** 200 * We only paint full tile images. 201 * <p> 202 * We handle that the correct tile images are in front by sorting the list of tiles accordingly. 203 * @param g The graphics to paint on 204 * @param tile The tile to paint 205 * @param image The image to paint for the tile 206 * @param converter The coordinate converter. 207 */ 208 private void paintTileImage(Graphics2D g, Tile tile, BufferedImage image, TileCoordinateConverter converter) { 209 AffineTransform transform = converter.getTransformForTile(new TilePosition(tile), 0, 0, 0, 1, 1, 1); 210 transform.scale(1.0 / image.getWidth(), 1.0 / image.getHeight()); 211 212 g.drawImage(image, transform, layer); 213 214 if (ImageryLayer.PROP_FADE_AMOUNT.get() != 0) { 215 // dimm by painting opaque rect... 216 // TODO: Convert this to a filter. 217 g.setColor(ImageryLayer.getFadeColorWithAlpha()); 218 AffineTransform oldTrans = g.getTransform(); 219 g.transform(transform); 220 g.fillRect(0, 0, image.getWidth(), image.getHeight()); 221 g.setTransform(oldTrans); 222 } 223 224 if (Main.isTraceEnabled()) { 225 textPainter.drawTileString(tile.getKey(), new TilePosition(tile), converter); 226 } 227 } 228 229 private void paintAttribution(Graphics2D defaultGraphics, MapViewRectangle rect) { 230 Rectangle2D inView = rect.getInView(); 231 Graphics2D g = (Graphics2D) defaultGraphics.create(); 232 g.translate(inView.getMinX(), inView.getMinY()); 233 Bounds boundsBox = rect.getLatLonBoundsBox(); 234 attribution.paintAttribution(g, (int) inView.getWidth(), (int) inView.getHeight(), 235 boundsBox.getMin().toCoordinate(), boundsBox.getMax().toCoordinate(), zoom.getDisplayZoomLevel(), 236 layer); 237 } 238 239 private void paintStatus(TileRange baseRange, Projection projection) { 240 if (isTooLarge(baseRange)) { 241 textPainter.addTextOverlay(tr("zoom in to load more tiles")); 242 } else if (!getSettings().isAutoZoom() && isTooSmall(baseRange)) { 243 textPainter.addTextOverlay(tr("increase tiles zoom level (change resolution) to see more detail")); 244 } else if (getSettings().isAutoZoom() && getSettings().isAutoLoad() && !hasTiles(baseRange, TileSourcePainter::isVisible) 245 && (!hasTiles(baseRange, TileSourcePainter::isLoading) || hasTiles(baseRange, TileSourcePainter::isOverzoomed))) { 246 textPainter.addTextOverlay(tr("No tiles at this zoom level")); 247 } 248 249 if (!isProjectionSupported(projection)) { 250 textPainter.addTextOverlay(tr("The tile source does not support the current projection natively")); 251 } 252 } 253 254 private void paintDebug() { 255 for (String s : zoom.getDebugInfo(generateCoordinateConverter())) { 256 textPainter.addDebug(s); 257 } 258 textPainter.addDebug(tr("Estimated cache size: {0}", estimateTileCacheSize())); 259 if (tileLoader instanceof TMSCachedTileLoader) { 260 TMSCachedTileLoader cachedTileLoader = (TMSCachedTileLoader) tileLoader; 261 for (String part : cachedTileLoader.getStats().split("\n")) { 262 textPainter.addDebug(tr("Cache stats: {0}", part)); 263 } 264 } 265 } 266 267 private void loadTilesInView(TileCoordinateConverter converter) { 268 int zoomToLoad = zoom.getDisplayZoomLevel(); 269 TileRange range = converter.getViewAtZoom(zoomToLoad); 270 271 if (getSettings().isAutoZoom()) { 272 // If all tiles at displayZoomLevel is loaded, load all tiles at next zoom level 273 // to make sure there're really no more zoom levels 274 if (zoomToLoad < zoom.getCurrentZoomLevel() && !hasTiles(range, TileSourcePainter::isMissing)) { 275 zoomToLoad++; 276 range = converter.getViewAtZoom(zoomToLoad); 277 } else { 278 // When we have overzoomed tiles and all tiles at current zoomlevel is loaded, 279 // load tiles at previovus zoomlevels until we have all tiles on screen is loaded. 280 // loading is done in the next if section 281 while (zoomToLoad > zoom.getMinZoom() && hasTiles(range, TileSourcePainter::isOverzoomed) 282 && !hasTiles(range, TileSourcePainter::isMissing)) { 283 zoomToLoad--; 284 range = converter.getViewAtZoom(zoomToLoad); 285 } 286 } 287 } 288 loadTiles(range, false); 289 } 290 291 private void loadErrorTiles(TileRange range, boolean force) { 292 if (getSettings().isAutoLoad() || force) { 293 range.tilePositionsSorted().map(this::getOrCreateTile).filter(Tile::hasError) 294 .forEach(t -> tileLoader.createTileLoaderJob(t).submit(force)); 295 } 296 } 297 298 protected void loadAllErrorTiles(boolean force) { 299 loadErrorTiles(generateCoordinateConverter().getViewAtZoom(zoom.getCurrentZoomLevel()), force); 300 } 301 302 protected void loadAllTiles(boolean force) { 303 loadTiles(generateCoordinateConverter().getViewAtZoom(zoom.getCurrentZoomLevel()), force); 304 } 305 306 @Override 307 protected void loadTiles(TileRange range, boolean force) { 308 super.loadTiles(range, force || getSettings().isAutoLoad()); 309 } 310 311 private boolean imageLoaded(Image i) { 312 if (i == null) { 313 return false; 314 } else { 315 int status = Toolkit.getDefaultToolkit().checkImage(i, -1, -1, layer); 316 return (status & ImageObserver.ALLBITS) != 0; 317 } 318 } 319 320 private TileCoordinateConverter generateCoordinateConverter() { 321 return new TileCoordinateConverter(mapView.getState(), tileSource, getSettings()); 322 } 323 324 /** 325 * Check whether this layer supports the given projection 326 * @param projection The projection to search 327 * @return <code>true</code> if supported. 328 */ 329 protected boolean isProjectionSupported(Projection projection) { 330 return true; 331 } 332 333 private TileSourceDisplaySettings getSettings() { 334 return layer.getDisplaySettings(); 335 } 336 337 /** 338 * Gets the menu entries for this layer 339 * @return The menu entries 340 */ 341 public List<Action> getMenuEntries() { 342 return Arrays.asList(zoom.new IncreaseZoomAction(), zoom.new DecreaseZoomAction(), 343 zoom.new ZoomToBestAction(mapView), zoom.new ZoomToNativeLevelAction(mapView), 344 new FlushTileCacheAction(), new LoadErroneusTilesAction(), new LoadAllTilesAction()); 345 } 346 347 /** 348 * Gets the current zoom level as String 349 * @return The zoom level. 350 */ 351 public String getZoomString() { 352 return Integer.toString(zoom.getCurrentZoomLevel()); 353 } 354 355 @Override 356 public void detachFromMapView(MapViewEvent event) { 357 event.getMapView().removeMouseListener(adapter); 358 MapView.removeZoomChangeListener(this); 359 freeCacheMemory(); 360 layer.detach(this); 361 } 362 363 private final class TilePainterMouseAdapter extends MouseAdapter { 364 @Override 365 public void mouseClicked(MouseEvent e) { 366 if (e.getButton() == MouseEvent.BUTTON3) { 367 TilePosition tilePos = getTileForPixelpos(mapView.getState().getForView(e.getPoint())); 368 JPopupMenu popup = layer.new TileSourceLayerPopup(mapView); 369 popup.addPopupMenuListener(new PopupMenuListener() { 370 @Override 371 public void popupMenuWillBecomeVisible(PopupMenuEvent e) { 372 highlightPosition = tilePos; 373 // triggers repaint 374 layer.invalidate(); 375 } 376 377 @Override 378 public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { 379 highlightPosition = null; 380 layer.invalidate(); 381 } 382 383 @Override 384 public void popupMenuCanceled(PopupMenuEvent e) { 385 // ignore 386 } 387 }); 388 if (tilePos != null) { 389 popup.add(new JSeparator()); 390 popup.add(new JMenuItem(new LoadTileAction(tilePos))); 391 Tile tile = getOrCreateTile(tilePos); 392 if (tile != null) { 393 popup.add(new JMenuItem(new ShowTileInfoAction(tile))); 394 } 395 } 396 popup.show(e.getComponent(), e.getX(), e.getY()); 397 } else if (e.getButton() == MouseEvent.BUTTON1) { 398 attribution.handleAttribution(e.getPoint(), true); 399 } 400 } 401 402 /** 403 * Returns tile for a pixel position.<p> 404 * This isn't very efficient, but it is only used when the user right-clicks on the map. 405 * @param mapViewPoint pixel coordinate 406 * @return Tile at pixel position 407 */ 408 private TilePosition getTileForPixelpos(MapViewPoint mapViewPoint) { 409 Main.trace("getTileForPixelpos({0})", mapViewPoint); 410 TileCoordinateConverter converter = generateCoordinateConverter(); 411 412 TileRange ts = converter.getViewAtZoom(zoom.getCurrentZoomLevel()); 413 414 Stream<TilePosition> clickedTiles = ts.tilePositions() 415 .filter(t -> converter.getAreaForTile(t).contains(mapViewPoint)); 416 if (Main.isTraceEnabled()) { 417 clickedTiles = clickedTiles.peek(t -> Main.trace("Clicked on tile: {0}, {1}; currentZoomLevel: {2}", 418 t.getX(), t.getY(), zoom.getCurrentZoomLevel())); 419 } 420 return clickedTiles.findAny().orElse(null); 421 } 422 } 423 424 private class LoadAllTilesAction extends AbstractAction { 425 LoadAllTilesAction() { 426 super(tr("Load all tiles")); 427 } 428 429 @Override 430 public void actionPerformed(ActionEvent ae) { 431 loadAllTiles(true); 432 } 433 } 434 435 private class LoadErroneusTilesAction extends AbstractAction { 436 LoadErroneusTilesAction() { 437 super(tr("Load all error tiles")); 438 } 439 440 @Override 441 public void actionPerformed(ActionEvent ae) { 442 loadAllErrorTiles(true); 443 } 444 } 445 446 private final class ShowTileInfoAction extends AbstractAction { 447 448 private final transient Tile clickedTile; 449 450 private ShowTileInfoAction(Tile clickedTile) { 451 super(tr("Show tile info")); 452 this.clickedTile = clickedTile; 453 } 454 455 private String getSizeString(int size) { 456 StringBuilder ret = new StringBuilder(); 457 return ret.append(size).append('x').append(size).toString(); 458 } 459 460 private JTextField createTextField(String text) { 461 JTextField ret = new JTextField(text); 462 ret.setEditable(false); 463 ret.setBorder(BorderFactory.createEmptyBorder()); 464 return ret; 465 } 466 467 @Override 468 public void actionPerformed(ActionEvent ae) { 469 ExtendedDialog ed = new ExtendedDialog(Main.parent, tr("Tile Info"), new String[] { tr("OK") }); 470 JPanel panel = new JPanel(new GridBagLayout()); 471 MapViewLatLonRectangle displaySize = generateCoordinateConverter().getAreaForTile(new TilePosition(clickedTile)); 472 Rectangle2D bounds = displaySize.getInView().getBounds2D(); 473 String[][] content = { { "Tile name", clickedTile.getKey() }, { "Tile url", getUrl() }, 474 { "Tile size", getSizeString(clickedTile.getTileSource().getTileSize()) }, 475 { "Position in view", MessageFormat.format("x={0}..{1}, y={2}..{3}", 476 bounds.getMinX(), bounds.getMaxX(), 477 bounds.getMinY(), bounds.getMaxY())}, 478 { "Position on projection",MessageFormat.format("east={0}..{1}, north={2}..{3}", 479 displaySize.getProjectionBounds().minEast, displaySize.getProjectionBounds().maxEast, 480 displaySize.getProjectionBounds().minNorth, displaySize.getProjectionBounds().maxNorth)}, 481 { "Position on world",MessageFormat.format("lat={0}..{1}, lon={2}..{3}", 482 displaySize.getLatLonBoundsBox().getMinLat(), displaySize.getLatLonBoundsBox().getMaxLat(), 483 displaySize.getLatLonBoundsBox().getMinLon(), displaySize.getLatLonBoundsBox().getMaxLon())}, 484 }; 485 486 for (String[] entry : content) { 487 panel.add(new JLabel(tr(entry[0]) + ':'), GBC.std()); 488 panel.add(GBC.glue(5, 0), GBC.std()); 489 panel.add(createTextField(entry[1]), GBC.eol().fill(GBC.HORIZONTAL)); 490 } 491 492 for (Entry<String, String> e : clickedTile.getMetadata().entrySet()) { 493 panel.add(new JLabel(tr("Metadata ") + tr(e.getKey()) + ':'), GBC.std()); 494 panel.add(GBC.glue(5, 0), GBC.std()); 495 String value = e.getValue(); 496 if ("lastModification".equals(e.getKey()) || "expirationTime".equals(e.getKey())) { 497 value = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(Long.parseLong(value))); 498 } 499 panel.add(createTextField(value), GBC.eol().fill(GBC.HORIZONTAL)); 500 501 } 502 ed.setIcon(JOptionPane.INFORMATION_MESSAGE); 503 ed.setContent(panel); 504 ed.showDialog(); 505 } 506 507 private String getUrl() { 508 try { 509 return clickedTile.getUrl(); 510 } catch (IOException e) { 511 // silence exceptions 512 Main.trace(e); 513 return ""; 514 } 515 } 516 } 517 518 private final class LoadTileAction extends AbstractAction { 519 520 private final transient TilePosition clickedTile; 521 522 private LoadTileAction(TilePosition clickedTile) { 523 super(tr("Load tile")); 524 this.clickedTile = clickedTile; 525 setEnabled(clickedTile != null); 526 } 527 528 @Override 529 public void actionPerformed(ActionEvent ae) { 530 loadTile(clickedTile, true); 531 layer.invalidate(); 532 } 533 } 534 535 @Override 536 public Bounds getBounds(TilePosition tilePos) { 537 ICoordinate min = tileSource.tileXYToLatLon(tilePos.getX(), tilePos.getY(), tilePos.getZoom()); 538 ICoordinate max = tileSource.tileXYToLatLon(tilePos.getX() + 1, tilePos.getY() + 1, tilePos.getZoom()); 539 Bounds bounds = new Bounds(min.getLat(), min.getLon(), false); 540 bounds.extend(max.getLat(), max.getLon()); 541 return bounds; 542 } 543 544 @Override 545 public TileRange toRangeAtZoom(Bounds bounds, int zoom) { 546 TileXY t1 = tileSource.latLonToTileXY(bounds.getMinLat(), bounds.getMinLon(), zoom); 547 TileXY t2 = tileSource.latLonToTileXY(bounds.getMaxLat(), bounds.getMaxLon(), zoom); 548 return new TileRange(t1, t2, zoom); 549 } 550 551 @Override 552 public boolean isAvailable(TilePosition tilePos) { 553 Tile tile = getTile(tilePos); 554 return tile != null && !tile.hasError() 555 && !(isOverzoomed(tile) && tilePos.getZoom() > zoom.getDisplayZoomLevel()) 556 && isImageAvailable(tile); 557 } 558 559 private boolean isImageAvailable(Tile tile) { 560 BufferedImage image = tile.getImage(); 561 return imageLoaded(image); 562 } 563 } -
new file src/org/openstreetmap/josm/gui/layer/imagery/ZoomLevelManager.java
diff --git a/src/org/openstreetmap/josm/gui/layer/imagery/ZoomLevelManager.java b/src/org/openstreetmap/josm/gui/layer/imagery/ZoomLevelManager.java new file mode 100644 index 0000000..fe0fea4
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.layer.imagery; 3 4 import static org.openstreetmap.josm.tools.I18n.tr; 5 6 import java.awt.event.ActionEvent; 7 import java.text.MessageFormat; 8 import java.util.Arrays; 9 import java.util.List; 10 11 import javax.swing.AbstractAction; 12 13 import org.openstreetmap.gui.jmapviewer.Tile; 14 import org.openstreetmap.gui.jmapviewer.interfaces.TileSource; 15 import org.openstreetmap.josm.Main; 16 import org.openstreetmap.josm.data.imagery.ImageryInfo; 17 import org.openstreetmap.josm.gui.MapView; 18 import org.openstreetmap.josm.gui.layer.AbstractTileSourceLayer; 19 20 /** 21 * This class manages the zoom level of a {@link TileSourcePainter} 22 * @author Michael Zangl 23 * @since xxx 24 */ 25 public class ZoomLevelManager { 26 /** 27 * Zoomlevel selected by the user. 28 */ 29 private int currentZoomLevel; 30 /** 31 * The zoom level at which tiles are currently displayed 32 */ 33 private int displayZoomLevel; 34 private TileSourceDisplaySettings settings; 35 private TileSource source; 36 37 private int minZoom; 38 private int maxZoom; 39 40 /** 41 * Create a new zoom level manager 42 * @param settings The zoom settings 43 * @param source The tile source to use. 44 * @param initialZoomState The initial state to compute the zoom factor from. 45 */ 46 public ZoomLevelManager(TileSourceDisplaySettings settings, TileSource source, TileCoordinateConverter initialZoomState) { 47 this.settings = settings; 48 this.source = source; 49 50 setZoomLevel(clampZoom(initialZoomState.getBestZoom())); 51 setZoomBounds(source.getMinZoom(), source.getMaxZoom()); 52 } 53 54 /** 55 * Set the zoom bounds 56 * @param bounds An info to get the zoom bounds from 57 */ 58 public void setZoomBounds(ImageryInfo bounds) { 59 setZoomBounds(bounds.getMinZoom(), bounds.getMaxZoom()); 60 } 61 62 /** 63 * Sets the zoom bounds 64 * @param minZoom The minimum zoom 65 * @param maxZoom The maximum zoom. 66 */ 67 public void setZoomBounds(int minZoom, int maxZoom) { 68 if (minZoom > maxZoom || minZoom < 0) { 69 throw new IllegalArgumentException(MessageFormat.format("Zoom range not valid: {0}..{1}", minZoom, maxZoom)); 70 } 71 this.minZoom = AbstractTileSourceLayer.checkMinZoomLvl(minZoom, source); 72 this.maxZoom = AbstractTileSourceLayer.checkMaxZoomLvl(maxZoom, source); 73 } 74 75 /** 76 * @return The min zoom that was set. 77 */ 78 public int getMinZoom() { 79 return minZoom; 80 } 81 82 /** 83 * @return The max zoom that was set. 84 */ 85 public int getMaxZoom() { 86 return maxZoom; 87 } 88 89 /** 90 * 91 * @return if its allowed to zoom in 92 */ 93 public boolean zoomIncreaseAllowed() { 94 boolean zia = currentZoomLevel < this.getMaxZoom(); 95 if (Main.isDebugEnabled()) { 96 Main.debug("zoomIncreaseAllowed(): " + zia + ' ' + currentZoomLevel + " vs. " + this.getMaxZoom()); 97 } 98 return zia; 99 } 100 101 /** 102 * Zoom in, go closer to map. 103 * 104 * @return true, if zoom increasing was successful, false otherwise 105 */ 106 public boolean increaseZoomLevel() { 107 return setZoomLevel(currentZoomLevel + 1); 108 } 109 110 /** 111 * Sets the zoom level of the layer 112 * @param zoom zoom level 113 * @return true, when zoom has changed to desired value, false if it was outside supported zoom levels 114 */ 115 public boolean setZoomLevel(int zoom) { 116 if (zoom == currentZoomLevel && zoom == displayZoomLevel) { 117 return true; 118 } else if (zoom < getMinZoom() || zoom > getMaxZoom()) { 119 Main.warn("Current zoom level ({0}) could not be changed to {1}: out of range {2} .. {3}", currentZoomLevel, 120 zoom, getMinZoom(), getMaxZoom()); 121 return false; 122 } else { 123 Main.debug("changing zoom level to: {0}", currentZoomLevel); 124 currentZoomLevel = zoom; 125 displayZoomLevel = zoom; 126 return true; 127 } 128 } 129 130 /** 131 * Check if zooming out is allowed 132 * 133 * @return true, if zooming out is allowed (currentZoomLevel > minZoomLevel) 134 */ 135 public boolean zoomDecreaseAllowed() { 136 boolean zda = currentZoomLevel > this.getMinZoom(); 137 if (Main.isDebugEnabled()) { 138 Main.debug("zoomDecreaseAllowed(): " + zda + ' ' + currentZoomLevel + " vs. " + this.getMinZoom()); 139 } 140 return zda; 141 } 142 143 /** 144 * Zoom out from map. 145 * 146 * @return true, if zoom increasing was successfull, false othervise 147 */ 148 public boolean decreaseZoomLevel() { 149 return setZoomLevel(currentZoomLevel - 1); 150 } 151 152 /** 153 * Update the zoom level that should be displayed 154 * @param currentZoomState The coordinate converter that holds the current view 155 * @param viewStatus An accessor for finding out if the given tiles are available. 156 */ 157 public void updateZoomLevel(TileCoordinateConverter currentZoomState, TileSourcePainter<?> viewStatus) { 158 if (settings.isAutoZoom()) { 159 int zoom = clampZoom(currentZoomState.getBestZoom()); 160 setZoomLevel(zoom); 161 if (settings.isAutoLoad()) { 162 // Find highest zoom level with at least one visible tile 163 164 for (int tmpZoom = zoom; tmpZoom >= getMinZoom(); tmpZoom--) { 165 TileRange area = currentZoomState.getViewAtZoom(zoom); 166 if (viewStatus.hasTiles(area, ZoomLevelManager::visibleOrOverzoomed)) { 167 displayZoomLevel = tmpZoom; 168 break; 169 } 170 } 171 } 172 } else { 173 displayZoomLevel = currentZoomLevel; 174 } 175 } 176 177 private static boolean visibleOrOverzoomed(Tile t) { 178 return TileSourcePainter.isVisible(t) || TileSourcePainter.isOverzoomed(t); 179 } 180 181 private int clampZoom(int intResult) { 182 return Math.max(Math.min(intResult, getMaxZoom()), getMinZoom()); 183 } 184 185 /** 186 * Gets the current zoom level that is requested by the user for displaying the tiles. 187 * @return The current zoom level of the view 188 */ 189 public int getCurrentZoomLevel() { 190 return currentZoomLevel; 191 } 192 193 /** 194 * Gets the zoom level that is suggested to be displayed. This may be different depending on the tile loading settings. 195 * @return The suggested zoom. 196 */ 197 public int getDisplayZoomLevel() { 198 return displayZoomLevel; 199 } 200 201 /** 202 * Gets the debug information that should be added about the zoom level. 203 * @param currentZoomState A coordinate converter 204 * @return The current zoom status. 205 */ 206 public List<String> getDebugInfo(TileCoordinateConverter currentZoomState) { 207 int bestZoom = currentZoomState.getBestZoom(); 208 return Arrays.asList( 209 tr("Current zoom: {0}", getCurrentZoomLevel()), 210 tr("Display zoom: {0}", displayZoomLevel), 211 tr("Pixel scale: {0}", currentZoomState.getScaleFactor(getCurrentZoomLevel())), 212 tr("Best zoom: {0} (clamped to: {1})", bestZoom, clampZoom(bestZoom)) 213 ); 214 } 215 216 /** 217 * Zooms to the native level of the current view 218 */ 219 public class ZoomToNativeLevelAction extends AbstractAction { 220 private final MapView forView; 221 222 /** 223 * Create a new {@link ZoomToNativeLevelAction} 224 * @param forView The map view to zoom 225 */ 226 public ZoomToNativeLevelAction(MapView forView) { 227 super(tr("Zoom to native resolution")); 228 this.forView = forView; 229 } 230 231 @Override 232 public void actionPerformed(ActionEvent ae) { 233 TileCoordinateConverter converter = new TileCoordinateConverter(forView.getState(), source, settings); 234 double newFactor = Math.sqrt(converter.getScaleFactor(currentZoomLevel)); 235 forView.zoomToFactor(newFactor); 236 } 237 } 238 239 /** 240 * Zooms the layer to the best display zoom for the current map view state 241 */ 242 public class ZoomToBestAction extends AbstractAction { 243 private final int bestZoom; 244 245 /** 246 * Create a new {@link ZoomToBestAction} 247 * @param forView The view to use as reference. 248 */ 249 public ZoomToBestAction(MapView forView) { 250 super(tr("Change resolution")); 251 bestZoom = clampZoom(new TileCoordinateConverter(forView.getState(), source, settings).getBestZoom()); 252 setEnabled(!settings.isAutoZoom() && bestZoom != currentZoomLevel); 253 } 254 255 @Override 256 public void actionPerformed(ActionEvent ae) { 257 setZoomLevel(bestZoom); 258 } 259 } 260 261 /** 262 * Increase the zoom by 1. 263 */ 264 public class IncreaseZoomAction extends AbstractAction { 265 /** 266 * Create a new {@link IncreaseZoomAction} 267 */ 268 public IncreaseZoomAction() { 269 super(tr("Increase zoom")); 270 setEnabled(!settings.isAutoZoom() && zoomIncreaseAllowed()); 271 } 272 273 @Override 274 public void actionPerformed(ActionEvent ae) { 275 increaseZoomLevel(); 276 } 277 } 278 279 /** 280 * Decrease the zoom by 1. 281 */ 282 public class DecreaseZoomAction extends AbstractAction { 283 /** 284 * Create a new {@link DecreaseZoomAction} 285 */ 286 public DecreaseZoomAction() { 287 super(tr("Decrease zoom")); 288 setEnabled(!settings.isAutoZoom() && zoomDecreaseAllowed()); 289 } 290 291 @Override 292 public void actionPerformed(ActionEvent ae) { 293 decreaseZoomLevel(); 294 } 295 } 296 297 } -
new file test/unit/org/openstreetmap/josm/gui/layer/imagery/TileCoordinateConverterTest.java
diff --git a/test/unit/org/openstreetmap/josm/gui/layer/imagery/TileCoordinateConverterTest.java b/test/unit/org/openstreetmap/josm/gui/layer/imagery/TileCoordinateConverterTest.java new file mode 100644 index 0000000..b673d61
- + 1 // License: GPL. For details, see LICENSE file. 2 package org.openstreetmap.josm.gui.layer.imagery; 3 4 import static org.junit.Assert.assertEquals; 5 6 import java.awt.Image; 7 import java.awt.Point; 8 import java.awt.geom.AffineTransform; 9 import java.awt.geom.Point2D; 10 import java.io.IOException; 11 import java.util.List; 12 import java.util.Map; 13 14 import org.junit.Before; 15 import org.junit.BeforeClass; 16 import org.junit.Test; 17 import org.openstreetmap.gui.jmapviewer.Coordinate; 18 import org.openstreetmap.gui.jmapviewer.Tile; 19 import org.openstreetmap.gui.jmapviewer.TileXY; 20 import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate; 21 import org.openstreetmap.gui.jmapviewer.interfaces.TileSource; 22 import org.openstreetmap.josm.JOSMFixture; 23 import org.openstreetmap.josm.Main; 24 import org.openstreetmap.josm.data.coor.EastNorth; 25 import org.openstreetmap.josm.data.coor.LatLon; 26 import org.openstreetmap.josm.gui.MapFrame; 27 import org.openstreetmap.josm.gui.MapViewState; 28 import org.openstreetmap.josm.gui.MapViewState.MapViewLatLonRectangle; 29 import org.openstreetmap.josm.gui.MapViewState.MapViewPoint; 30 import org.openstreetmap.josm.gui.layer.LayerManagerTest; 31 import org.openstreetmap.josm.gui.util.GuiHelper; 32 33 /** 34 * Test {@link TileCoordinateConverter} 35 * @author Michael Zangl 36 * @since xxx 37 */ 38 public class TileCoordinateConverterTest { 39 private static final class TransformingConverter extends TileCoordinateConverter { 40 AffineTransform transform = new AffineTransform(); 41 42 private TransformingConverter(MapViewState mapView, TileSource tileSource, TileSourceDisplaySettings settings) { 43 super(mapView, tileSource, settings); 44 } 45 46 @Override 47 protected MapViewPoint pos(ICoordinate ll) { 48 Point2D transformed = transform.transform(new Point2D.Double(ll.getLat(), ll.getLon()), null); 49 return super.pos(new Coordinate(transformed.getX(), transformed.getY())); 50 } 51 } 52 53 private static final class TestTileSource implements TileSource { 54 @Override 55 public boolean requiresAttribution() { 56 throw new UnsupportedOperationException(); 57 } 58 59 @Override 60 public String getTermsOfUseURL() { 61 throw new UnsupportedOperationException(); 62 } 63 64 @Override 65 public String getTermsOfUseText() { 66 throw new UnsupportedOperationException(); 67 } 68 69 @Override 70 public String getAttributionText(int zoom, ICoordinate topLeft, ICoordinate botRight) { 71 throw new UnsupportedOperationException(); 72 } 73 74 @Override 75 public String getAttributionLinkURL() { 76 throw new UnsupportedOperationException(); 77 } 78 79 @Override 80 public String getAttributionImageURL() { 81 throw new UnsupportedOperationException(); 82 } 83 84 @Override 85 public Image getAttributionImage() { 86 throw new UnsupportedOperationException(); 87 } 88 89 @Override 90 public Point latLonToXY(ICoordinate point, int zoom) { 91 return latLonToXY(point.getLat(), point.getLon(), zoom); 92 } 93 94 @Override 95 public ICoordinate xyToLatLon(Point point, int zoom) { 96 return xyToLatLon(point.x, point.y, zoom); 97 } 98 99 @Override 100 public TileXY latLonToTileXY(ICoordinate point, int zoom) { 101 return latLonToTileXY(point.getLat(), point.getLon(), zoom); 102 } 103 104 @Override 105 public ICoordinate tileXYToLatLon(TileXY xy, int zoom) { 106 return tileXYToLatLon(xy.getXIndex(), xy.getYIndex(), zoom); 107 } 108 109 @Override 110 public ICoordinate tileXYToLatLon(Tile tile) { 111 return tileXYToLatLon(tile.getXtile(), tile.getYtile(), tile.getZoom()); 112 } 113 114 @Override 115 public boolean isNoTileAtZoom(Map<String, List<String>> headers, int statusCode, byte[] content) { 116 throw new UnsupportedOperationException(); 117 } 118 119 @Override 120 public int getTileYMin(int zoom) { 121 throw new UnsupportedOperationException(); 122 } 123 124 @Override 125 public int getTileYMax(int zoom) { 126 throw new UnsupportedOperationException(); 127 } 128 129 @Override 130 public int getTileXMin(int zoom) { 131 throw new UnsupportedOperationException(); 132 } 133 134 @Override 135 public int getTileXMax(int zoom) { 136 throw new UnsupportedOperationException(); 137 } 138 139 @Override 140 public String getTileUrl(int zoom, int tilex, int tiley) throws IOException { 141 throw new UnsupportedOperationException(); 142 } 143 144 @Override 145 public int getTileSize() { 146 throw new UnsupportedOperationException(); 147 } 148 149 @Override 150 public String getTileId(int zoom, int tilex, int tiley) { 151 throw new UnsupportedOperationException(); 152 } 153 154 @Override 155 public String getName() { 156 throw new UnsupportedOperationException(); 157 } 158 159 @Override 160 public int getMinZoom() { 161 throw new UnsupportedOperationException(); 162 } 163 164 @Override 165 public Map<String, String> getMetadata(Map<String, List<String>> headers) { 166 throw new UnsupportedOperationException(); 167 } 168 169 @Override 170 public int getMaxZoom() { 171 throw new UnsupportedOperationException(); 172 } 173 174 @Override 175 public String getId() { 176 throw new UnsupportedOperationException(); 177 } 178 179 @Override 180 public double getDistance(double la1, double lo1, double la2, double lo2) { 181 throw new UnsupportedOperationException(); 182 } 183 184 @Override 185 public int getDefaultTileSize() { 186 throw new UnsupportedOperationException(); 187 } 188 189 @Override 190 public Point latLonToXY(double lat, double lon, int zoom) { 191 return new Point((int) lat / 13, (int) lon - 4); 192 } 193 194 @Override 195 public ICoordinate xyToLatLon(int x, int y, int zoom) { 196 return new Coordinate(x * 13, y + 4); 197 } 198 199 @Override 200 public TileXY latLonToTileXY(double lat, double lon, int zoom) { 201 return new TileXY(lat / 13, lon - 4); 202 } 203 204 @Override 205 public ICoordinate tileXYToLatLon(int x, int y, int zoom) { 206 return new Coordinate(x * 13, y + 4); 207 } 208 } 209 210 private TransformingConverter converter; 211 212 /** 213 * Setup test. 214 */ 215 @BeforeClass 216 public static void setUpBeforeClass() { 217 JOSMFixture.createUnitTestFixture().init(true); 218 Main.getLayerManager().addLayer(new LayerManagerTest.TestLayer()); 219 GuiHelper.runInEDTAndWait(() -> {}); 220 } 221 222 /** 223 * Sets up a fake tile source. 224 */ 225 @Before 226 public void setUp() { 227 MapFrame map = Main.map; 228 map.mapView.zoomTo(new EastNorth(0, 0), 1); 229 converter = new TransformingConverter(map.mapView.getState(), new TestTileSource(), new TileSourceDisplaySettings()); 230 } 231 232 /** 233 * Test {@link TileCoordinateConverter#getAreaForTile(TilePosition)} 234 */ 235 @Test 236 public void testGetAreaForTile() { 237 EastNorth p1 = Main.getProjection().latlon2eastNorth(new LatLon(26, 7)); 238 MapViewLatLonRectangle rect = converter.getAreaForTile(new TilePosition(2, 3, 1)); 239 assertEquals(p1.getX(), rect.getProjectionBounds().minEast, 1e-10); 240 assertEquals(p1.getY(), rect.getProjectionBounds().minNorth, 1e-10); 241 } 242 243 /** 244 * Test {@link TileCoordinateConverter#getTransformForTile(TilePosition, double, double, double, double, double, double)} 245 */ 246 @Test 247 public void testGetTransformForTile() { 248 TilePosition tile = new TilePosition(2, 3, 1); 249 Point2D p1 = Main.map.mapView.getState().getPointFor(new LatLon(26, 7)).getInView(); 250 Point2D p2 = Main.map.mapView.getState().getPointFor(new LatLon(39, 8)).getInView(); 251 252 for (AffineTransform transform : new AffineTransform[] { 253 converter.getTransformForTile(tile, 0, 0, 0, 1, 1, 1), 254 converter.getTransformForTile(tile, 0, 0, 1, 0, 1, 1), 255 converter.getTransformForTile(tile, 0, 0, 1, 1, 1, 0), 256 converter.getTransformForTile(tile, 0, 0, 0, 1, 1, 0), 257 }) { 258 assertEquals(p1.getX(), transform.getTranslateX(), 1e-10); 259 assertEquals(p1.getY(), transform.getTranslateY(), 1e-10); 260 261 Point2D p1converted = transform.transform(new Point2D.Double(0, 0), null); 262 Point2D p2converted = transform.transform(new Point2D.Double(1, 1), null); 263 264 assertEquals(p1.getX(), p1converted.getX(), 1e-10); 265 assertEquals(p1.getY(), p1converted.getY(), 1e-10); 266 assertEquals(p2.getX(), p2converted.getX(), 1e-10); 267 assertEquals(p2.getY(), p2converted.getY(), 1e-10); 268 } 269 } 270 /** 271 * Test {@link TileCoordinateConverter#getTransformForTile(TilePosition, double, double, double, double, double, double)} 272 * ignores unsolveable 273 */ 274 @Test 275 public void testGetTransformForTileIgnoresUnsolveable() { 276 TilePosition tile = new TilePosition(2, 3, 1); 277 for (AffineTransform transform : new AffineTransform[] { 278 converter.getTransformForTile(tile, 0, 0, 0, 0, 1, 1), 279 converter.getTransformForTile(tile, 0, 0, 1, 0, 0.5, 0), 280 converter.getTransformForTile(tile, 0, 0, 1, 1, 0, 0), 281 converter.getTransformForTile(tile, 0, 0, 0, 0e-30, 1, 0), 282 }) { 283 assertEquals(1, transform.getScaleX(), 1e-10); 284 assertEquals(1, transform.getScaleY(), 1e-10); 285 assertEquals(0, transform.getTranslateX(), 1e-10); 286 assertEquals(0, transform.getTranslateY(), 1e-10); 287 } 288 } 289 }
