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