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

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

Temporary fix for #12681.

Check, from which side we are approaching 180 meridian and use min/max X tile as index.

During planned rollback of [9558] this change should be also removed

See: #12681

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