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