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