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

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

see #7427 - sonar - squid:S2259 - Null pointers should not be dereferenced

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