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

Last change on this file since 9464 was 9464, checked in by bastiK, 8 years ago

applied #12363 - layer context menus were different at two locations (patch by kolesar)

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