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

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

findbugs - UPM_UNCALLED_PRIVATE_METHOD

  • Property svn:eol-style set to native
File size: 73.2 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.Dimension;
9import java.awt.Font;
10import java.awt.Graphics;
11import java.awt.Graphics2D;
12import java.awt.GridBagLayout;
13import java.awt.Image;
14import java.awt.Point;
15import java.awt.Shape;
16import java.awt.Toolkit;
17import java.awt.event.ActionEvent;
18import java.awt.event.MouseAdapter;
19import java.awt.event.MouseEvent;
20import java.awt.geom.AffineTransform;
21import java.awt.geom.Point2D;
22import java.awt.geom.Rectangle2D;
23import java.awt.image.BufferedImage;
24import java.awt.image.ImageObserver;
25import java.io.File;
26import java.io.IOException;
27import java.net.MalformedURLException;
28import java.net.URL;
29import java.text.SimpleDateFormat;
30import java.util.ArrayList;
31import java.util.Arrays;
32import java.util.Collection;
33import java.util.Collections;
34import java.util.Comparator;
35import java.util.Date;
36import java.util.LinkedList;
37import java.util.List;
38import java.util.Map;
39import java.util.Map.Entry;
40import java.util.Objects;
41import java.util.Set;
42import java.util.concurrent.ConcurrentSkipListSet;
43import java.util.concurrent.atomic.AtomicInteger;
44import java.util.function.Consumer;
45import java.util.function.Function;
46import java.util.stream.Collectors;
47import java.util.stream.IntStream;
48import java.util.stream.Stream;
49
50import javax.swing.AbstractAction;
51import javax.swing.Action;
52import javax.swing.JCheckBoxMenuItem;
53import javax.swing.JLabel;
54import javax.swing.JMenuItem;
55import javax.swing.JOptionPane;
56import javax.swing.JPanel;
57import javax.swing.JPopupMenu;
58import javax.swing.JSeparator;
59import javax.swing.Timer;
60
61import org.openstreetmap.gui.jmapviewer.AttributionSupport;
62import org.openstreetmap.gui.jmapviewer.MemoryTileCache;
63import org.openstreetmap.gui.jmapviewer.OsmTileLoader;
64import org.openstreetmap.gui.jmapviewer.Tile;
65import org.openstreetmap.gui.jmapviewer.TileRange;
66import org.openstreetmap.gui.jmapviewer.TileXY;
67import org.openstreetmap.gui.jmapviewer.interfaces.CachedTileLoader;
68import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
69import org.openstreetmap.gui.jmapviewer.interfaces.IProjected;
70import org.openstreetmap.gui.jmapviewer.interfaces.TemplatedTileSource;
71import org.openstreetmap.gui.jmapviewer.interfaces.TileCache;
72import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
73import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
74import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
75import org.openstreetmap.gui.jmapviewer.tilesources.AbstractTMSTileSource;
76import org.openstreetmap.josm.Main;
77import org.openstreetmap.josm.actions.ImageryAdjustAction;
78import org.openstreetmap.josm.actions.RenameLayerAction;
79import org.openstreetmap.josm.actions.SaveActionBase;
80import org.openstreetmap.josm.data.Bounds;
81import org.openstreetmap.josm.data.ProjectionBounds;
82import org.openstreetmap.josm.data.coor.EastNorth;
83import org.openstreetmap.josm.data.coor.LatLon;
84import org.openstreetmap.josm.data.imagery.ImageryInfo;
85import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader;
86import org.openstreetmap.josm.data.imagery.TileLoaderFactory;
87import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
88import org.openstreetmap.josm.data.preferences.IntegerProperty;
89import org.openstreetmap.josm.data.projection.Projection;
90import org.openstreetmap.josm.data.projection.Projections;
91import org.openstreetmap.josm.gui.ExtendedDialog;
92import org.openstreetmap.josm.gui.MapFrame;
93import org.openstreetmap.josm.gui.MapView;
94import org.openstreetmap.josm.gui.NavigatableComponent.ZoomChangeListener;
95import org.openstreetmap.josm.gui.PleaseWaitRunnable;
96import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
97import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
98import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings.FilterChangeListener;
99import org.openstreetmap.josm.gui.layer.imagery.ReprojectionTile;
100import org.openstreetmap.josm.gui.layer.imagery.TileAnchor;
101import org.openstreetmap.josm.gui.layer.imagery.TileCoordinateConverter;
102import org.openstreetmap.josm.gui.layer.imagery.TilePosition;
103import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings;
104import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings.DisplaySettingsChangeEvent;
105import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings.DisplaySettingsChangeListener;
106import org.openstreetmap.josm.gui.progress.ProgressMonitor;
107import org.openstreetmap.josm.gui.util.GuiHelper;
108import org.openstreetmap.josm.io.WMSLayerImporter;
109import org.openstreetmap.josm.tools.GBC;
110import org.openstreetmap.josm.tools.MemoryManager;
111import org.openstreetmap.josm.tools.MemoryManager.MemoryHandle;
112import org.openstreetmap.josm.tools.MemoryManager.NotEnoughMemoryException;
113import org.openstreetmap.josm.tools.Utils;
114
115/**
116 * Base abstract class that supports displaying images provided by TileSource. It might be TMS source, WMS or WMTS
117 * It implements all standard functions of tilesource based layers: autozoom, tile reloads, layer saving, loading,etc.
118 *
119 * @author Upliner
120 * @author Wiktor Niesiobędzki
121 * @param <T> Tile Source class used for this layer
122 * @since 3715
123 * @since 8526 (copied from TMSLayer)
124 */
125public abstract class AbstractTileSourceLayer<T extends AbstractTMSTileSource> extends ImageryLayer
126implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeListener, DisplaySettingsChangeListener {
127 private static final String PREFERENCE_PREFIX = "imagery.generic";
128 static { // Registers all setting properties
129 new TileSourceDisplaySettings();
130 }
131
132 /** maximum zoom level supported */
133 public static final int MAX_ZOOM = 30;
134 /** minium zoom level supported */
135 public static final int MIN_ZOOM = 2;
136 private static final Font InfoFont = new Font("sansserif", Font.BOLD, 13);
137
138 /** additional layer menu actions */
139 private static List<MenuAddition> menuAdditions = new LinkedList<>();
140
141 /** minimum zoom level to show to user */
142 public static final IntegerProperty PROP_MIN_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".min_zoom_lvl", 2);
143 /** maximum zoom level to show to user */
144 public static final IntegerProperty PROP_MAX_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".max_zoom_lvl", 20);
145
146 //public static final BooleanProperty PROP_DRAW_DEBUG = new BooleanProperty(PREFERENCE_PREFIX + ".draw_debug", false);
147 /** Zoomlevel at which tiles is currently downloaded. Initial zoom lvl is set to bestZoom */
148 public int currentZoomLevel;
149
150 private final AttributionSupport attribution = new AttributionSupport();
151 private final TileHolder clickedTileHolder = new TileHolder();
152
153 /**
154 * Offset between calculated zoom level and zoom level used to download and show tiles. Negative values will result in
155 * lower resolution of imagery useful in "retina" displays, positive values will result in higher resolution
156 */
157 public static final IntegerProperty ZOOM_OFFSET = new IntegerProperty(PREFERENCE_PREFIX + ".zoom_offset", 0);
158
159 /*
160 * use MemoryTileCache instead of tileLoader JCS cache, as tileLoader caches only content (byte[] of image)
161 * and MemoryTileCache caches whole Tile. This gives huge performance improvement when a lot of tiles are visible
162 * in MapView (for example - when limiting min zoom in imagery)
163 *
164 * Use per-layer tileCache instance, as the more layers there are, the more tiles needs to be cached
165 */
166 protected TileCache tileCache; // initialized together with tileSource
167 protected T tileSource;
168 protected TileLoader tileLoader;
169
170 /** A timer that is used to delay invalidation events if required. */
171 private final Timer invalidateLaterTimer = new Timer(100, e -> this.invalidate());
172
173 private final MouseAdapter adapter = new MouseAdapter() {
174 @Override
175 public void mouseClicked(MouseEvent e) {
176 if (!isVisible()) return;
177 if (e.getButton() == MouseEvent.BUTTON3) {
178 clickedTileHolder.setTile(getTileForPixelpos(e.getX(), e.getY()));
179 new TileSourceLayerPopup().show(e.getComponent(), e.getX(), e.getY());
180 } else if (e.getButton() == MouseEvent.BUTTON1) {
181 attribution.handleAttribution(e.getPoint(), true);
182 }
183 }
184 };
185
186 private final TileSourceDisplaySettings displaySettings = createDisplaySettings();
187
188 private final ImageryAdjustAction adjustAction = new ImageryAdjustAction(this);
189 // prepared to be moved to the painter
190 protected TileCoordinateConverter coordinateConverter;
191
192 /**
193 * Creates Tile Source based Imagery Layer based on Imagery Info
194 * @param info imagery info
195 */
196 public AbstractTileSourceLayer(ImageryInfo info) {
197 super(info);
198 setBackgroundLayer(true);
199 this.setVisible(true);
200 getFilterSettings().addFilterChangeListener(this);
201 getDisplaySettings().addSettingsChangeListener(this);
202 }
203
204 /**
205 * This method creates the {@link TileSourceDisplaySettings} object. Subclasses may implement it to e.g. change the prefix.
206 * @return The object.
207 * @since 10568
208 */
209 protected TileSourceDisplaySettings createDisplaySettings() {
210 return new TileSourceDisplaySettings();
211 }
212
213 /**
214 * Gets the {@link TileSourceDisplaySettings} instance associated with this tile source.
215 * @return The tile source display settings
216 * @since 10568
217 */
218 public TileSourceDisplaySettings getDisplaySettings() {
219 return displaySettings;
220 }
221
222 @Override
223 public void filterChanged() {
224 invalidate();
225 }
226
227 protected abstract TileLoaderFactory getTileLoaderFactory();
228
229 /**
230 * Get projections this imagery layer supports natively.
231 *
232 * For example projection of tiles that are downloaded from a server. Layer
233 * may support even more projections (by reprojecting the tiles), but with a
234 * certain loss in image quality and performance.
235 * @return projections this imagery layer supports natively; null if
236 * layer is projection agnostic.
237 */
238 public abstract Collection<String> getNativeProjections();
239
240 /**
241 * Creates and returns a new {@link TileSource} instance depending on {@link #info} specified in the constructor.
242 *
243 * @return TileSource for specified ImageryInfo
244 * @throws IllegalArgumentException when Imagery is not supported by layer
245 */
246 protected abstract T getTileSource();
247
248 protected Map<String, String> getHeaders(T tileSource) {
249 if (tileSource instanceof TemplatedTileSource) {
250 return ((TemplatedTileSource) tileSource).getHeaders();
251 }
252 return null;
253 }
254
255 protected void initTileSource(T tileSource) {
256 coordinateConverter = new TileCoordinateConverter(Main.map.mapView, tileSource, getDisplaySettings());
257 attribution.initialize(tileSource);
258
259 currentZoomLevel = getBestZoom();
260
261 Map<String, String> headers = getHeaders(tileSource);
262
263 tileLoader = getTileLoaderFactory().makeTileLoader(this, headers);
264
265 try {
266 if ("file".equalsIgnoreCase(new URL(tileSource.getBaseUrl()).getProtocol())) {
267 tileLoader = new OsmTileLoader(this);
268 }
269 } catch (MalformedURLException e) {
270 // ignore, assume that this is not a file
271 if (Main.isDebugEnabled()) {
272 Main.debug(e.getMessage());
273 }
274 }
275
276 if (tileLoader == null)
277 tileLoader = new OsmTileLoader(this, headers);
278
279 tileCache = new MemoryTileCache(estimateTileCacheSize());
280 }
281
282 @Override
283 public synchronized void tileLoadingFinished(Tile tile, boolean success) {
284 if (tile.hasError()) {
285 success = false;
286 tile.setImage(null);
287 }
288 tile.setLoaded(success);
289 invalidateLater();
290 if (Main.isDebugEnabled()) {
291 Main.debug("tileLoadingFinished() tile: " + tile + " success: " + success);
292 }
293 }
294
295 /**
296 * Clears the tile cache.
297 *
298 * If the current tileLoader is an instance of OsmTileLoader, a new
299 * TmsTileClearController is created and passed to the according clearCache method.
300 */
301 public void clearTileCache() {
302 if (tileLoader instanceof CachedTileLoader) {
303 ((CachedTileLoader) tileLoader).clearCache(tileSource);
304 }
305 tileCache.clear();
306 }
307
308 /**
309 * Initiates a repaint of Main.map
310 *
311 * @see Main#map
312 * @see MapFrame#repaint()
313 * @see #invalidate() To trigger a repaint of all places where the layer is displayed.
314 */
315 protected void redraw() {
316 invalidate();
317 }
318
319 /**
320 * {@inheritDoc}
321 * @deprecated Use {@link TileSourceDisplaySettings#getDx()}
322 */
323 @Override
324 @Deprecated
325 public double getDx() {
326 return getDisplaySettings().getDx();
327 }
328
329 /**
330 * {@inheritDoc}
331 * @deprecated Use {@link TileSourceDisplaySettings#getDy()}
332 */
333 @Override
334 @Deprecated
335 public double getDy() {
336 return getDisplaySettings().getDy();
337 }
338
339 /**
340 * {@inheritDoc}
341 * @deprecated Use {@link TileSourceDisplaySettings}
342 */
343 @Override
344 @Deprecated
345 public void displace(double dx, double dy) {
346 getDisplaySettings().addDisplacement(new EastNorth(dx, dy));
347 }
348
349 /**
350 * {@inheritDoc}
351 * @deprecated Use {@link TileSourceDisplaySettings}
352 */
353 @Override
354 @Deprecated
355 public void setOffset(double dx, double dy) {
356 getDisplaySettings().setDisplacement(new EastNorth(dx, dy));
357 }
358
359 @Override
360 public Object getInfoComponent() {
361 JPanel panel = (JPanel) super.getInfoComponent();
362 List<List<String>> content = new ArrayList<>();
363 Collection<String> nativeProjections = getNativeProjections();
364 if (nativeProjections != null) {
365 content.add(Arrays.asList(tr("Native projections"), Utils.join(", ", getNativeProjections())));
366 }
367 EastNorth offset = getDisplaySettings().getDisplacement();
368 if (offset.distanceSq(0, 0) > 1e-10) {
369 content.add(Arrays.asList(tr("Offset"), offset.east() + ";" + offset.north()));
370 }
371 if (coordinateConverter.requiresReprojection()) {
372 content.add(Arrays.asList(tr("Tile download projection"), tileSource.getServerCRS()));
373 content.add(Arrays.asList(tr("Tile display projection"), Main.getProjection().toCode()));
374 }
375 content.add(Arrays.asList(tr("Current zoom"), Integer.toString(currentZoomLevel)));
376 for (List<String> entry: content) {
377 panel.add(new JLabel(entry.get(0) + ':'), GBC.std());
378 panel.add(GBC.glue(5, 0), GBC.std());
379 panel.add(createTextField(entry.get(1)), GBC.eol().fill(GBC.HORIZONTAL));
380 }
381 return panel;
382 }
383
384 @Override
385 protected Action getAdjustAction() {
386 return adjustAction;
387 }
388
389 /**
390 * Returns average number of screen pixels per tile pixel for current mapview
391 * @param zoom zoom level
392 * @return average number of screen pixels per tile pixel
393 */
394 protected double getScaleFactor(int zoom) {
395 if (coordinateConverter != null) {
396 return coordinateConverter.getScaleFactor(zoom);
397 } else {
398 return 1;
399 }
400 }
401
402 protected int getBestZoom() {
403 double factor = getScaleFactor(1); // check the ratio between area of tilesize at zoom 1 to current view
404 double result = Math.log(factor)/Math.log(2)/2;
405 /*
406 * Math.log(factor)/Math.log(2) - gives log base 2 of factor
407 * We divide result by 2, as factor contains ratio between areas. We could do Math.sqrt before log, or just divide log by 2
408 *
409 * ZOOM_OFFSET controls, whether we work with overzoomed or underzoomed tiles. Positive ZOOM_OFFSET
410 * is for working with underzoomed tiles (higher quality when working with aerial imagery), negative ZOOM_OFFSET
411 * is for working with overzoomed tiles (big, pixelated), which is good when working with high-dpi screens and/or
412 * maps as a imagery layer
413 */
414 int intResult = (int) Math.round(result + 1 + ZOOM_OFFSET.get() / 1.9);
415 intResult = Utils.clamp(intResult, getMinZoomLvl(), getMaxZoomLvl());
416 return intResult;
417 }
418
419 private static boolean actionSupportLayers(List<Layer> layers) {
420 return layers.size() == 1 && layers.get(0) instanceof TMSLayer;
421 }
422
423 private final class ShowTileInfoAction extends AbstractAction {
424
425 private ShowTileInfoAction() {
426 super(tr("Show tile info"));
427 setEnabled(clickedTileHolder.getTile() != null);
428 }
429
430 private String getSizeString(int size) {
431 return new StringBuilder().append(size).append('x').append(size).toString();
432 }
433
434 @Override
435 public void actionPerformed(ActionEvent ae) {
436 Tile clickedTile = clickedTileHolder.getTile();
437 if (clickedTile != null) {
438 ExtendedDialog ed = new ExtendedDialog(Main.parent, tr("Tile Info"), new String[]{tr("OK")});
439 JPanel panel = new JPanel(new GridBagLayout());
440 Rectangle2D displaySize = coordinateConverter.getRectangleForTile(clickedTile);
441 String url = "";
442 try {
443 url = clickedTile.getUrl();
444 } catch (IOException e) {
445 // silence exceptions
446 Main.trace(e);
447 }
448
449 List<List<String>> content = new ArrayList<>();
450 content.add(Arrays.asList(tr("Tile name"), clickedTile.getKey()));
451 content.add(Arrays.asList(tr("Tile URL"), url));
452 content.add(Arrays.asList(tr("Tile size"),
453 getSizeString(clickedTile.getTileSource().getTileSize())));
454 content.add(Arrays.asList(tr("Tile display size"),
455 new StringBuilder().append(displaySize.getWidth())
456 .append('x')
457 .append(displaySize.getHeight()).toString()));
458 if (coordinateConverter.requiresReprojection()) {
459 content.add(Arrays.asList(tr("Reprojection"),
460 clickedTile.getTileSource().getServerCRS() +
461 " -> " + Main.getProjection().toCode()));
462 BufferedImage img = clickedTile.getImage();
463 if (img != null) {
464 content.add(Arrays.asList(tr("Reprojected tile size"),
465 img.getWidth() + "x" + img.getHeight()));
466
467 }
468 }
469 for (List<String> entry: content) {
470 panel.add(new JLabel(entry.get(0) + ':'), GBC.std());
471 panel.add(GBC.glue(5, 0), GBC.std());
472 panel.add(createTextField(entry.get(1)), GBC.eol().fill(GBC.HORIZONTAL));
473 }
474
475 for (Entry<String, String> e: clickedTile.getMetadata().entrySet()) {
476 panel.add(new JLabel(tr("Metadata ") + tr(e.getKey()) + ':'), GBC.std());
477 panel.add(GBC.glue(5, 0), GBC.std());
478 String value = e.getValue();
479 if ("lastModification".equals(e.getKey()) || "expirationTime".equals(e.getKey())) {
480 value = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(Long.parseLong(value)));
481 }
482 panel.add(createTextField(value), GBC.eol().fill(GBC.HORIZONTAL));
483
484 }
485 ed.setIcon(JOptionPane.INFORMATION_MESSAGE);
486 ed.setContent(panel);
487 ed.showDialog();
488 }
489 }
490 }
491
492 private final class LoadTileAction extends AbstractAction {
493
494 private LoadTileAction() {
495 super(tr("Load tile"));
496 setEnabled(clickedTileHolder.getTile() != null);
497 }
498
499 @Override
500 public void actionPerformed(ActionEvent ae) {
501 Tile clickedTile = clickedTileHolder.getTile();
502 if (clickedTile != null) {
503 loadTile(clickedTile, true);
504 invalidate();
505 }
506 }
507 }
508
509 private class AutoZoomAction extends AbstractAction implements LayerAction {
510 AutoZoomAction() {
511 super(tr("Auto zoom"));
512 }
513
514 @Override
515 public void actionPerformed(ActionEvent ae) {
516 getDisplaySettings().setAutoZoom(!getDisplaySettings().isAutoZoom());
517 }
518
519 @Override
520 public Component createMenuComponent() {
521 JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
522 item.setSelected(getDisplaySettings().isAutoZoom());
523 return item;
524 }
525
526 @Override
527 public boolean supportLayers(List<Layer> layers) {
528 return actionSupportLayers(layers);
529 }
530 }
531
532 private class AutoLoadTilesAction extends AbstractAction implements LayerAction {
533 AutoLoadTilesAction() {
534 super(tr("Auto load tiles"));
535 }
536
537 @Override
538 public void actionPerformed(ActionEvent ae) {
539 getDisplaySettings().setAutoLoad(!getDisplaySettings().isAutoLoad());
540 }
541
542 @Override
543 public Component createMenuComponent() {
544 JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
545 item.setSelected(getDisplaySettings().isAutoLoad());
546 return item;
547 }
548
549 @Override
550 public boolean supportLayers(List<Layer> layers) {
551 return actionSupportLayers(layers);
552 }
553 }
554
555 private class ShowErrorsAction extends AbstractAction implements LayerAction {
556 ShowErrorsAction() {
557 super(tr("Show errors"));
558 }
559
560 @Override
561 public void actionPerformed(ActionEvent ae) {
562 getDisplaySettings().setShowErrors(!getDisplaySettings().isShowErrors());
563 }
564
565 @Override
566 public Component createMenuComponent() {
567 JCheckBoxMenuItem item = new JCheckBoxMenuItem(this);
568 item.setSelected(getDisplaySettings().isShowErrors());
569 return item;
570 }
571
572 @Override
573 public boolean supportLayers(List<Layer> layers) {
574 return actionSupportLayers(layers);
575 }
576 }
577
578 private class LoadAllTilesAction extends AbstractAction {
579 LoadAllTilesAction() {
580 super(tr("Load all tiles"));
581 }
582
583 @Override
584 public void actionPerformed(ActionEvent ae) {
585 loadAllTiles(true);
586 }
587 }
588
589 private class LoadErroneusTilesAction extends AbstractAction {
590 LoadErroneusTilesAction() {
591 super(tr("Load all error tiles"));
592 }
593
594 @Override
595 public void actionPerformed(ActionEvent ae) {
596 loadAllErrorTiles(true);
597 }
598 }
599
600 private class ZoomToNativeLevelAction extends AbstractAction {
601 ZoomToNativeLevelAction() {
602 super(tr("Zoom to native resolution"));
603 }
604
605 @Override
606 public void actionPerformed(ActionEvent ae) {
607 double newFactor = Math.sqrt(getScaleFactor(currentZoomLevel));
608 Main.map.mapView.zoomToFactor(newFactor);
609 redraw();
610 }
611 }
612
613 private class ZoomToBestAction extends AbstractAction {
614 ZoomToBestAction() {
615 super(tr("Change resolution"));
616 setEnabled(!getDisplaySettings().isAutoZoom() && getBestZoom() != currentZoomLevel);
617 }
618
619 @Override
620 public void actionPerformed(ActionEvent ae) {
621 setZoomLevel(getBestZoom());
622 }
623 }
624
625 private class IncreaseZoomAction extends AbstractAction {
626 IncreaseZoomAction() {
627 super(tr("Increase zoom"));
628 setEnabled(!getDisplaySettings().isAutoZoom() && zoomIncreaseAllowed());
629 }
630
631 @Override
632 public void actionPerformed(ActionEvent ae) {
633 increaseZoomLevel();
634 }
635 }
636
637 private class DecreaseZoomAction extends AbstractAction {
638 DecreaseZoomAction() {
639 super(tr("Decrease zoom"));
640 setEnabled(!getDisplaySettings().isAutoZoom() && zoomDecreaseAllowed());
641 }
642
643 @Override
644 public void actionPerformed(ActionEvent ae) {
645 decreaseZoomLevel();
646 }
647 }
648
649 private class FlushTileCacheAction extends AbstractAction {
650 FlushTileCacheAction() {
651 super(tr("Flush tile cache"));
652 }
653
654 @Override
655 public void actionPerformed(ActionEvent ae) {
656 new PleaseWaitRunnable(tr("Flush tile cache")) {
657 @Override
658 protected void realRun() {
659 clearTileCache();
660 }
661
662 @Override
663 protected void finish() {
664 // empty - flush is instaneus
665 }
666
667 @Override
668 protected void cancel() {
669 // empty - flush is instaneus
670 }
671 }.run();
672 }
673 }
674
675 /**
676 * Simple class to keep clickedTile within hookUpMapView
677 */
678 private static final class TileHolder {
679 private Tile t;
680
681 public Tile getTile() {
682 return t;
683 }
684
685 public void setTile(Tile t) {
686 this.t = t;
687 }
688 }
689
690 /**
691 * Creates popup menu items and binds to mouse actions
692 */
693 @Override
694 public void hookUpMapView() {
695 // this needs to be here and not in constructor to allow empty TileSource class construction using SessionWriter
696 initializeIfRequired();
697
698 super.hookUpMapView();
699 }
700
701 @Override
702 public LayerPainter attachToMapView(MapViewEvent event) {
703 initializeIfRequired();
704
705 event.getMapView().addMouseListener(adapter);
706 MapView.addZoomChangeListener(this);
707
708 if (this instanceof NativeScaleLayer) {
709 event.getMapView().setNativeScaleLayer((NativeScaleLayer) this);
710 }
711
712 // FIXME: why do we need this? Without this, if you add a WMS layer and do not move the mouse, sometimes, tiles do not start loading.
713 // FIXME: Check if this is still required.
714 event.getMapView().repaint(500);
715
716 return super.attachToMapView(event);
717 }
718
719 private void initializeIfRequired() {
720 if (tileSource == null) {
721 tileSource = getTileSource();
722 if (tileSource == null) {
723 throw new IllegalArgumentException(tr("Failed to create tile source"));
724 }
725 // check if projection is supported
726 projectionChanged(null, Main.getProjection());
727 initTileSource(this.tileSource);
728 }
729 }
730
731 @Override
732 protected LayerPainter createMapViewPainter(MapViewEvent event) {
733 return new TileSourcePainter();
734 }
735
736 /**
737 * Tile source layer popup menu.
738 */
739 public class TileSourceLayerPopup extends JPopupMenu {
740 /**
741 * Constructs a new {@code TileSourceLayerPopup}.
742 */
743 public TileSourceLayerPopup() {
744 for (Action a : getCommonEntries()) {
745 if (a instanceof LayerAction) {
746 add(((LayerAction) a).createMenuComponent());
747 } else {
748 add(new JMenuItem(a));
749 }
750 }
751 add(new JSeparator());
752 add(new JMenuItem(new LoadTileAction()));
753 add(new JMenuItem(new ShowTileInfoAction()));
754 }
755 }
756
757 protected int estimateTileCacheSize() {
758 Dimension screenSize = GuiHelper.getMaximumScreenSize();
759 int height = screenSize.height;
760 int width = screenSize.width;
761 int tileSize = 256; // default tile size
762 if (tileSource != null) {
763 tileSize = tileSource.getTileSize();
764 }
765 // as we can see part of the tile at the top and at the bottom, use Math.ceil(...) + 1 to accommodate for that
766 int visibileTiles = (int) (Math.ceil((double) height / tileSize + 1) * Math.ceil((double) width / tileSize + 1));
767 // add 10% for tiles from different zoom levels
768 int ret = (int) Math.ceil(
769 Math.pow(2d, ZOOM_OFFSET.get()) * visibileTiles // use offset to decide, how many tiles are visible
770 * 4);
771 Main.info("AbstractTileSourceLayer: estimated visible tiles: {0}, estimated cache size: {1}", visibileTiles, ret);
772 return ret;
773 }
774
775 @Override
776 public void displaySettingsChanged(DisplaySettingsChangeEvent e) {
777 if (tileSource == null) {
778 return;
779 }
780 switch (e.getChangedSetting()) {
781 case TileSourceDisplaySettings.AUTO_ZOOM:
782 if (getDisplaySettings().isAutoZoom() && getBestZoom() != currentZoomLevel) {
783 setZoomLevel(getBestZoom());
784 invalidate();
785 }
786 break;
787 case TileSourceDisplaySettings.AUTO_LOAD:
788 if (getDisplaySettings().isAutoLoad()) {
789 invalidate();
790 }
791 break;
792 default:
793 // trigger a redraw just to be sure.
794 invalidate();
795 }
796 }
797
798 /**
799 * Checks zoom level against settings
800 * @param maxZoomLvl zoom level to check
801 * @param ts tile source to crosscheck with
802 * @return maximum zoom level, not higher than supported by tilesource nor set by the user
803 */
804 public static int checkMaxZoomLvl(int maxZoomLvl, TileSource ts) {
805 if (maxZoomLvl > MAX_ZOOM) {
806 maxZoomLvl = MAX_ZOOM;
807 }
808 if (maxZoomLvl < PROP_MIN_ZOOM_LVL.get()) {
809 maxZoomLvl = PROP_MIN_ZOOM_LVL.get();
810 }
811 if (ts != null && ts.getMaxZoom() != 0 && ts.getMaxZoom() < maxZoomLvl) {
812 maxZoomLvl = ts.getMaxZoom();
813 }
814 return maxZoomLvl;
815 }
816
817 /**
818 * Checks zoom level against settings
819 * @param minZoomLvl zoom level to check
820 * @param ts tile source to crosscheck with
821 * @return minimum zoom level, not higher than supported by tilesource nor set by the user
822 */
823 public static int checkMinZoomLvl(int minZoomLvl, TileSource ts) {
824 if (minZoomLvl < MIN_ZOOM) {
825 minZoomLvl = MIN_ZOOM;
826 }
827 if (minZoomLvl > PROP_MAX_ZOOM_LVL.get()) {
828 minZoomLvl = getMaxZoomLvl(ts);
829 }
830 if (ts != null && ts.getMinZoom() > minZoomLvl) {
831 minZoomLvl = ts.getMinZoom();
832 }
833 return minZoomLvl;
834 }
835
836 /**
837 * @param ts TileSource for which we want to know maximum zoom level
838 * @return maximum max zoom level, that will be shown on layer
839 */
840 public static int getMaxZoomLvl(TileSource ts) {
841 return checkMaxZoomLvl(PROP_MAX_ZOOM_LVL.get(), ts);
842 }
843
844 /**
845 * @param ts TileSource for which we want to know minimum zoom level
846 * @return minimum zoom level, that will be shown on layer
847 */
848 public static int getMinZoomLvl(TileSource ts) {
849 return checkMinZoomLvl(PROP_MIN_ZOOM_LVL.get(), ts);
850 }
851
852 /**
853 * Sets maximum zoom level, that layer will attempt show
854 * @param maxZoomLvl maximum zoom level
855 */
856 public static void setMaxZoomLvl(int maxZoomLvl) {
857 PROP_MAX_ZOOM_LVL.put(checkMaxZoomLvl(maxZoomLvl, null));
858 }
859
860 /**
861 * Sets minimum zoom level, that layer will attempt show
862 * @param minZoomLvl minimum zoom level
863 */
864 public static void setMinZoomLvl(int minZoomLvl) {
865 PROP_MIN_ZOOM_LVL.put(checkMinZoomLvl(minZoomLvl, null));
866 }
867
868 /**
869 * This fires every time the user changes the zoom, but also (due to ZoomChangeListener) - on all
870 * changes to visible map (panning/zooming)
871 */
872 @Override
873 public void zoomChanged() {
874 if (Main.isDebugEnabled()) {
875 Main.debug("zoomChanged(): " + currentZoomLevel);
876 }
877 if (tileLoader instanceof TMSCachedTileLoader) {
878 ((TMSCachedTileLoader) tileLoader).cancelOutstandingTasks();
879 }
880 invalidate();
881 }
882
883 protected int getMaxZoomLvl() {
884 if (info.getMaxZoom() != 0)
885 return checkMaxZoomLvl(info.getMaxZoom(), tileSource);
886 else
887 return getMaxZoomLvl(tileSource);
888 }
889
890 protected int getMinZoomLvl() {
891 if (info.getMinZoom() != 0)
892 return checkMinZoomLvl(info.getMinZoom(), tileSource);
893 else
894 return getMinZoomLvl(tileSource);
895 }
896
897 /**
898 *
899 * @return if its allowed to zoom in
900 */
901 public boolean zoomIncreaseAllowed() {
902 boolean zia = currentZoomLevel < this.getMaxZoomLvl();
903 if (Main.isDebugEnabled()) {
904 Main.debug("zoomIncreaseAllowed(): " + zia + ' ' + currentZoomLevel + " vs. " + this.getMaxZoomLvl());
905 }
906 return zia;
907 }
908
909 /**
910 * Zoom in, go closer to map.
911 *
912 * @return true, if zoom increasing was successful, false otherwise
913 */
914 public boolean increaseZoomLevel() {
915 if (zoomIncreaseAllowed()) {
916 currentZoomLevel++;
917 if (Main.isDebugEnabled()) {
918 Main.debug("increasing zoom level to: " + currentZoomLevel);
919 }
920 zoomChanged();
921 } else {
922 Main.warn("Current zoom level ("+currentZoomLevel+") could not be increased. "+
923 "Max.zZoom Level "+this.getMaxZoomLvl()+" reached.");
924 return false;
925 }
926 return true;
927 }
928
929 /**
930 * Sets the zoom level of the layer
931 * @param zoom zoom level
932 * @return true, when zoom has changed to desired value, false if it was outside supported zoom levels
933 */
934 public boolean setZoomLevel(int zoom) {
935 if (zoom == currentZoomLevel) return true;
936 if (zoom > this.getMaxZoomLvl()) return false;
937 if (zoom < this.getMinZoomLvl()) return false;
938 currentZoomLevel = zoom;
939 zoomChanged();
940 return true;
941 }
942
943 /**
944 * Check if zooming out is allowed
945 *
946 * @return true, if zooming out is allowed (currentZoomLevel &gt; minZoomLevel)
947 */
948 public boolean zoomDecreaseAllowed() {
949 boolean zda = currentZoomLevel > this.getMinZoomLvl();
950 if (Main.isDebugEnabled()) {
951 Main.debug("zoomDecreaseAllowed(): " + zda + ' ' + currentZoomLevel + " vs. " + this.getMinZoomLvl());
952 }
953 return zda;
954 }
955
956 /**
957 * Zoom out from map.
958 *
959 * @return true, if zoom increasing was successfull, false othervise
960 */
961 public boolean decreaseZoomLevel() {
962 if (zoomDecreaseAllowed()) {
963 if (Main.isDebugEnabled()) {
964 Main.debug("decreasing zoom level to: " + currentZoomLevel);
965 }
966 currentZoomLevel--;
967 zoomChanged();
968 } else {
969 return false;
970 }
971 return true;
972 }
973
974 private Tile getOrCreateTile(TilePosition tilePosition) {
975 return getOrCreateTile(tilePosition.getX(), tilePosition.getY(), tilePosition.getZoom());
976 }
977
978 private Tile getOrCreateTile(int x, int y, int zoom) {
979 Tile tile = getTile(x, y, zoom);
980 if (tile == null) {
981 if (coordinateConverter.requiresReprojection()) {
982 tile = new ReprojectionTile(tileSource, x, y, zoom);
983 } else {
984 tile = new Tile(tileSource, x, y, zoom);
985 }
986 tileCache.addTile(tile);
987 }
988 return tile;
989 }
990
991 private Tile getTile(TilePosition tilePosition) {
992 return getTile(tilePosition.getX(), tilePosition.getY(), tilePosition.getZoom());
993 }
994
995 /**
996 * Returns tile at given position.
997 * This can and will return null for tiles that are not already in the cache.
998 * @param x tile number on the x axis of the tile to be retrieved
999 * @param y tile number on the y axis of the tile to be retrieved
1000 * @param zoom zoom level of the tile to be retrieved
1001 * @return tile at given position
1002 */
1003 private Tile getTile(int x, int y, int zoom) {
1004 if (x < tileSource.getTileXMin(zoom) || x > tileSource.getTileXMax(zoom)
1005 || y < tileSource.getTileYMin(zoom) || y > tileSource.getTileYMax(zoom))
1006 return null;
1007 return tileCache.getTile(tileSource, x, y, zoom);
1008 }
1009
1010 private boolean loadTile(Tile tile, boolean force) {
1011 if (tile == null)
1012 return false;
1013 if (!force && (tile.isLoaded() || tile.hasError()))
1014 return false;
1015 if (tile.isLoading())
1016 return false;
1017 tileLoader.createTileLoaderJob(tile).submit(force);
1018 return true;
1019 }
1020
1021 private TileSet getVisibleTileSet() {
1022 ProjectionBounds bounds = Main.map.mapView.getState().getViewArea().getProjectionBounds();
1023 return getTileSet(bounds, currentZoomLevel);
1024 }
1025
1026 protected void loadAllTiles(boolean force) {
1027 TileSet ts = getVisibleTileSet();
1028
1029 // if there is more than 18 tiles on screen in any direction, do not load all tiles!
1030 if (ts.tooLarge()) {
1031 Main.warn("Not downloading all tiles because there is more than 18 tiles on an axis!");
1032 return;
1033 }
1034 ts.loadAllTiles(force);
1035 invalidate();
1036 }
1037
1038 protected void loadAllErrorTiles(boolean force) {
1039 TileSet ts = getVisibleTileSet();
1040 ts.loadAllErrorTiles(force);
1041 invalidate();
1042 }
1043
1044 @Override
1045 public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
1046 boolean done = (infoflags & (ERROR | FRAMEBITS | ALLBITS)) != 0;
1047 if (Main.isDebugEnabled()) {
1048 Main.debug("imageUpdate() done: " + done + " calling repaint");
1049 }
1050
1051 if (done) {
1052 invalidate();
1053 } else {
1054 invalidateLater();
1055 }
1056 return !done;
1057 }
1058
1059 /**
1060 * Invalidate the layer at a time in the future so that the user still sees the interface responsive.
1061 */
1062 private void invalidateLater() {
1063 GuiHelper.runInEDT(() -> {
1064 if (!invalidateLaterTimer.isRunning()) {
1065 invalidateLaterTimer.setRepeats(false);
1066 invalidateLaterTimer.start();
1067 }
1068 });
1069 }
1070
1071 private boolean imageLoaded(Image i) {
1072 if (i == null)
1073 return false;
1074 int status = Toolkit.getDefaultToolkit().checkImage(i, -1, -1, this);
1075 if ((status & ALLBITS) != 0)
1076 return true;
1077 return false;
1078 }
1079
1080 /**
1081 * Returns the image for the given tile image is loaded.
1082 * Otherwise returns null.
1083 *
1084 * @param tile the Tile for which the image should be returned
1085 * @return the image of the tile or null.
1086 */
1087 private BufferedImage getLoadedTileImage(Tile tile) {
1088 BufferedImage img = tile.getImage();
1089 if (!imageLoaded(img))
1090 return null;
1091 return img;
1092 }
1093
1094 /**
1095 * Draw a tile image on screen.
1096 * @param g the Graphics2D
1097 * @param toDrawImg tile image
1098 * @param anchorImage tile anchor in image coordinates
1099 * @param anchorScreen tile anchor in screen coordinates
1100 * @param clip clipping region in screen coordinates (can be null)
1101 */
1102 private void drawImageInside(Graphics2D g, BufferedImage toDrawImg, TileAnchor anchorImage, TileAnchor anchorScreen, Shape clip) {
1103 AffineTransform imageToScreen = anchorImage.convert(anchorScreen);
1104 Point2D screen0 = imageToScreen.transform(new Point.Double(0, 0), null);
1105 Point2D screen1 = imageToScreen.transform(new Point.Double(
1106 toDrawImg.getWidth(), toDrawImg.getHeight()), null);
1107
1108 Shape oldClip = null;
1109 if (clip != null) {
1110 oldClip = g.getClip();
1111 g.clip(clip);
1112 }
1113 g.drawImage(toDrawImg, (int) Math.round(screen0.getX()), (int) Math.round(screen0.getY()),
1114 (int) Math.round(screen1.getX() - screen0.getX()), (int) Math.round(screen1.getY() - screen0.getY()), this);
1115 if (clip != null) {
1116 g.setClip(oldClip);
1117 }
1118 }
1119
1120 private List<Tile> paintTileImages(Graphics2D g, TileSet ts) {
1121 Object paintMutex = new Object();
1122 List<TilePosition> missed = Collections.synchronizedList(new ArrayList<>());
1123 ts.visitTiles(tile -> {
1124 boolean miss = false;
1125 BufferedImage img = null;
1126 TileAnchor anchorImage = null;
1127 if (!tile.isLoaded() || tile.hasError()) {
1128 miss = true;
1129 } else {
1130 synchronized (tile) {
1131 img = getLoadedTileImage(tile);
1132 anchorImage = getAnchor(tile, img);
1133 }
1134 if (img == null || anchorImage == null) {
1135 miss = true;
1136 }
1137 }
1138 if (miss) {
1139 missed.add(new TilePosition(tile));
1140 return;
1141 }
1142
1143 img = applyImageProcessors(img);
1144
1145 TileAnchor anchorScreen = coordinateConverter.getScreenAnchorForTile(tile);
1146 synchronized (paintMutex) {
1147 //cannot paint in parallel
1148 drawImageInside(g, img, anchorImage, anchorScreen, null);
1149 }
1150 if (tile instanceof ReprojectionTile) {
1151 // This means we have a reprojected tile in memory cache, but not at
1152 // current scale. Generally, the positioning of the tile will still
1153 // be correct, but for best image quality, the tile should be
1154 // reprojected to the target scale. The original tile image should
1155 // still be in disk cache, so this is fairly cheap.
1156 if (((ReprojectionTile) tile).needsUpdate(Main.map.mapView.getScale())) {
1157 loadTile(tile, true);
1158 }
1159 }
1160
1161 }, missed::add);
1162
1163 return missed.stream().map(this::getOrCreateTile).collect(Collectors.toList());
1164 }
1165
1166 // This function is called for several zoom levels, not just the current one.
1167 // It should not trigger any tiles to be downloaded.
1168 // It should also avoid polluting the tile cache with any tiles since these tiles are not mandatory.
1169 //
1170 // The "border" tile tells us the boundaries of where we may drawn.
1171 // It will not be from the zoom level that is being drawn currently.
1172 // If drawing the displayZoomLevel, border is null and we draw the entire tile set.
1173 private List<Tile> paintTileImages(Graphics2D g, TileSet ts, int zoom, Tile border) {
1174 if (zoom <= 0) return Collections.emptyList();
1175 Shape borderClip = coordinateConverter.getScreenQuadrilateralForTile(border);
1176 List<Tile> missedTiles = new LinkedList<>();
1177 // The callers of this code *require* that we return any tiles that we do not draw in missedTiles.
1178 // ts.allExistingTiles() by default will only return already-existing tiles.
1179 // However, we need to return *all* tiles to the callers, so force creation here.
1180 for (Tile tile : ts.allTilesCreate()) {
1181 boolean miss = false;
1182 BufferedImage img = null;
1183 TileAnchor anchorImage = null;
1184 if (!tile.isLoaded() || tile.hasError()) {
1185 miss = true;
1186 } else {
1187 synchronized (tile) {
1188 img = getLoadedTileImage(tile);
1189 anchorImage = getAnchor(tile, img);
1190 }
1191
1192 if (img == null || anchorImage == null) {
1193 miss = true;
1194 }
1195 }
1196 if (miss) {
1197 missedTiles.add(tile);
1198 continue;
1199 }
1200
1201 // applying all filters to this layer
1202 img = applyImageProcessors(img);
1203
1204 Shape clip;
1205 if (tileSource.isInside(tile, border)) {
1206 clip = null;
1207 } else if (tileSource.isInside(border, tile)) {
1208 clip = borderClip;
1209 } else {
1210 continue;
1211 }
1212 TileAnchor anchorScreen = coordinateConverter.getScreenAnchorForTile(tile);
1213 drawImageInside(g, img, anchorImage, anchorScreen, clip);
1214 }
1215 return missedTiles;
1216 }
1217
1218 private TileAnchor getAnchor(Tile tile, BufferedImage image) {
1219 if (tile instanceof ReprojectionTile) {
1220 return ((ReprojectionTile) tile).getAnchor();
1221 } else if (image != null) {
1222 return new TileAnchor(new Point.Double(0, 0), new Point.Double(image.getWidth(), image.getHeight()));
1223 } else {
1224 return null;
1225 }
1226 }
1227
1228 private void myDrawString(Graphics g, String text, int x, int y) {
1229 Color oldColor = g.getColor();
1230 String textToDraw = text;
1231 if (g.getFontMetrics().stringWidth(text) > tileSource.getTileSize()) {
1232 // text longer than tile size, split it
1233 StringBuilder line = new StringBuilder();
1234 StringBuilder ret = new StringBuilder();
1235 for (String s: text.split(" ")) {
1236 if (g.getFontMetrics().stringWidth(line.toString() + s) > tileSource.getTileSize()) {
1237 ret.append(line).append('\n');
1238 line.setLength(0);
1239 }
1240 line.append(s).append(' ');
1241 }
1242 ret.append(line);
1243 textToDraw = ret.toString();
1244 }
1245 int offset = 0;
1246 for (String s: textToDraw.split("\n")) {
1247 g.setColor(Color.black);
1248 g.drawString(s, x + 1, y + offset + 1);
1249 g.setColor(oldColor);
1250 g.drawString(s, x, y + offset);
1251 offset += g.getFontMetrics().getHeight() + 3;
1252 }
1253 }
1254
1255 private void paintTileText(Tile tile, Graphics2D g) {
1256 if (tile == null) {
1257 return;
1258 }
1259 Point2D p = coordinateConverter.getPixelForTile(tile);
1260 int fontHeight = g.getFontMetrics().getHeight();
1261 int x = (int) p.getX();
1262 int y = (int) p.getY();
1263 int texty = y + 2 + fontHeight;
1264
1265 /*if (PROP_DRAW_DEBUG.get()) {
1266 myDrawString(g, "x=" + t.getXtile() + " y=" + t.getYtile() + " z=" + zoom + "", p.x + 2, texty);
1267 texty += 1 + fontHeight;
1268 if ((t.getXtile() % 32 == 0) && (t.getYtile() % 32 == 0)) {
1269 myDrawString(g, "x=" + t.getXtile() / 32 + " y=" + t.getYtile() / 32 + " z=7", p.x + 2, texty);
1270 texty += 1 + fontHeight;
1271 }
1272 }
1273
1274 String tileStatus = tile.getStatus();
1275 if (!tile.isLoaded() && PROP_DRAW_DEBUG.get()) {
1276 myDrawString(g, tr("image " + tileStatus), p.x + 2, texty);
1277 texty += 1 + fontHeight;
1278 }*/
1279
1280 if (tile.hasError() && getDisplaySettings().isShowErrors()) {
1281 myDrawString(g, tr("Error") + ": " + tr(tile.getErrorMessage()), x + 2, texty);
1282 //texty += 1 + fontHeight;
1283 }
1284
1285 if (Main.isDebugEnabled()) {
1286 // draw tile outline in semi-transparent red
1287 g.setColor(new Color(255, 0, 0, 50));
1288 g.draw(coordinateConverter.getScreenQuadrilateralForTile(tile));
1289 }
1290 }
1291
1292 private LatLon getShiftedLatLon(EastNorth en) {
1293 return coordinateConverter.getProjecting().eastNorth2latlonClamped(en);
1294 }
1295
1296 private ICoordinate getShiftedCoord(EastNorth en) {
1297 return getShiftedLatLon(en).toCoordinate();
1298 }
1299
1300 private final TileSet nullTileSet = new TileSet();
1301
1302 protected class TileSet extends TileRange {
1303
1304 protected TileSet(TileXY t1, TileXY t2, int zoom) {
1305 super(t1, t2, zoom);
1306 sanitize();
1307 }
1308
1309 protected TileSet(TileRange range) {
1310 super(range);
1311 sanitize();
1312 }
1313
1314 /**
1315 * null tile set
1316 */
1317 private TileSet() {
1318 // default
1319 }
1320
1321 protected void sanitize() {
1322 if (minX < tileSource.getTileXMin(zoom)) {
1323 minX = tileSource.getTileXMin(zoom);
1324 }
1325 if (minY < tileSource.getTileYMin(zoom)) {
1326 minY = tileSource.getTileYMin(zoom);
1327 }
1328 if (maxX > tileSource.getTileXMax(zoom)) {
1329 maxX = tileSource.getTileXMax(zoom);
1330 }
1331 if (maxY > tileSource.getTileYMax(zoom)) {
1332 maxY = tileSource.getTileYMax(zoom);
1333 }
1334 }
1335
1336 private boolean tooSmall() {
1337 return this.tilesSpanned() < 2.1;
1338 }
1339
1340 private boolean tooLarge() {
1341 return insane() || this.tilesSpanned() > 20;
1342 }
1343
1344 private boolean insane() {
1345 return tileCache == null || size() > tileCache.getCacheSize();
1346 }
1347
1348 /**
1349 * Get all tiles represented by this TileSet that are already in the tileCache.
1350 * @return all tiles represented by this TileSet that are already in the tileCache
1351 */
1352 private List<Tile> allExistingTiles() {
1353 return allTiles(AbstractTileSourceLayer.this::getTile);
1354 }
1355
1356 private List<Tile> allTilesCreate() {
1357 return allTiles(AbstractTileSourceLayer.this::getOrCreateTile);
1358 }
1359
1360 private List<Tile> allTiles(Function<TilePosition, Tile> mapper) {
1361 return tilePositions().map(mapper).filter(Objects::nonNull).collect(Collectors.toList());
1362 }
1363
1364 /**
1365 * Gets a stream of all tile positions in this set
1366 * @return A stream of all positions
1367 */
1368 public Stream<TilePosition> tilePositions() {
1369 if (zoom == 0 || this.insane()) {
1370 return Stream.empty(); // Tileset is either empty or too large
1371 } else {
1372 return IntStream.rangeClosed(minX, maxX).mapToObj(
1373 x -> IntStream.rangeClosed(minY, maxY).mapToObj(y -> new TilePosition(x, y, zoom))
1374 ).flatMap(Function.identity());
1375 }
1376 }
1377
1378 private List<Tile> allLoadedTiles() {
1379 return allExistingTiles().stream().filter(Tile::isLoaded).collect(Collectors.toList());
1380 }
1381
1382 /**
1383 * @return comparator, that sorts the tiles from the center to the edge of the current screen
1384 */
1385 private Comparator<Tile> getTileDistanceComparator() {
1386 final int centerX = (int) Math.ceil((minX + maxX) / 2d);
1387 final int centerY = (int) Math.ceil((minY + maxY) / 2d);
1388 return Comparator.comparingInt(t -> Math.abs(t.getXtile() - centerX) + Math.abs(t.getYtile() - centerY));
1389 }
1390
1391 private void loadAllTiles(boolean force) {
1392 if (!getDisplaySettings().isAutoLoad() && !force)
1393 return;
1394 List<Tile> allTiles = allTilesCreate();
1395 allTiles.sort(getTileDistanceComparator());
1396 for (Tile t : allTiles) {
1397 loadTile(t, force);
1398 }
1399 }
1400
1401 private void loadAllErrorTiles(boolean force) {
1402 if (!getDisplaySettings().isAutoLoad() && !force)
1403 return;
1404 for (Tile t : this.allTilesCreate()) {
1405 if (t.hasError()) {
1406 tileLoader.createTileLoaderJob(t).submit(force);
1407 }
1408 }
1409 }
1410
1411 /**
1412 * Call the given paint method for all tiles in this tile set.<p>
1413 * Uses a parallel stream.
1414 * @param visitor A visitor to call for each tile.
1415 * @param missed a consumer to call for each missed tile.
1416 */
1417 public void visitTiles(Consumer<Tile> visitor, Consumer<TilePosition> missed) {
1418 tilePositions().parallel().forEach(tp -> visitTilePosition(visitor, tp, missed));
1419 }
1420
1421 private void visitTilePosition(Consumer<Tile> visitor, TilePosition tp, Consumer<TilePosition> missed) {
1422 Tile tile = getTile(tp);
1423 if (tile == null) {
1424 missed.accept(tp);
1425 } else {
1426 visitor.accept(tile);
1427 }
1428 }
1429
1430 @Override
1431 public String toString() {
1432 return getClass().getName() + ": zoom: " + zoom + " X(" + minX + ", " + maxX + ") Y(" + minY + ", " + maxY + ") size: " + size();
1433 }
1434 }
1435
1436 /**
1437 * Create a TileSet by EastNorth bbox taking a layer shift in account
1438 * @param bounds the EastNorth bounds
1439 * @param zoom zoom level
1440 * @return the tile set
1441 */
1442 protected TileSet getTileSet(ProjectionBounds bounds, int zoom) {
1443 if (zoom == 0)
1444 return new TileSet();
1445 TileXY t1, t2;
1446 if (coordinateConverter.requiresReprojection()) {
1447 Projection projCurrent = Main.getProjection();
1448 Projection projServer = Projections.getProjectionByCode(tileSource.getServerCRS());
1449 bounds = new ProjectionBounds(
1450 new EastNorth(coordinateConverter.shiftDisplayToServer(bounds.getMin())),
1451 new EastNorth(coordinateConverter.shiftDisplayToServer(bounds.getMax())));
1452 bounds = projServer.getEastNorthBoundsBox(bounds, projCurrent);
1453 t1 = tileSource.projectedToTileXY(bounds.getMin().toProjected(), zoom);
1454 t2 = tileSource.projectedToTileXY(bounds.getMax().toProjected(), zoom);
1455 } else {
1456 IProjected topLeftUnshifted = coordinateConverter.shiftDisplayToServer(bounds.getMin());
1457 IProjected botRightUnshifted = coordinateConverter.shiftDisplayToServer(bounds.getMax());
1458 t1 = tileSource.projectedToTileXY(topLeftUnshifted, zoom);
1459 t2 = tileSource.projectedToTileXY(botRightUnshifted, zoom);
1460 }
1461 return new TileSet(t1, t2, zoom);
1462 }
1463
1464 private static class TileSetInfo {
1465 boolean hasVisibleTiles;
1466 boolean hasOverzoomedTiles;
1467 boolean hasLoadingTiles;
1468 boolean hasAllLoadedTiles;
1469 }
1470
1471 private static <S extends AbstractTMSTileSource> TileSetInfo getTileSetInfo(AbstractTileSourceLayer<S>.TileSet ts) {
1472 List<Tile> allTiles = ts.allExistingTiles();
1473 TileSetInfo result = new TileSetInfo();
1474 result.hasLoadingTiles = allTiles.size() < ts.size();
1475 result.hasAllLoadedTiles = true;
1476 for (Tile t : allTiles) {
1477 if ("no-tile".equals(t.getValue("tile-info"))) {
1478 result.hasOverzoomedTiles = true;
1479 }
1480 if (t.isLoaded()) {
1481 if (!t.hasError()) {
1482 result.hasVisibleTiles = true;
1483 }
1484 } else {
1485 result.hasAllLoadedTiles = false;
1486 if (t.isLoading()) {
1487 result.hasLoadingTiles = true;
1488 }
1489 }
1490 }
1491 return result;
1492 }
1493
1494 private class DeepTileSet {
1495 private final ProjectionBounds bounds;
1496 private final int minZoom, maxZoom;
1497 private final TileSet[] tileSets;
1498 private final TileSetInfo[] tileSetInfos;
1499
1500 @SuppressWarnings("unchecked")
1501 DeepTileSet(ProjectionBounds bounds, int minZoom, int maxZoom) {
1502 this.bounds = bounds;
1503 this.minZoom = minZoom;
1504 this.maxZoom = maxZoom;
1505 this.tileSets = new AbstractTileSourceLayer.TileSet[maxZoom - minZoom + 1];
1506 this.tileSetInfos = new TileSetInfo[maxZoom - minZoom + 1];
1507 }
1508
1509 public TileSet getTileSet(int zoom) {
1510 if (zoom < minZoom)
1511 return nullTileSet;
1512 synchronized (tileSets) {
1513 TileSet ts = tileSets[zoom-minZoom];
1514 if (ts == null) {
1515 ts = AbstractTileSourceLayer.this.getTileSet(bounds, zoom);
1516 tileSets[zoom-minZoom] = ts;
1517 }
1518 return ts;
1519 }
1520 }
1521
1522 public TileSetInfo getTileSetInfo(int zoom) {
1523 if (zoom < minZoom)
1524 return new TileSetInfo();
1525 synchronized (tileSetInfos) {
1526 TileSetInfo tsi = tileSetInfos[zoom-minZoom];
1527 if (tsi == null) {
1528 tsi = AbstractTileSourceLayer.getTileSetInfo(getTileSet(zoom));
1529 tileSetInfos[zoom-minZoom] = tsi;
1530 }
1531 return tsi;
1532 }
1533 }
1534 }
1535
1536 @Override
1537 public void paint(Graphics2D g, MapView mv, Bounds bounds) {
1538 // old and unused.
1539 }
1540
1541 private void drawInViewArea(Graphics2D g, MapView mv, ProjectionBounds pb) {
1542 int zoom = currentZoomLevel;
1543 if (getDisplaySettings().isAutoZoom()) {
1544 zoom = getBestZoom();
1545 }
1546
1547 DeepTileSet dts = new DeepTileSet(pb, getMinZoomLvl(), zoom);
1548 TileSet ts = dts.getTileSet(zoom);
1549
1550 int displayZoomLevel = zoom;
1551
1552 boolean noTilesAtZoom = false;
1553 if (getDisplaySettings().isAutoZoom() && getDisplaySettings().isAutoLoad()) {
1554 // Auto-detection of tilesource maxzoom (currently fully works only for Bing)
1555 TileSetInfo tsi = dts.getTileSetInfo(zoom);
1556 if (!tsi.hasVisibleTiles && (!tsi.hasLoadingTiles || tsi.hasOverzoomedTiles)) {
1557 noTilesAtZoom = true;
1558 }
1559 // Find highest zoom level with at least one visible tile
1560 for (int tmpZoom = zoom; tmpZoom > dts.minZoom; tmpZoom--) {
1561 if (dts.getTileSetInfo(tmpZoom).hasVisibleTiles) {
1562 displayZoomLevel = tmpZoom;
1563 break;
1564 }
1565 }
1566 // Do binary search between currentZoomLevel and displayZoomLevel
1567 while (zoom > displayZoomLevel && !tsi.hasVisibleTiles && tsi.hasOverzoomedTiles) {
1568 zoom = (zoom + displayZoomLevel)/2;
1569 tsi = dts.getTileSetInfo(zoom);
1570 }
1571
1572 setZoomLevel(zoom);
1573
1574 // If all tiles at displayZoomLevel is loaded, load all tiles at next zoom level
1575 // to make sure there're really no more zoom levels
1576 // loading is done in the next if section
1577 if (zoom == displayZoomLevel && !tsi.hasLoadingTiles && zoom < dts.maxZoom) {
1578 zoom++;
1579 tsi = dts.getTileSetInfo(zoom);
1580 }
1581 // When we have overzoomed tiles and all tiles at current zoomlevel is loaded,
1582 // load tiles at previovus zoomlevels until we have all tiles on screen is loaded.
1583 // loading is done in the next if section
1584 while (zoom > dts.minZoom && tsi.hasOverzoomedTiles && !tsi.hasLoadingTiles) {
1585 zoom--;
1586 tsi = dts.getTileSetInfo(zoom);
1587 }
1588 ts = dts.getTileSet(zoom);
1589 } else if (getDisplaySettings().isAutoZoom()) {
1590 setZoomLevel(zoom);
1591 }
1592
1593 // Too many tiles... refuse to download
1594 if (!ts.tooLarge()) {
1595 // try to load tiles from desired zoom level, no matter what we will show (for example, tiles from previous zoom level
1596 // on zoom in)
1597 ts.loadAllTiles(false);
1598 }
1599
1600 if (displayZoomLevel != zoom) {
1601 ts = dts.getTileSet(displayZoomLevel);
1602 if (!dts.getTileSetInfo(displayZoomLevel).hasAllLoadedTiles && displayZoomLevel < zoom) {
1603 // if we are showing tiles from lower zoom level, ensure that all tiles are loaded as they are few,
1604 // and should not trash the tile cache
1605 // This is especially needed when dts.getTileSet(zoom).tooLarge() is true and we are not loading tiles
1606 ts.loadAllTiles(false);
1607 }
1608 }
1609
1610 g.setColor(Color.DARK_GRAY);
1611
1612 List<Tile> missedTiles = this.paintTileImages(g, ts);
1613 int[] otherZooms = {1, 2, -1, -2, -3, -4, -5};
1614 for (int zoomOffset : otherZooms) {
1615 if (!getDisplaySettings().isAutoZoom()) {
1616 break;
1617 }
1618 int newzoom = displayZoomLevel + zoomOffset;
1619 if (newzoom < getMinZoomLvl() || newzoom > getMaxZoomLvl()) {
1620 continue;
1621 }
1622 if (missedTiles.isEmpty()) {
1623 break;
1624 }
1625 List<Tile> newlyMissedTiles = new LinkedList<>();
1626 for (Tile missed : missedTiles) {
1627 if (zoomOffset > 0 && "no-tile".equals(missed.getValue("tile-info"))) {
1628 // Don't try to paint from higher zoom levels when tile is overzoomed
1629 newlyMissedTiles.add(missed);
1630 continue;
1631 }
1632 TileSet ts2 = new TileSet(tileSource.getCoveringTileRange(missed, newzoom));
1633 // Instantiating large TileSets is expensive. If there are no loaded tiles, don't bother even trying.
1634 if (ts2.allLoadedTiles().isEmpty()) {
1635 newlyMissedTiles.add(missed);
1636 continue;
1637 }
1638 if (ts2.tooLarge()) {
1639 continue;
1640 }
1641 newlyMissedTiles.addAll(this.paintTileImages(g, ts2, newzoom, missed));
1642 }
1643 missedTiles = newlyMissedTiles;
1644 }
1645 if (Main.isDebugEnabled() && !missedTiles.isEmpty()) {
1646 Main.debug("still missed "+missedTiles.size()+" in the end");
1647 }
1648 g.setColor(Color.red);
1649 g.setFont(InfoFont);
1650
1651 // The current zoom tileset should have all of its tiles due to the loadAllTiles(), unless it to tooLarge()
1652 for (Tile t : ts.allExistingTiles()) {
1653 this.paintTileText(t, g);
1654 }
1655
1656 EastNorth min = pb.getMin();
1657 EastNorth max = pb.getMax();
1658 attribution.paintAttribution(g, mv.getWidth(), mv.getHeight(), getShiftedCoord(min), getShiftedCoord(max),
1659 displayZoomLevel, this);
1660
1661 g.setColor(Color.lightGray);
1662
1663 if (ts.insane()) {
1664 myDrawString(g, tr("zoom in to load any tiles"), 120, 120);
1665 } else if (ts.tooLarge()) {
1666 myDrawString(g, tr("zoom in to load more tiles"), 120, 120);
1667 } else if (!getDisplaySettings().isAutoZoom() && ts.tooSmall()) {
1668 myDrawString(g, tr("increase tiles zoom level (change resolution) to see more detail"), 120, 120);
1669 }
1670 if (noTilesAtZoom) {
1671 myDrawString(g, tr("No tiles at this zoom level"), 120, 120);
1672 }
1673 if (Main.isDebugEnabled()) {
1674 myDrawString(g, tr("Current zoom: {0}", currentZoomLevel), 50, 140);
1675 myDrawString(g, tr("Display zoom: {0}", displayZoomLevel), 50, 155);
1676 myDrawString(g, tr("Pixel scale: {0}", getScaleFactor(currentZoomLevel)), 50, 170);
1677 myDrawString(g, tr("Best zoom: {0}", getBestZoom()), 50, 185);
1678 myDrawString(g, tr("Estimated cache size: {0}", estimateTileCacheSize()), 50, 200);
1679 if (tileLoader instanceof TMSCachedTileLoader) {
1680 int offset = 200;
1681 for (String part: ((TMSCachedTileLoader) tileLoader).getStats().split("\n")) {
1682 offset += 15;
1683 myDrawString(g, tr("Cache stats: {0}", part), 50, offset);
1684 }
1685 }
1686 }
1687 }
1688
1689 /**
1690 * Returns tile for a pixel position.<p>
1691 * This isn't very efficient, but it is only used when the user right-clicks on the map.
1692 * @param px pixel X coordinate
1693 * @param py pixel Y coordinate
1694 * @return Tile at pixel position
1695 */
1696 private Tile getTileForPixelpos(int px, int py) {
1697 if (Main.isDebugEnabled()) {
1698 Main.debug("getTileForPixelpos("+px+", "+py+')');
1699 }
1700 TileXY xy = coordinateConverter.getTileforPixel(px, py, currentZoomLevel);
1701 return getTile(xy.getXIndex(), xy.getYIndex(), currentZoomLevel);
1702 }
1703
1704 /**
1705 * Class to store a menu action and the class it belongs to.
1706 */
1707 private static class MenuAddition {
1708 final Action addition;
1709 @SuppressWarnings("rawtypes")
1710 final Class<? extends AbstractTileSourceLayer> clazz;
1711
1712 @SuppressWarnings("rawtypes")
1713 MenuAddition(Action addition, Class<? extends AbstractTileSourceLayer> clazz) {
1714 this.addition = addition;
1715 this.clazz = clazz;
1716 }
1717 }
1718
1719 /**
1720 * Register an additional layer context menu entry.
1721 *
1722 * @param addition additional menu action
1723 * @since 11197
1724 */
1725 public static void registerMenuAddition(Action addition) {
1726 menuAdditions.add(new MenuAddition(addition, AbstractTileSourceLayer.class));
1727 }
1728
1729 /**
1730 * Register an additional layer context menu entry for a imagery layer
1731 * class. The menu entry is valid for the specified class and subclasses
1732 * thereof only.
1733 * <p>
1734 * Example:
1735 * <pre>
1736 * TMSLayer.registerMenuAddition(new TMSSpecificAction(), TMSLayer.class);
1737 * </pre>
1738 *
1739 * @param addition additional menu action
1740 * @param clazz class the menu action is registered for
1741 * @since 11197
1742 */
1743 public static void registerMenuAddition(Action addition,
1744 Class<? extends AbstractTileSourceLayer<?>> clazz) {
1745 menuAdditions.add(new MenuAddition(addition, clazz));
1746 }
1747
1748 /**
1749 * Prepare list of additional layer context menu entries. The list is
1750 * empty if there are no additional menu entries.
1751 *
1752 * @return list of additional layer context menu entries
1753 */
1754 private List<Action> getMenuAdditions() {
1755 final LinkedList<Action> menuAdds = new LinkedList<>();
1756 for (MenuAddition menuAdd: menuAdditions) {
1757 if (menuAdd.clazz.isInstance(this)) {
1758 menuAdds.add(menuAdd.addition);
1759 }
1760 }
1761 if (!menuAdds.isEmpty()) {
1762 menuAdds.addFirst(SeparatorLayerAction.INSTANCE);
1763 }
1764 return menuAdds;
1765 }
1766
1767 @Override
1768 public Action[] getMenuEntries() {
1769 ArrayList<Action> actions = new ArrayList<>();
1770 actions.addAll(Arrays.asList(getLayerListEntries()));
1771 actions.addAll(Arrays.asList(getCommonEntries()));
1772 actions.addAll(getMenuAdditions());
1773 actions.add(SeparatorLayerAction.INSTANCE);
1774 actions.add(new LayerListPopup.InfoAction(this));
1775 return actions.toArray(new Action[actions.size()]);
1776 }
1777
1778 /**
1779 * Returns the contextual menu entries in layer list dialog.
1780 * @return the contextual menu entries in layer list dialog
1781 */
1782 public Action[] getLayerListEntries() {
1783 return new Action[] {
1784 LayerListDialog.getInstance().createActivateLayerAction(this),
1785 LayerListDialog.getInstance().createShowHideLayerAction(),
1786 LayerListDialog.getInstance().createDeleteLayerAction(),
1787 SeparatorLayerAction.INSTANCE,
1788 // color,
1789 new OffsetAction(),
1790 new RenameLayerAction(this.getAssociatedFile(), this),
1791 SeparatorLayerAction.INSTANCE
1792 };
1793 }
1794
1795 /**
1796 * Returns the common menu entries.
1797 * @return the common menu entries
1798 */
1799 public Action[] getCommonEntries() {
1800 return new Action[] {
1801 new AutoLoadTilesAction(),
1802 new AutoZoomAction(),
1803 new ShowErrorsAction(),
1804 new IncreaseZoomAction(),
1805 new DecreaseZoomAction(),
1806 new ZoomToBestAction(),
1807 new ZoomToNativeLevelAction(),
1808 new FlushTileCacheAction(),
1809 new LoadErroneusTilesAction(),
1810 new LoadAllTilesAction()
1811 };
1812 }
1813
1814 @Override
1815 public String getToolTipText() {
1816 if (getDisplaySettings().isAutoLoad()) {
1817 return tr("{0} ({1}), automatically downloading in zoom {2}", this.getClass().getSimpleName(), getName(), currentZoomLevel);
1818 } else {
1819 return tr("{0} ({1}), downloading in zoom {2}", this.getClass().getSimpleName(), getName(), currentZoomLevel);
1820 }
1821 }
1822
1823 @Override
1824 public void visitBoundingBox(BoundingXYVisitor v) {
1825 }
1826
1827 @Override
1828 public boolean isChanged() {
1829 return false; // we use #invalidate()
1830 }
1831
1832 /**
1833 * Task responsible for precaching imagery along the gpx track
1834 *
1835 */
1836 public class PrecacheTask implements TileLoaderListener {
1837 private final ProgressMonitor progressMonitor;
1838 private int totalCount;
1839 private final AtomicInteger processedCount = new AtomicInteger(0);
1840 private final TileLoader tileLoader;
1841
1842 /**
1843 * @param progressMonitor that will be notified about progess of the task
1844 */
1845 public PrecacheTask(ProgressMonitor progressMonitor) {
1846 this.progressMonitor = progressMonitor;
1847 this.tileLoader = getTileLoaderFactory().makeTileLoader(this, getHeaders(tileSource));
1848 if (this.tileLoader instanceof TMSCachedTileLoader) {
1849 ((TMSCachedTileLoader) this.tileLoader).setDownloadExecutor(
1850 TMSCachedTileLoader.getNewThreadPoolExecutor("Precache downloader"));
1851 }
1852 }
1853
1854 /**
1855 * @return true, if all is done
1856 */
1857 public boolean isFinished() {
1858 return processedCount.get() >= totalCount;
1859 }
1860
1861 /**
1862 * @return total number of tiles to download
1863 */
1864 public int getTotalCount() {
1865 return totalCount;
1866 }
1867
1868 /**
1869 * cancel the task
1870 */
1871 public void cancel() {
1872 if (tileLoader instanceof TMSCachedTileLoader) {
1873 ((TMSCachedTileLoader) tileLoader).cancelOutstandingTasks();
1874 }
1875 }
1876
1877 @Override
1878 public void tileLoadingFinished(Tile tile, boolean success) {
1879 int processed = this.processedCount.incrementAndGet();
1880 if (success) {
1881 this.progressMonitor.worked(1);
1882 this.progressMonitor.setCustomText(tr("Downloaded {0}/{1} tiles", processed, totalCount));
1883 } else {
1884 Main.warn("Tile loading failure: " + tile + " - " + tile.getErrorMessage());
1885 }
1886 }
1887
1888 /**
1889 * @return tile loader that is used to load the tiles
1890 */
1891 public TileLoader getTileLoader() {
1892 return tileLoader;
1893 }
1894 }
1895
1896 /**
1897 * Calculates tiles, that needs to be downloaded to cache, gets a current tile loader and creates a task to download
1898 * all of the tiles. Buffer contains at least one tile.
1899 *
1900 * To prevent accidental clear of the queue, new download executor is created with separate queue
1901 *
1902 * @param progressMonitor progress monitor for download task
1903 * @param points lat/lon coordinates to download
1904 * @param bufferX how many units in current Coordinate Reference System to cover in X axis in both sides
1905 * @param bufferY how many units in current Coordinate Reference System to cover in Y axis in both sides
1906 * @return precache task representing download task
1907 */
1908 public AbstractTileSourceLayer<T>.PrecacheTask downloadAreaToCache(final ProgressMonitor progressMonitor, List<LatLon> points,
1909 double bufferX, double bufferY) {
1910 PrecacheTask precacheTask = new PrecacheTask(progressMonitor);
1911 final Set<Tile> requestedTiles = new ConcurrentSkipListSet<>(
1912 (o1, o2) -> String.CASE_INSENSITIVE_ORDER.compare(o1.getKey(), o2.getKey()));
1913 for (LatLon point: points) {
1914 TileXY minTile = tileSource.latLonToTileXY(point.lat() - bufferY, point.lon() - bufferX, currentZoomLevel);
1915 TileXY curTile = tileSource.latLonToTileXY(point.toCoordinate(), currentZoomLevel);
1916 TileXY maxTile = tileSource.latLonToTileXY(point.lat() + bufferY, point.lon() + bufferX, currentZoomLevel);
1917
1918 // take at least one tile of buffer
1919 int minY = Math.min(curTile.getYIndex() - 1, minTile.getYIndex());
1920 int maxY = Math.max(curTile.getYIndex() + 1, maxTile.getYIndex());
1921 int minX = Math.min(curTile.getXIndex() - 1, minTile.getXIndex());
1922 int maxX = Math.max(curTile.getXIndex() + 1, maxTile.getXIndex());
1923
1924 for (int x = minX; x <= maxX; x++) {
1925 for (int y = minY; y <= maxY; y++) {
1926 requestedTiles.add(new Tile(tileSource, x, y, currentZoomLevel));
1927 }
1928 }
1929 }
1930
1931 precacheTask.totalCount = requestedTiles.size();
1932 precacheTask.progressMonitor.setTicksCount(requestedTiles.size());
1933
1934 TileLoader loader = precacheTask.getTileLoader();
1935 for (Tile t: requestedTiles) {
1936 loader.createTileLoaderJob(t).submit();
1937 }
1938 return precacheTask;
1939 }
1940
1941 @Override
1942 public boolean isSavable() {
1943 return true; // With WMSLayerExporter
1944 }
1945
1946 @Override
1947 public File createAndOpenSaveFileChooser() {
1948 return SaveActionBase.createAndOpenSaveFileChooser(tr("Save WMS file"), WMSLayerImporter.FILE_FILTER);
1949 }
1950
1951 @Override
1952 public void destroy() {
1953 super.destroy();
1954 adjustAction.destroy();
1955 }
1956
1957 private class TileSourcePainter extends CompatibilityModeLayerPainter {
1958 /** The memory handle that will hold our tile source. */
1959 private MemoryHandle<?> memory;
1960
1961 @Override
1962 public void paint(MapViewGraphics graphics) {
1963 allocateCacheMemory();
1964 if (memory != null) {
1965 doPaint(graphics);
1966 }
1967 }
1968
1969 private void doPaint(MapViewGraphics graphics) {
1970 drawInViewArea(graphics.getDefaultGraphics(), graphics.getMapView(), graphics.getClipBounds().getProjectionBounds());
1971 }
1972
1973 private void allocateCacheMemory() {
1974 if (memory == null) {
1975 MemoryManager manager = MemoryManager.getInstance();
1976 if (manager.isAvailable(getEstimatedCacheSize())) {
1977 try {
1978 memory = manager.allocateMemory("tile source layer", getEstimatedCacheSize(), Object::new);
1979 } catch (NotEnoughMemoryException e) {
1980 Main.warn("Could not allocate tile source memory", e);
1981 }
1982 }
1983 }
1984 }
1985
1986 protected long getEstimatedCacheSize() {
1987 return 4L * tileSource.getTileSize() * tileSource.getTileSize() * estimateTileCacheSize();
1988 }
1989
1990 @Override
1991 public void detachFromMapView(MapViewEvent event) {
1992 event.getMapView().removeMouseListener(adapter);
1993 MapView.removeZoomChangeListener(AbstractTileSourceLayer.this);
1994 super.detachFromMapView(event);
1995 if (memory != null) {
1996 memory.free();
1997 }
1998 }
1999 }
2000}
Note: See TracBrowser for help on using the repository browser.