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

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

fix #13169 - Extract imagery layer settings to new class (patch by michael2402, modified) - gsoc-core

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