001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.layer; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.Color; 007import java.awt.Dimension; 008import java.awt.Font; 009import java.awt.Graphics; 010import java.awt.Graphics2D; 011import java.awt.GridBagLayout; 012import java.awt.Image; 013import java.awt.Point; 014import java.awt.Shape; 015import java.awt.Toolkit; 016import java.awt.event.ActionEvent; 017import java.awt.event.MouseAdapter; 018import java.awt.event.MouseEvent; 019import java.awt.geom.AffineTransform; 020import java.awt.geom.Point2D; 021import java.awt.geom.Rectangle2D; 022import java.awt.image.BufferedImage; 023import java.awt.image.ImageObserver; 024import java.io.File; 025import java.io.IOException; 026import java.net.MalformedURLException; 027import java.net.URL; 028import java.text.SimpleDateFormat; 029import java.util.ArrayList; 030import java.util.Arrays; 031import java.util.Collection; 032import java.util.Collections; 033import java.util.Comparator; 034import java.util.Date; 035import java.util.LinkedList; 036import java.util.List; 037import java.util.Map; 038import java.util.Map.Entry; 039import java.util.Objects; 040import java.util.Set; 041import java.util.concurrent.ConcurrentSkipListSet; 042import java.util.concurrent.atomic.AtomicInteger; 043import java.util.function.Consumer; 044import java.util.function.Function; 045import java.util.stream.Collectors; 046import java.util.stream.IntStream; 047import java.util.stream.Stream; 048 049import javax.swing.AbstractAction; 050import javax.swing.Action; 051import javax.swing.JLabel; 052import javax.swing.JMenuItem; 053import javax.swing.JOptionPane; 054import javax.swing.JPanel; 055import javax.swing.JPopupMenu; 056import javax.swing.JSeparator; 057import javax.swing.Timer; 058 059import org.openstreetmap.gui.jmapviewer.AttributionSupport; 060import org.openstreetmap.gui.jmapviewer.MemoryTileCache; 061import org.openstreetmap.gui.jmapviewer.OsmTileLoader; 062import org.openstreetmap.gui.jmapviewer.Tile; 063import org.openstreetmap.gui.jmapviewer.TileRange; 064import org.openstreetmap.gui.jmapviewer.TileXY; 065import org.openstreetmap.gui.jmapviewer.interfaces.CachedTileLoader; 066import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate; 067import org.openstreetmap.gui.jmapviewer.interfaces.IProjected; 068import org.openstreetmap.gui.jmapviewer.interfaces.TemplatedTileSource; 069import org.openstreetmap.gui.jmapviewer.interfaces.TileCache; 070import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader; 071import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener; 072import org.openstreetmap.gui.jmapviewer.interfaces.TileSource; 073import org.openstreetmap.gui.jmapviewer.tilesources.AbstractTMSTileSource; 074import org.openstreetmap.josm.Main; 075import org.openstreetmap.josm.actions.ExpertToggleAction; 076import org.openstreetmap.josm.actions.ImageryAdjustAction; 077import org.openstreetmap.josm.actions.RenameLayerAction; 078import org.openstreetmap.josm.actions.SaveActionBase; 079import org.openstreetmap.josm.data.Bounds; 080import org.openstreetmap.josm.data.ProjectionBounds; 081import org.openstreetmap.josm.data.coor.EastNorth; 082import org.openstreetmap.josm.data.coor.LatLon; 083import org.openstreetmap.josm.data.imagery.CoordinateConversion; 084import org.openstreetmap.josm.data.imagery.ImageryInfo; 085import org.openstreetmap.josm.data.imagery.OffsetBookmark; 086import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader; 087import org.openstreetmap.josm.data.imagery.TileLoaderFactory; 088import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor; 089import org.openstreetmap.josm.data.preferences.IntegerProperty; 090import org.openstreetmap.josm.data.projection.Projection; 091import org.openstreetmap.josm.data.projection.Projections; 092import org.openstreetmap.josm.gui.ExtendedDialog; 093import org.openstreetmap.josm.gui.MainApplication; 094import org.openstreetmap.josm.gui.MapView; 095import org.openstreetmap.josm.gui.NavigatableComponent.ZoomChangeListener; 096import org.openstreetmap.josm.gui.Notification; 097import org.openstreetmap.josm.gui.dialogs.LayerListDialog; 098import org.openstreetmap.josm.gui.dialogs.LayerListPopup; 099import org.openstreetmap.josm.gui.io.importexport.WMSLayerImporter; 100import org.openstreetmap.josm.gui.layer.imagery.AutoLoadTilesAction; 101import org.openstreetmap.josm.gui.layer.imagery.AutoZoomAction; 102import org.openstreetmap.josm.gui.layer.imagery.DecreaseZoomAction; 103import org.openstreetmap.josm.gui.layer.imagery.FlushTileCacheAction; 104import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings.FilterChangeListener; 105import org.openstreetmap.josm.gui.layer.imagery.IncreaseZoomAction; 106import org.openstreetmap.josm.gui.layer.imagery.LoadAllTilesAction; 107import org.openstreetmap.josm.gui.layer.imagery.LoadErroneousTilesAction; 108import org.openstreetmap.josm.gui.layer.imagery.ReprojectionTile; 109import org.openstreetmap.josm.gui.layer.imagery.ShowErrorsAction; 110import org.openstreetmap.josm.gui.layer.imagery.TileAnchor; 111import org.openstreetmap.josm.gui.layer.imagery.TileCoordinateConverter; 112import org.openstreetmap.josm.gui.layer.imagery.TilePosition; 113import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings; 114import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings.DisplaySettingsChangeEvent; 115import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings.DisplaySettingsChangeListener; 116import org.openstreetmap.josm.gui.layer.imagery.ZoomToBestAction; 117import org.openstreetmap.josm.gui.layer.imagery.ZoomToNativeLevelAction; 118import org.openstreetmap.josm.gui.progress.ProgressMonitor; 119import org.openstreetmap.josm.gui.util.GuiHelper; 120import org.openstreetmap.josm.tools.GBC; 121import org.openstreetmap.josm.tools.HttpClient; 122import org.openstreetmap.josm.tools.Logging; 123import org.openstreetmap.josm.tools.MemoryManager; 124import org.openstreetmap.josm.tools.MemoryManager.MemoryHandle; 125import org.openstreetmap.josm.tools.MemoryManager.NotEnoughMemoryException; 126import org.openstreetmap.josm.tools.Utils; 127import org.openstreetmap.josm.tools.bugreport.BugReport; 128 129/** 130 * Base abstract class that supports displaying images provided by TileSource. It might be TMS source, WMS or WMTS 131 * It implements all standard functions of tilesource based layers: autozoom, tile reloads, layer saving, loading,etc. 132 * 133 * @author Upliner 134 * @author Wiktor Niesiobędzki 135 * @param <T> Tile Source class used for this layer 136 * @since 3715 137 * @since 8526 (copied from TMSLayer) 138 */ 139public abstract class AbstractTileSourceLayer<T extends AbstractTMSTileSource> extends ImageryLayer 140implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeListener, DisplaySettingsChangeListener { 141 private static final String PREFERENCE_PREFIX = "imagery.generic"; 142 static { // Registers all setting properties 143 new TileSourceDisplaySettings(); 144 } 145 146 /** maximum zoom level supported */ 147 public static final int MAX_ZOOM = 30; 148 /** minium zoom level supported */ 149 public static final int MIN_ZOOM = 2; 150 private static final Font InfoFont = new Font("sansserif", Font.BOLD, 13); 151 152 /** additional layer menu actions */ 153 private static List<MenuAddition> menuAdditions = new LinkedList<>(); 154 155 /** minimum zoom level to show to user */ 156 public static final IntegerProperty PROP_MIN_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".min_zoom_lvl", 2); 157 /** maximum zoom level to show to user */ 158 public static final IntegerProperty PROP_MAX_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".max_zoom_lvl", 20); 159 160 //public static final BooleanProperty PROP_DRAW_DEBUG = new BooleanProperty(PREFERENCE_PREFIX + ".draw_debug", false); 161 /** Zoomlevel at which tiles is currently downloaded. Initial zoom lvl is set to bestZoom */ 162 private int currentZoomLevel; 163 164 private final AttributionSupport attribution = new AttributionSupport(); 165 private final TileHolder clickedTileHolder = new TileHolder(); 166 167 /** 168 * Offset between calculated zoom level and zoom level used to download and show tiles. Negative values will result in 169 * lower resolution of imagery useful in "retina" displays, positive values will result in higher resolution 170 */ 171 public static final IntegerProperty ZOOM_OFFSET = new IntegerProperty(PREFERENCE_PREFIX + ".zoom_offset", 0); 172 173 /* 174 * use MemoryTileCache instead of tileLoader JCS cache, as tileLoader caches only content (byte[] of image) 175 * and MemoryTileCache caches whole Tile. This gives huge performance improvement when a lot of tiles are visible 176 * in MapView (for example - when limiting min zoom in imagery) 177 * 178 * Use per-layer tileCache instance, as the more layers there are, the more tiles needs to be cached 179 */ 180 protected TileCache tileCache; // initialized together with tileSource 181 protected T tileSource; 182 protected TileLoader tileLoader; 183 184 /** A timer that is used to delay invalidation events if required. */ 185 private final Timer invalidateLaterTimer = new Timer(100, e -> this.invalidate()); 186 187 private final MouseAdapter adapter = new MouseAdapter() { 188 @Override 189 public void mouseClicked(MouseEvent e) { 190 if (!isVisible()) return; 191 if (e.getButton() == MouseEvent.BUTTON3) { 192 clickedTileHolder.setTile(getTileForPixelpos(e.getX(), e.getY())); 193 new TileSourceLayerPopup().show(e.getComponent(), e.getX(), e.getY()); 194 } else if (e.getButton() == MouseEvent.BUTTON1) { 195 attribution.handleAttribution(e.getPoint(), true); 196 } 197 } 198 }; 199 200 private final TileSourceDisplaySettings displaySettings = createDisplaySettings(); 201 202 private final ImageryAdjustAction adjustAction = new ImageryAdjustAction(this); 203 // prepared to be moved to the painter 204 protected TileCoordinateConverter coordinateConverter; 205 206 /** 207 * Creates Tile Source based Imagery Layer based on Imagery Info 208 * @param info imagery info 209 */ 210 public AbstractTileSourceLayer(ImageryInfo info) { 211 super(info); 212 setBackgroundLayer(true); 213 this.setVisible(true); 214 getFilterSettings().addFilterChangeListener(this); 215 getDisplaySettings().addSettingsChangeListener(this); 216 } 217 218 /** 219 * This method creates the {@link TileSourceDisplaySettings} object. Subclasses may implement it to e.g. change the prefix. 220 * @return The object. 221 * @since 10568 222 */ 223 protected TileSourceDisplaySettings createDisplaySettings() { 224 return new TileSourceDisplaySettings(); 225 } 226 227 /** 228 * Gets the {@link TileSourceDisplaySettings} instance associated with this tile source. 229 * @return The tile source display settings 230 * @since 10568 231 */ 232 public TileSourceDisplaySettings getDisplaySettings() { 233 return displaySettings; 234 } 235 236 @Override 237 public void filterChanged() { 238 invalidate(); 239 } 240 241 protected abstract TileLoaderFactory getTileLoaderFactory(); 242 243 /** 244 * Get projections this imagery layer supports natively. 245 * 246 * For example projection of tiles that are downloaded from a server. Layer 247 * may support even more projections (by reprojecting the tiles), but with a 248 * certain loss in image quality and performance. 249 * @return projections this imagery layer supports natively; null if layer is projection agnostic. 250 */ 251 public abstract Collection<String> getNativeProjections(); 252 253 /** 254 * Creates and returns a new {@link TileSource} instance depending on {@link #info} specified in the constructor. 255 * 256 * @return TileSource for specified ImageryInfo 257 * @throws IllegalArgumentException when Imagery is not supported by layer 258 */ 259 protected abstract T getTileSource(); 260 261 protected Map<String, String> getHeaders(T tileSource) { 262 if (tileSource instanceof TemplatedTileSource) { 263 return ((TemplatedTileSource) tileSource).getHeaders(); 264 } 265 return null; 266 } 267 268 protected void initTileSource(T tileSource) { 269 coordinateConverter = new TileCoordinateConverter(MainApplication.getMap().mapView, tileSource, getDisplaySettings()); 270 attribution.initialize(tileSource); 271 272 currentZoomLevel = getBestZoom(); 273 274 Map<String, String> headers = getHeaders(tileSource); 275 276 tileLoader = getTileLoaderFactory().makeTileLoader(this, headers); 277 278 try { 279 if ("file".equalsIgnoreCase(new URL(tileSource.getBaseUrl()).getProtocol())) { 280 tileLoader = new OsmTileLoader(this); 281 } 282 } catch (MalformedURLException e) { 283 // ignore, assume that this is not a file 284 Logging.log(Logging.LEVEL_DEBUG, e); 285 } 286 287 if (tileLoader == null) 288 tileLoader = new OsmTileLoader(this, headers); 289 290 tileCache = new MemoryTileCache(estimateTileCacheSize()); 291 } 292 293 @Override 294 public synchronized void tileLoadingFinished(Tile tile, boolean success) { 295 if (tile.hasError()) { 296 success = false; 297 tile.setImage(null); 298 } 299 tile.setLoaded(success); 300 invalidateLater(); 301 Logging.debug("tileLoadingFinished() tile: {0} success: {1}", tile, success); 302 } 303 304 /** 305 * Clears the tile cache. 306 */ 307 public void clearTileCache() { 308 if (tileLoader instanceof CachedTileLoader) { 309 ((CachedTileLoader) tileLoader).clearCache(tileSource); 310 } 311 tileCache.clear(); 312 } 313 314 /** 315 * {@inheritDoc} 316 * @deprecated Use {@link TileSourceDisplaySettings#getDx()} 317 */ 318 @Override 319 @Deprecated 320 public double getDx() { 321 return getDisplaySettings().getDx(); 322 } 323 324 /** 325 * {@inheritDoc} 326 * @deprecated Use {@link TileSourceDisplaySettings#getDy()} 327 */ 328 @Override 329 @Deprecated 330 public double getDy() { 331 return getDisplaySettings().getDy(); 332 } 333 334 /** 335 * {@inheritDoc} 336 * @deprecated Use {@link TileSourceDisplaySettings} 337 */ 338 @Override 339 @Deprecated 340 public void setOffset(OffsetBookmark offset) { 341 getDisplaySettings().setOffsetBookmark(offset); 342 } 343 344 @Override 345 public Object getInfoComponent() { 346 JPanel panel = (JPanel) super.getInfoComponent(); 347 List<List<String>> content = new ArrayList<>(); 348 Collection<String> nativeProjections = getNativeProjections(); 349 if (nativeProjections != null) { 350 content.add(Arrays.asList(tr("Native projections"), Utils.join(", ", getNativeProjections()))); 351 } 352 EastNorth offset = getDisplaySettings().getDisplacement(); 353 if (offset.distanceSq(0, 0) > 1e-10) { 354 content.add(Arrays.asList(tr("Offset"), offset.east() + ";" + offset.north())); 355 } 356 if (coordinateConverter.requiresReprojection()) { 357 content.add(Arrays.asList(tr("Tile download projection"), tileSource.getServerCRS())); 358 content.add(Arrays.asList(tr("Tile display projection"), Main.getProjection().toCode())); 359 } 360 content.add(Arrays.asList(tr("Current zoom"), Integer.toString(currentZoomLevel))); 361 for (List<String> entry: content) { 362 panel.add(new JLabel(entry.get(0) + ':'), GBC.std()); 363 panel.add(GBC.glue(5, 0), GBC.std()); 364 panel.add(createTextField(entry.get(1)), GBC.eol().fill(GBC.HORIZONTAL)); 365 } 366 return panel; 367 } 368 369 @Override 370 protected Action getAdjustAction() { 371 return adjustAction; 372 } 373 374 /** 375 * Returns average number of screen pixels per tile pixel for current mapview 376 * @param zoom zoom level 377 * @return average number of screen pixels per tile pixel 378 */ 379 public double getScaleFactor(int zoom) { 380 if (coordinateConverter != null) { 381 return coordinateConverter.getScaleFactor(zoom); 382 } else { 383 return 1; 384 } 385 } 386 387 public int getBestZoom() { 388 double factor = getScaleFactor(1); // check the ratio between area of tilesize at zoom 1 to current view 389 double result = Math.log(factor)/Math.log(2)/2; 390 /* 391 * Math.log(factor)/Math.log(2) - gives log base 2 of factor 392 * We divide result by 2, as factor contains ratio between areas. We could do Math.sqrt before log, or just divide log by 2 393 * 394 * ZOOM_OFFSET controls, whether we work with overzoomed or underzoomed tiles. Positive ZOOM_OFFSET 395 * is for working with underzoomed tiles (higher quality when working with aerial imagery), negative ZOOM_OFFSET 396 * is for working with overzoomed tiles (big, pixelated), which is good when working with high-dpi screens and/or 397 * maps as a imagery layer 398 */ 399 int intResult = (int) Math.round(result + 1 + ZOOM_OFFSET.get() / 1.9); 400 int minZoom = getMinZoomLvl(); 401 int maxZoom = getMaxZoomLvl(); 402 if (minZoom <= maxZoom) { 403 intResult = Utils.clamp(intResult, minZoom, maxZoom); 404 } else if (intResult > maxZoom) { 405 intResult = maxZoom; 406 } 407 return intResult; 408 } 409 410 /** 411 * Default implementation of {@link org.openstreetmap.josm.gui.layer.Layer.LayerAction#supportLayers(List)}. 412 * @param layers layers 413 * @return {@code true} is layers contains only a {@code TMSLayer} 414 */ 415 public static boolean actionSupportLayers(List<Layer> layers) { 416 return layers.size() == 1 && layers.get(0) instanceof TMSLayer; 417 } 418 419 private final class ShowTileInfoAction extends AbstractAction { 420 421 private ShowTileInfoAction() { 422 super(tr("Show tile info")); 423 setEnabled(clickedTileHolder.getTile() != null); 424 } 425 426 private String getSizeString(int size) { 427 return new StringBuilder().append(size).append('x').append(size).toString(); 428 } 429 430 @Override 431 public void actionPerformed(ActionEvent ae) { 432 Tile clickedTile = clickedTileHolder.getTile(); 433 if (clickedTile != null) { 434 ExtendedDialog ed = new ExtendedDialog(Main.parent, tr("Tile Info"), tr("OK")); 435 JPanel panel = new JPanel(new GridBagLayout()); 436 Rectangle2D displaySize = coordinateConverter.getRectangleForTile(clickedTile); 437 String url = ""; 438 try { 439 url = clickedTile.getUrl(); 440 } catch (IOException e) { 441 // silence exceptions 442 Logging.trace(e); 443 } 444 445 List<List<String>> content = new ArrayList<>(); 446 content.add(Arrays.asList(tr("Tile name"), clickedTile.getKey())); 447 content.add(Arrays.asList(tr("Tile URL"), url)); 448 content.add(Arrays.asList(tr("Tile size"), 449 getSizeString(clickedTile.getTileSource().getTileSize()))); 450 content.add(Arrays.asList(tr("Tile display size"), 451 new StringBuilder().append(displaySize.getWidth()) 452 .append('x') 453 .append(displaySize.getHeight()).toString())); 454 if (coordinateConverter.requiresReprojection()) { 455 content.add(Arrays.asList(tr("Reprojection"), 456 clickedTile.getTileSource().getServerCRS() + 457 " -> " + Main.getProjection().toCode())); 458 BufferedImage img = clickedTile.getImage(); 459 if (img != null) { 460 content.add(Arrays.asList(tr("Reprojected tile size"), 461 img.getWidth() + "x" + img.getHeight())); 462 463 } 464 } 465 for (List<String> entry: content) { 466 panel.add(new JLabel(entry.get(0) + ':'), GBC.std()); 467 panel.add(GBC.glue(5, 0), GBC.std()); 468 panel.add(createTextField(entry.get(1)), GBC.eol().fill(GBC.HORIZONTAL)); 469 } 470 471 for (Entry<String, String> e: clickedTile.getMetadata().entrySet()) { 472 panel.add(new JLabel(tr("Metadata ") + tr(e.getKey()) + ':'), GBC.std()); 473 panel.add(GBC.glue(5, 0), GBC.std()); 474 String value = e.getValue(); 475 if ("lastModification".equals(e.getKey()) || "expirationTime".equals(e.getKey())) { 476 value = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(Long.parseLong(value))); 477 } 478 panel.add(createTextField(value), GBC.eol().fill(GBC.HORIZONTAL)); 479 480 } 481 ed.setIcon(JOptionPane.INFORMATION_MESSAGE); 482 ed.setContent(panel); 483 ed.showDialog(); 484 } 485 } 486 } 487 488 private final class LoadTileAction extends AbstractAction { 489 490 private LoadTileAction() { 491 super(tr("Load tile")); 492 setEnabled(clickedTileHolder.getTile() != null); 493 } 494 495 @Override 496 public void actionPerformed(ActionEvent ae) { 497 Tile clickedTile = clickedTileHolder.getTile(); 498 if (clickedTile != null) { 499 loadTile(clickedTile, true); 500 invalidate(); 501 } 502 } 503 } 504 505 private void sendOsmTileRequest(String request) { 506 Tile clickedTile = clickedTileHolder.getTile(); 507 if (clickedTile != null) { 508 try { 509 new Notification(HttpClient.create(new URL(clickedTile.getUrl() + '/' + request)) 510 .connect().fetchContent()).show(); 511 } catch (IOException ex) { 512 Logging.error(ex); 513 } 514 } 515 } 516 517 private final class GetOsmTileStatusAction extends AbstractAction { 518 private GetOsmTileStatusAction() { 519 super(tr("Get tile status")); 520 setEnabled(clickedTileHolder.getTile() != null); 521 } 522 523 @Override 524 public void actionPerformed(ActionEvent e) { 525 sendOsmTileRequest("status"); 526 } 527 } 528 529 private final class MarkOsmTileDirtyAction extends AbstractAction { 530 private MarkOsmTileDirtyAction() { 531 super(tr("Force tile rendering")); 532 setEnabled(clickedTileHolder.getTile() != null); 533 } 534 535 @Override 536 public void actionPerformed(ActionEvent e) { 537 sendOsmTileRequest("dirty"); 538 } 539 } 540 541 /** 542 * Simple class to keep clickedTile within hookUpMapView 543 */ 544 private static final class TileHolder { 545 private Tile t; 546 547 public Tile getTile() { 548 return t; 549 } 550 551 public void setTile(Tile t) { 552 this.t = t; 553 } 554 } 555 556 /** 557 * Creates popup menu items and binds to mouse actions 558 */ 559 @Override 560 public void hookUpMapView() { 561 // this needs to be here and not in constructor to allow empty TileSource class construction using SessionWriter 562 initializeIfRequired(); 563 super.hookUpMapView(); 564 } 565 566 @Override 567 public LayerPainter attachToMapView(MapViewEvent event) { 568 initializeIfRequired(); 569 570 event.getMapView().addMouseListener(adapter); 571 MapView.addZoomChangeListener(this); 572 573 if (this instanceof NativeScaleLayer) { 574 event.getMapView().setNativeScaleLayer((NativeScaleLayer) this); 575 } 576 577 // FIXME: why do we need this? Without this, if you add a WMS layer and do not move the mouse, sometimes, tiles do not start loading. 578 // FIXME: Check if this is still required. 579 event.getMapView().repaint(500); 580 581 return super.attachToMapView(event); 582 } 583 584 private void initializeIfRequired() { 585 if (tileSource == null) { 586 tileSource = getTileSource(); 587 if (tileSource == null) { 588 throw new IllegalArgumentException(tr("Failed to create tile source")); 589 } 590 // check if projection is supported 591 projectionChanged(null, Main.getProjection()); 592 initTileSource(this.tileSource); 593 } 594 } 595 596 @Override 597 protected LayerPainter createMapViewPainter(MapViewEvent event) { 598 return new TileSourcePainter(); 599 } 600 601 /** 602 * Tile source layer popup menu. 603 */ 604 public class TileSourceLayerPopup extends JPopupMenu { 605 /** 606 * Constructs a new {@code TileSourceLayerPopup}. 607 */ 608 public TileSourceLayerPopup() { 609 for (Action a : getCommonEntries()) { 610 if (a instanceof LayerAction) { 611 add(((LayerAction) a).createMenuComponent()); 612 } else { 613 add(new JMenuItem(a)); 614 } 615 } 616 add(new JSeparator()); 617 add(new JMenuItem(new LoadTileAction())); 618 add(new JMenuItem(new ShowTileInfoAction())); 619 if (ExpertToggleAction.isExpert() && tileSource != null && tileSource.isModTileFeatures()) { 620 add(new JMenuItem(new GetOsmTileStatusAction())); 621 add(new JMenuItem(new MarkOsmTileDirtyAction())); 622 } 623 } 624 } 625 626 protected int estimateTileCacheSize() { 627 Dimension screenSize = GuiHelper.getMaximumScreenSize(); 628 int height = screenSize.height; 629 int width = screenSize.width; 630 int tileSize = 256; // default tile size 631 if (tileSource != null) { 632 tileSize = tileSource.getTileSize(); 633 } 634 // as we can see part of the tile at the top and at the bottom, use Math.ceil(...) + 1 to accommodate for that 635 int visibileTiles = (int) (Math.ceil((double) height / tileSize + 1) * Math.ceil((double) width / tileSize + 1)); 636 // add 10% for tiles from different zoom levels 637 int ret = (int) Math.ceil( 638 Math.pow(2d, ZOOM_OFFSET.get()) * visibileTiles // use offset to decide, how many tiles are visible 639 * 4); 640 Logging.info("AbstractTileSourceLayer: estimated visible tiles: {0}, estimated cache size: {1}", visibileTiles, ret); 641 return ret; 642 } 643 644 @Override 645 public void displaySettingsChanged(DisplaySettingsChangeEvent e) { 646 if (tileSource == null) { 647 return; 648 } 649 switch (e.getChangedSetting()) { 650 case TileSourceDisplaySettings.AUTO_ZOOM: 651 if (getDisplaySettings().isAutoZoom() && getBestZoom() != currentZoomLevel) { 652 setZoomLevel(getBestZoom()); 653 invalidate(); 654 } 655 break; 656 case TileSourceDisplaySettings.AUTO_LOAD: 657 if (getDisplaySettings().isAutoLoad()) { 658 invalidate(); 659 } 660 break; 661 default: 662 // e.g. displacement 663 // trigger a redraw in every case 664 invalidate(); 665 } 666 } 667 668 /** 669 * Checks zoom level against settings 670 * @param maxZoomLvl zoom level to check 671 * @param ts tile source to crosscheck with 672 * @return maximum zoom level, not higher than supported by tilesource nor set by the user 673 */ 674 public static int checkMaxZoomLvl(int maxZoomLvl, TileSource ts) { 675 if (maxZoomLvl > MAX_ZOOM) { 676 maxZoomLvl = MAX_ZOOM; 677 } 678 if (maxZoomLvl < PROP_MIN_ZOOM_LVL.get()) { 679 maxZoomLvl = PROP_MIN_ZOOM_LVL.get(); 680 } 681 if (ts != null && ts.getMaxZoom() != 0 && ts.getMaxZoom() < maxZoomLvl) { 682 maxZoomLvl = ts.getMaxZoom(); 683 } 684 return maxZoomLvl; 685 } 686 687 /** 688 * Checks zoom level against settings 689 * @param minZoomLvl zoom level to check 690 * @param ts tile source to crosscheck with 691 * @return minimum zoom level, not higher than supported by tilesource nor set by the user 692 */ 693 public static int checkMinZoomLvl(int minZoomLvl, TileSource ts) { 694 if (minZoomLvl < MIN_ZOOM) { 695 minZoomLvl = MIN_ZOOM; 696 } 697 if (minZoomLvl > PROP_MAX_ZOOM_LVL.get()) { 698 minZoomLvl = getMaxZoomLvl(ts); 699 } 700 if (ts != null && ts.getMinZoom() > minZoomLvl) { 701 minZoomLvl = ts.getMinZoom(); 702 } 703 return minZoomLvl; 704 } 705 706 /** 707 * @param ts TileSource for which we want to know maximum zoom level 708 * @return maximum max zoom level, that will be shown on layer 709 */ 710 public static int getMaxZoomLvl(TileSource ts) { 711 return checkMaxZoomLvl(PROP_MAX_ZOOM_LVL.get(), ts); 712 } 713 714 /** 715 * @param ts TileSource for which we want to know minimum zoom level 716 * @return minimum zoom level, that will be shown on layer 717 */ 718 public static int getMinZoomLvl(TileSource ts) { 719 return checkMinZoomLvl(PROP_MIN_ZOOM_LVL.get(), ts); 720 } 721 722 /** 723 * Sets maximum zoom level, that layer will attempt show 724 * @param maxZoomLvl maximum zoom level 725 */ 726 public static void setMaxZoomLvl(int maxZoomLvl) { 727 PROP_MAX_ZOOM_LVL.put(checkMaxZoomLvl(maxZoomLvl, null)); 728 } 729 730 /** 731 * Sets minimum zoom level, that layer will attempt show 732 * @param minZoomLvl minimum zoom level 733 */ 734 public static void setMinZoomLvl(int minZoomLvl) { 735 PROP_MIN_ZOOM_LVL.put(checkMinZoomLvl(minZoomLvl, null)); 736 } 737 738 /** 739 * This fires every time the user changes the zoom, but also (due to ZoomChangeListener) - on all 740 * changes to visible map (panning/zooming) 741 */ 742 @Override 743 public void zoomChanged() { 744 zoomChanged(true); 745 } 746 747 private void zoomChanged(boolean invalidate) { 748 Logging.debug("zoomChanged(): {0}", currentZoomLevel); 749 if (tileLoader instanceof TMSCachedTileLoader) { 750 ((TMSCachedTileLoader) tileLoader).cancelOutstandingTasks(); 751 } 752 if (invalidate) { 753 invalidate(); 754 } 755 } 756 757 protected int getMaxZoomLvl() { 758 if (info.getMaxZoom() != 0) 759 return checkMaxZoomLvl(info.getMaxZoom(), tileSource); 760 else 761 return getMaxZoomLvl(tileSource); 762 } 763 764 protected int getMinZoomLvl() { 765 if (info.getMinZoom() != 0) 766 return checkMinZoomLvl(info.getMinZoom(), tileSource); 767 else 768 return getMinZoomLvl(tileSource); 769 } 770 771 /** 772 * 773 * @return if its allowed to zoom in 774 */ 775 public boolean zoomIncreaseAllowed() { 776 boolean zia = currentZoomLevel < this.getMaxZoomLvl(); 777 Logging.debug("zoomIncreaseAllowed(): {0} {1} vs. {2}", zia, currentZoomLevel, this.getMaxZoomLvl()); 778 return zia; 779 } 780 781 /** 782 * Zoom in, go closer to map. 783 * 784 * @return true, if zoom increasing was successful, false otherwise 785 */ 786 public boolean increaseZoomLevel() { 787 if (zoomIncreaseAllowed()) { 788 currentZoomLevel++; 789 Logging.debug("increasing zoom level to: {0}", currentZoomLevel); 790 zoomChanged(); 791 } else { 792 Logging.warn("Current zoom level ("+currentZoomLevel+") could not be increased. "+ 793 "Max.zZoom Level "+this.getMaxZoomLvl()+" reached."); 794 return false; 795 } 796 return true; 797 } 798 799 /** 800 * Get the current zoom level of the layer 801 * @return the current zoom level 802 * @since 12603 803 */ 804 public int getZoomLevel() { 805 return currentZoomLevel; 806 } 807 808 /** 809 * Sets the zoom level of the layer 810 * @param zoom zoom level 811 * @return true, when zoom has changed to desired value, false if it was outside supported zoom levels 812 */ 813 public boolean setZoomLevel(int zoom) { 814 return setZoomLevel(zoom, true); 815 } 816 817 private boolean setZoomLevel(int zoom, boolean invalidate) { 818 if (zoom == currentZoomLevel) return true; 819 if (zoom > this.getMaxZoomLvl()) return false; 820 if (zoom < this.getMinZoomLvl()) return false; 821 currentZoomLevel = zoom; 822 zoomChanged(invalidate); 823 return true; 824 } 825 826 /** 827 * Check if zooming out is allowed 828 * 829 * @return true, if zooming out is allowed (currentZoomLevel > minZoomLevel) 830 */ 831 public boolean zoomDecreaseAllowed() { 832 boolean zda = currentZoomLevel > this.getMinZoomLvl(); 833 Logging.debug("zoomDecreaseAllowed(): {0} {1} vs. {2}", zda, currentZoomLevel, this.getMinZoomLvl()); 834 return zda; 835 } 836 837 /** 838 * Zoom out from map. 839 * 840 * @return true, if zoom increasing was successfull, false othervise 841 */ 842 public boolean decreaseZoomLevel() { 843 if (zoomDecreaseAllowed()) { 844 Logging.debug("decreasing zoom level to: {0}", currentZoomLevel); 845 currentZoomLevel--; 846 zoomChanged(); 847 } else { 848 return false; 849 } 850 return true; 851 } 852 853 private Tile getOrCreateTile(TilePosition tilePosition) { 854 return getOrCreateTile(tilePosition.getX(), tilePosition.getY(), tilePosition.getZoom()); 855 } 856 857 private Tile getOrCreateTile(int x, int y, int zoom) { 858 Tile tile = getTile(x, y, zoom); 859 if (tile == null) { 860 if (coordinateConverter.requiresReprojection()) { 861 tile = new ReprojectionTile(tileSource, x, y, zoom); 862 } else { 863 tile = new Tile(tileSource, x, y, zoom); 864 } 865 tileCache.addTile(tile); 866 } 867 return tile; 868 } 869 870 private Tile getTile(TilePosition tilePosition) { 871 return getTile(tilePosition.getX(), tilePosition.getY(), tilePosition.getZoom()); 872 } 873 874 /** 875 * Returns tile at given position. 876 * This can and will return null for tiles that are not already in the cache. 877 * @param x tile number on the x axis of the tile to be retrieved 878 * @param y tile number on the y axis of the tile to be retrieved 879 * @param zoom zoom level of the tile to be retrieved 880 * @return tile at given position 881 */ 882 private Tile getTile(int x, int y, int zoom) { 883 if (x < tileSource.getTileXMin(zoom) || x > tileSource.getTileXMax(zoom) 884 || y < tileSource.getTileYMin(zoom) || y > tileSource.getTileYMax(zoom)) 885 return null; 886 return tileCache.getTile(tileSource, x, y, zoom); 887 } 888 889 private boolean loadTile(Tile tile, boolean force) { 890 if (tile == null) 891 return false; 892 if (!force && (tile.isLoaded() || tile.hasError())) 893 return false; 894 if (tile.isLoading()) 895 return false; 896 tileLoader.createTileLoaderJob(tile).submit(force); 897 return true; 898 } 899 900 private TileSet getVisibleTileSet() { 901 ProjectionBounds bounds = MainApplication.getMap().mapView.getState().getViewArea().getProjectionBounds(); 902 return getTileSet(bounds, currentZoomLevel); 903 } 904 905 /** 906 * Load all visible tiles. 907 * @param force {@code true} to force loading if auto-load is disabled 908 * @since 11950 909 */ 910 public void loadAllTiles(boolean force) { 911 TileSet ts = getVisibleTileSet(); 912 913 // if there is more than 18 tiles on screen in any direction, do not load all tiles! 914 if (ts.tooLarge()) { 915 Logging.warn("Not downloading all tiles because there is more than 18 tiles on an axis!"); 916 return; 917 } 918 ts.loadAllTiles(force); 919 invalidate(); 920 } 921 922 /** 923 * Load all visible tiles in error. 924 * @param force {@code true} to force loading if auto-load is disabled 925 * @since 11950 926 */ 927 public void loadAllErrorTiles(boolean force) { 928 TileSet ts = getVisibleTileSet(); 929 ts.loadAllErrorTiles(force); 930 invalidate(); 931 } 932 933 @Override 934 public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) { 935 boolean done = (infoflags & (ERROR | FRAMEBITS | ALLBITS)) != 0; 936 Logging.debug("imageUpdate() done: {0} calling repaint", done); 937 938 if (done) { 939 invalidate(); 940 } else { 941 invalidateLater(); 942 } 943 return !done; 944 } 945 946 /** 947 * Invalidate the layer at a time in the future so that the user still sees the interface responsive. 948 */ 949 private void invalidateLater() { 950 GuiHelper.runInEDT(() -> { 951 if (!invalidateLaterTimer.isRunning()) { 952 invalidateLaterTimer.setRepeats(false); 953 invalidateLaterTimer.start(); 954 } 955 }); 956 } 957 958 private boolean imageLoaded(Image i) { 959 if (i == null) 960 return false; 961 int status = Toolkit.getDefaultToolkit().checkImage(i, -1, -1, this); 962 return (status & ALLBITS) != 0; 963 } 964 965 /** 966 * Returns the image for the given tile image is loaded. 967 * Otherwise returns null. 968 * 969 * @param tile the Tile for which the image should be returned 970 * @return the image of the tile or null. 971 */ 972 private BufferedImage getLoadedTileImage(Tile tile) { 973 BufferedImage img = tile.getImage(); 974 if (!imageLoaded(img)) 975 return null; 976 return img; 977 } 978 979 /** 980 * Draw a tile image on screen. 981 * @param g the Graphics2D 982 * @param toDrawImg tile image 983 * @param anchorImage tile anchor in image coordinates 984 * @param anchorScreen tile anchor in screen coordinates 985 * @param clip clipping region in screen coordinates (can be null) 986 */ 987 private void drawImageInside(Graphics2D g, BufferedImage toDrawImg, TileAnchor anchorImage, TileAnchor anchorScreen, Shape clip) { 988 AffineTransform imageToScreen = anchorImage.convert(anchorScreen); 989 Point2D screen0 = imageToScreen.transform(new Point.Double(0, 0), null); 990 Point2D screen1 = imageToScreen.transform(new Point.Double( 991 toDrawImg.getWidth(), toDrawImg.getHeight()), null); 992 993 Shape oldClip = null; 994 if (clip != null) { 995 oldClip = g.getClip(); 996 g.clip(clip); 997 } 998 g.drawImage(toDrawImg, (int) Math.round(screen0.getX()), (int) Math.round(screen0.getY()), 999 (int) Math.round(screen1.getX()) - (int) Math.round(screen0.getX()), 1000 (int) Math.round(screen1.getY()) - (int) Math.round(screen0.getY()), this); 1001 if (clip != null) { 1002 g.setClip(oldClip); 1003 } 1004 } 1005 1006 private List<Tile> paintTileImages(Graphics2D g, TileSet ts) { 1007 Object paintMutex = new Object(); 1008 List<TilePosition> missed = Collections.synchronizedList(new ArrayList<>()); 1009 ts.visitTiles(tile -> { 1010 boolean miss = false; 1011 BufferedImage img = null; 1012 TileAnchor anchorImage = null; 1013 if (!tile.isLoaded() || tile.hasError()) { 1014 miss = true; 1015 } else { 1016 synchronized (tile) { 1017 img = getLoadedTileImage(tile); 1018 anchorImage = getAnchor(tile, img); 1019 } 1020 if (img == null || anchorImage == null) { 1021 miss = true; 1022 } 1023 } 1024 if (miss) { 1025 missed.add(new TilePosition(tile)); 1026 return; 1027 } 1028 1029 img = applyImageProcessors(img); 1030 1031 TileAnchor anchorScreen = coordinateConverter.getScreenAnchorForTile(tile); 1032 synchronized (paintMutex) { 1033 //cannot paint in parallel 1034 drawImageInside(g, img, anchorImage, anchorScreen, null); 1035 } 1036 MapView mapView = MainApplication.getMap().mapView; 1037 if (tile instanceof ReprojectionTile && ((ReprojectionTile) tile).needsUpdate(mapView.getScale())) { 1038 // This means we have a reprojected tile in memory cache, but not at 1039 // current scale. Generally, the positioning of the tile will still 1040 // be correct, but for best image quality, the tile should be 1041 // reprojected to the target scale. The original tile image should 1042 // still be in disk cache, so this is fairly cheap. 1043 ((ReprojectionTile) tile).invalidate(); 1044 loadTile(tile, false); 1045 } 1046 1047 }, missed::add); 1048 1049 return missed.stream().map(this::getOrCreateTile).collect(Collectors.toList()); 1050 } 1051 1052 // This function is called for several zoom levels, not just the current one. 1053 // It should not trigger any tiles to be downloaded. 1054 // It should also avoid polluting the tile cache with any tiles since these tiles are not mandatory. 1055 // 1056 // The "border" tile tells us the boundaries of where we may drawn. 1057 // It will not be from the zoom level that is being drawn currently. 1058 // If drawing the displayZoomLevel, border is null and we draw the entire tile set. 1059 private List<Tile> paintTileImages(Graphics2D g, TileSet ts, int zoom, Tile border) { 1060 if (zoom <= 0) return Collections.emptyList(); 1061 Shape borderClip = coordinateConverter.getTileShapeScreen(border); 1062 List<Tile> missedTiles = new LinkedList<>(); 1063 // The callers of this code *require* that we return any tiles that we do not draw in missedTiles. 1064 // ts.allExistingTiles() by default will only return already-existing tiles. 1065 // However, we need to return *all* tiles to the callers, so force creation here. 1066 for (Tile tile : ts.allTilesCreate()) { 1067 boolean miss = false; 1068 BufferedImage img = null; 1069 TileAnchor anchorImage = null; 1070 if (!tile.isLoaded() || tile.hasError()) { 1071 miss = true; 1072 } else { 1073 synchronized (tile) { 1074 img = getLoadedTileImage(tile); 1075 anchorImage = getAnchor(tile, img); 1076 } 1077 1078 if (img == null || anchorImage == null) { 1079 miss = true; 1080 } 1081 } 1082 if (miss) { 1083 missedTiles.add(tile); 1084 continue; 1085 } 1086 1087 // applying all filters to this layer 1088 img = applyImageProcessors(img); 1089 1090 Shape clip; 1091 if (tileSource.isInside(tile, border)) { 1092 clip = null; 1093 } else if (tileSource.isInside(border, tile)) { 1094 clip = borderClip; 1095 } else { 1096 continue; 1097 } 1098 TileAnchor anchorScreen = coordinateConverter.getScreenAnchorForTile(tile); 1099 drawImageInside(g, img, anchorImage, anchorScreen, clip); 1100 } 1101 return missedTiles; 1102 } 1103 1104 private static TileAnchor getAnchor(Tile tile, BufferedImage image) { 1105 if (tile instanceof ReprojectionTile) { 1106 return ((ReprojectionTile) tile).getAnchor(); 1107 } else if (image != null) { 1108 return new TileAnchor(new Point.Double(0, 0), new Point.Double(image.getWidth(), image.getHeight())); 1109 } else { 1110 return null; 1111 } 1112 } 1113 1114 private void myDrawString(Graphics g, String text, int x, int y) { 1115 Color oldColor = g.getColor(); 1116 String textToDraw = text; 1117 if (g.getFontMetrics().stringWidth(text) > tileSource.getTileSize()) { 1118 // text longer than tile size, split it 1119 StringBuilder line = new StringBuilder(); 1120 StringBuilder ret = new StringBuilder(); 1121 for (String s: text.split(" ")) { 1122 if (g.getFontMetrics().stringWidth(line.toString() + s) > tileSource.getTileSize()) { 1123 ret.append(line).append('\n'); 1124 line.setLength(0); 1125 } 1126 line.append(s).append(' '); 1127 } 1128 ret.append(line); 1129 textToDraw = ret.toString(); 1130 } 1131 int offset = 0; 1132 for (String s: textToDraw.split("\n")) { 1133 g.setColor(Color.black); 1134 g.drawString(s, x + 1, y + offset + 1); 1135 g.setColor(oldColor); 1136 g.drawString(s, x, y + offset); 1137 offset += g.getFontMetrics().getHeight() + 3; 1138 } 1139 } 1140 1141 private void paintTileText(Tile tile, Graphics2D g) { 1142 if (tile == null) { 1143 return; 1144 } 1145 Point2D p = coordinateConverter.getPixelForTile(tile); 1146 int fontHeight = g.getFontMetrics().getHeight(); 1147 int x = (int) p.getX(); 1148 int y = (int) p.getY(); 1149 int texty = y + 2 + fontHeight; 1150 1151 /*if (PROP_DRAW_DEBUG.get()) { 1152 myDrawString(g, "x=" + t.getXtile() + " y=" + t.getYtile() + " z=" + zoom + "", p.x + 2, texty); 1153 texty += 1 + fontHeight; 1154 if ((t.getXtile() % 32 == 0) && (t.getYtile() % 32 == 0)) { 1155 myDrawString(g, "x=" + t.getXtile() / 32 + " y=" + t.getYtile() / 32 + " z=7", p.x + 2, texty); 1156 texty += 1 + fontHeight; 1157 } 1158 } 1159 1160 String tileStatus = tile.getStatus(); 1161 if (!tile.isLoaded() && PROP_DRAW_DEBUG.get()) { 1162 myDrawString(g, tr("image " + tileStatus), p.x + 2, texty); 1163 texty += 1 + fontHeight; 1164 }*/ 1165 1166 if (tile.hasError() && getDisplaySettings().isShowErrors()) { 1167 myDrawString(g, tr("Error") + ": " + tr(tile.getErrorMessage()), x + 2, texty); 1168 //texty += 1 + fontHeight; 1169 } 1170 1171 if (Logging.isDebugEnabled()) { 1172 // draw tile outline in semi-transparent red 1173 g.setColor(new Color(255, 0, 0, 50)); 1174 g.draw(coordinateConverter.getTileShapeScreen(tile)); 1175 } 1176 } 1177 1178 private LatLon getShiftedLatLon(EastNorth en) { 1179 return coordinateConverter.getProjecting().eastNorth2latlonClamped(en); 1180 } 1181 1182 private ICoordinate getShiftedCoord(EastNorth en) { 1183 return CoordinateConversion.llToCoor(getShiftedLatLon(en)); 1184 } 1185 1186 private final TileSet nullTileSet = new TileSet(); 1187 1188 protected class TileSet extends TileRange { 1189 1190 private volatile TileSetInfo info; 1191 1192 protected TileSet(TileXY t1, TileXY t2, int zoom) { 1193 super(t1, t2, zoom); 1194 sanitize(); 1195 } 1196 1197 protected TileSet(TileRange range) { 1198 super(range); 1199 sanitize(); 1200 } 1201 1202 /** 1203 * null tile set 1204 */ 1205 private TileSet() { 1206 // default 1207 } 1208 1209 protected void sanitize() { 1210 minX = Utils.clamp(minX, tileSource.getTileXMin(zoom), tileSource.getTileXMax(zoom)); 1211 maxX = Utils.clamp(maxX, tileSource.getTileXMin(zoom), tileSource.getTileXMax(zoom)); 1212 minY = Utils.clamp(minY, tileSource.getTileYMin(zoom), tileSource.getTileYMax(zoom)); 1213 maxY = Utils.clamp(maxY, tileSource.getTileYMin(zoom), tileSource.getTileYMax(zoom)); 1214 } 1215 1216 private boolean tooSmall() { 1217 return this.tilesSpanned() < 2.1; 1218 } 1219 1220 private boolean tooLarge() { 1221 return insane() || this.tilesSpanned() > 20; 1222 } 1223 1224 private boolean insane() { 1225 return tileCache == null || size() > tileCache.getCacheSize(); 1226 } 1227 1228 /** 1229 * Get all tiles represented by this TileSet that are already in the tileCache. 1230 * @return all tiles represented by this TileSet that are already in the tileCache 1231 */ 1232 private List<Tile> allExistingTiles() { 1233 return allTiles(AbstractTileSourceLayer.this::getTile); 1234 } 1235 1236 private List<Tile> allTilesCreate() { 1237 return allTiles(AbstractTileSourceLayer.this::getOrCreateTile); 1238 } 1239 1240 private List<Tile> allTiles(Function<TilePosition, Tile> mapper) { 1241 return tilePositions().map(mapper).filter(Objects::nonNull).collect(Collectors.toList()); 1242 } 1243 1244 /** 1245 * Gets a stream of all tile positions in this set 1246 * @return A stream of all positions 1247 */ 1248 public Stream<TilePosition> tilePositions() { 1249 if (zoom == 0 || this.insane()) { 1250 return Stream.empty(); // Tileset is either empty or too large 1251 } else { 1252 return IntStream.rangeClosed(minX, maxX).mapToObj( 1253 x -> IntStream.rangeClosed(minY, maxY).mapToObj(y -> new TilePosition(x, y, zoom)) 1254 ).flatMap(Function.identity()); 1255 } 1256 } 1257 1258 private List<Tile> allLoadedTiles() { 1259 return allExistingTiles().stream().filter(Tile::isLoaded).collect(Collectors.toList()); 1260 } 1261 1262 /** 1263 * @return comparator, that sorts the tiles from the center to the edge of the current screen 1264 */ 1265 private Comparator<Tile> getTileDistanceComparator() { 1266 final int centerX = (int) Math.ceil((minX + maxX) / 2d); 1267 final int centerY = (int) Math.ceil((minY + maxY) / 2d); 1268 return Comparator.comparingInt(t -> Math.abs(t.getXtile() - centerX) + Math.abs(t.getYtile() - centerY)); 1269 } 1270 1271 private void loadAllTiles(boolean force) { 1272 if (!getDisplaySettings().isAutoLoad() && !force) 1273 return; 1274 List<Tile> allTiles = allTilesCreate(); 1275 allTiles.sort(getTileDistanceComparator()); 1276 for (Tile t : allTiles) { 1277 loadTile(t, force); 1278 } 1279 } 1280 1281 private void loadAllErrorTiles(boolean force) { 1282 if (!getDisplaySettings().isAutoLoad() && !force) 1283 return; 1284 for (Tile t : this.allTilesCreate()) { 1285 if (t.hasError()) { 1286 tileLoader.createTileLoaderJob(t).submit(force); 1287 } 1288 } 1289 } 1290 1291 /** 1292 * Call the given paint method for all tiles in this tile set.<p> 1293 * Uses a parallel stream. 1294 * @param visitor A visitor to call for each tile. 1295 * @param missed a consumer to call for each missed tile. 1296 */ 1297 public void visitTiles(Consumer<Tile> visitor, Consumer<TilePosition> missed) { 1298 tilePositions().parallel().forEach(tp -> visitTilePosition(visitor, tp, missed)); 1299 } 1300 1301 private void visitTilePosition(Consumer<Tile> visitor, TilePosition tp, Consumer<TilePosition> missed) { 1302 Tile tile = getTile(tp); 1303 if (tile == null) { 1304 missed.accept(tp); 1305 } else { 1306 visitor.accept(tile); 1307 } 1308 } 1309 1310 /** 1311 * Check if there is any tile fully loaded without error. 1312 * @return true if there is any tile fully loaded without error 1313 */ 1314 public boolean hasVisibleTiles() { 1315 return getTileSetInfo().hasVisibleTiles; 1316 } 1317 1318 /** 1319 * Check if there there is a tile that is overzoomed. 1320 * <p> 1321 * I.e. the server response for one tile was "there is no tile here". 1322 * This usually happens when zoomed in too much. The limit depends on 1323 * the region, so at the edge of such a region, some tiles may be 1324 * available and some not. 1325 * @return true if there there is a tile that is overzoomed 1326 */ 1327 public boolean hasOverzoomedTiles() { 1328 return getTileSetInfo().hasOverzoomedTiles; 1329 } 1330 1331 /** 1332 * Check if there are tiles still loading. 1333 * <p> 1334 * This is the case if there is a tile not yet in the cache, or in the 1335 * cache but marked as loading ({@link Tile#isLoading()}. 1336 * @return true if there are tiles still loading 1337 */ 1338 public boolean hasLoadingTiles() { 1339 return getTileSetInfo().hasLoadingTiles; 1340 } 1341 1342 /** 1343 * Check if all tiles in the range are fully loaded. 1344 * <p> 1345 * A tile is considered to be fully loaded even if the result of loading 1346 * the tile was an error. 1347 * @return true if all tiles in the range are fully loaded 1348 */ 1349 public boolean hasAllLoadedTiles() { 1350 return getTileSetInfo().hasAllLoadedTiles; 1351 } 1352 1353 private TileSetInfo getTileSetInfo() { 1354 if (info == null) { 1355 synchronized (this) { 1356 if (info == null) { 1357 List<Tile> allTiles = this.allExistingTiles(); 1358 info = new TileSetInfo(); 1359 info.hasLoadingTiles = allTiles.size() < this.size(); 1360 info.hasAllLoadedTiles = true; 1361 for (Tile t : allTiles) { 1362 if ("no-tile".equals(t.getValue("tile-info"))) { 1363 info.hasOverzoomedTiles = true; 1364 } 1365 if (t.isLoaded()) { 1366 if (!t.hasError()) { 1367 info.hasVisibleTiles = true; 1368 } 1369 } else { 1370 info.hasAllLoadedTiles = false; 1371 if (t.isLoading()) { 1372 info.hasLoadingTiles = true; 1373 } 1374 } 1375 } 1376 } 1377 } 1378 } 1379 return info; 1380 } 1381 1382 @Override 1383 public String toString() { 1384 return getClass().getName() + ": zoom: " + zoom + " X(" + minX + ", " + maxX + ") Y(" + minY + ", " + maxY + ") size: " + size(); 1385 } 1386 } 1387 1388 /** 1389 * Data container to hold information about a {@code TileSet} class. 1390 */ 1391 private static class TileSetInfo { 1392 boolean hasVisibleTiles; 1393 boolean hasOverzoomedTiles; 1394 boolean hasLoadingTiles; 1395 boolean hasAllLoadedTiles; 1396 } 1397 1398 /** 1399 * Create a TileSet by EastNorth bbox taking a layer shift in account 1400 * @param bounds the EastNorth bounds 1401 * @param zoom zoom level 1402 * @return the tile set 1403 */ 1404 protected TileSet getTileSet(ProjectionBounds bounds, int zoom) { 1405 if (zoom == 0) 1406 return new TileSet(); 1407 TileXY t1, t2; 1408 IProjected topLeftUnshifted = coordinateConverter.shiftDisplayToServer(bounds.getMin()); 1409 IProjected botRightUnshifted = coordinateConverter.shiftDisplayToServer(bounds.getMax()); 1410 if (coordinateConverter.requiresReprojection()) { 1411 Projection projServer = Projections.getProjectionByCode(tileSource.getServerCRS()); 1412 if (projServer == null) { 1413 throw new IllegalStateException(tileSource.toString()); 1414 } 1415 ProjectionBounds projBounds = new ProjectionBounds( 1416 CoordinateConversion.projToEn(topLeftUnshifted), 1417 CoordinateConversion.projToEn(botRightUnshifted)); 1418 ProjectionBounds bbox = projServer.getEastNorthBoundsBox(projBounds, Main.getProjection()); 1419 t1 = tileSource.projectedToTileXY(CoordinateConversion.enToProj(bbox.getMin()), zoom); 1420 t2 = tileSource.projectedToTileXY(CoordinateConversion.enToProj(bbox.getMax()), zoom); 1421 } else { 1422 t1 = tileSource.projectedToTileXY(topLeftUnshifted, zoom); 1423 t2 = tileSource.projectedToTileXY(botRightUnshifted, zoom); 1424 } 1425 return new TileSet(t1, t2, zoom); 1426 } 1427 1428 private class DeepTileSet { 1429 private final ProjectionBounds bounds; 1430 private final int minZoom, maxZoom; 1431 private final TileSet[] tileSets; 1432 1433 @SuppressWarnings("unchecked") 1434 DeepTileSet(ProjectionBounds bounds, int minZoom, int maxZoom) { 1435 this.bounds = bounds; 1436 this.minZoom = minZoom; 1437 this.maxZoom = maxZoom; 1438 if (minZoom > maxZoom) { 1439 throw new IllegalArgumentException(minZoom + " > " + maxZoom); 1440 } 1441 this.tileSets = new AbstractTileSourceLayer.TileSet[maxZoom - minZoom + 1]; 1442 } 1443 1444 public TileSet getTileSet(int zoom) { 1445 if (zoom < minZoom) 1446 return nullTileSet; 1447 synchronized (tileSets) { 1448 TileSet ts = tileSets[zoom-minZoom]; 1449 if (ts == null) { 1450 ts = AbstractTileSourceLayer.this.getTileSet(bounds, zoom); 1451 tileSets[zoom-minZoom] = ts; 1452 } 1453 return ts; 1454 } 1455 } 1456 } 1457 1458 @Override 1459 public void paint(Graphics2D g, MapView mv, Bounds bounds) { 1460 // old and unused. 1461 } 1462 1463 private void drawInViewArea(Graphics2D g, MapView mv, ProjectionBounds pb) { 1464 int zoom = currentZoomLevel; 1465 if (getDisplaySettings().isAutoZoom()) { 1466 zoom = getBestZoom(); 1467 } 1468 1469 DeepTileSet dts = new DeepTileSet(pb, getMinZoomLvl(), zoom); 1470 1471 int displayZoomLevel = zoom; 1472 1473 boolean noTilesAtZoom = false; 1474 if (getDisplaySettings().isAutoZoom() && getDisplaySettings().isAutoLoad()) { 1475 // Auto-detection of tilesource maxzoom (currently fully works only for Bing) 1476 TileSet ts0 = dts.getTileSet(zoom); 1477 if (!ts0.hasVisibleTiles() && (!ts0.hasLoadingTiles() || ts0.hasOverzoomedTiles())) { 1478 noTilesAtZoom = true; 1479 } 1480 // Find highest zoom level with at least one visible tile 1481 for (int tmpZoom = zoom; tmpZoom > dts.minZoom; tmpZoom--) { 1482 if (dts.getTileSet(tmpZoom).hasVisibleTiles()) { 1483 displayZoomLevel = tmpZoom; 1484 break; 1485 } 1486 } 1487 // Do binary search between currentZoomLevel and displayZoomLevel 1488 while (zoom > displayZoomLevel && !ts0.hasVisibleTiles() && ts0.hasOverzoomedTiles()) { 1489 zoom = (zoom + displayZoomLevel)/2; 1490 ts0 = dts.getTileSet(zoom); 1491 } 1492 1493 setZoomLevel(zoom, false); 1494 1495 // If all tiles at displayZoomLevel is loaded, load all tiles at next zoom level 1496 // to make sure there're really no more zoom levels 1497 // loading is done in the next if section 1498 if (zoom == displayZoomLevel && !ts0.hasLoadingTiles() && zoom < dts.maxZoom) { 1499 zoom++; 1500 ts0 = dts.getTileSet(zoom); 1501 } 1502 // When we have overzoomed tiles and all tiles at current zoomlevel is loaded, 1503 // load tiles at previovus zoomlevels until we have all tiles on screen is loaded. 1504 // loading is done in the next if section 1505 while (zoom > dts.minZoom && ts0.hasOverzoomedTiles() && !ts0.hasLoadingTiles()) { 1506 zoom--; 1507 ts0 = dts.getTileSet(zoom); 1508 } 1509 } else if (getDisplaySettings().isAutoZoom()) { 1510 setZoomLevel(zoom, false); 1511 } 1512 TileSet ts = dts.getTileSet(zoom); 1513 1514 // Too many tiles... refuse to download 1515 if (!ts.tooLarge()) { 1516 // try to load tiles from desired zoom level, no matter what we will show (for example, tiles from previous zoom level 1517 // on zoom in) 1518 ts.loadAllTiles(false); 1519 } 1520 1521 if (displayZoomLevel != zoom) { 1522 ts = dts.getTileSet(displayZoomLevel); 1523 if (!dts.getTileSet(displayZoomLevel).hasAllLoadedTiles() && displayZoomLevel < zoom) { 1524 // if we are showing tiles from lower zoom level, ensure that all tiles are loaded as they are few, 1525 // and should not trash the tile cache 1526 // This is especially needed when dts.getTileSet(zoom).tooLarge() is true and we are not loading tiles 1527 ts.loadAllTiles(false); 1528 } 1529 } 1530 1531 g.setColor(Color.DARK_GRAY); 1532 1533 List<Tile> missedTiles = this.paintTileImages(g, ts); 1534 int[] otherZooms = {1, 2, -1, -2, -3, -4, -5}; 1535 for (int zoomOffset : otherZooms) { 1536 if (!getDisplaySettings().isAutoZoom()) { 1537 break; 1538 } 1539 int newzoom = displayZoomLevel + zoomOffset; 1540 if (newzoom < getMinZoomLvl() || newzoom > getMaxZoomLvl()) { 1541 continue; 1542 } 1543 if (missedTiles.isEmpty()) { 1544 break; 1545 } 1546 List<Tile> newlyMissedTiles = new LinkedList<>(); 1547 for (Tile missed : missedTiles) { 1548 if (zoomOffset > 0 && "no-tile".equals(missed.getValue("tile-info"))) { 1549 // Don't try to paint from higher zoom levels when tile is overzoomed 1550 newlyMissedTiles.add(missed); 1551 continue; 1552 } 1553 TileSet ts2 = new TileSet(tileSource.getCoveringTileRange(missed, newzoom)); 1554 // Instantiating large TileSets is expensive. If there are no loaded tiles, don't bother even trying. 1555 if (ts2.allLoadedTiles().isEmpty()) { 1556 newlyMissedTiles.add(missed); 1557 continue; 1558 } 1559 if (ts2.tooLarge()) { 1560 continue; 1561 } 1562 newlyMissedTiles.addAll(this.paintTileImages(g, ts2, newzoom, missed)); 1563 } 1564 missedTiles = newlyMissedTiles; 1565 } 1566 if (Logging.isDebugEnabled() && !missedTiles.isEmpty()) { 1567 Logging.debug("still missed {0} in the end", missedTiles.size()); 1568 } 1569 g.setColor(Color.red); 1570 g.setFont(InfoFont); 1571 1572 // The current zoom tileset should have all of its tiles due to the loadAllTiles(), unless it to tooLarge() 1573 for (Tile t : ts.allExistingTiles()) { 1574 this.paintTileText(t, g); 1575 } 1576 1577 EastNorth min = pb.getMin(); 1578 EastNorth max = pb.getMax(); 1579 attribution.paintAttribution(g, mv.getWidth(), mv.getHeight(), getShiftedCoord(min), getShiftedCoord(max), 1580 displayZoomLevel, this); 1581 1582 g.setColor(Color.lightGray); 1583 1584 if (ts.insane()) { 1585 myDrawString(g, tr("zoom in to load any tiles"), 120, 120); 1586 } else if (ts.tooLarge()) { 1587 myDrawString(g, tr("zoom in to load more tiles"), 120, 120); 1588 } else if (!getDisplaySettings().isAutoZoom() && ts.tooSmall()) { 1589 myDrawString(g, tr("increase tiles zoom level (change resolution) to see more detail"), 120, 120); 1590 } 1591 if (noTilesAtZoom) { 1592 myDrawString(g, tr("No tiles at this zoom level"), 120, 120); 1593 } 1594 if (Logging.isDebugEnabled()) { 1595 myDrawString(g, tr("Current zoom: {0}", currentZoomLevel), 50, 140); 1596 myDrawString(g, tr("Display zoom: {0}", displayZoomLevel), 50, 155); 1597 myDrawString(g, tr("Pixel scale: {0}", getScaleFactor(currentZoomLevel)), 50, 170); 1598 myDrawString(g, tr("Best zoom: {0}", getBestZoom()), 50, 185); 1599 myDrawString(g, tr("Estimated cache size: {0}", estimateTileCacheSize()), 50, 200); 1600 if (tileLoader instanceof TMSCachedTileLoader) { 1601 int offset = 200; 1602 for (String part: ((TMSCachedTileLoader) tileLoader).getStats().split("\n")) { 1603 offset += 15; 1604 myDrawString(g, tr("Cache stats: {0}", part), 50, offset); 1605 } 1606 } 1607 } 1608 } 1609 1610 /** 1611 * Returns tile for a pixel position.<p> 1612 * This isn't very efficient, but it is only used when the user right-clicks on the map. 1613 * @param px pixel X coordinate 1614 * @param py pixel Y coordinate 1615 * @return Tile at pixel position 1616 */ 1617 private Tile getTileForPixelpos(int px, int py) { 1618 Logging.debug("getTileForPixelpos({0}, {1})", px, py); 1619 TileXY xy = coordinateConverter.getTileforPixel(px, py, currentZoomLevel); 1620 return getTile(xy.getXIndex(), xy.getYIndex(), currentZoomLevel); 1621 } 1622 1623 /** 1624 * Class to store a menu action and the class it belongs to. 1625 */ 1626 private static class MenuAddition { 1627 final Action addition; 1628 @SuppressWarnings("rawtypes") 1629 final Class<? extends AbstractTileSourceLayer> clazz; 1630 1631 @SuppressWarnings("rawtypes") 1632 MenuAddition(Action addition, Class<? extends AbstractTileSourceLayer> clazz) { 1633 this.addition = addition; 1634 this.clazz = clazz; 1635 } 1636 } 1637 1638 /** 1639 * Register an additional layer context menu entry. 1640 * 1641 * @param addition additional menu action 1642 * @since 11197 1643 */ 1644 public static void registerMenuAddition(Action addition) { 1645 menuAdditions.add(new MenuAddition(addition, AbstractTileSourceLayer.class)); 1646 } 1647 1648 /** 1649 * Register an additional layer context menu entry for a imagery layer 1650 * class. The menu entry is valid for the specified class and subclasses 1651 * thereof only. 1652 * <p> 1653 * Example: 1654 * <pre> 1655 * TMSLayer.registerMenuAddition(new TMSSpecificAction(), TMSLayer.class); 1656 * </pre> 1657 * 1658 * @param addition additional menu action 1659 * @param clazz class the menu action is registered for 1660 * @since 11197 1661 */ 1662 public static void registerMenuAddition(Action addition, 1663 Class<? extends AbstractTileSourceLayer<?>> clazz) { 1664 menuAdditions.add(new MenuAddition(addition, clazz)); 1665 } 1666 1667 /** 1668 * Prepare list of additional layer context menu entries. The list is 1669 * empty if there are no additional menu entries. 1670 * 1671 * @return list of additional layer context menu entries 1672 */ 1673 private List<Action> getMenuAdditions() { 1674 final LinkedList<Action> menuAdds = new LinkedList<>(); 1675 for (MenuAddition menuAdd: menuAdditions) { 1676 if (menuAdd.clazz.isInstance(this)) { 1677 menuAdds.add(menuAdd.addition); 1678 } 1679 } 1680 if (!menuAdds.isEmpty()) { 1681 menuAdds.addFirst(SeparatorLayerAction.INSTANCE); 1682 } 1683 return menuAdds; 1684 } 1685 1686 @Override 1687 public Action[] getMenuEntries() { 1688 ArrayList<Action> actions = new ArrayList<>(); 1689 actions.addAll(Arrays.asList(getLayerListEntries())); 1690 actions.addAll(Arrays.asList(getCommonEntries())); 1691 actions.addAll(getMenuAdditions()); 1692 actions.add(SeparatorLayerAction.INSTANCE); 1693 actions.add(new LayerListPopup.InfoAction(this)); 1694 return actions.toArray(new Action[0]); 1695 } 1696 1697 /** 1698 * Returns the contextual menu entries in layer list dialog. 1699 * @return the contextual menu entries in layer list dialog 1700 */ 1701 public Action[] getLayerListEntries() { 1702 return new Action[] { 1703 LayerListDialog.getInstance().createActivateLayerAction(this), 1704 LayerListDialog.getInstance().createShowHideLayerAction(), 1705 LayerListDialog.getInstance().createDeleteLayerAction(), 1706 SeparatorLayerAction.INSTANCE, 1707 // color, 1708 new OffsetAction(), 1709 new RenameLayerAction(this.getAssociatedFile(), this), 1710 SeparatorLayerAction.INSTANCE 1711 }; 1712 } 1713 1714 /** 1715 * Returns the common menu entries. 1716 * @return the common menu entries 1717 */ 1718 public Action[] getCommonEntries() { 1719 return new Action[] { 1720 new AutoLoadTilesAction(this), 1721 new AutoZoomAction(this), 1722 new ShowErrorsAction(this), 1723 new IncreaseZoomAction(this), 1724 new DecreaseZoomAction(this), 1725 new ZoomToBestAction(this), 1726 new ZoomToNativeLevelAction(this), 1727 new FlushTileCacheAction(this), 1728 new LoadErroneousTilesAction(this), 1729 new LoadAllTilesAction(this) 1730 }; 1731 } 1732 1733 @Override 1734 public String getToolTipText() { 1735 if (getDisplaySettings().isAutoLoad()) { 1736 return tr("{0} ({1}), automatically downloading in zoom {2}", this.getClass().getSimpleName(), getName(), currentZoomLevel); 1737 } else { 1738 return tr("{0} ({1}), downloading in zoom {2}", this.getClass().getSimpleName(), getName(), currentZoomLevel); 1739 } 1740 } 1741 1742 @Override 1743 public void visitBoundingBox(BoundingXYVisitor v) { 1744 } 1745 1746 /** 1747 * Task responsible for precaching imagery along the gpx track 1748 * 1749 */ 1750 public class PrecacheTask implements TileLoaderListener { 1751 private final ProgressMonitor progressMonitor; 1752 private int totalCount; 1753 private final AtomicInteger processedCount = new AtomicInteger(0); 1754 private final TileLoader tileLoader; 1755 1756 /** 1757 * @param progressMonitor that will be notified about progess of the task 1758 */ 1759 public PrecacheTask(ProgressMonitor progressMonitor) { 1760 this.progressMonitor = progressMonitor; 1761 this.tileLoader = getTileLoaderFactory().makeTileLoader(this, getHeaders(tileSource)); 1762 if (this.tileLoader instanceof TMSCachedTileLoader) { 1763 ((TMSCachedTileLoader) this.tileLoader).setDownloadExecutor( 1764 TMSCachedTileLoader.getNewThreadPoolExecutor("Precache downloader")); 1765 } 1766 } 1767 1768 /** 1769 * @return true, if all is done 1770 */ 1771 public boolean isFinished() { 1772 return processedCount.get() >= totalCount; 1773 } 1774 1775 /** 1776 * @return total number of tiles to download 1777 */ 1778 public int getTotalCount() { 1779 return totalCount; 1780 } 1781 1782 /** 1783 * cancel the task 1784 */ 1785 public void cancel() { 1786 if (tileLoader instanceof TMSCachedTileLoader) { 1787 ((TMSCachedTileLoader) tileLoader).cancelOutstandingTasks(); 1788 } 1789 } 1790 1791 @Override 1792 public void tileLoadingFinished(Tile tile, boolean success) { 1793 int processed = this.processedCount.incrementAndGet(); 1794 if (success) { 1795 this.progressMonitor.worked(1); 1796 this.progressMonitor.setCustomText(tr("Downloaded {0}/{1} tiles", processed, totalCount)); 1797 } else { 1798 Logging.warn("Tile loading failure: " + tile + " - " + tile.getErrorMessage()); 1799 } 1800 } 1801 1802 /** 1803 * @return tile loader that is used to load the tiles 1804 */ 1805 public TileLoader getTileLoader() { 1806 return tileLoader; 1807 } 1808 } 1809 1810 /** 1811 * Calculates tiles, that needs to be downloaded to cache, gets a current tile loader and creates a task to download 1812 * all of the tiles. Buffer contains at least one tile. 1813 * 1814 * To prevent accidental clear of the queue, new download executor is created with separate queue 1815 * 1816 * @param progressMonitor progress monitor for download task 1817 * @param points lat/lon coordinates to download 1818 * @param bufferX how many units in current Coordinate Reference System to cover in X axis in both sides 1819 * @param bufferY how many units in current Coordinate Reference System to cover in Y axis in both sides 1820 * @return precache task representing download task 1821 */ 1822 public AbstractTileSourceLayer<T>.PrecacheTask downloadAreaToCache(final ProgressMonitor progressMonitor, List<LatLon> points, 1823 double bufferX, double bufferY) { 1824 PrecacheTask precacheTask = new PrecacheTask(progressMonitor); 1825 final Set<Tile> requestedTiles = new ConcurrentSkipListSet<>( 1826 (o1, o2) -> String.CASE_INSENSITIVE_ORDER.compare(o1.getKey(), o2.getKey())); 1827 for (LatLon point: points) { 1828 TileXY minTile = tileSource.latLonToTileXY(point.lat() - bufferY, point.lon() - bufferX, currentZoomLevel); 1829 TileXY curTile = tileSource.latLonToTileXY(CoordinateConversion.llToCoor(point), currentZoomLevel); 1830 TileXY maxTile = tileSource.latLonToTileXY(point.lat() + bufferY, point.lon() + bufferX, currentZoomLevel); 1831 1832 // take at least one tile of buffer 1833 int minY = Math.min(curTile.getYIndex() - 1, minTile.getYIndex()); 1834 int maxY = Math.max(curTile.getYIndex() + 1, maxTile.getYIndex()); 1835 int minX = Math.min(curTile.getXIndex() - 1, minTile.getXIndex()); 1836 int maxX = Math.max(curTile.getXIndex() + 1, maxTile.getXIndex()); 1837 1838 for (int x = minX; x <= maxX; x++) { 1839 for (int y = minY; y <= maxY; y++) { 1840 requestedTiles.add(new Tile(tileSource, x, y, currentZoomLevel)); 1841 } 1842 } 1843 } 1844 1845 precacheTask.totalCount = requestedTiles.size(); 1846 precacheTask.progressMonitor.setTicksCount(requestedTiles.size()); 1847 1848 TileLoader loader = precacheTask.getTileLoader(); 1849 for (Tile t: requestedTiles) { 1850 loader.createTileLoaderJob(t).submit(); 1851 } 1852 return precacheTask; 1853 } 1854 1855 @Override 1856 public boolean isSavable() { 1857 return true; // With WMSLayerExporter 1858 } 1859 1860 @Override 1861 public File createAndOpenSaveFileChooser() { 1862 return SaveActionBase.createAndOpenSaveFileChooser(tr("Save WMS file"), WMSLayerImporter.FILE_FILTER); 1863 } 1864 1865 @Override 1866 public synchronized void destroy() { 1867 super.destroy(); 1868 adjustAction.destroy(); 1869 } 1870 1871 private class TileSourcePainter extends CompatibilityModeLayerPainter { 1872 /** The memory handle that will hold our tile source. */ 1873 private MemoryHandle<?> memory; 1874 1875 @Override 1876 public void paint(MapViewGraphics graphics) { 1877 allocateCacheMemory(); 1878 if (memory != null) { 1879 doPaint(graphics); 1880 } 1881 } 1882 1883 private void doPaint(MapViewGraphics graphics) { 1884 try { 1885 drawInViewArea(graphics.getDefaultGraphics(), graphics.getMapView(), graphics.getClipBounds().getProjectionBounds()); 1886 } catch (IllegalArgumentException | IllegalStateException e) { 1887 throw BugReport.intercept(e) 1888 .put("graphics", graphics).put("tileSource", tileSource).put("currentZoomLevel", currentZoomLevel); 1889 } 1890 } 1891 1892 private void allocateCacheMemory() { 1893 if (memory == null) { 1894 MemoryManager manager = MemoryManager.getInstance(); 1895 if (manager.isAvailable(getEstimatedCacheSize())) { 1896 try { 1897 memory = manager.allocateMemory("tile source layer", getEstimatedCacheSize(), Object::new); 1898 } catch (NotEnoughMemoryException e) { 1899 Logging.warn("Could not allocate tile source memory", e); 1900 } 1901 } 1902 } 1903 } 1904 1905 protected long getEstimatedCacheSize() { 1906 return 4L * tileSource.getTileSize() * tileSource.getTileSize() * estimateTileCacheSize(); 1907 } 1908 1909 @Override 1910 public void detachFromMapView(MapViewEvent event) { 1911 event.getMapView().removeMouseListener(adapter); 1912 MapView.removeZoomChangeListener(AbstractTileSourceLayer.this); 1913 super.detachFromMapView(event); 1914 if (memory != null) { 1915 memory.free(); 1916 } 1917 } 1918 } 1919 1920 @Override 1921 public void projectionChanged(Projection oldValue, Projection newValue) { 1922 super.projectionChanged(oldValue, newValue); 1923 displaySettings.setOffsetBookmark(displaySettings.getOffsetBookmark()); 1924 if (tileCache != null) { 1925 tileCache.clear(); 1926 } 1927 } 1928 1929 @Override 1930 protected List<OffsetMenuEntry> getOffsetMenuEntries() { 1931 return OffsetBookmark.getBookmarks() 1932 .stream() 1933 .filter(b -> b.isUsable(this)) 1934 .map(OffsetMenuBookmarkEntry::new) 1935 .collect(Collectors.toList()); 1936 } 1937 1938 /** 1939 * An entry for a bookmark in the offset menu. 1940 * @author Michael Zangl 1941 */ 1942 private class OffsetMenuBookmarkEntry implements OffsetMenuEntry { 1943 private final OffsetBookmark bookmark; 1944 1945 OffsetMenuBookmarkEntry(OffsetBookmark bookmark) { 1946 this.bookmark = bookmark; 1947 1948 } 1949 1950 @Override 1951 public String getLabel() { 1952 return bookmark.getName(); 1953 } 1954 1955 @Override 1956 public boolean isActive() { 1957 EastNorth offset = bookmark.getDisplacement(Main.getProjection()); 1958 EastNorth active = getDisplaySettings().getDisplacement(); 1959 return Utils.equalsEpsilon(offset.east(), active.east()) && Utils.equalsEpsilon(offset.north(), active.north()); 1960 } 1961 1962 @Override 1963 public void actionPerformed() { 1964 getDisplaySettings().setOffsetBookmark(bookmark); 1965 } 1966 } 1967}