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

Last change on this file since 17494 was 17494, checked in by wiktorn, 3 years ago

Remove unneeded WMS download threads.

Once the WMS layer is destroyed, remove threadpool, as it will never be GC as the
threads are alive.

Refactor fetching tiles from different zoom levels for easier debugging.

See: #20443, #20497

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