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

Last change on this file since 8625 was 8625, checked in by bastiK, 9 years ago

applied #11713 - JOSM raster layers filters plugin development (patch by Nipel-Crumple, slightly modified)

  • Property svn:eol-style set to native
File size: 60.7 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 projectionChanged(null, Main.getProjection()); // check if projection is supported
500 initTileSource(this.tileSource);
501
502 ;
503 // keep them final here, so we avoid namespace clutter in the class
504 final JPopupMenu tileOptionMenu = new JPopupMenu();
505 final TileHolder clickedTileHolder = new TileHolder();
506 Field autoZoomField;
507 Field autoLoadField;
508 Field showErrorsField;
509 try {
510 autoZoomField = AbstractTileSourceLayer.class.getField("autoZoom");
511 autoLoadField = AbstractTileSourceLayer.class.getDeclaredField("autoLoad");
512 showErrorsField = AbstractTileSourceLayer.class.getDeclaredField("showErrors");
513 } catch (NoSuchFieldException | SecurityException e) {
514 // shoud not happen
515 throw new RuntimeException(e);
516 }
517
518 autoZoom = PROP_DEFAULT_AUTOZOOM.get();
519 JCheckBoxMenuItem autoZoomPopup = new JCheckBoxMenuItem();
520 autoZoomPopup.setModel(new BooleanButtonModel(autoZoomField));
521 autoZoomPopup.setAction(new AutoZoomAction());
522 tileOptionMenu.add(autoZoomPopup);
523
524 autoLoad = PROP_DEFAULT_AUTOLOAD.get();
525 JCheckBoxMenuItem autoLoadPopup = new JCheckBoxMenuItem();
526 autoLoadPopup.setAction(new AutoLoadTilesAction());
527 autoLoadPopup.setModel(new BooleanButtonModel(autoLoadField));
528 tileOptionMenu.add(autoLoadPopup);
529
530 showErrors = PROP_DEFAULT_SHOWERRORS.get();
531 JCheckBoxMenuItem showErrorsPopup = new JCheckBoxMenuItem();
532 showErrorsPopup.setAction(new AbstractAction(tr("Show Errors")) {
533 @Override
534 public void actionPerformed(ActionEvent ae) {
535 showErrors = !showErrors;
536 }
537 });
538 showErrorsPopup.setModel(new BooleanButtonModel(showErrorsField));
539 tileOptionMenu.add(showErrorsPopup);
540
541 tileOptionMenu.add(new JMenuItem(new AbstractAction(tr("Load Tile")) {
542 @Override
543 public void actionPerformed(ActionEvent ae) {
544 Tile clickedTile = clickedTileHolder.getTile();
545 if (clickedTile != null) {
546 loadTile(clickedTile, true);
547 redraw();
548 }
549 }
550 }));
551
552 tileOptionMenu.add(new JMenuItem(new ShowTileInfoAction(clickedTileHolder)));
553
554 tileOptionMenu.add(new JMenuItem(new LoadAllTilesAction()));
555 tileOptionMenu.add(new JMenuItem(new LoadErroneusTilesAction()));
556
557 // increase and decrease commands
558 tileOptionMenu.add(new JMenuItem(new AbstractAction(
559 tr("Increase zoom")) {
560 @Override
561 public void actionPerformed(ActionEvent ae) {
562 increaseZoomLevel();
563 redraw();
564 }
565 }));
566
567 tileOptionMenu.add(new JMenuItem(new AbstractAction(
568 tr("Decrease zoom")) {
569 @Override
570 public void actionPerformed(ActionEvent ae) {
571 decreaseZoomLevel();
572 redraw();
573 }
574 }));
575
576 tileOptionMenu.add(new JMenuItem(new AbstractAction(
577 tr("Snap to tile size")) {
578 @Override
579 public void actionPerformed(ActionEvent ae) {
580 double newFactor = Math.sqrt(getScaleFactor(currentZoomLevel));
581 Main.map.mapView.zoomToFactor(newFactor);
582 redraw();
583 }
584 }));
585
586 tileOptionMenu.add(new JMenuItem(new AbstractAction(
587 tr("Flush Tile Cache")) {
588 @Override
589 public void actionPerformed(ActionEvent ae) {
590 new PleaseWaitRunnable(tr("Flush Tile Cache")) {
591 @Override
592 protected void realRun() {
593 clearTileCache(getProgressMonitor());
594 }
595
596 @Override
597 protected void finish() {
598 // empty - flush is instaneus
599 }
600
601 @Override
602 protected void cancel() {
603 // empty - flush is instaneus
604 }
605 }.run();
606 }
607 }));
608
609 final MouseAdapter adapter = new MouseAdapter() {
610 @Override
611 public void mouseClicked(MouseEvent e) {
612 if (!isVisible()) return;
613 if (e.getButton() == MouseEvent.BUTTON3) {
614 clickedTileHolder.setTile(getTileForPixelpos(e.getX(), e.getY()));
615 tileOptionMenu.show(e.getComponent(), e.getX(), e.getY());
616 } else if (e.getButton() == MouseEvent.BUTTON1) {
617 attribution.handleAttribution(e.getPoint(), true);
618 }
619 }
620 };
621 Main.map.mapView.addMouseListener(adapter);
622
623 MapView.addLayerChangeListener(new LayerChangeListener() {
624 @Override
625 public void activeLayerChange(Layer oldLayer, Layer newLayer) {
626 //
627 }
628
629 @Override
630 public void layerAdded(Layer newLayer) {
631 //
632 }
633
634 @Override
635 public void layerRemoved(Layer oldLayer) {
636 if (oldLayer == AbstractTileSourceLayer.this) {
637 Main.map.mapView.removeMouseListener(adapter);
638 MapView.removeLayerChangeListener(this);
639 MapView.removeZoomChangeListener(AbstractTileSourceLayer.this);
640 }
641 }
642 });
643
644 // FIXME: why do we need this? Without this, if you add a WMS layer and do not move the mouse, sometimes, tiles do not
645 // start loading.
646 Main.map.repaint(500);
647 }
648
649 /**
650 * Checks zoom level against settings
651 * @param maxZoomLvl zoom level to check
652 * @param ts tile source to crosscheck with
653 * @return maximum zoom level, not higher than supported by tilesource nor set by the user
654 */
655 public static int checkMaxZoomLvl(int maxZoomLvl, TileSource ts) {
656 if (maxZoomLvl > MAX_ZOOM) {
657 maxZoomLvl = MAX_ZOOM;
658 }
659 if (maxZoomLvl < PROP_MIN_ZOOM_LVL.get()) {
660 maxZoomLvl = PROP_MIN_ZOOM_LVL.get();
661 }
662 if (ts != null && ts.getMaxZoom() != 0 && ts.getMaxZoom() < maxZoomLvl) {
663 maxZoomLvl = ts.getMaxZoom();
664 }
665 return maxZoomLvl;
666 }
667
668 /**
669 * Checks zoom level against settings
670 * @param minZoomLvl zoom level to check
671 * @param ts tile source to crosscheck with
672 * @return minimum zoom level, not higher than supported by tilesource nor set by the user
673 */
674 public static int checkMinZoomLvl(int minZoomLvl, TileSource ts) {
675 if (minZoomLvl < MIN_ZOOM) {
676 minZoomLvl = MIN_ZOOM;
677 }
678 if (minZoomLvl > PROP_MAX_ZOOM_LVL.get()) {
679 minZoomLvl = getMaxZoomLvl(ts);
680 }
681 if (ts != null && ts.getMinZoom() > minZoomLvl) {
682 minZoomLvl = ts.getMinZoom();
683 }
684 return minZoomLvl;
685 }
686
687 /**
688 * @param ts TileSource for which we want to know maximum zoom level
689 * @return maximum max zoom level, that will be shown on layer
690 */
691 public static int getMaxZoomLvl(TileSource ts) {
692 return checkMaxZoomLvl(PROP_MAX_ZOOM_LVL.get(), ts);
693 }
694
695 /**
696 * @param ts TileSource for which we want to know minimum zoom level
697 * @return minimum zoom level, that will be shown on layer
698 */
699 public static int getMinZoomLvl(TileSource ts) {
700 return checkMinZoomLvl(PROP_MIN_ZOOM_LVL.get(), ts);
701 }
702
703 /**
704 * Sets maximum zoom level, that layer will attempt show
705 * @param maxZoomLvl maximum zoom level
706 */
707 public static void setMaxZoomLvl(int maxZoomLvl) {
708 PROP_MAX_ZOOM_LVL.put(checkMaxZoomLvl(maxZoomLvl, null));
709 }
710
711 /**
712 * Sets minimum zoom level, that layer will attempt show
713 * @param minZoomLvl minimum zoom level
714 */
715 public static void setMinZoomLvl(int minZoomLvl) {
716 PROP_MIN_ZOOM_LVL.put(checkMinZoomLvl(minZoomLvl, null));
717 }
718
719 /**
720 * This fires every time the user changes the zoom, but also (due to ZoomChangeListener) - on all
721 * changes to visible map (panning/zooming)
722 */
723 @Override
724 public void zoomChanged() {
725 if (Main.isDebugEnabled()) {
726 Main.debug("zoomChanged(): " + currentZoomLevel);
727 }
728 if (tileLoader instanceof TMSCachedTileLoader) {
729 ((TMSCachedTileLoader) tileLoader).cancelOutstandingTasks();
730 }
731 needRedraw = true;
732 }
733
734 protected int getMaxZoomLvl() {
735 if (info.getMaxZoom() != 0)
736 return checkMaxZoomLvl(info.getMaxZoom(), tileSource);
737 else
738 return getMaxZoomLvl(tileSource);
739 }
740
741 protected int getMinZoomLvl() {
742 return getMinZoomLvl(tileSource);
743 }
744
745 /**
746 *
747 * @return if its allowed to zoom in
748 */
749 public boolean zoomIncreaseAllowed() {
750 boolean zia = currentZoomLevel < this.getMaxZoomLvl();
751 if (Main.isDebugEnabled()) {
752 Main.debug("zoomIncreaseAllowed(): " + zia + " " + currentZoomLevel + " vs. " + this.getMaxZoomLvl());
753 }
754 return zia;
755 }
756
757 /**
758 * Zoom in, go closer to map.
759 *
760 * @return true, if zoom increasing was successful, false otherwise
761 */
762 public boolean increaseZoomLevel() {
763 if (zoomIncreaseAllowed()) {
764 currentZoomLevel++;
765 if (Main.isDebugEnabled()) {
766 Main.debug("increasing zoom level to: " + currentZoomLevel);
767 }
768 zoomChanged();
769 } else {
770 Main.warn("Current zoom level ("+currentZoomLevel+") could not be increased. "+
771 "Max.zZoom Level "+this.getMaxZoomLvl()+" reached.");
772 return false;
773 }
774 return true;
775 }
776
777 /**
778 * Sets the zoom level of the layer
779 * @param zoom zoom level
780 * @return true, when zoom has changed to desired value, false if it was outside supported zoom levels
781 */
782 public boolean setZoomLevel(int zoom) {
783 if (zoom == currentZoomLevel) return true;
784 if (zoom > this.getMaxZoomLvl()) return false;
785 if (zoom < this.getMinZoomLvl()) return false;
786 currentZoomLevel = zoom;
787 zoomChanged();
788 return true;
789 }
790
791 /**
792 * Check if zooming out is allowed
793 *
794 * @return true, if zooming out is allowed (currentZoomLevel &gt; minZoomLevel)
795 */
796 public boolean zoomDecreaseAllowed() {
797 return currentZoomLevel > this.getMinZoomLvl();
798 }
799
800 /**
801 * Zoom out from map.
802 *
803 * @return true, if zoom increasing was successfull, false othervise
804 */
805 public boolean decreaseZoomLevel() {
806 if (zoomDecreaseAllowed()) {
807 if (Main.isDebugEnabled()) {
808 Main.debug("decreasing zoom level to: " + currentZoomLevel);
809 }
810 currentZoomLevel--;
811 zoomChanged();
812 } else {
813 return false;
814 }
815 return true;
816 }
817
818 /*
819 * We use these for quick, hackish calculations. They
820 * are temporary only and intentionally not inserted
821 * into the tileCache.
822 */
823 private Tile tempCornerTile(Tile t) {
824 int x = t.getXtile() + 1;
825 int y = t.getYtile() + 1;
826 int zoom = t.getZoom();
827 Tile tile = getTile(x, y, zoom);
828 if (tile != null)
829 return tile;
830 return new Tile(tileSource, x, y, zoom);
831 }
832
833 private Tile getOrCreateTile(int x, int y, int zoom) {
834 Tile tile = getTile(x, y, zoom);
835 if (tile == null) {
836 tile = new Tile(tileSource, x, y, zoom);
837 tileCache.addTile(tile);
838 tile.loadPlaceholderFromCache(tileCache);
839 }
840 return tile;
841 }
842
843 /*
844 * This can and will return null for tiles that are not
845 * already in the cache.
846 */
847 private Tile getTile(int x, int y, int zoom) {
848 if (x < 0 || x >= tileSource.getTileXMax(zoom) || y < 0 || y >= tileSource.getTileYMax(zoom))
849 return null;
850 return tileCache.getTile(tileSource, x, y, zoom);
851 }
852
853 private boolean loadTile(Tile tile, boolean force) {
854 if (tile == null)
855 return false;
856 if (!force && (tile.isLoaded() || tile.hasError()))
857 return false;
858 if (tile.isLoading())
859 return false;
860 tileLoader.createTileLoaderJob(tile).submit();
861 return true;
862 }
863
864 private TileSet getVisibleTileSet() {
865 MapView mv = Main.map.mapView;
866 EastNorth topLeft = mv.getEastNorth(0, 0);
867 EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
868 return new TileSet(topLeft, botRight, currentZoomLevel);
869 }
870
871 private void loadAllTiles(boolean force) {
872 TileSet ts = getVisibleTileSet();
873
874 // if there is more than 18 tiles on screen in any direction, do not load all tiles!
875 if (ts.tooLarge()) {
876 Main.warn("Not downloading all tiles because there is more than 18 tiles on an axis!");
877 return;
878 }
879 ts.loadAllTiles(force);
880 }
881
882 private void loadAllErrorTiles(boolean force) {
883 TileSet ts = getVisibleTileSet();
884 ts.loadAllErrorTiles(force);
885 }
886
887 @Override
888 public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
889 boolean done = (infoflags & (ERROR | FRAMEBITS | ALLBITS)) != 0;
890 needRedraw = true;
891 if (Main.isDebugEnabled()) {
892 Main.debug("imageUpdate() done: " + done + " calling repaint");
893 }
894 Main.map.repaint(done ? 0 : 100);
895 return !done;
896 }
897
898 private boolean imageLoaded(Image i) {
899 if (i == null)
900 return false;
901 int status = Toolkit.getDefaultToolkit().checkImage(i, -1, -1, this);
902 if ((status & ALLBITS) != 0)
903 return true;
904 return false;
905 }
906
907 /**
908 * Returns the image for the given tile if both tile and image are loaded.
909 * Otherwise returns null.
910 *
911 * @param tile the Tile for which the image should be returned
912 * @return the image of the tile or null.
913 */
914 private Image getLoadedTileImage(Tile tile) {
915 if (!tile.isLoaded())
916 return null;
917 Image img = tile.getImage();
918 if (!imageLoaded(img))
919 return null;
920 return img;
921 }
922
923 private Rectangle tileToRect(Tile t1) {
924 /*
925 * We need to get a box in which to draw, so advance by one tile in
926 * each direction to find the other corner of the box.
927 * Note: this somewhat pollutes the tile cache
928 */
929 Tile t2 = tempCornerTile(t1);
930 Rectangle rect = new Rectangle(pixelPos(t1));
931 rect.add(pixelPos(t2));
932 return rect;
933 }
934
935 // 'source' is the pixel coordinates for the area that
936 // the img is capable of filling in. However, we probably
937 // only want a portion of it.
938 //
939 // 'border' is the screen cordinates that need to be drawn.
940 // We must not draw outside of it.
941 private void drawImageInside(Graphics g, Image sourceImg, Rectangle source, Rectangle border) {
942 Rectangle target = source;
943
944 // If a border is specified, only draw the intersection
945 // if what we have combined with what we are supposed to draw.
946 if (border != null) {
947 target = source.intersection(border);
948 if (Main.isDebugEnabled()) {
949 Main.debug("source: " + source + "\nborder: " + border + "\nintersection: " + target);
950 }
951 }
952
953 // All of the rectangles are in screen coordinates. We need
954 // to how these correlate to the sourceImg pixels. We could
955 // avoid doing this by scaling the image up to the 'source' size,
956 // but this should be cheaper.
957 //
958 // In some projections, x any y are scaled differently enough to
959 // cause a pixel or two of fudge. Calculate them separately.
960 double imageYScaling = sourceImg.getHeight(this) / source.getHeight();
961 double imageXScaling = sourceImg.getWidth(this) / source.getWidth();
962
963 // How many pixels into the 'source' rectangle are we drawing?
964 int screen_x_offset = target.x - source.x;
965 int screen_y_offset = target.y - source.y;
966 // And how many pixels into the image itself does that correlate to?
967 int img_x_offset = (int) (screen_x_offset * imageXScaling + 0.5);
968 int img_y_offset = (int) (screen_y_offset * imageYScaling + 0.5);
969 // Now calculate the other corner of the image that we need
970 // by scaling the 'target' rectangle's dimensions.
971 int img_x_end = img_x_offset + (int) (target.getWidth() * imageXScaling + 0.5);
972 int img_y_end = img_y_offset + (int) (target.getHeight() * imageYScaling + 0.5);
973
974 if (Main.isDebugEnabled()) {
975 Main.debug("drawing image into target rect: " + target);
976 }
977 g.drawImage(sourceImg,
978 target.x, target.y,
979 target.x + target.width, target.y + target.height,
980 img_x_offset, img_y_offset,
981 img_x_end, img_y_end,
982 this);
983 if (PROP_FADE_AMOUNT.get() != 0) {
984 // dimm by painting opaque rect...
985 g.setColor(getFadeColorWithAlpha());
986 g.fillRect(target.x, target.y,
987 target.width, target.height);
988 }
989 }
990
991 // This function is called for several zoom levels, not just
992 // the current one. It should not trigger any tiles to be
993 // downloaded. It should also avoid polluting the tile cache
994 // with any tiles since these tiles are not mandatory.
995 //
996 // The "border" tile tells us the boundaries of where we may
997 // draw. It will not be from the zoom level that is being
998 // drawn currently. If drawing the displayZoomLevel,
999 // border is null and we draw the entire tile set.
1000 private List<Tile> paintTileImages(Graphics g, TileSet ts, int zoom, Tile border) {
1001 if (zoom <= 0) return Collections.emptyList();
1002 Rectangle borderRect = null;
1003 if (border != null) {
1004 borderRect = tileToRect(border);
1005 }
1006 List<Tile> missedTiles = new LinkedList<>();
1007 // The callers of this code *require* that we return any tiles
1008 // that we do not draw in missedTiles. ts.allExistingTiles() by
1009 // default will only return already-existing tiles. However, we
1010 // need to return *all* tiles to the callers, so force creation here.
1011 for (Tile tile : ts.allTilesCreate()) {
1012 Image img = getLoadedTileImage(tile);
1013 if (img == null || tile.hasError()) {
1014 if (Main.isDebugEnabled()) {
1015 Main.debug("missed tile: " + tile);
1016 }
1017 missedTiles.add(tile);
1018 continue;
1019 }
1020
1021 // applying all filters to this layer
1022 img = applyImageProcessors((BufferedImage) img);
1023
1024 Rectangle sourceRect = tileToRect(tile);
1025 if (borderRect != null && !sourceRect.intersects(borderRect)) {
1026 continue;
1027 }
1028 drawImageInside(g, img, sourceRect, borderRect);
1029 }
1030 return missedTiles;
1031 }
1032
1033 private void myDrawString(Graphics g, String text, int x, int y) {
1034 Color oldColor = g.getColor();
1035 g.setColor(Color.black);
1036 g.drawString(text, x+1, y+1);
1037 g.setColor(oldColor);
1038 g.drawString(text, x, y);
1039 }
1040
1041 private void paintTileText(TileSet ts, Tile tile, Graphics g, MapView mv, int zoom, Tile t) {
1042 int fontHeight = g.getFontMetrics().getHeight();
1043 if (tile == null)
1044 return;
1045 Point p = pixelPos(t);
1046 int texty = p.y + 2 + fontHeight;
1047
1048 /*if (PROP_DRAW_DEBUG.get()) {
1049 myDrawString(g, "x=" + t.getXtile() + " y=" + t.getYtile() + " z=" + zoom + "", p.x + 2, texty);
1050 texty += 1 + fontHeight;
1051 if ((t.getXtile() % 32 == 0) && (t.getYtile() % 32 == 0)) {
1052 myDrawString(g, "x=" + t.getXtile() / 32 + " y=" + t.getYtile() / 32 + " z=7", p.x + 2, texty);
1053 texty += 1 + fontHeight;
1054 }
1055 }*/
1056
1057 /*String tileStatus = tile.getStatus();
1058 if (!tile.isLoaded() && PROP_DRAW_DEBUG.get()) {
1059 myDrawString(g, tr("image " + tileStatus), p.x + 2, texty);
1060 texty += 1 + fontHeight;
1061 }*/
1062
1063 if (tile.hasError() && showErrors) {
1064 myDrawString(g, tr("Error") + ": " + tr(tile.getErrorMessage()), p.x + 2, texty);
1065 //texty += 1 + fontHeight;
1066 }
1067
1068 int xCursor = -1;
1069 int yCursor = -1;
1070 if (Main.isDebugEnabled()) {
1071 if (yCursor < t.getYtile()) {
1072 if (t.getYtile() % 32 == 31) {
1073 g.fillRect(0, p.y - 1, mv.getWidth(), 3);
1074 } else {
1075 g.drawLine(0, p.y, mv.getWidth(), p.y);
1076 }
1077 //yCursor = t.getYtile();
1078 }
1079 // This draws the vertical lines for the entire column. Only draw them for the top tile in the column.
1080 if (xCursor < t.getXtile()) {
1081 if (t.getXtile() % 32 == 0) {
1082 // level 7 tile boundary
1083 g.fillRect(p.x - 1, 0, 3, mv.getHeight());
1084 } else {
1085 g.drawLine(p.x, 0, p.x, mv.getHeight());
1086 }
1087 //xCursor = t.getXtile();
1088 }
1089 }
1090 }
1091
1092 private Point pixelPos(LatLon ll) {
1093 return Main.map.mapView.getPoint(Main.getProjection().latlon2eastNorth(ll).add(getDx(), getDy()));
1094 }
1095
1096 private Point pixelPos(Tile t) {
1097 ICoordinate coord = tileSource.tileXYToLatLon(t);
1098 return pixelPos(new LatLon(coord));
1099 }
1100
1101 private LatLon getShiftedLatLon(EastNorth en) {
1102 return Main.getProjection().eastNorth2latlon(en.add(-getDx(), -getDy()));
1103 }
1104
1105 private ICoordinate getShiftedCoord(EastNorth en) {
1106 return getShiftedLatLon(en).toCoordinate();
1107 }
1108
1109 private final TileSet nullTileSet = new TileSet((LatLon) null, (LatLon) null, 0);
1110 private final class TileSet {
1111 int x0, x1, y0, y1;
1112 int zoom;
1113
1114 /**
1115 * Create a TileSet by EastNorth bbox taking a layer shift in account
1116 */
1117 private TileSet(EastNorth topLeft, EastNorth botRight, int zoom) {
1118 this(getShiftedLatLon(topLeft), getShiftedLatLon(botRight), zoom);
1119 }
1120
1121 /**
1122 * Create a TileSet by known LatLon bbox without layer shift correction
1123 */
1124 private TileSet(LatLon topLeft, LatLon botRight, int zoom) {
1125 this.zoom = zoom;
1126 if (zoom == 0)
1127 return;
1128
1129 TileXY t1 = tileSource.latLonToTileXY(topLeft.toCoordinate(), zoom);
1130 TileXY t2 = tileSource.latLonToTileXY(botRight.toCoordinate(), zoom);
1131
1132 x0 = t1.getXIndex();
1133 y0 = t1.getYIndex();
1134 x1 = t2.getXIndex();
1135 y1 = t2.getYIndex();
1136
1137 if (x0 > x1) {
1138 int tmp = x0;
1139 x0 = x1;
1140 x1 = tmp;
1141 }
1142 if (y0 > y1) {
1143 int tmp = y0;
1144 y0 = y1;
1145 y1 = tmp;
1146 }
1147
1148 if (x0 < tileSource.getTileXMin(zoom)) {
1149 x0 = tileSource.getTileXMin(zoom);
1150 }
1151 if (y0 < tileSource.getTileYMin(zoom)) {
1152 y0 = tileSource.getTileYMin(zoom);
1153 }
1154 if (x1 > tileSource.getTileXMax(zoom)) {
1155 x1 = tileSource.getTileXMax(zoom);
1156 }
1157 if (y1 > tileSource.getTileYMax(zoom)) {
1158 y1 = tileSource.getTileYMax(zoom);
1159 }
1160 }
1161
1162 private boolean tooSmall() {
1163 return this.tilesSpanned() < 2.1;
1164 }
1165
1166 private boolean tooLarge() {
1167 return this.tilesSpanned() > 10;
1168 }
1169
1170 private boolean insane() {
1171 return this.tilesSpanned() > 100;
1172 }
1173
1174 private double tilesSpanned() {
1175 return Math.sqrt(1.0 * this.size());
1176 }
1177
1178 private int size() {
1179 int xSpan = x1 - x0 + 1;
1180 int ySpan = y1 - y0 + 1;
1181 return xSpan * ySpan;
1182 }
1183
1184 /*
1185 * Get all tiles represented by this TileSet that are
1186 * already in the tileCache.
1187 */
1188 private List<Tile> allExistingTiles() {
1189 return this.__allTiles(false);
1190 }
1191
1192 private List<Tile> allTilesCreate() {
1193 return this.__allTiles(true);
1194 }
1195
1196 private List<Tile> __allTiles(boolean create) {
1197 // Tileset is either empty or too large
1198 if (zoom == 0 || this.insane())
1199 return Collections.emptyList();
1200 List<Tile> ret = new ArrayList<>();
1201 for (int x = x0; x <= x1; x++) {
1202 for (int y = y0; y <= y1; y++) {
1203 Tile t;
1204 if (create) {
1205 t = getOrCreateTile(x, y , zoom);
1206 } else {
1207 t = getTile(x, y, zoom);
1208 }
1209 if (t != null) {
1210 ret.add(t);
1211 }
1212 }
1213 }
1214 return ret;
1215 }
1216
1217 private List<Tile> allLoadedTiles() {
1218 List<Tile> ret = new ArrayList<>();
1219 for (Tile t : this.allExistingTiles()) {
1220 if (t.isLoaded())
1221 ret.add(t);
1222 }
1223 return ret;
1224 }
1225
1226 /**
1227 * @return comparator, that sorts the tiles from the center to the edge of the current screen
1228 */
1229 private Comparator<Tile> getTileDistanceComparator() {
1230 final int centerX = (int) Math.ceil((x0 + x1) / 2d);
1231 final int centerY = (int) Math.ceil((y0 + y1) / 2d);
1232 return new Comparator<Tile>() {
1233 private int getDistance(Tile t) {
1234 return Math.abs(t.getXtile() - centerX) + Math.abs(t.getYtile() - centerY);
1235 }
1236
1237 @Override
1238 public int compare(Tile o1, Tile o2) {
1239 int distance1 = getDistance(o1);
1240 int distance2 = getDistance(o2);
1241 return Integer.compare(distance1, distance2);
1242 }
1243 };
1244 }
1245
1246 private void loadAllTiles(boolean force) {
1247 if (!autoLoad && !force)
1248 return;
1249 List<Tile> allTiles = allTilesCreate();
1250 Collections.sort(allTiles, getTileDistanceComparator());
1251 for (Tile t : allTiles) {
1252 loadTile(t, force);
1253 }
1254 }
1255
1256 private void loadAllErrorTiles(boolean force) {
1257 if (!autoLoad && !force)
1258 return;
1259 for (Tile t : this.allTilesCreate()) {
1260 if (t.hasError()) {
1261 loadTile(t, true);
1262 }
1263 }
1264 }
1265 }
1266
1267 private static class TileSetInfo {
1268 public boolean hasVisibleTiles = false;
1269 public boolean hasOverzoomedTiles = false;
1270 public boolean hasLoadingTiles = false;
1271 }
1272
1273 private static TileSetInfo getTileSetInfo(TileSet ts) {
1274 List<Tile> allTiles = ts.allExistingTiles();
1275 TileSetInfo result = new TileSetInfo();
1276 result.hasLoadingTiles = allTiles.size() < ts.size();
1277 for (Tile t : allTiles) {
1278 if (t.isLoaded()) {
1279 if (!t.hasError()) {
1280 result.hasVisibleTiles = true;
1281 }
1282 if ("no-tile".equals(t.getValue("tile-info"))) {
1283 result.hasOverzoomedTiles = true;
1284 }
1285 } else {
1286 result.hasLoadingTiles = true;
1287 }
1288 }
1289 return result;
1290 }
1291
1292 private class DeepTileSet {
1293 private final EastNorth topLeft, botRight;
1294 private final int minZoom, maxZoom;
1295 private final TileSet[] tileSets;
1296 private final TileSetInfo[] tileSetInfos;
1297 public DeepTileSet(EastNorth topLeft, EastNorth botRight, int minZoom, int maxZoom) {
1298 this.topLeft = topLeft;
1299 this.botRight = botRight;
1300 this.minZoom = minZoom;
1301 this.maxZoom = maxZoom;
1302 this.tileSets = new TileSet[maxZoom - minZoom + 1];
1303 this.tileSetInfos = new TileSetInfo[maxZoom - minZoom + 1];
1304 }
1305
1306 public TileSet getTileSet(int zoom) {
1307 if (zoom < minZoom)
1308 return nullTileSet;
1309 synchronized (tileSets) {
1310 TileSet ts = tileSets[zoom-minZoom];
1311 if (ts == null) {
1312 ts = new TileSet(topLeft, botRight, zoom);
1313 tileSets[zoom-minZoom] = ts;
1314 }
1315 return ts;
1316 }
1317 }
1318
1319 public TileSetInfo getTileSetInfo(int zoom) {
1320 if (zoom < minZoom)
1321 return new TileSetInfo();
1322 synchronized (tileSetInfos) {
1323 TileSetInfo tsi = tileSetInfos[zoom-minZoom];
1324 if (tsi == null) {
1325 tsi = AbstractTileSourceLayer.getTileSetInfo(getTileSet(zoom));
1326 tileSetInfos[zoom-minZoom] = tsi;
1327 }
1328 return tsi;
1329 }
1330 }
1331 }
1332
1333 @Override
1334 public void paint(Graphics2D g, MapView mv, Bounds bounds) {
1335 EastNorth topLeft = mv.getEastNorth(0, 0);
1336 EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
1337
1338 if (botRight.east() == 0 || botRight.north() == 0) {
1339 /*Main.debug("still initializing??");*/
1340 // probably still initializing
1341 return;
1342 }
1343
1344 needRedraw = false;
1345
1346 int zoom = currentZoomLevel;
1347 if (autoZoom) {
1348 double pixelScaling = getScaleFactor(zoom);
1349 if (pixelScaling > 3 || pixelScaling < 0.7) {
1350 zoom = getBestZoom();
1351 }
1352 }
1353
1354 DeepTileSet dts = new DeepTileSet(topLeft, botRight, getMinZoomLvl(), zoom);
1355 TileSet ts = dts.getTileSet(zoom);
1356
1357 int displayZoomLevel = zoom;
1358
1359 boolean noTilesAtZoom = false;
1360 if (autoZoom && autoLoad) {
1361 // Auto-detection of tilesource maxzoom (currently fully works only for Bing)
1362 TileSetInfo tsi = dts.getTileSetInfo(zoom);
1363 if (!tsi.hasVisibleTiles && (!tsi.hasLoadingTiles || tsi.hasOverzoomedTiles)) {
1364 noTilesAtZoom = true;
1365 }
1366 // Find highest zoom level with at least one visible tile
1367 for (int tmpZoom = zoom; tmpZoom > dts.minZoom; tmpZoom--) {
1368 if (dts.getTileSetInfo(tmpZoom).hasVisibleTiles) {
1369 displayZoomLevel = tmpZoom;
1370 break;
1371 }
1372 }
1373 // Do binary search between currentZoomLevel and displayZoomLevel
1374 while (zoom > displayZoomLevel && !tsi.hasVisibleTiles && tsi.hasOverzoomedTiles) {
1375 zoom = (zoom + displayZoomLevel)/2;
1376 tsi = dts.getTileSetInfo(zoom);
1377 }
1378
1379 setZoomLevel(zoom);
1380
1381 // If all tiles at displayZoomLevel is loaded, load all tiles at next zoom level
1382 // to make sure there're really no more zoom levels
1383 if (zoom == displayZoomLevel && !tsi.hasLoadingTiles && zoom < dts.maxZoom) {
1384 zoom++;
1385 tsi = dts.getTileSetInfo(zoom);
1386 }
1387 // When we have overzoomed tiles and all tiles at current zoomlevel is loaded,
1388 // load tiles at previovus zoomlevels until we have all tiles on screen is loaded.
1389 while (zoom > dts.minZoom && tsi.hasOverzoomedTiles && !tsi.hasLoadingTiles) {
1390 zoom--;
1391 tsi = dts.getTileSetInfo(zoom);
1392 }
1393 ts = dts.getTileSet(zoom);
1394 } else if (autoZoom) {
1395 setZoomLevel(zoom);
1396 }
1397
1398 // Too many tiles... refuse to download
1399 if (!ts.tooLarge()) {
1400 //Main.debug("size: " + ts.size() + " spanned: " + ts.tilesSpanned());
1401 ts.loadAllTiles(false);
1402 }
1403
1404 if (displayZoomLevel != zoom) {
1405 ts = dts.getTileSet(displayZoomLevel);
1406 }
1407
1408 g.setColor(Color.DARK_GRAY);
1409
1410 List<Tile> missedTiles = this.paintTileImages(g, ts, displayZoomLevel, null);
1411 int[] otherZooms = {-1, 1, -2, 2, -3, -4, -5};
1412 for (int zoomOffset : otherZooms) {
1413 if (!autoZoom) {
1414 break;
1415 }
1416 int newzoom = displayZoomLevel + zoomOffset;
1417 if (newzoom < getMinZoomLvl() || newzoom > getMaxZoomLvl()) {
1418 continue;
1419 }
1420 if (missedTiles.isEmpty()) {
1421 break;
1422 }
1423 List<Tile> newlyMissedTiles = new LinkedList<>();
1424 for (Tile missed : missedTiles) {
1425 if ("no-tile".equals(missed.getValue("tile-info")) && zoomOffset > 0) {
1426 // Don't try to paint from higher zoom levels when tile is overzoomed
1427 newlyMissedTiles.add(missed);
1428 continue;
1429 }
1430 Tile t2 = tempCornerTile(missed);
1431 LatLon topLeft2 = new LatLon(tileSource.tileXYToLatLon(missed));
1432 LatLon botRight2 = new LatLon(tileSource.tileXYToLatLon(t2));
1433 TileSet ts2 = new TileSet(topLeft2, botRight2, newzoom);
1434 // Instantiating large TileSets is expensive. If there
1435 // are no loaded tiles, don't bother even trying.
1436 if (ts2.allLoadedTiles().isEmpty()) {
1437 newlyMissedTiles.add(missed);
1438 continue;
1439 }
1440 if (ts2.tooLarge()) {
1441 continue;
1442 }
1443 newlyMissedTiles.addAll(this.paintTileImages(g, ts2, newzoom, missed));
1444 }
1445 missedTiles = newlyMissedTiles;
1446 }
1447 if (Main.isDebugEnabled() && !missedTiles.isEmpty()) {
1448 Main.debug("still missed "+missedTiles.size()+" in the end");
1449 }
1450 g.setColor(Color.red);
1451 g.setFont(InfoFont);
1452
1453 // The current zoom tileset should have all of its tiles due to the loadAllTiles(), unless it to tooLarge()
1454 for (Tile t : ts.allExistingTiles()) {
1455 this.paintTileText(ts, t, g, mv, displayZoomLevel, t);
1456 }
1457
1458 attribution.paintAttribution(g, mv.getWidth(), mv.getHeight(), getShiftedCoord(topLeft), getShiftedCoord(botRight),
1459 displayZoomLevel, this);
1460
1461 //g.drawString("currentZoomLevel=" + currentZoomLevel, 120, 120);
1462 g.setColor(Color.lightGray);
1463 if (!autoZoom) {
1464 if (ts.insane()) {
1465 myDrawString(g, tr("zoom in to load any tiles"), 120, 120);
1466 } else if (ts.tooLarge()) {
1467 myDrawString(g, tr("zoom in to load more tiles"), 120, 120);
1468 } else if (ts.tooSmall()) {
1469 myDrawString(g, tr("increase zoom level to see more detail"), 120, 120);
1470 }
1471 }
1472
1473 if (zoom < getMinZoomLvl() && (ts.insane() || ts.tooLarge())) {
1474 myDrawString(g, tr("zoom in to load any tiles"), 120, 120);
1475 }
1476
1477 if (noTilesAtZoom) {
1478 myDrawString(g, tr("No tiles at this zoom level"), 120, 120);
1479 }
1480 if (Main.isDebugEnabled()) {
1481 myDrawString(g, tr("Current zoom: {0}", currentZoomLevel), 50, 140);
1482 myDrawString(g, tr("Display zoom: {0}", displayZoomLevel), 50, 155);
1483 myDrawString(g, tr("Pixel scale: {0}", getScaleFactor(currentZoomLevel)), 50, 170);
1484 myDrawString(g, tr("Best zoom: {0}", getBestZoom()), 50, 185);
1485 if (tileLoader instanceof TMSCachedTileLoader) {
1486 TMSCachedTileLoader cachedTileLoader = (TMSCachedTileLoader) tileLoader;
1487 int offset = 185;
1488 for (String part: cachedTileLoader.getStats().split("\n")) {
1489 myDrawString(g, tr("Cache stats: {0}", part), 50, offset += 15);
1490 }
1491
1492 }
1493 }
1494 }
1495
1496 /**
1497 * This isn't very efficient, but it is only used when the
1498 * user right-clicks on the map.
1499 */
1500 private Tile getTileForPixelpos(int px, int py) {
1501 if (Main.isDebugEnabled()) {
1502 Main.debug("getTileForPixelpos("+px+", "+py+")");
1503 }
1504 MapView mv = Main.map.mapView;
1505 Point clicked = new Point(px, py);
1506 EastNorth topLeft = mv.getEastNorth(0, 0);
1507 EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
1508 int z = currentZoomLevel;
1509 TileSet ts = new TileSet(topLeft, botRight, z);
1510
1511 if (!ts.tooLarge()) {
1512 ts.loadAllTiles(false); // make sure there are tile objects for all tiles
1513 }
1514 Tile clickedTile = null;
1515 for (Tile t1 : ts.allExistingTiles()) {
1516 Tile t2 = tempCornerTile(t1);
1517 Rectangle r = new Rectangle(pixelPos(t1));
1518 r.add(pixelPos(t2));
1519 if (Main.isDebugEnabled()) {
1520 Main.debug("r: " + r + " clicked: " + clicked);
1521 }
1522 if (!r.contains(clicked)) {
1523 continue;
1524 }
1525 clickedTile = t1;
1526 break;
1527 }
1528 if (clickedTile == null)
1529 return null;
1530 /*Main.debug("Clicked on tile: " + clickedTile.getXtile() + " " + clickedTile.getYtile() +
1531 " currentZoomLevel: " + currentZoomLevel);*/
1532 return clickedTile;
1533 }
1534
1535 @Override
1536 public Action[] getMenuEntries() {
1537 return new Action[] {
1538 LayerListDialog.getInstance().createActivateLayerAction(this),
1539 LayerListDialog.getInstance().createShowHideLayerAction(),
1540 LayerListDialog.getInstance().createDeleteLayerAction(),
1541 SeparatorLayerAction.INSTANCE,
1542 // color,
1543 new OffsetAction(),
1544 new RenameLayerAction(this.getAssociatedFile(), this),
1545 SeparatorLayerAction.INSTANCE,
1546 new AutoLoadTilesAction(),
1547 new AutoZoomAction(),
1548 new ZoomToBestAction(),
1549 new ZoomToNativeLevelAction(),
1550 new LoadErroneusTilesAction(),
1551 new LoadAllTilesAction(),
1552 new LayerListPopup.InfoAction(this)
1553 };
1554 }
1555
1556 @Override
1557 public String getToolTipText() {
1558 if (autoLoad) {
1559 return tr("{0} ({1}), automatically downloading in zoom {2}", this.getClass().getSimpleName(), getName(), currentZoomLevel);
1560 } else {
1561 return tr("{0} ({1}), downloading in zoom {2}", this.getClass().getSimpleName(), getName(), currentZoomLevel);
1562 }
1563 }
1564
1565 @Override
1566 public void visitBoundingBox(BoundingXYVisitor v) {
1567 }
1568
1569 @Override
1570 public boolean isChanged() {
1571 return needRedraw;
1572 }
1573
1574 /**
1575 * Task responsible for precaching imagery along the gpx track
1576 *
1577 */
1578 public class PrecacheTask implements TileLoaderListener {
1579 private final ProgressMonitor progressMonitor;
1580 private int totalCount;
1581 private AtomicInteger processedCount = new AtomicInteger(0);
1582 private final TileLoader tileLoader;
1583
1584 /**
1585 * @param progressMonitor that will be notified about progess of the task
1586 */
1587 public PrecacheTask(ProgressMonitor progressMonitor) {
1588 this.progressMonitor = progressMonitor;
1589 this.tileLoader = getTileLoaderFactory().makeTileLoader(this, getHeaders(tileSource));
1590 if (this.tileLoader instanceof TMSCachedTileLoader) {
1591 ((TMSCachedTileLoader) this.tileLoader).setDownloadExecutor(
1592 TMSCachedTileLoader.getNewThreadPoolExecutor("Precache downloader"));
1593 }
1594
1595 }
1596
1597 /**
1598 * @return true, if all is done
1599 */
1600 public boolean isFinished() {
1601 return processedCount.get() >= totalCount;
1602 }
1603
1604 /**
1605 * @return total number of tiles to download
1606 */
1607 public int getTotalCount() {
1608 return totalCount;
1609 }
1610
1611 /**
1612 * cancel the task
1613 */
1614 public void cancel() {
1615 if (tileLoader instanceof TMSCachedTileLoader) {
1616 ((TMSCachedTileLoader) tileLoader).cancelOutstandingTasks();
1617 }
1618 }
1619
1620 @Override
1621 public void tileLoadingFinished(Tile tile, boolean success) {
1622 if (success) {
1623 int processed = this.processedCount.incrementAndGet();
1624 this.progressMonitor.worked(1);
1625 this.progressMonitor.setCustomText(tr("Downloaded {0}/{1} tiles", processed, totalCount));
1626 }
1627 }
1628
1629 /**
1630 * @return tile loader that is used to load the tiles
1631 */
1632 public TileLoader getTileLoader() {
1633 return tileLoader;
1634 }
1635 }
1636
1637 /**
1638 * Calculates tiles, that needs to be downloaded to cache, gets a current tile loader and creates a task to download
1639 * all of the tiles. Buffer contains at least one tile.
1640 *
1641 * To prevent accidental clear of the queue, new download executor is created with separate queue
1642 *
1643 * @param precacheTask Task responsible for precaching imagery
1644 * @param points lat/lon coordinates to download
1645 * @param bufferX how many units in current Coordinate Reference System to cover in X axis in both sides
1646 * @param bufferY how many units in current Coordinate Reference System to cover in Y axis in both sides
1647 */
1648 public void downloadAreaToCache(final PrecacheTask precacheTask, List<LatLon> points, double bufferX, double bufferY) {
1649 final Set<Tile> requestedTiles = new ConcurrentSkipListSet<>(new Comparator<Tile>() {
1650 public int compare(Tile o1, Tile o2) {
1651 return String.CASE_INSENSITIVE_ORDER.compare(o1.getKey(), o2.getKey());
1652 }
1653 });
1654 for (LatLon point: points) {
1655
1656 TileXY minTile = tileSource.latLonToTileXY(point.lat() - bufferY, point.lon() - bufferX, currentZoomLevel);
1657 TileXY curTile = tileSource.latLonToTileXY(point.toCoordinate(), currentZoomLevel);
1658 TileXY maxTile = tileSource.latLonToTileXY(point.lat() + bufferY, point.lon() + bufferX, currentZoomLevel);
1659
1660 // take at least one tile of buffer
1661 int minY = Math.min(curTile.getYIndex() - 1, minTile.getYIndex());
1662 int maxY = Math.max(curTile.getYIndex() + 1, maxTile.getYIndex());
1663 int minX = Math.min(curTile.getXIndex() - 1, minTile.getXIndex());
1664 int maxX = Math.min(curTile.getXIndex() + 1, minTile.getXIndex());
1665
1666 for (int x = minX; x <= maxX; x++) {
1667 for (int y = minY; y <= maxY; y++) {
1668 requestedTiles.add(new Tile(tileSource, x, y, currentZoomLevel));
1669 }
1670 }
1671 }
1672
1673 precacheTask.totalCount = requestedTiles.size();
1674 precacheTask.progressMonitor.setTicksCount(requestedTiles.size());
1675
1676 TileLoader loader = precacheTask.getTileLoader();
1677 for (Tile t: requestedTiles) {
1678 loader.createTileLoaderJob(t).submit();
1679 }
1680 }
1681
1682 @Override
1683 public boolean isSavable() {
1684 return true; // With WMSLayerExporter
1685 }
1686
1687 @Override
1688 public File createAndOpenSaveFileChooser() {
1689 return SaveActionBase.createAndOpenSaveFileChooser(tr("Save WMS file"), WMSLayerImporter.FILE_FILTER);
1690 }
1691}
Note: See TracBrowser for help on using the repository browser.