source: josm/trunk/src/org/openstreetmap/josm/gui/layer/AbstractTileSourceLayer.java@ 9004

Last change on this file since 9004 was 9004, checked in by wiktorn, 8 years ago

Fix high CPU usage when showing small tiles.

  • AbstractTileSourceLayer - check insane() zoom level based on TileCache size instead of hardcoded value
  • TileCache - add interface to check size of the cache
  • TMSCachedTileLoader - remove unnnecessary TileCache interface and its methods
  • HostLimitQueue - correct log message
  • JCSCachedTileLoaderJob - add debug logs for putting request into queue and start of processing
  • Property svn:eol-style set to native
File size: 63.0 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.layer;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.Color;
7import java.awt.Component;
8import java.awt.Font;
9import java.awt.Graphics;
10import java.awt.Graphics2D;
11import java.awt.GridBagLayout;
12import java.awt.Image;
13import java.awt.Point;
14import java.awt.Rectangle;
15import java.awt.Toolkit;
16import java.awt.event.ActionEvent;
17import java.awt.event.MouseAdapter;
18import java.awt.event.MouseEvent;
19import java.awt.image.BufferedImage;
20import java.awt.image.ImageObserver;
21import java.io.File;
22import java.io.IOException;
23import java.lang.reflect.Field;
24import java.net.MalformedURLException;
25import java.net.URL;
26import java.text.SimpleDateFormat;
27import java.util.ArrayList;
28import java.util.Collections;
29import java.util.Comparator;
30import java.util.Date;
31import java.util.LinkedList;
32import java.util.List;
33import java.util.Map;
34import java.util.Map.Entry;
35import java.util.Set;
36import java.util.concurrent.ConcurrentSkipListSet;
37import java.util.concurrent.atomic.AtomicInteger;
38
39import javax.swing.AbstractAction;
40import javax.swing.Action;
41import javax.swing.BorderFactory;
42import javax.swing.DefaultButtonModel;
43import javax.swing.JCheckBoxMenuItem;
44import javax.swing.JLabel;
45import javax.swing.JMenuItem;
46import javax.swing.JOptionPane;
47import javax.swing.JPanel;
48import javax.swing.JPopupMenu;
49import javax.swing.JTextField;
50
51import org.openstreetmap.gui.jmapviewer.AttributionSupport;
52import org.openstreetmap.gui.jmapviewer.MemoryTileCache;
53import org.openstreetmap.gui.jmapviewer.OsmTileLoader;
54import org.openstreetmap.gui.jmapviewer.Tile;
55import org.openstreetmap.gui.jmapviewer.TileXY;
56import org.openstreetmap.gui.jmapviewer.interfaces.CachedTileLoader;
57import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
58import org.openstreetmap.gui.jmapviewer.interfaces.TemplatedTileSource;
59import org.openstreetmap.gui.jmapviewer.interfaces.TileCache;
60import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
61import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
62import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
63import org.openstreetmap.gui.jmapviewer.tilesources.AbstractTMSTileSource;
64import org.openstreetmap.josm.Main;
65import org.openstreetmap.josm.actions.RenameLayerAction;
66import org.openstreetmap.josm.actions.SaveActionBase;
67import org.openstreetmap.josm.data.Bounds;
68import org.openstreetmap.josm.data.coor.EastNorth;
69import org.openstreetmap.josm.data.coor.LatLon;
70import org.openstreetmap.josm.data.imagery.ImageryInfo;
71import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader;
72import org.openstreetmap.josm.data.imagery.TileLoaderFactory;
73import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
74import org.openstreetmap.josm.data.preferences.BooleanProperty;
75import org.openstreetmap.josm.data.preferences.IntegerProperty;
76import org.openstreetmap.josm.gui.ExtendedDialog;
77import org.openstreetmap.josm.gui.MapFrame;
78import org.openstreetmap.josm.gui.MapView;
79import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
80import org.openstreetmap.josm.gui.NavigatableComponent.ZoomChangeListener;
81import org.openstreetmap.josm.gui.PleaseWaitRunnable;
82import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
83import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
84import org.openstreetmap.josm.gui.progress.ProgressMonitor;
85import org.openstreetmap.josm.io.WMSLayerImporter;
86import org.openstreetmap.josm.tools.GBC;
87
88/**
89 * Base abstract class that supports displaying images provided by TileSource. It might be TMS source, WMS or WMTS
90 *
91 * It implements all standard functions of tilesource based layers: autozoom, tile reloads, layer saving, loading,etc.
92 *
93 * @author Upliner
94 * @author Wiktor Niesiobędzki
95 * @since 3715
96 * @since 8526 (copied from TMSLayer)
97 */
98public abstract class AbstractTileSourceLayer extends ImageryLayer implements ImageObserver, TileLoaderListener, ZoomChangeListener {
99 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 * Creates popup menu items and binds to mouse actions
501 */
502 @Override
503 public void hookUpMapView() {
504 // this needs to be here and not in constructor to allow empty TileSource class construction
505 // using SessionWriter
506 this.tileSource = getTileSource(info);
507 if (this.tileSource == null) {
508 throw new IllegalArgumentException(tr("Failed to create tile source"));
509 }
510
511 super.hookUpMapView();
512 projectionChanged(null, Main.getProjection()); // check if projection is supported
513 initTileSource(this.tileSource);
514
515 // keep them final here, so we avoid namespace clutter in the class
516 final JPopupMenu tileOptionMenu = new JPopupMenu();
517 final TileHolder clickedTileHolder = new TileHolder();
518 Field autoZoomField;
519 Field autoLoadField;
520 Field showErrorsField;
521 try {
522 autoZoomField = AbstractTileSourceLayer.class.getField("autoZoom");
523 autoLoadField = AbstractTileSourceLayer.class.getDeclaredField("autoLoad");
524 showErrorsField = AbstractTileSourceLayer.class.getDeclaredField("showErrors");
525 } catch (NoSuchFieldException | SecurityException e) {
526 // shoud not happen
527 throw new RuntimeException(e);
528 }
529
530 autoZoom = PROP_DEFAULT_AUTOZOOM.get();
531 JCheckBoxMenuItem autoZoomPopup = new JCheckBoxMenuItem();
532 autoZoomPopup.setModel(new BooleanButtonModel(autoZoomField));
533 autoZoomPopup.setAction(new AutoZoomAction());
534 tileOptionMenu.add(autoZoomPopup);
535
536 autoLoad = PROP_DEFAULT_AUTOLOAD.get();
537 JCheckBoxMenuItem autoLoadPopup = new JCheckBoxMenuItem();
538 autoLoadPopup.setAction(new AutoLoadTilesAction());
539 autoLoadPopup.setModel(new BooleanButtonModel(autoLoadField));
540 tileOptionMenu.add(autoLoadPopup);
541
542 showErrors = PROP_DEFAULT_SHOWERRORS.get();
543 JCheckBoxMenuItem showErrorsPopup = new JCheckBoxMenuItem();
544 showErrorsPopup.setAction(new AbstractAction(tr("Show Errors")) {
545 @Override
546 public void actionPerformed(ActionEvent ae) {
547 showErrors = !showErrors;
548 }
549 });
550 showErrorsPopup.setModel(new BooleanButtonModel(showErrorsField));
551 tileOptionMenu.add(showErrorsPopup);
552
553 tileOptionMenu.add(new JMenuItem(new AbstractAction(tr("Load Tile")) {
554 @Override
555 public void actionPerformed(ActionEvent ae) {
556 Tile clickedTile = clickedTileHolder.getTile();
557 if (clickedTile != null) {
558 loadTile(clickedTile, true);
559 redraw();
560 }
561 }
562 }));
563
564 tileOptionMenu.add(new JMenuItem(new ShowTileInfoAction(clickedTileHolder)));
565
566 tileOptionMenu.add(new JMenuItem(new LoadAllTilesAction()));
567 tileOptionMenu.add(new JMenuItem(new LoadErroneusTilesAction()));
568
569 // increase and decrease commands
570 tileOptionMenu.add(new JMenuItem(new AbstractAction(
571 tr("Increase zoom")) {
572 @Override
573 public void actionPerformed(ActionEvent ae) {
574 increaseZoomLevel();
575 redraw();
576 }
577 }));
578
579 tileOptionMenu.add(new JMenuItem(new AbstractAction(
580 tr("Decrease zoom")) {
581 @Override
582 public void actionPerformed(ActionEvent ae) {
583 decreaseZoomLevel();
584 redraw();
585 }
586 }));
587
588 tileOptionMenu.add(new JMenuItem(new AbstractAction(
589 tr("Snap to tile size")) {
590 @Override
591 public void actionPerformed(ActionEvent ae) {
592 double newFactor = Math.sqrt(getScaleFactor(currentZoomLevel));
593 Main.map.mapView.zoomToFactor(newFactor);
594 redraw();
595 }
596 }));
597
598 tileOptionMenu.add(new JMenuItem(new AbstractAction(
599 tr("Flush Tile Cache")) {
600 @Override
601 public void actionPerformed(ActionEvent ae) {
602 new PleaseWaitRunnable(tr("Flush Tile Cache")) {
603 @Override
604 protected void realRun() {
605 clearTileCache(getProgressMonitor());
606 }
607
608 @Override
609 protected void finish() {
610 // empty - flush is instaneus
611 }
612
613 @Override
614 protected void cancel() {
615 // empty - flush is instaneus
616 }
617 }.run();
618 }
619 }));
620
621 final MouseAdapter adapter = new MouseAdapter() {
622 @Override
623 public void mouseClicked(MouseEvent e) {
624 if (!isVisible()) return;
625 if (e.getButton() == MouseEvent.BUTTON3) {
626 clickedTileHolder.setTile(getTileForPixelpos(e.getX(), e.getY()));
627 tileOptionMenu.show(e.getComponent(), e.getX(), e.getY());
628 } else if (e.getButton() == MouseEvent.BUTTON1) {
629 attribution.handleAttribution(e.getPoint(), true);
630 }
631 }
632 };
633 Main.map.mapView.addMouseListener(adapter);
634
635 MapView.addLayerChangeListener(new LayerChangeListener() {
636 @Override
637 public void activeLayerChange(Layer oldLayer, Layer newLayer) {
638 //
639 }
640
641 @Override
642 public void layerAdded(Layer newLayer) {
643 //
644 }
645
646 @Override
647 public void layerRemoved(Layer oldLayer) {
648 if (oldLayer == AbstractTileSourceLayer.this) {
649 Main.map.mapView.removeMouseListener(adapter);
650 MapView.removeLayerChangeListener(this);
651 MapView.removeZoomChangeListener(AbstractTileSourceLayer.this);
652 }
653 }
654 });
655
656 // FIXME: why do we need this? Without this, if you add a WMS layer and do not move the mouse, sometimes, tiles do not
657 // start loading.
658 Main.map.repaint(500);
659 }
660
661 @Override
662 protected long estimateMemoryUsage() {
663 return 4L * tileSource.getTileSize() * tileSource.getTileSize() * estimateTileCacheSize();
664 }
665
666 protected int estimateTileCacheSize() {
667 int height = (int) Toolkit.getDefaultToolkit().getScreenSize().getHeight();
668 int width = (int) Toolkit.getDefaultToolkit().getScreenSize().getWidth();
669 int tileSize = 256; // default tile size
670 if (tileSource != null) {
671 tileSize = tileSource.getTileSize();
672 }
673 // as we can see part of the tile at the top and at the bottom, use Math.ceil(...) + 1 to accommodate for that
674 int visibileTiles = (int) (Math.ceil((double) height / tileSize + 1) * Math.ceil((double) width / tileSize + 1));
675 // add 10% for tiles from different zoom levels
676 return (int) Math.ceil(
677 Math.pow(2d, ZOOM_OFFSET.get()) * visibileTiles // use offset to decide, how many tiles are visible
678 * 2);
679 }
680 /**
681 * Checks zoom level against settings
682 * @param maxZoomLvl zoom level to check
683 * @param ts tile source to crosscheck with
684 * @return maximum zoom level, not higher than supported by tilesource nor set by the user
685 */
686 public static int checkMaxZoomLvl(int maxZoomLvl, TileSource ts) {
687 if (maxZoomLvl > MAX_ZOOM) {
688 maxZoomLvl = MAX_ZOOM;
689 }
690 if (maxZoomLvl < PROP_MIN_ZOOM_LVL.get()) {
691 maxZoomLvl = PROP_MIN_ZOOM_LVL.get();
692 }
693 if (ts != null && ts.getMaxZoom() != 0 && ts.getMaxZoom() < maxZoomLvl) {
694 maxZoomLvl = ts.getMaxZoom();
695 }
696 return maxZoomLvl;
697 }
698
699 /**
700 * Checks zoom level against settings
701 * @param minZoomLvl zoom level to check
702 * @param ts tile source to crosscheck with
703 * @return minimum zoom level, not higher than supported by tilesource nor set by the user
704 */
705 public static int checkMinZoomLvl(int minZoomLvl, TileSource ts) {
706 if (minZoomLvl < MIN_ZOOM) {
707 minZoomLvl = MIN_ZOOM;
708 }
709 if (minZoomLvl > PROP_MAX_ZOOM_LVL.get()) {
710 minZoomLvl = getMaxZoomLvl(ts);
711 }
712 if (ts != null && ts.getMinZoom() > minZoomLvl) {
713 minZoomLvl = ts.getMinZoom();
714 }
715 return minZoomLvl;
716 }
717
718 /**
719 * @param ts TileSource for which we want to know maximum zoom level
720 * @return maximum max zoom level, that will be shown on layer
721 */
722 public static int getMaxZoomLvl(TileSource ts) {
723 return checkMaxZoomLvl(PROP_MAX_ZOOM_LVL.get(), ts);
724 }
725
726 /**
727 * @param ts TileSource for which we want to know minimum zoom level
728 * @return minimum zoom level, that will be shown on layer
729 */
730 public static int getMinZoomLvl(TileSource ts) {
731 return checkMinZoomLvl(PROP_MIN_ZOOM_LVL.get(), ts);
732 }
733
734 /**
735 * Sets maximum zoom level, that layer will attempt show
736 * @param maxZoomLvl maximum zoom level
737 */
738 public static void setMaxZoomLvl(int maxZoomLvl) {
739 PROP_MAX_ZOOM_LVL.put(checkMaxZoomLvl(maxZoomLvl, null));
740 }
741
742 /**
743 * Sets minimum zoom level, that layer will attempt show
744 * @param minZoomLvl minimum zoom level
745 */
746 public static void setMinZoomLvl(int minZoomLvl) {
747 PROP_MIN_ZOOM_LVL.put(checkMinZoomLvl(minZoomLvl, null));
748 }
749
750 /**
751 * This fires every time the user changes the zoom, but also (due to ZoomChangeListener) - on all
752 * changes to visible map (panning/zooming)
753 */
754 @Override
755 public void zoomChanged() {
756 if (Main.isDebugEnabled()) {
757 Main.debug("zoomChanged(): " + currentZoomLevel);
758 }
759 if (tileLoader instanceof TMSCachedTileLoader) {
760 ((TMSCachedTileLoader) tileLoader).cancelOutstandingTasks();
761 }
762 needRedraw = true;
763 }
764
765 protected int getMaxZoomLvl() {
766 if (info.getMaxZoom() != 0)
767 return checkMaxZoomLvl(info.getMaxZoom(), tileSource);
768 else
769 return getMaxZoomLvl(tileSource);
770 }
771
772 protected int getMinZoomLvl() {
773 return getMinZoomLvl(tileSource);
774 }
775
776 /**
777 *
778 * @return if its allowed to zoom in
779 */
780 public boolean zoomIncreaseAllowed() {
781 boolean zia = currentZoomLevel < this.getMaxZoomLvl();
782 if (Main.isDebugEnabled()) {
783 Main.debug("zoomIncreaseAllowed(): " + zia + ' ' + currentZoomLevel + " vs. " + this.getMaxZoomLvl());
784 }
785 return zia;
786 }
787
788 /**
789 * Zoom in, go closer to map.
790 *
791 * @return true, if zoom increasing was successful, false otherwise
792 */
793 public boolean increaseZoomLevel() {
794 if (zoomIncreaseAllowed()) {
795 currentZoomLevel++;
796 if (Main.isDebugEnabled()) {
797 Main.debug("increasing zoom level to: " + currentZoomLevel);
798 }
799 zoomChanged();
800 } else {
801 Main.warn("Current zoom level ("+currentZoomLevel+") could not be increased. "+
802 "Max.zZoom Level "+this.getMaxZoomLvl()+" reached.");
803 return false;
804 }
805 return true;
806 }
807
808 /**
809 * Sets the zoom level of the layer
810 * @param zoom zoom level
811 * @return true, when zoom has changed to desired value, false if it was outside supported zoom levels
812 */
813 public boolean setZoomLevel(int zoom) {
814 if (zoom == currentZoomLevel) return true;
815 if (zoom > this.getMaxZoomLvl()) return false;
816 if (zoom < this.getMinZoomLvl()) return false;
817 currentZoomLevel = zoom;
818 zoomChanged();
819 return true;
820 }
821
822 /**
823 * Check if zooming out is allowed
824 *
825 * @return true, if zooming out is allowed (currentZoomLevel &gt; minZoomLevel)
826 */
827 public boolean zoomDecreaseAllowed() {
828 return currentZoomLevel > this.getMinZoomLvl();
829 }
830
831 /**
832 * Zoom out from map.
833 *
834 * @return true, if zoom increasing was successfull, false othervise
835 */
836 public boolean decreaseZoomLevel() {
837 if (zoomDecreaseAllowed()) {
838 if (Main.isDebugEnabled()) {
839 Main.debug("decreasing zoom level to: " + currentZoomLevel);
840 }
841 currentZoomLevel--;
842 zoomChanged();
843 } else {
844 return false;
845 }
846 return true;
847 }
848
849 /*
850 * We use these for quick, hackish calculations. They
851 * are temporary only and intentionally not inserted
852 * into the tileCache.
853 */
854 private Tile tempCornerTile(Tile t) {
855 int x = t.getXtile() + 1;
856 int y = t.getYtile() + 1;
857 int zoom = t.getZoom();
858 Tile tile = getTile(x, y, zoom);
859 if (tile != null)
860 return tile;
861 return new Tile(tileSource, x, y, zoom);
862 }
863
864 private Tile getOrCreateTile(int x, int y, int zoom) {
865 Tile tile = getTile(x, y, zoom);
866 if (tile == null) {
867 tile = new Tile(tileSource, x, y, zoom);
868 tileCache.addTile(tile);
869 tile.loadPlaceholderFromCache(tileCache);
870 }
871 return tile;
872 }
873
874 /*
875 * This can and will return null for tiles that are not
876 * already in the cache.
877 */
878 private Tile getTile(int x, int y, int zoom) {
879 if (x < 0 || x > tileSource.getTileXMax(zoom) || y < 0 || y > tileSource.getTileYMax(zoom))
880 return null;
881 return tileCache.getTile(tileSource, x, y, zoom);
882 }
883
884 private boolean loadTile(Tile tile, boolean force) {
885 if (tile == null)
886 return false;
887 if (!force && (tile.isLoaded() || tile.hasError()))
888 return false;
889 if (tile.isLoading())
890 return false;
891 tileLoader.createTileLoaderJob(tile).submit(force);
892 return true;
893 }
894
895 private TileSet getVisibleTileSet() {
896 MapView mv = Main.map.mapView;
897 EastNorth topLeft = mv.getEastNorth(0, 0);
898 EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
899 return new TileSet(topLeft, botRight, currentZoomLevel);
900 }
901
902 protected void loadAllTiles(boolean force) {
903 TileSet ts = getVisibleTileSet();
904
905 // if there is more than 18 tiles on screen in any direction, do not load all tiles!
906 if (ts.tooLarge()) {
907 Main.warn("Not downloading all tiles because there is more than 18 tiles on an axis!");
908 return;
909 }
910 ts.loadAllTiles(force);
911 }
912
913 protected void loadAllErrorTiles(boolean force) {
914 TileSet ts = getVisibleTileSet();
915 ts.loadAllErrorTiles(force);
916 }
917
918 @Override
919 public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
920 boolean done = (infoflags & (ERROR | FRAMEBITS | ALLBITS)) != 0;
921 needRedraw = true;
922 if (Main.isDebugEnabled()) {
923 Main.debug("imageUpdate() done: " + done + " calling repaint");
924 }
925 Main.map.repaint(done ? 0 : 100);
926 return !done;
927 }
928
929 private boolean imageLoaded(Image i) {
930 if (i == null)
931 return false;
932 int status = Toolkit.getDefaultToolkit().checkImage(i, -1, -1, this);
933 if ((status & ALLBITS) != 0)
934 return true;
935 return false;
936 }
937
938 /**
939 * Returns the image for the given tile if both tile and image are loaded.
940 * Otherwise returns null.
941 *
942 * @param tile the Tile for which the image should be returned
943 * @return the image of the tile or null.
944 */
945 private Image getLoadedTileImage(Tile tile) {
946 if (!tile.isLoaded())
947 return null;
948 Image img = tile.getImage();
949 if (!imageLoaded(img))
950 return null;
951 return img;
952 }
953
954 private Rectangle tileToRect(Tile t1) {
955 /*
956 * We need to get a box in which to draw, so advance by one tile in
957 * each direction to find the other corner of the box.
958 * Note: this somewhat pollutes the tile cache
959 */
960 Tile t2 = tempCornerTile(t1);
961 Rectangle rect = new Rectangle(pixelPos(t1));
962 rect.add(pixelPos(t2));
963 return rect;
964 }
965
966 // 'source' is the pixel coordinates for the area that
967 // the img is capable of filling in. However, we probably
968 // only want a portion of it.
969 //
970 // 'border' is the screen cordinates that need to be drawn.
971 // We must not draw outside of it.
972 private void drawImageInside(Graphics g, Image sourceImg, Rectangle source, Rectangle border) {
973 Rectangle target = source;
974
975 // If a border is specified, only draw the intersection
976 // if what we have combined with what we are supposed to draw.
977 if (border != null) {
978 target = source.intersection(border);
979 if (Main.isDebugEnabled()) {
980 Main.debug("source: " + source + "\nborder: " + border + "\nintersection: " + target);
981 }
982 }
983
984 // All of the rectangles are in screen coordinates. We need
985 // to how these correlate to the sourceImg pixels. We could
986 // avoid doing this by scaling the image up to the 'source' size,
987 // but this should be cheaper.
988 //
989 // In some projections, x any y are scaled differently enough to
990 // cause a pixel or two of fudge. Calculate them separately.
991 double imageYScaling = sourceImg.getHeight(this) / source.getHeight();
992 double imageXScaling = sourceImg.getWidth(this) / source.getWidth();
993
994 // How many pixels into the 'source' rectangle are we drawing?
995 int screen_x_offset = target.x - source.x;
996 int screen_y_offset = target.y - source.y;
997 // And how many pixels into the image itself does that correlate to?
998 int img_x_offset = (int) (screen_x_offset * imageXScaling + 0.5);
999 int img_y_offset = (int) (screen_y_offset * imageYScaling + 0.5);
1000 // Now calculate the other corner of the image that we need
1001 // by scaling the 'target' rectangle's dimensions.
1002 int img_x_end = img_x_offset + (int) (target.getWidth() * imageXScaling + 0.5);
1003 int img_y_end = img_y_offset + (int) (target.getHeight() * imageYScaling + 0.5);
1004
1005 if (Main.isDebugEnabled()) {
1006 Main.debug("drawing image into target rect: " + target);
1007 }
1008 g.drawImage(sourceImg,
1009 target.x, target.y,
1010 target.x + target.width, target.y + target.height,
1011 img_x_offset, img_y_offset,
1012 img_x_end, img_y_end,
1013 this);
1014 if (PROP_FADE_AMOUNT.get() != 0) {
1015 // dimm by painting opaque rect...
1016 g.setColor(getFadeColorWithAlpha());
1017 g.fillRect(target.x, target.y,
1018 target.width, target.height);
1019 }
1020 }
1021
1022 // This function is called for several zoom levels, not just
1023 // the current one. It should not trigger any tiles to be
1024 // downloaded. It should also avoid polluting the tile cache
1025 // with any tiles since these tiles are not mandatory.
1026 //
1027 // The "border" tile tells us the boundaries of where we may
1028 // draw. It will not be from the zoom level that is being
1029 // drawn currently. If drawing the displayZoomLevel,
1030 // border is null and we draw the entire tile set.
1031 private List<Tile> paintTileImages(Graphics g, TileSet ts, int zoom, Tile border) {
1032 if (zoom <= 0) return Collections.emptyList();
1033 Rectangle borderRect = null;
1034 if (border != null) {
1035 borderRect = tileToRect(border);
1036 }
1037 List<Tile> missedTiles = new LinkedList<>();
1038 // The callers of this code *require* that we return any tiles
1039 // that we do not draw in missedTiles. ts.allExistingTiles() by
1040 // default will only return already-existing tiles. However, we
1041 // need to return *all* tiles to the callers, so force creation here.
1042 for (Tile tile : ts.allTilesCreate()) {
1043 Image img = getLoadedTileImage(tile);
1044 if (img == null || tile.hasError()) {
1045 if (Main.isDebugEnabled()) {
1046 Main.debug("missed tile: " + tile);
1047 }
1048 missedTiles.add(tile);
1049 continue;
1050 }
1051
1052 // applying all filters to this layer
1053 img = applyImageProcessors((BufferedImage) img);
1054
1055 Rectangle sourceRect = tileToRect(tile);
1056 if (borderRect != null && !sourceRect.intersects(borderRect)) {
1057 continue;
1058 }
1059 drawImageInside(g, img, sourceRect, borderRect);
1060 }
1061 return missedTiles;
1062 }
1063
1064 private void myDrawString(Graphics g, String text, int x, int y) {
1065 Color oldColor = g.getColor();
1066 String textToDraw = text;
1067 if (g.getFontMetrics().stringWidth(text) > tileSource.getTileSize()) {
1068 // text longer than tile size, split it
1069 StringBuilder line = new StringBuilder();
1070 StringBuilder ret = new StringBuilder();
1071 for (String s: text.split(" ")) {
1072 if (g.getFontMetrics().stringWidth(line.toString() + s) > tileSource.getTileSize()) {
1073 ret.append(line).append('\n');
1074 line.setLength(0);
1075 }
1076 line.append(s).append(' ');
1077 }
1078 ret.append(line);
1079 textToDraw = ret.toString();
1080 }
1081 int offset = 0;
1082 for (String s: textToDraw.split("\n")) {
1083 g.setColor(Color.black);
1084 g.drawString(s, x + 1, y + offset + 1);
1085 g.setColor(oldColor);
1086 g.drawString(s, x, y + offset);
1087 offset += g.getFontMetrics().getHeight() + 3;
1088 }
1089 }
1090
1091 private void paintTileText(TileSet ts, Tile tile, Graphics g, MapView mv, int zoom, Tile t) {
1092 int fontHeight = g.getFontMetrics().getHeight();
1093 if (tile == null)
1094 return;
1095 Point p = pixelPos(t);
1096 int texty = p.y + 2 + fontHeight;
1097
1098 /*if (PROP_DRAW_DEBUG.get()) {
1099 myDrawString(g, "x=" + t.getXtile() + " y=" + t.getYtile() + " z=" + zoom + "", p.x + 2, texty);
1100 texty += 1 + fontHeight;
1101 if ((t.getXtile() % 32 == 0) && (t.getYtile() % 32 == 0)) {
1102 myDrawString(g, "x=" + t.getXtile() / 32 + " y=" + t.getYtile() / 32 + " z=7", p.x + 2, texty);
1103 texty += 1 + fontHeight;
1104 }
1105 }*/
1106
1107 /*String tileStatus = tile.getStatus();
1108 if (!tile.isLoaded() && PROP_DRAW_DEBUG.get()) {
1109 myDrawString(g, tr("image " + tileStatus), p.x + 2, texty);
1110 texty += 1 + fontHeight;
1111 }*/
1112
1113 if (tile.hasError() && showErrors) {
1114 myDrawString(g, tr("Error") + ": " + tr(tile.getErrorMessage()), p.x + 2, texty);
1115 //texty += 1 + fontHeight;
1116 }
1117
1118 int xCursor = -1;
1119 int yCursor = -1;
1120 if (Main.isDebugEnabled()) {
1121 if (yCursor < t.getYtile()) {
1122 if (t.getYtile() % 32 == 31) {
1123 g.fillRect(0, p.y - 1, mv.getWidth(), 3);
1124 } else {
1125 g.drawLine(0, p.y, mv.getWidth(), p.y);
1126 }
1127 //yCursor = t.getYtile();
1128 }
1129 // This draws the vertical lines for the entire column. Only draw them for the top tile in the column.
1130 if (xCursor < t.getXtile()) {
1131 if (t.getXtile() % 32 == 0) {
1132 // level 7 tile boundary
1133 g.fillRect(p.x - 1, 0, 3, mv.getHeight());
1134 } else {
1135 g.drawLine(p.x, 0, p.x, mv.getHeight());
1136 }
1137 //xCursor = t.getXtile();
1138 }
1139 }
1140 }
1141
1142 private Point pixelPos(LatLon ll) {
1143 return Main.map.mapView.getPoint(Main.getProjection().latlon2eastNorth(ll).add(getDx(), getDy()));
1144 }
1145
1146 private Point pixelPos(Tile t) {
1147 ICoordinate coord = tileSource.tileXYToLatLon(t);
1148 return pixelPos(new LatLon(coord));
1149 }
1150
1151 private LatLon getShiftedLatLon(EastNorth en) {
1152 return Main.getProjection().eastNorth2latlon(en.add(-getDx(), -getDy()));
1153 }
1154
1155 private ICoordinate getShiftedCoord(EastNorth en) {
1156 return getShiftedLatLon(en).toCoordinate();
1157 }
1158
1159 private final TileSet nullTileSet = new TileSet((LatLon) null, (LatLon) null, 0);
1160 private final class TileSet {
1161 int x0, x1, y0, y1;
1162 int zoom;
1163
1164 /**
1165 * Create a TileSet by EastNorth bbox taking a layer shift in account
1166 */
1167 private TileSet(EastNorth topLeft, EastNorth botRight, int zoom) {
1168 this(getShiftedLatLon(topLeft), getShiftedLatLon(botRight), zoom);
1169 }
1170
1171 /**
1172 * Create a TileSet by known LatLon bbox without layer shift correction
1173 */
1174 private TileSet(LatLon topLeft, LatLon botRight, int zoom) {
1175 this.zoom = zoom;
1176 if (zoom == 0)
1177 return;
1178
1179 TileXY t1 = tileSource.latLonToTileXY(topLeft.toCoordinate(), zoom);
1180 TileXY t2 = tileSource.latLonToTileXY(botRight.toCoordinate(), zoom);
1181
1182 x0 = t1.getXIndex();
1183 y0 = t1.getYIndex();
1184 x1 = t2.getXIndex();
1185 y1 = t2.getYIndex();
1186
1187 if (x0 > x1) {
1188 int tmp = x0;
1189 x0 = x1;
1190 x1 = tmp;
1191 }
1192 if (y0 > y1) {
1193 int tmp = y0;
1194 y0 = y1;
1195 y1 = tmp;
1196 }
1197
1198 if (x0 < tileSource.getTileXMin(zoom)) {
1199 x0 = tileSource.getTileXMin(zoom);
1200 }
1201 if (y0 < tileSource.getTileYMin(zoom)) {
1202 y0 = tileSource.getTileYMin(zoom);
1203 }
1204 if (x1 > tileSource.getTileXMax(zoom)) {
1205 x1 = tileSource.getTileXMax(zoom);
1206 }
1207 if (y1 > tileSource.getTileYMax(zoom)) {
1208 y1 = tileSource.getTileYMax(zoom);
1209 }
1210 }
1211
1212 private boolean tooSmall() {
1213 return this.tilesSpanned() < 2.1;
1214 }
1215
1216 private boolean tooLarge() {
1217 return insane() || this.tilesSpanned() > 20;
1218 }
1219
1220 private boolean insane() {
1221 return size() > tileCache.getCacheSize();
1222 }
1223
1224 private double tilesSpanned() {
1225 return Math.sqrt(1.0 * this.size());
1226 }
1227
1228 private int size() {
1229 int xSpan = x1 - x0 + 1;
1230 int ySpan = y1 - y0 + 1;
1231 return xSpan * ySpan;
1232 }
1233
1234 /*
1235 * Get all tiles represented by this TileSet that are
1236 * already in the tileCache.
1237 */
1238 private List<Tile> allExistingTiles() {
1239 return this.__allTiles(false);
1240 }
1241
1242 private List<Tile> allTilesCreate() {
1243 return this.__allTiles(true);
1244 }
1245
1246 private List<Tile> __allTiles(boolean create) {
1247 // Tileset is either empty or too large
1248 if (zoom == 0 || this.insane())
1249 return Collections.emptyList();
1250 List<Tile> ret = new ArrayList<>();
1251 for (int x = x0; x <= x1; x++) {
1252 for (int y = y0; y <= y1; y++) {
1253 Tile t;
1254 if (create) {
1255 t = getOrCreateTile(x, y, zoom);
1256 } else {
1257 t = getTile(x, y, zoom);
1258 }
1259 if (t != null) {
1260 ret.add(t);
1261 }
1262 }
1263 }
1264 return ret;
1265 }
1266
1267 private List<Tile> allLoadedTiles() {
1268 List<Tile> ret = new ArrayList<>();
1269 for (Tile t : this.allExistingTiles()) {
1270 if (t.isLoaded())
1271 ret.add(t);
1272 }
1273 return ret;
1274 }
1275
1276 /**
1277 * @return comparator, that sorts the tiles from the center to the edge of the current screen
1278 */
1279 private Comparator<Tile> getTileDistanceComparator() {
1280 final int centerX = (int) Math.ceil((x0 + x1) / 2d);
1281 final int centerY = (int) Math.ceil((y0 + y1) / 2d);
1282 return new Comparator<Tile>() {
1283 private int getDistance(Tile t) {
1284 return Math.abs(t.getXtile() - centerX) + Math.abs(t.getYtile() - centerY);
1285 }
1286
1287 @Override
1288 public int compare(Tile o1, Tile o2) {
1289 int distance1 = getDistance(o1);
1290 int distance2 = getDistance(o2);
1291 return Integer.compare(distance1, distance2);
1292 }
1293 };
1294 }
1295
1296 private void loadAllTiles(boolean force) {
1297 if (!autoLoad && !force)
1298 return;
1299 List<Tile> allTiles = allTilesCreate();
1300 Collections.sort(allTiles, getTileDistanceComparator());
1301 for (Tile t : allTiles) {
1302 loadTile(t, force);
1303 }
1304 }
1305
1306 private void loadAllErrorTiles(boolean force) {
1307 if (!autoLoad && !force)
1308 return;
1309 for (Tile t : this.allTilesCreate()) {
1310 if (t.hasError()) {
1311 loadTile(t, true);
1312 }
1313 }
1314 }
1315 }
1316
1317 private static class TileSetInfo {
1318 public boolean hasVisibleTiles;
1319 public boolean hasOverzoomedTiles;
1320 public boolean hasLoadingTiles;
1321 }
1322
1323 private static TileSetInfo getTileSetInfo(TileSet ts) {
1324 List<Tile> allTiles = ts.allExistingTiles();
1325 TileSetInfo result = new TileSetInfo();
1326 result.hasLoadingTiles = allTiles.size() < ts.size();
1327 for (Tile t : allTiles) {
1328 if ("no-tile".equals(t.getValue("tile-info"))) {
1329 result.hasOverzoomedTiles = true;
1330 }
1331
1332 if (t.isLoaded()) {
1333 if (!t.hasError()) {
1334 result.hasVisibleTiles = true;
1335 }
1336 } else if (t.isLoading()) {
1337 result.hasLoadingTiles = true;
1338 }
1339 }
1340 return result;
1341 }
1342
1343 private class DeepTileSet {
1344 private final EastNorth topLeft, botRight;
1345 private final int minZoom, maxZoom;
1346 private final TileSet[] tileSets;
1347 private final TileSetInfo[] tileSetInfos;
1348 DeepTileSet(EastNorth topLeft, EastNorth botRight, int minZoom, int maxZoom) {
1349 this.topLeft = topLeft;
1350 this.botRight = botRight;
1351 this.minZoom = minZoom;
1352 this.maxZoom = maxZoom;
1353 this.tileSets = new TileSet[maxZoom - minZoom + 1];
1354 this.tileSetInfos = new TileSetInfo[maxZoom - minZoom + 1];
1355 }
1356
1357 public TileSet getTileSet(int zoom) {
1358 if (zoom < minZoom)
1359 return nullTileSet;
1360 synchronized (tileSets) {
1361 TileSet ts = tileSets[zoom-minZoom];
1362 if (ts == null) {
1363 ts = new TileSet(topLeft, botRight, zoom);
1364 tileSets[zoom-minZoom] = ts;
1365 }
1366 return ts;
1367 }
1368 }
1369
1370 public TileSetInfo getTileSetInfo(int zoom) {
1371 if (zoom < minZoom)
1372 return new TileSetInfo();
1373 synchronized (tileSetInfos) {
1374 TileSetInfo tsi = tileSetInfos[zoom-minZoom];
1375 if (tsi == null) {
1376 tsi = AbstractTileSourceLayer.getTileSetInfo(getTileSet(zoom));
1377 tileSetInfos[zoom-minZoom] = tsi;
1378 }
1379 return tsi;
1380 }
1381 }
1382 }
1383
1384 @Override
1385 public void paint(Graphics2D g, MapView mv, Bounds bounds) {
1386 EastNorth topLeft = mv.getEastNorth(0, 0);
1387 EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
1388
1389 if (botRight.east() == 0 || botRight.north() == 0) {
1390 /*Main.debug("still initializing??");*/
1391 // probably still initializing
1392 return;
1393 }
1394
1395 needRedraw = false;
1396
1397 int zoom = currentZoomLevel;
1398 if (autoZoom) {
1399 zoom = getBestZoom();
1400 }
1401
1402 DeepTileSet dts = new DeepTileSet(topLeft, botRight, getMinZoomLvl(), zoom);
1403 TileSet ts = dts.getTileSet(zoom);
1404
1405 int displayZoomLevel = zoom;
1406
1407 boolean noTilesAtZoom = false;
1408 if (autoZoom && autoLoad) {
1409 // Auto-detection of tilesource maxzoom (currently fully works only for Bing)
1410 TileSetInfo tsi = dts.getTileSetInfo(zoom);
1411 if (!tsi.hasVisibleTiles && (!tsi.hasLoadingTiles || tsi.hasOverzoomedTiles)) {
1412 noTilesAtZoom = true;
1413 }
1414 // Find highest zoom level with at least one visible tile
1415 for (int tmpZoom = zoom; tmpZoom > dts.minZoom; tmpZoom--) {
1416 if (dts.getTileSetInfo(tmpZoom).hasVisibleTiles) {
1417 displayZoomLevel = tmpZoom;
1418 break;
1419 }
1420 }
1421 // Do binary search between currentZoomLevel and displayZoomLevel
1422 while (zoom > displayZoomLevel && !tsi.hasVisibleTiles && tsi.hasOverzoomedTiles) {
1423 zoom = (zoom + displayZoomLevel)/2;
1424 tsi = dts.getTileSetInfo(zoom);
1425 }
1426
1427 setZoomLevel(zoom);
1428
1429 // If all tiles at displayZoomLevel is loaded, load all tiles at next zoom level
1430 // to make sure there're really no more zoom levels
1431 // loading is done in the next if section
1432 if (zoom == displayZoomLevel && !tsi.hasLoadingTiles && zoom < dts.maxZoom) {
1433 zoom++;
1434 tsi = dts.getTileSetInfo(zoom);
1435 }
1436 // When we have overzoomed tiles and all tiles at current zoomlevel is loaded,
1437 // load tiles at previovus zoomlevels until we have all tiles on screen is loaded.
1438 // loading is done in the next if section
1439 while (zoom > dts.minZoom && tsi.hasOverzoomedTiles && !tsi.hasLoadingTiles) {
1440 zoom--;
1441 tsi = dts.getTileSetInfo(zoom);
1442 }
1443 ts = dts.getTileSet(zoom);
1444 } else if (autoZoom) {
1445 setZoomLevel(zoom);
1446 }
1447
1448 // Too many tiles... refuse to download
1449 if (!ts.tooLarge()) {
1450 //Main.debug("size: " + ts.size() + " spanned: " + ts.tilesSpanned());
1451 ts.loadAllTiles(false);
1452 }
1453
1454 if (displayZoomLevel != zoom) {
1455 ts = dts.getTileSet(displayZoomLevel);
1456 }
1457
1458 g.setColor(Color.DARK_GRAY);
1459
1460 List<Tile> missedTiles = this.paintTileImages(g, ts, displayZoomLevel, null);
1461 int[] otherZooms = {-1, 1, -2, 2, -3, -4, -5};
1462 for (int zoomOffset : otherZooms) {
1463 if (!autoZoom) {
1464 break;
1465 }
1466 int newzoom = displayZoomLevel + zoomOffset;
1467 if (newzoom < getMinZoomLvl() || newzoom > getMaxZoomLvl()) {
1468 continue;
1469 }
1470 if (missedTiles.isEmpty()) {
1471 break;
1472 }
1473 List<Tile> newlyMissedTiles = new LinkedList<>();
1474 for (Tile missed : missedTiles) {
1475 if ("no-tile".equals(missed.getValue("tile-info")) && zoomOffset > 0) {
1476 // Don't try to paint from higher zoom levels when tile is overzoomed
1477 newlyMissedTiles.add(missed);
1478 continue;
1479 }
1480 Tile t2 = tempCornerTile(missed);
1481 LatLon topLeft2 = new LatLon(tileSource.tileXYToLatLon(missed));
1482 LatLon botRight2 = new LatLon(tileSource.tileXYToLatLon(t2));
1483 TileSet ts2 = new TileSet(topLeft2, botRight2, newzoom);
1484 // Instantiating large TileSets is expensive. If there
1485 // are no loaded tiles, don't bother even trying.
1486 if (ts2.allLoadedTiles().isEmpty()) {
1487 newlyMissedTiles.add(missed);
1488 continue;
1489 }
1490 if (ts2.tooLarge()) {
1491 continue;
1492 }
1493 newlyMissedTiles.addAll(this.paintTileImages(g, ts2, newzoom, missed));
1494 }
1495 missedTiles = newlyMissedTiles;
1496 }
1497 if (Main.isDebugEnabled() && !missedTiles.isEmpty()) {
1498 Main.debug("still missed "+missedTiles.size()+" in the end");
1499 }
1500 g.setColor(Color.red);
1501 g.setFont(InfoFont);
1502
1503 // The current zoom tileset should have all of its tiles due to the loadAllTiles(), unless it to tooLarge()
1504 for (Tile t : ts.allExistingTiles()) {
1505 this.paintTileText(ts, t, g, mv, displayZoomLevel, t);
1506 }
1507
1508 attribution.paintAttribution(g, mv.getWidth(), mv.getHeight(), getShiftedCoord(topLeft), getShiftedCoord(botRight),
1509 displayZoomLevel, this);
1510
1511 //g.drawString("currentZoomLevel=" + currentZoomLevel, 120, 120);
1512 g.setColor(Color.lightGray);
1513
1514 if (ts.insane()) {
1515 myDrawString(g, tr("zoom in to load any tiles"), 120, 120);
1516 } else if (ts.tooLarge()) {
1517 myDrawString(g, tr("zoom in to load more tiles"), 120, 120);
1518 } else if (!autoZoom && ts.tooSmall()) {
1519 myDrawString(g, tr("increase zoom level to see more detail"), 120, 120);
1520 }
1521
1522 if (noTilesAtZoom) {
1523 myDrawString(g, tr("No tiles at this zoom level"), 120, 120);
1524 }
1525 if (Main.isDebugEnabled()) {
1526 myDrawString(g, tr("Current zoom: {0}", currentZoomLevel), 50, 140);
1527 myDrawString(g, tr("Display zoom: {0}", displayZoomLevel), 50, 155);
1528 myDrawString(g, tr("Pixel scale: {0}", getScaleFactor(currentZoomLevel)), 50, 170);
1529 myDrawString(g, tr("Best zoom: {0}", getBestZoom()), 50, 185);
1530 myDrawString(g, tr("Estimated cache size: {0}", estimateTileCacheSize()), 50, 200);
1531 if (tileLoader instanceof TMSCachedTileLoader) {
1532 TMSCachedTileLoader cachedTileLoader = (TMSCachedTileLoader) tileLoader;
1533 int offset = 200;
1534 for (String part: cachedTileLoader.getStats().split("\n")) {
1535 myDrawString(g, tr("Cache stats: {0}", part), 50, offset += 15);
1536 }
1537
1538 }
1539 }
1540 }
1541
1542 /**
1543 * This isn't very efficient, but it is only used when the
1544 * user right-clicks on the map.
1545 */
1546 private Tile getTileForPixelpos(int px, int py) {
1547 if (Main.isDebugEnabled()) {
1548 Main.debug("getTileForPixelpos("+px+", "+py+')');
1549 }
1550 MapView mv = Main.map.mapView;
1551 Point clicked = new Point(px, py);
1552 EastNorth topLeft = mv.getEastNorth(0, 0);
1553 EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
1554 int z = currentZoomLevel;
1555 TileSet ts = new TileSet(topLeft, botRight, z);
1556
1557 if (!ts.tooLarge()) {
1558 ts.loadAllTiles(false); // make sure there are tile objects for all tiles
1559 }
1560 Tile clickedTile = null;
1561 for (Tile t1 : ts.allExistingTiles()) {
1562 Tile t2 = tempCornerTile(t1);
1563 Rectangle r = new Rectangle(pixelPos(t1));
1564 r.add(pixelPos(t2));
1565 if (Main.isDebugEnabled()) {
1566 Main.debug("r: " + r + " clicked: " + clicked);
1567 }
1568 if (!r.contains(clicked)) {
1569 continue;
1570 }
1571 clickedTile = t1;
1572 break;
1573 }
1574 if (clickedTile == null)
1575 return null;
1576 /*Main.debug("Clicked on tile: " + clickedTile.getXtile() + " " + clickedTile.getYtile() +
1577 " currentZoomLevel: " + currentZoomLevel);*/
1578 return clickedTile;
1579 }
1580
1581 @Override
1582 public Action[] getMenuEntries() {
1583 return new Action[] {
1584 LayerListDialog.getInstance().createActivateLayerAction(this),
1585 LayerListDialog.getInstance().createShowHideLayerAction(),
1586 LayerListDialog.getInstance().createDeleteLayerAction(),
1587 SeparatorLayerAction.INSTANCE,
1588 // color,
1589 new OffsetAction(),
1590 new RenameLayerAction(this.getAssociatedFile(), this),
1591 SeparatorLayerAction.INSTANCE,
1592 new AutoLoadTilesAction(),
1593 new AutoZoomAction(),
1594 new ZoomToBestAction(),
1595 new ZoomToNativeLevelAction(),
1596 new LoadErroneusTilesAction(),
1597 new LoadAllTilesAction(),
1598 new LayerListPopup.InfoAction(this)
1599 };
1600 }
1601
1602 @Override
1603 public String getToolTipText() {
1604 if (autoLoad) {
1605 return tr("{0} ({1}), automatically downloading in zoom {2}", this.getClass().getSimpleName(), getName(), currentZoomLevel);
1606 } else {
1607 return tr("{0} ({1}), downloading in zoom {2}", this.getClass().getSimpleName(), getName(), currentZoomLevel);
1608 }
1609 }
1610
1611 @Override
1612 public void visitBoundingBox(BoundingXYVisitor v) {
1613 }
1614
1615 @Override
1616 public boolean isChanged() {
1617 return needRedraw;
1618 }
1619
1620 /**
1621 * Task responsible for precaching imagery along the gpx track
1622 *
1623 */
1624 public class PrecacheTask implements TileLoaderListener {
1625 private final ProgressMonitor progressMonitor;
1626 private int totalCount;
1627 private AtomicInteger processedCount = new AtomicInteger(0);
1628 private final TileLoader tileLoader;
1629
1630 /**
1631 * @param progressMonitor that will be notified about progess of the task
1632 */
1633 public PrecacheTask(ProgressMonitor progressMonitor) {
1634 this.progressMonitor = progressMonitor;
1635 this.tileLoader = getTileLoaderFactory().makeTileLoader(this, getHeaders(tileSource));
1636 if (this.tileLoader instanceof TMSCachedTileLoader) {
1637 ((TMSCachedTileLoader) this.tileLoader).setDownloadExecutor(
1638 TMSCachedTileLoader.getNewThreadPoolExecutor("Precache downloader"));
1639 }
1640
1641 }
1642
1643 /**
1644 * @return true, if all is done
1645 */
1646 public boolean isFinished() {
1647 return processedCount.get() >= totalCount;
1648 }
1649
1650 /**
1651 * @return total number of tiles to download
1652 */
1653 public int getTotalCount() {
1654 return totalCount;
1655 }
1656
1657 /**
1658 * cancel the task
1659 */
1660 public void cancel() {
1661 if (tileLoader instanceof TMSCachedTileLoader) {
1662 ((TMSCachedTileLoader) tileLoader).cancelOutstandingTasks();
1663 }
1664 }
1665
1666 @Override
1667 public void tileLoadingFinished(Tile tile, boolean success) {
1668 if (success) {
1669 int processed = this.processedCount.incrementAndGet();
1670 this.progressMonitor.worked(1);
1671 this.progressMonitor.setCustomText(tr("Downloaded {0}/{1} tiles", processed, totalCount));
1672 }
1673 }
1674
1675 /**
1676 * @return tile loader that is used to load the tiles
1677 */
1678 public TileLoader getTileLoader() {
1679 return tileLoader;
1680 }
1681 }
1682
1683 /**
1684 * Calculates tiles, that needs to be downloaded to cache, gets a current tile loader and creates a task to download
1685 * all of the tiles. Buffer contains at least one tile.
1686 *
1687 * To prevent accidental clear of the queue, new download executor is created with separate queue
1688 *
1689 * @param precacheTask Task responsible for precaching imagery
1690 * @param points lat/lon coordinates to download
1691 * @param bufferX how many units in current Coordinate Reference System to cover in X axis in both sides
1692 * @param bufferY how many units in current Coordinate Reference System to cover in Y axis in both sides
1693 */
1694 public void downloadAreaToCache(final PrecacheTask precacheTask, List<LatLon> points, double bufferX, double bufferY) {
1695 final Set<Tile> requestedTiles = new ConcurrentSkipListSet<>(new Comparator<Tile>() {
1696 public int compare(Tile o1, Tile o2) {
1697 return String.CASE_INSENSITIVE_ORDER.compare(o1.getKey(), o2.getKey());
1698 }
1699 });
1700 for (LatLon point: points) {
1701
1702 TileXY minTile = tileSource.latLonToTileXY(point.lat() - bufferY, point.lon() - bufferX, currentZoomLevel);
1703 TileXY curTile = tileSource.latLonToTileXY(point.toCoordinate(), currentZoomLevel);
1704 TileXY maxTile = tileSource.latLonToTileXY(point.lat() + bufferY, point.lon() + bufferX, currentZoomLevel);
1705
1706 // take at least one tile of buffer
1707 int minY = Math.min(curTile.getYIndex() - 1, minTile.getYIndex());
1708 int maxY = Math.max(curTile.getYIndex() + 1, maxTile.getYIndex());
1709 int minX = Math.min(curTile.getXIndex() - 1, minTile.getXIndex());
1710 int maxX = Math.min(curTile.getXIndex() + 1, minTile.getXIndex());
1711
1712 for (int x = minX; x <= maxX; x++) {
1713 for (int y = minY; y <= maxY; y++) {
1714 requestedTiles.add(new Tile(tileSource, x, y, currentZoomLevel));
1715 }
1716 }
1717 }
1718
1719 precacheTask.totalCount = requestedTiles.size();
1720 precacheTask.progressMonitor.setTicksCount(requestedTiles.size());
1721
1722 TileLoader loader = precacheTask.getTileLoader();
1723 for (Tile t: requestedTiles) {
1724 loader.createTileLoaderJob(t).submit();
1725 }
1726 }
1727
1728 @Override
1729 public boolean isSavable() {
1730 return true; // With WMSLayerExporter
1731 }
1732
1733 @Override
1734 public File createAndOpenSaveFileChooser() {
1735 return SaveActionBase.createAndOpenSaveFileChooser(tr("Save WMS file"), WMSLayerImporter.FILE_FILTER);
1736 }
1737}
Note: See TracBrowser for help on using the repository browser.