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

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

see #11255 - checkstyle/findbugs

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