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

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

fix #13159 - Move image processors out of imagery layer (patch by michael2402) - gsoc-core + fix checkstyle violations

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