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

Last change on this file since 8751 was 8751, checked in by wiktorn, 9 years ago

Introduce zoom offseting. See: #11856

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