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

Last change on this file since 9478 was 9478, checked in by Don-vip, 8 years ago

see #12363 - javadoc/checkstyle

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