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