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