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

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

Small TMS fixes.

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