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