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

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

fix Checkstyle issues

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