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

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

Fix regression from [17494]

Do not shutdown the thread pool, if this is default one. We do share one thread pool
for all TMS downloads.

  • Property svn:eol-style set to native
File size: 80.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.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 /**
642 * As we can see part of the tile at the top and at the bottom, use Math.ceil(...) + 1 to accommodate for that
643 * Add another 2 tiles on each axis, as overloadTiles adds one tile in each direction that might be overloaded
644 *
645 * @see #overloadTiles()
646 */
647 int maxYtiles = (int) Math.ceil((double) height / tileSize + 1) + 2;
648 int maxXtiles = (int) Math.ceil((double) width / tileSize + 1) + 2;
649 int visibileTiles = maxXtiles * maxYtiles;
650 /**
651 * Take into account ZOOM_OFFSET to calculate real number of tiles and multiply by 8, to cover all tiles, that might be
652 * accessed when looking for tiles outside current zoom level.
653 *
654 * Currently we use otherZooms = {1, 2, -1, -2, -3, -4, -5}
655 *
656 * Check call to tryLoadFromDifferentZoom
657 * @see #tryLoadFromDifferentZoom(Graphics2D, int, List<Tile>,int)
658 * @see #drawInViewArea((Graphics2D, MapView, ProjectionBounds)
659 *
660 */
661 int ret = (int) Math.ceil(
662 Math.pow(2d, ZOOM_OFFSET.get()) * visibileTiles // use offset to decide, how many tiles are visible
663 * 8);
664 Logging.info("AbstractTileSourceLayer: estimated visible tiles: {0}, estimated cache size: {1}", visibileTiles, ret);
665 return ret;
666 }
667
668 @Override
669 public void displaySettingsChanged(DisplaySettingsChangeEvent e) {
670 if (tileSource == null) {
671 return;
672 }
673 switch (e.getChangedSetting()) {
674 case TileSourceDisplaySettings.AUTO_ZOOM:
675 if (getDisplaySettings().isAutoZoom() && getBestZoom() != currentZoomLevel) {
676 setZoomLevel(getBestZoom());
677 invalidate();
678 }
679 break;
680 case TileSourceDisplaySettings.AUTO_LOAD:
681 if (getDisplaySettings().isAutoLoad()) {
682 invalidate();
683 }
684 break;
685 default:
686 // e.g. displacement
687 // trigger a redraw in every case
688 invalidate();
689 }
690 }
691
692 /**
693 * Checks zoom level against settings
694 * @param maxZoomLvl zoom level to check
695 * @param ts tile source to crosscheck with
696 * @return maximum zoom level, not higher than supported by tilesource nor set by the user
697 */
698 public static int checkMaxZoomLvl(int maxZoomLvl, TileSource ts) {
699 if (maxZoomLvl > MAX_ZOOM) {
700 maxZoomLvl = MAX_ZOOM;
701 }
702 if (maxZoomLvl < PROP_MIN_ZOOM_LVL.get()) {
703 maxZoomLvl = PROP_MIN_ZOOM_LVL.get();
704 }
705 if (ts != null && ts.getMaxZoom() != 0 && ts.getMaxZoom() < maxZoomLvl) {
706 maxZoomLvl = ts.getMaxZoom();
707 }
708 return maxZoomLvl;
709 }
710
711 /**
712 * Checks zoom level against settings
713 * @param minZoomLvl zoom level to check
714 * @param ts tile source to crosscheck with
715 * @return minimum zoom level, not higher than supported by tilesource nor set by the user
716 */
717 public static int checkMinZoomLvl(int minZoomLvl, TileSource ts) {
718 if (minZoomLvl < MIN_ZOOM) {
719 minZoomLvl = MIN_ZOOM;
720 }
721 if (minZoomLvl > PROP_MAX_ZOOM_LVL.get()) {
722 minZoomLvl = getMaxZoomLvl(ts);
723 }
724 if (ts != null && ts.getMinZoom() > minZoomLvl) {
725 minZoomLvl = ts.getMinZoom();
726 }
727 return minZoomLvl;
728 }
729
730 /**
731 * Returns maximum max zoom level, that will be shown on layer.
732 * @param ts TileSource for which we want to know maximum zoom level
733 * @return maximum max zoom level, that will be shown on layer
734 */
735 public static int getMaxZoomLvl(TileSource ts) {
736 return checkMaxZoomLvl(PROP_MAX_ZOOM_LVL.get(), ts);
737 }
738
739 /**
740 * Returns minimum zoom level, that will be shown on layer.
741 * @param ts TileSource for which we want to know minimum zoom level
742 * @return minimum zoom level, that will be shown on layer
743 */
744 public static int getMinZoomLvl(TileSource ts) {
745 return checkMinZoomLvl(PROP_MIN_ZOOM_LVL.get(), ts);
746 }
747
748 /**
749 * Sets maximum zoom level, that layer will attempt show
750 * @param maxZoomLvl maximum zoom level
751 */
752 public static void setMaxZoomLvl(int maxZoomLvl) {
753 PROP_MAX_ZOOM_LVL.put(checkMaxZoomLvl(maxZoomLvl, null));
754 }
755
756 /**
757 * Sets minimum zoom level, that layer will attempt show
758 * @param minZoomLvl minimum zoom level
759 */
760 public static void setMinZoomLvl(int minZoomLvl) {
761 PROP_MIN_ZOOM_LVL.put(checkMinZoomLvl(minZoomLvl, null));
762 }
763
764 /**
765 * This fires every time the user changes the zoom, but also (due to ZoomChangeListener) - on all
766 * changes to visible map (panning/zooming)
767 */
768 @Override
769 public void zoomChanged() {
770 zoomChanged(true);
771 }
772
773 private void zoomChanged(boolean invalidate) {
774 Logging.debug("zoomChanged(): {0}", currentZoomLevel);
775 if (tileLoader instanceof TMSCachedTileLoader) {
776 ((TMSCachedTileLoader) tileLoader).cancelOutstandingTasks();
777 }
778 if (invalidate) {
779 invalidate();
780 }
781 }
782
783 protected int getMaxZoomLvl() {
784 if (info.getMaxZoom() != 0)
785 return checkMaxZoomLvl(info.getMaxZoom(), tileSource);
786 else
787 return getMaxZoomLvl(tileSource);
788 }
789
790 protected int getMinZoomLvl() {
791 if (info.getMinZoom() != 0)
792 return checkMinZoomLvl(info.getMinZoom(), tileSource);
793 else
794 return getMinZoomLvl(tileSource);
795 }
796
797 /**
798 * Determines if it is allowed to zoom in.
799 * @return if it is allowed to zoom in
800 */
801 public boolean zoomIncreaseAllowed() {
802 boolean zia = currentZoomLevel < this.getMaxZoomLvl();
803 Logging.debug("zoomIncreaseAllowed(): {0} {1} vs. {2}", zia, currentZoomLevel, this.getMaxZoomLvl());
804 return zia;
805 }
806
807 /**
808 * Zoom in, go closer to map.
809 *
810 * @return true, if zoom increasing was successful, false otherwise
811 */
812 public boolean increaseZoomLevel() {
813 if (zoomIncreaseAllowed()) {
814 currentZoomLevel++;
815 Logging.debug("increasing zoom level to: {0}", currentZoomLevel);
816 zoomChanged();
817 } else {
818 Logging.warn("Current zoom level ("+currentZoomLevel+") could not be increased. "+
819 "Max.zZoom Level "+this.getMaxZoomLvl()+" reached.");
820 return false;
821 }
822 return true;
823 }
824
825 /**
826 * Get the current zoom level of the layer
827 * @return the current zoom level
828 * @since 12603
829 */
830 public int getZoomLevel() {
831 return currentZoomLevel;
832 }
833
834 /**
835 * Sets the zoom level of the layer
836 * @param zoom zoom level
837 * @return true, when zoom has changed to desired value, false if it was outside supported zoom levels
838 */
839 public boolean setZoomLevel(int zoom) {
840 return setZoomLevel(zoom, true);
841 }
842
843 private boolean setZoomLevel(int zoom, boolean invalidate) {
844 if (zoom == currentZoomLevel) return true;
845 if (zoom > this.getMaxZoomLvl()) return false;
846 if (zoom < this.getMinZoomLvl()) return false;
847 currentZoomLevel = zoom;
848 zoomChanged(invalidate);
849 return true;
850 }
851
852 /**
853 * Check if zooming out is allowed
854 *
855 * @return true, if zooming out is allowed (currentZoomLevel &gt; minZoomLevel)
856 */
857 public boolean zoomDecreaseAllowed() {
858 boolean zda = currentZoomLevel > this.getMinZoomLvl();
859 Logging.debug("zoomDecreaseAllowed(): {0} {1} vs. {2}", zda, currentZoomLevel, this.getMinZoomLvl());
860 return zda;
861 }
862
863 /**
864 * Zoom out from map.
865 *
866 * @return true, if zoom increasing was successful, false othervise
867 */
868 public boolean decreaseZoomLevel() {
869 if (zoomDecreaseAllowed()) {
870 Logging.debug("decreasing zoom level to: {0}", currentZoomLevel);
871 currentZoomLevel--;
872 zoomChanged();
873 } else {
874 return false;
875 }
876 return true;
877 }
878
879 private Tile getOrCreateTile(TilePosition tilePosition) {
880 return getOrCreateTile(tilePosition.getX(), tilePosition.getY(), tilePosition.getZoom());
881 }
882
883 private Tile getOrCreateTile(int x, int y, int zoom) {
884 Tile tile = getTile(x, y, zoom);
885 if (tile == null) {
886 if (coordinateConverter.requiresReprojection()) {
887 tile = new ReprojectionTile(tileSource, x, y, zoom);
888 } else {
889 tile = new Tile(tileSource, x, y, zoom);
890 }
891 tileCache.addTile(tile);
892 }
893 return tile;
894 }
895
896 private Tile getTile(TilePosition tilePosition) {
897 return getTile(tilePosition.getX(), tilePosition.getY(), tilePosition.getZoom());
898 }
899
900 /**
901 * Returns tile at given position.
902 * This can and will return null for tiles that are not already in the cache.
903 * @param x tile number on the x axis of the tile to be retrieved
904 * @param y tile number on the y axis of the tile to be retrieved
905 * @param zoom zoom level of the tile to be retrieved
906 * @return tile at given position
907 */
908 private Tile getTile(int x, int y, int zoom) {
909 if (x < tileSource.getTileXMin(zoom) || x > tileSource.getTileXMax(zoom)
910 || y < tileSource.getTileYMin(zoom) || y > tileSource.getTileYMax(zoom))
911 return null;
912 return tileCache.getTile(tileSource, x, y, zoom);
913 }
914
915 private boolean loadTile(Tile tile, boolean force) {
916 if (tile == null)
917 return false;
918 if (!force && tile.isLoaded())
919 return false;
920 if (tile.isLoading())
921 return false;
922 tileLoader.createTileLoaderJob(tile).submit(force);
923 return true;
924 }
925
926 private TileSet getVisibleTileSet() {
927 if (!MainApplication.isDisplayingMapView())
928 return new TileSet();
929 ProjectionBounds bounds = MainApplication.getMap().mapView.getProjectionBounds();
930 return getTileSet(bounds, currentZoomLevel);
931 }
932
933 /**
934 * Load all visible tiles.
935 * @param force {@code true} to force loading if auto-load is disabled
936 * @since 11950
937 */
938 public void loadAllTiles(boolean force) {
939 TileSet ts = getVisibleTileSet();
940 ts.loadAllTiles(force);
941 invalidate();
942 }
943
944 /**
945 * Load all visible tiles in error.
946 * @param force {@code true} to force loading if auto-load is disabled
947 * @since 11950
948 */
949 public void loadAllErrorTiles(boolean force) {
950 TileSet ts = getVisibleTileSet();
951 ts.loadAllErrorTiles(force);
952 invalidate();
953 }
954
955 @Override
956 public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
957 boolean done = (infoflags & (ERROR | FRAMEBITS | ALLBITS)) != 0;
958 Logging.debug("imageUpdate() done: {0} calling repaint", done);
959
960 if (done) {
961 invalidate();
962 } else {
963 invalidateLater();
964 }
965 return !done;
966 }
967
968 /**
969 * Invalidate the layer at a time in the future so that the user still sees the interface responsive.
970 */
971 private void invalidateLater() {
972 GuiHelper.runInEDT(() -> {
973 if (!invalidateLaterTimer.isRunning()) {
974 invalidateLaterTimer.setRepeats(false);
975 invalidateLaterTimer.start();
976 }
977 });
978 }
979
980 private boolean imageLoaded(Image i) {
981 if (i == null)
982 return false;
983 int status = Toolkit.getDefaultToolkit().checkImage(i, -1, -1, this);
984 return (status & ALLBITS) != 0;
985 }
986
987 /**
988 * Returns the image for the given tile image is loaded.
989 * Otherwise returns null.
990 *
991 * @param tile the Tile for which the image should be returned
992 * @return the image of the tile or null.
993 */
994 private BufferedImage getLoadedTileImage(Tile tile) {
995 BufferedImage img = tile.getImage();
996 if (!imageLoaded(img))
997 return null;
998 return img;
999 }
1000
1001 /**
1002 * Draw a tile image on screen.
1003 * @param g the Graphics2D
1004 * @param toDrawImg tile image
1005 * @param anchorImage tile anchor in image coordinates
1006 * @param anchorScreen tile anchor in screen coordinates
1007 * @param clip clipping region in screen coordinates (can be null)
1008 */
1009 private void drawImageInside(Graphics2D g, BufferedImage toDrawImg, TileAnchor anchorImage, TileAnchor anchorScreen, Shape clip) {
1010 AffineTransform imageToScreen = anchorImage.convert(anchorScreen);
1011 Point2D screen0 = imageToScreen.transform(new Point2D.Double(0, 0), null);
1012 Point2D screen1 = imageToScreen.transform(new Point2D.Double(
1013 toDrawImg.getWidth(), toDrawImg.getHeight()), null);
1014
1015 Shape oldClip = null;
1016 if (clip != null) {
1017 oldClip = g.getClip();
1018 g.clip(clip);
1019 }
1020 g.drawImage(toDrawImg, (int) Math.round(screen0.getX()), (int) Math.round(screen0.getY()),
1021 (int) Math.round(screen1.getX()) - (int) Math.round(screen0.getX()),
1022 (int) Math.round(screen1.getY()) - (int) Math.round(screen0.getY()), this);
1023 if (clip != null) {
1024 g.setClip(oldClip);
1025 }
1026 }
1027
1028 private List<Tile> paintTileImages(Graphics2D g, TileSet ts) {
1029 Object paintMutex = new Object();
1030 List<TilePosition> missed = Collections.synchronizedList(new ArrayList<>());
1031 ts.visitTiles(tile -> {
1032 boolean miss = false;
1033 BufferedImage img = null;
1034 TileAnchor anchorImage = null;
1035 if (!tile.isLoaded() || tile.hasError()) {
1036 miss = true;
1037 } else {
1038 synchronized (tile) {
1039 img = getLoadedTileImage(tile);
1040 anchorImage = getAnchor(tile, img);
1041 }
1042 if (img == null || anchorImage == null) {
1043 miss = true;
1044 }
1045 }
1046 if (miss) {
1047 missed.add(new TilePosition(tile));
1048 return;
1049 }
1050
1051 img = applyImageProcessors(img);
1052
1053 TileAnchor anchorScreen = coordinateConverter.getScreenAnchorForTile(tile);
1054 synchronized (paintMutex) {
1055 //cannot paint in parallel
1056 drawImageInside(g, img, anchorImage, anchorScreen, null);
1057 }
1058 MapView mapView = MainApplication.getMap().mapView;
1059 if (tile instanceof ReprojectionTile && ((ReprojectionTile) tile).needsUpdate(mapView.getScale())) {
1060 // This means we have a reprojected tile in memory cache, but not at
1061 // current scale. Generally, the positioning of the tile will still
1062 // be correct, but for best image quality, the tile should be
1063 // reprojected to the target scale. The original tile image should
1064 // still be in disk cache, so this is fairly cheap.
1065 ((ReprojectionTile) tile).invalidate();
1066 loadTile(tile, false);
1067 }
1068
1069 }, missed::add);
1070
1071 return missed.stream().map(this::getOrCreateTile).collect(Collectors.toList());
1072 }
1073
1074 // This function is called for several zoom levels, not just the current one.
1075 // It should not trigger any tiles to be downloaded.
1076 // It should also avoid polluting the tile cache with any tiles since these tiles are not mandatory.
1077 //
1078 // The "border" tile tells us the boundaries of where we may drawn.
1079 // It will not be from the zoom level that is being drawn currently.
1080 // If drawing the displayZoomLevel, border is null and we draw the entire tile set.
1081 private List<Tile> paintTileImages(Graphics2D g, TileSet ts, int zoom, Tile border) {
1082 if (zoom <= 0) return Collections.emptyList();
1083 Shape borderClip = coordinateConverter.getTileShapeScreen(border);
1084 List<Tile> missedTiles = new LinkedList<>();
1085 // The callers of this code *require* that we return any tiles that we do not draw in missedTiles.
1086 // ts.allExistingTiles() by default will only return already-existing tiles.
1087 // However, we need to return *all* tiles to the callers, so force creation here.
1088 for (Tile tile : ts.allTilesCreate()) {
1089 boolean miss = false;
1090 BufferedImage img = null;
1091 TileAnchor anchorImage = null;
1092 if (!tile.isLoaded() || tile.hasError()) {
1093 miss = true;
1094 } else {
1095 synchronized (tile) {
1096 img = getLoadedTileImage(tile);
1097 anchorImage = getAnchor(tile, img);
1098 }
1099
1100 if (img == null || anchorImage == null) {
1101 miss = true;
1102 }
1103 }
1104 if (miss) {
1105 missedTiles.add(tile);
1106 continue;
1107 }
1108
1109 // applying all filters to this layer
1110 img = applyImageProcessors(img);
1111
1112 Shape clip;
1113 if (tileSource.isInside(tile, border)) {
1114 clip = null;
1115 } else if (tileSource.isInside(border, tile)) {
1116 clip = borderClip;
1117 } else {
1118 continue;
1119 }
1120 TileAnchor anchorScreen = coordinateConverter.getScreenAnchorForTile(tile);
1121 drawImageInside(g, img, anchorImage, anchorScreen, clip);
1122 }
1123 return Collections.unmodifiableList(missedTiles);
1124 }
1125
1126 private static TileAnchor getAnchor(Tile tile, BufferedImage image) {
1127 if (tile instanceof ReprojectionTile) {
1128 return ((ReprojectionTile) tile).getAnchor();
1129 } else if (image != null) {
1130 return new TileAnchor(new Point2D.Double(0, 0), new Point2D.Double(image.getWidth(), image.getHeight()));
1131 } else {
1132 return null;
1133 }
1134 }
1135
1136 private void myDrawString(Graphics g, String text, int x, int y) {
1137 Color oldColor = g.getColor();
1138 String textToDraw = text;
1139 if (g.getFontMetrics().stringWidth(text) > tileSource.getTileSize()) {
1140 // text longer than tile size, split it
1141 StringBuilder line = new StringBuilder();
1142 StringBuilder ret = new StringBuilder();
1143 for (String s: text.split(" ", -1)) {
1144 if (g.getFontMetrics().stringWidth(line.toString() + s) > tileSource.getTileSize()) {
1145 ret.append(line).append('\n');
1146 line.setLength(0);
1147 }
1148 line.append(s).append(' ');
1149 }
1150 ret.append(line);
1151 textToDraw = ret.toString();
1152 }
1153 int offset = 0;
1154 for (String s: textToDraw.split("\n", -1)) {
1155 g.setColor(Color.black);
1156 g.drawString(s, x + 1, y + offset + 1);
1157 g.setColor(oldColor);
1158 g.drawString(s, x, y + offset);
1159 offset += g.getFontMetrics().getHeight() + 3;
1160 }
1161 }
1162
1163 private void paintTileText(Tile tile, Graphics2D g) {
1164 if (tile == null) {
1165 return;
1166 }
1167 Point2D p = coordinateConverter.getPixelForTile(tile);
1168 int fontHeight = g.getFontMetrics().getHeight();
1169 int x = (int) p.getX();
1170 int y = (int) p.getY();
1171 int texty = y + 2 + fontHeight;
1172
1173 /*if (PROP_DRAW_DEBUG.get()) {
1174 myDrawString(g, "x=" + tile.getXtile() + " y=" + tile.getYtile() + " z=" + tile.getZoom() + "", x + 2, texty);
1175 texty += 1 + fontHeight;
1176 if ((tile.getXtile() % 32 == 0) && (tile.getYtile() % 32 == 0)) {
1177 myDrawString(g, "x=" + tile.getXtile() / 32 + " y=" + tile.getYtile() / 32 + " z=7", x + 2, texty);
1178 texty += 1 + fontHeight;
1179 }
1180 }
1181
1182 String tileStatus = tile.getStatus();
1183 if (!tile.isLoaded() && PROP_DRAW_DEBUG.get()) {
1184 myDrawString(g, tr("image " + tileStatus), x, texty);
1185 texty += 1 + fontHeight;
1186 }*/
1187
1188 if (tile.hasError() && getDisplaySettings().isShowErrors()) {
1189 String errorMessage = tile.getErrorMessage();
1190 if (errorMessage != null) {
1191 try {
1192 errorMessage = tr(tile.getErrorMessage());
1193 } catch (IllegalArgumentException e) {
1194 Logging.debug(e);
1195 }
1196 if (!errorMessage.startsWith("Error") && !errorMessage.startsWith(tr("Error"))) {
1197 errorMessage = tr("Error") + ": " + errorMessage;
1198 }
1199 myDrawString(g, errorMessage, x + 2, texty);
1200 }
1201 //texty += 1 + fontHeight;
1202 }
1203
1204 if (Logging.isDebugEnabled()) {
1205 // draw tile outline in semi-transparent red
1206 g.setColor(new Color(255, 0, 0, 50));
1207 g.draw(coordinateConverter.getTileShapeScreen(tile));
1208 }
1209 }
1210
1211 private LatLon getShiftedLatLon(EastNorth en) {
1212 return coordinateConverter.getProjecting().eastNorth2latlonClamped(en);
1213 }
1214
1215 private ICoordinate getShiftedCoord(EastNorth en) {
1216 return CoordinateConversion.llToCoor(getShiftedLatLon(en));
1217 }
1218
1219 private final TileSet nullTileSet = new TileSet();
1220
1221 protected class TileSet extends TileRange {
1222
1223 private volatile TileSetInfo info;
1224
1225 protected TileSet(TileXY t1, TileXY t2, int zoom) {
1226 super(t1, t2, zoom);
1227 sanitize();
1228 }
1229
1230 protected TileSet(TileRange range) {
1231 super(range);
1232 sanitize();
1233 }
1234
1235 /**
1236 * null tile set
1237 */
1238 private TileSet() {
1239 // default
1240 }
1241
1242 protected void sanitize() {
1243 minX = Utils.clamp(minX, tileSource.getTileXMin(zoom), tileSource.getTileXMax(zoom));
1244 maxX = Utils.clamp(maxX, tileSource.getTileXMin(zoom), tileSource.getTileXMax(zoom));
1245 minY = Utils.clamp(minY, tileSource.getTileYMin(zoom), tileSource.getTileYMax(zoom));
1246 maxY = Utils.clamp(maxY, tileSource.getTileYMin(zoom), tileSource.getTileYMax(zoom));
1247 }
1248
1249 private boolean tooSmall() {
1250 return this.tilesSpanned() < 2.1;
1251 }
1252
1253 private boolean tooLarge() {
1254 return tileCache == null || size() > tileCache.getCacheSize();
1255 }
1256
1257 /**
1258 * Get all tiles represented by this TileSet that are already in the tileCache.
1259 * @return all tiles represented by this TileSet that are already in the tileCache
1260 */
1261 private List<Tile> allExistingTiles() {
1262 return allTiles(AbstractTileSourceLayer.this::getTile);
1263 }
1264
1265 private List<Tile> allTilesCreate() {
1266 return allTiles(AbstractTileSourceLayer.this::getOrCreateTile);
1267 }
1268
1269 private List<Tile> allTiles(Function<TilePosition, Tile> mapper) {
1270 return tilePositions().map(mapper).filter(Objects::nonNull).collect(Collectors.toList());
1271 }
1272
1273 /**
1274 * Gets a stream of all tile positions in this set
1275 * @return A stream of all positions
1276 */
1277 public Stream<TilePosition> tilePositions() {
1278 if (zoom == 0 || this.tooLarge()) {
1279 return Stream.empty(); // Tileset is either empty or too large
1280 } else {
1281 return IntStream.rangeClosed(minX, maxX).mapToObj(
1282 x -> IntStream.rangeClosed(minY, maxY).mapToObj(y -> new TilePosition(x, y, zoom))
1283 ).flatMap(Function.identity());
1284 }
1285 }
1286
1287 private List<Tile> allLoadedTiles() {
1288 return allExistingTiles().stream().filter(Tile::isLoaded).collect(Collectors.toList());
1289 }
1290
1291 /**
1292 * @return comparator, that sorts the tiles from the center to the edge of the current screen
1293 */
1294 private Comparator<Tile> getTileDistanceComparator() {
1295 final int centerX = (int) Math.ceil((minX + maxX) / 2d);
1296 final int centerY = (int) Math.ceil((minY + maxY) / 2d);
1297 return Comparator.comparingInt(t -> Math.abs(t.getXtile() - centerX) + Math.abs(t.getYtile() - centerY));
1298 }
1299
1300 private void loadAllTiles(boolean force) {
1301 if (!getDisplaySettings().isAutoLoad() && !force) {
1302 return;
1303 }
1304 if (tooLarge()) {
1305 // Too many tiles... refuse to download
1306 Logging.warn("Not downloading all tiles because there is more than {0} tiles on an axis!", MAX_TILES_SPANNED);
1307 return;
1308 }
1309 List<Tile> allTiles = allTilesCreate();
1310 allTiles.sort(getTileDistanceComparator());
1311 for (Tile t : allTiles) {
1312 loadTile(t, force);
1313 }
1314 }
1315
1316 /**
1317 * Extend tile loading corridor, so that no flickering happens when panning
1318 */
1319 private void overloadTiles() {
1320 int overload = 1;
1321
1322 int minXo = Utils.clamp(minX-overload, tileSource.getTileXMin(zoom), tileSource.getTileXMax(zoom));
1323 int maxXo = Utils.clamp(maxX+overload, tileSource.getTileXMin(zoom), tileSource.getTileXMax(zoom));
1324 int minYo = Utils.clamp(minY-overload, tileSource.getTileYMin(zoom), tileSource.getTileYMax(zoom));
1325 int maxYo = Utils.clamp(maxY+overload, tileSource.getTileYMin(zoom), tileSource.getTileYMax(zoom));
1326
1327 TileSet ts = new TileSet(new TileXY(minXo, minYo), new TileXY(maxXo, maxYo), zoom);
1328 ts.loadAllTiles(false);
1329 }
1330
1331 private void loadAllErrorTiles(boolean force) {
1332 if (!getDisplaySettings().isAutoLoad() && !force)
1333 return;
1334 for (Tile t : this.allTilesCreate()) {
1335 if (t.hasError()) {
1336 tileLoader.createTileLoaderJob(t).submit(force);
1337 }
1338 }
1339 }
1340
1341 /**
1342 * Call the given paint method for all tiles in this tile set.<p>
1343 * Uses a parallel stream.
1344 * @param visitor A visitor to call for each tile.
1345 * @param missed a consumer to call for each missed tile.
1346 */
1347 public void visitTiles(Consumer<Tile> visitor, Consumer<TilePosition> missed) {
1348 tilePositions().parallel().forEach(tp -> visitTilePosition(visitor, tp, missed));
1349 }
1350
1351 private void visitTilePosition(Consumer<Tile> visitor, TilePosition tp, Consumer<TilePosition> missed) {
1352 Tile tile = getTile(tp);
1353 if (tile == null) {
1354 missed.accept(tp);
1355 } else {
1356 visitor.accept(tile);
1357 }
1358 }
1359
1360 /**
1361 * Check if there is any tile fully loaded without error.
1362 * @return true if there is any tile fully loaded without error
1363 */
1364 public boolean hasVisibleTiles() {
1365 return getTileSetInfo().hasVisibleTiles;
1366 }
1367
1368 /**
1369 * Check if there there is a tile that is overzoomed.
1370 * <p>
1371 * I.e. the server response for one tile was "there is no tile here".
1372 * This usually happens when zoomed in too much. The limit depends on
1373 * the region, so at the edge of such a region, some tiles may be
1374 * available and some not.
1375 * @return true if there there is a tile that is overzoomed
1376 */
1377 public boolean hasOverzoomedTiles() {
1378 return getTileSetInfo().hasOverzoomedTiles;
1379 }
1380
1381 /**
1382 * Check if there are tiles still loading.
1383 * <p>
1384 * This is the case if there is a tile not yet in the cache, or in the
1385 * cache but marked as loading ({@link Tile#isLoading()}.
1386 * @return true if there are tiles still loading
1387 */
1388 public boolean hasLoadingTiles() {
1389 return getTileSetInfo().hasLoadingTiles;
1390 }
1391
1392 /**
1393 * Check if all tiles in the range are fully loaded.
1394 * <p>
1395 * A tile is considered to be fully loaded even if the result of loading
1396 * the tile was an error.
1397 * @return true if all tiles in the range are fully loaded
1398 */
1399 public boolean hasAllLoadedTiles() {
1400 return getTileSetInfo().hasAllLoadedTiles;
1401 }
1402
1403 private TileSetInfo getTileSetInfo() {
1404 if (info == null) {
1405 synchronized (this) {
1406 if (info == null) {
1407 List<Tile> allTiles = this.allExistingTiles();
1408 TileSetInfo newInfo = new TileSetInfo();
1409 newInfo.hasLoadingTiles = allTiles.size() < this.size();
1410 newInfo.hasAllLoadedTiles = true;
1411 for (Tile t : allTiles) {
1412 if ("no-tile".equals(t.getValue("tile-info"))) {
1413 newInfo.hasOverzoomedTiles = true;
1414 }
1415 if (t.isLoaded()) {
1416 if (!t.hasError()) {
1417 newInfo.hasVisibleTiles = true;
1418 }
1419 } else {
1420 newInfo.hasAllLoadedTiles = false;
1421 if (t.isLoading()) {
1422 newInfo.hasLoadingTiles = true;
1423 }
1424 }
1425 }
1426 info = newInfo;
1427 }
1428 }
1429 }
1430 return info;
1431 }
1432
1433 @Override
1434 public String toString() {
1435 return getClass().getName() + ": zoom: " + zoom + " X(" + minX + ", " + maxX + ") Y(" + minY + ", " + maxY + ") size: " + size();
1436 }
1437 }
1438
1439 /**
1440 * Data container to hold information about a {@code TileSet} class.
1441 */
1442 private static class TileSetInfo {
1443 boolean hasVisibleTiles;
1444 boolean hasOverzoomedTiles;
1445 boolean hasLoadingTiles;
1446 boolean hasAllLoadedTiles;
1447 }
1448
1449 /**
1450 * Create a TileSet by EastNorth bbox taking a layer shift in account
1451 * @param bounds the EastNorth bounds
1452 * @param zoom zoom level
1453 * @return the tile set
1454 */
1455 protected TileSet getTileSet(ProjectionBounds bounds, int zoom) {
1456 if (zoom == 0)
1457 return new TileSet();
1458 TileXY t1, t2;
1459 IProjected topLeftUnshifted = coordinateConverter.shiftDisplayToServer(bounds.getMin());
1460 IProjected botRightUnshifted = coordinateConverter.shiftDisplayToServer(bounds.getMax());
1461 if (coordinateConverter.requiresReprojection()) {
1462 Projection projServer = Projections.getProjectionByCode(tileSource.getServerCRS());
1463 if (projServer == null) {
1464 throw new IllegalStateException(tileSource.toString());
1465 }
1466 ProjectionBounds projBounds = new ProjectionBounds(
1467 CoordinateConversion.projToEn(topLeftUnshifted),
1468 CoordinateConversion.projToEn(botRightUnshifted));
1469 ProjectionBounds bbox = projServer.getEastNorthBoundsBox(projBounds, ProjectionRegistry.getProjection());
1470 t1 = tileSource.projectedToTileXY(CoordinateConversion.enToProj(bbox.getMin()), zoom);
1471 t2 = tileSource.projectedToTileXY(CoordinateConversion.enToProj(bbox.getMax()), zoom);
1472 } else {
1473 t1 = tileSource.projectedToTileXY(topLeftUnshifted, zoom);
1474 t2 = tileSource.projectedToTileXY(botRightUnshifted, zoom);
1475 }
1476 return new TileSet(t1, t2, zoom);
1477 }
1478
1479 private class DeepTileSet {
1480 private final ProjectionBounds bounds;
1481 private final int minZoom, maxZoom;
1482 private final TileSet[] tileSets;
1483
1484 @SuppressWarnings("unchecked")
1485 DeepTileSet(ProjectionBounds bounds, int minZoom, int maxZoom) {
1486 this.bounds = bounds;
1487 this.minZoom = minZoom;
1488 this.maxZoom = maxZoom;
1489 if (minZoom > maxZoom) {
1490 throw new IllegalArgumentException(minZoom + " > " + maxZoom);
1491 }
1492 this.tileSets = new AbstractTileSourceLayer.TileSet[maxZoom - minZoom + 1];
1493 }
1494
1495 public TileSet getTileSet(int zoom) {
1496 if (zoom < minZoom)
1497 return nullTileSet;
1498 synchronized (tileSets) {
1499 TileSet ts = tileSets[zoom-minZoom];
1500 if (ts == null) {
1501 ts = AbstractTileSourceLayer.this.getTileSet(bounds, zoom);
1502 tileSets[zoom-minZoom] = ts;
1503 }
1504 return ts;
1505 }
1506 }
1507 }
1508
1509 @Override
1510 public void paint(Graphics2D g, MapView mv, Bounds bounds) {
1511 // old and unused.
1512 }
1513
1514 private void drawInViewArea(Graphics2D g, MapView mv, ProjectionBounds pb) {
1515 int zoom = currentZoomLevel;
1516 if (getDisplaySettings().isAutoZoom()) {
1517 zoom = getBestZoom();
1518 }
1519
1520 DeepTileSet dts = new DeepTileSet(pb, getMinZoomLvl(), zoom);
1521
1522 int displayZoomLevel = zoom;
1523
1524 boolean noTilesAtZoom = false;
1525 if (getDisplaySettings().isAutoZoom() && getDisplaySettings().isAutoLoad()) {
1526 // Auto-detection of tilesource maxzoom (currently fully works only for Bing)
1527 TileSet ts0 = dts.getTileSet(zoom);
1528 if (!ts0.hasVisibleTiles() && (!ts0.hasLoadingTiles() || ts0.hasOverzoomedTiles())) {
1529 noTilesAtZoom = true;
1530 }
1531 // Find highest zoom level with at least one visible tile
1532 for (int tmpZoom = zoom; tmpZoom > dts.minZoom; tmpZoom--) {
1533 if (dts.getTileSet(tmpZoom).hasVisibleTiles()) {
1534 displayZoomLevel = tmpZoom;
1535 break;
1536 }
1537 }
1538 // Do binary search between currentZoomLevel and displayZoomLevel
1539 while (zoom > displayZoomLevel && !ts0.hasVisibleTiles() && ts0.hasOverzoomedTiles()) {
1540 zoom = (zoom + displayZoomLevel)/2;
1541 ts0 = dts.getTileSet(zoom);
1542 }
1543
1544 setZoomLevel(zoom, false);
1545
1546 // If all tiles at displayZoomLevel is loaded, load all tiles at next zoom level
1547 // to make sure there're really no more zoom levels
1548 // loading is done in the next if section
1549 if (zoom == displayZoomLevel && !ts0.hasLoadingTiles() && zoom < dts.maxZoom) {
1550 zoom++;
1551 ts0 = dts.getTileSet(zoom);
1552 }
1553 // When we have overzoomed tiles and all tiles at current zoomlevel is loaded,
1554 // load tiles at previovus zoomlevels until we have all tiles on screen is loaded.
1555 // loading is done in the next if section
1556 while (zoom > dts.minZoom && ts0.hasOverzoomedTiles() && !ts0.hasLoadingTiles()) {
1557 zoom--;
1558 ts0 = dts.getTileSet(zoom);
1559 }
1560 } else if (getDisplaySettings().isAutoZoom()) {
1561 setZoomLevel(zoom, false);
1562 }
1563 TileSet ts = dts.getTileSet(zoom);
1564
1565 // try to load tiles from desired zoom level, no matter what we will show (for example, tiles from previous zoom level
1566 // on zoom in)
1567 ts.loadAllTiles(false);
1568
1569 if (displayZoomLevel != zoom) {
1570 ts = dts.getTileSet(displayZoomLevel);
1571 if (!dts.getTileSet(displayZoomLevel).hasAllLoadedTiles() && displayZoomLevel < zoom) {
1572 // if we are showing tiles from lower zoom level, ensure that all tiles are loaded as they are few,
1573 // and should not trash the tile cache
1574 // This is especially needed when dts.getTileSet(zoom).tooLarge() is true and we are not loading tiles
1575 ts.loadAllTiles(false);
1576 }
1577 }
1578
1579 g.setColor(Color.DARK_GRAY);
1580
1581 List<Tile> missedTiles = this.paintTileImages(g, ts);
1582 if (getDisplaySettings().isAutoLoad()) {
1583 ts.overloadTiles();
1584 }
1585 if (getDisplaySettings().isAutoZoom()) {
1586 int[] otherZooms = {1, 2, -1, -2, -3, -4, -5};
1587
1588 for(int otherZoom: otherZooms) {
1589 missedTiles = tryLoadFromDifferentZoom(g, displayZoomLevel, missedTiles, otherZoom);
1590 if (missedTiles.isEmpty()) {
1591 break;
1592 }
1593 }
1594 }
1595
1596 if (Logging.isDebugEnabled() && !missedTiles.isEmpty()) {
1597 Logging.debug("still missed {0} in the end", missedTiles.size());
1598 }
1599 g.setColor(Color.red);
1600 g.setFont(InfoFont);
1601
1602 // The current zoom tileset should have all of its tiles due to the loadAllTiles(), unless it to tooLarge()
1603 for (Tile t : ts.allExistingTiles()) {
1604 this.paintTileText(t, g);
1605 }
1606
1607 EastNorth min = pb.getMin();
1608 EastNorth max = pb.getMax();
1609 attribution.paintAttribution(g, mv.getWidth(), mv.getHeight(), getShiftedCoord(min), getShiftedCoord(max),
1610 displayZoomLevel, this);
1611
1612 g.setColor(Color.lightGray);
1613
1614 if (ts.tooLarge()) {
1615 myDrawString(g, tr("zoom in to load more tiles"), 120, 120);
1616 } else if (!getDisplaySettings().isAutoZoom() && ts.tooSmall()) {
1617 myDrawString(g, tr("increase tiles zoom level (change resolution) to see more detail"), 120, 120);
1618 }
1619 if (noTilesAtZoom) {
1620 myDrawString(g, tr("No tiles at this zoom level"), 120, 120);
1621 }
1622 if (Logging.isDebugEnabled()) {
1623 myDrawString(g, tr("Current zoom: {0}", currentZoomLevel), 50, 140);
1624 myDrawString(g, tr("Display zoom: {0}", displayZoomLevel), 50, 155);
1625 myDrawString(g, tr("Pixel scale: {0}", getScaleFactor(currentZoomLevel)), 50, 170);
1626 myDrawString(g, tr("Best zoom: {0}", getBestZoom()), 50, 185);
1627 myDrawString(g, tr("Estimated cache size: {0}", estimateTileCacheSize()), 50, 200);
1628 if (tileLoader instanceof TMSCachedTileLoader) {
1629 int offset = 200;
1630 for (String part: ((TMSCachedTileLoader) tileLoader).getStats().split("\n", -1)) {
1631 offset += 15;
1632 myDrawString(g, tr("Cache stats: {0}", part), 50, offset);
1633 }
1634 }
1635 }
1636 }
1637
1638 private List<Tile> tryLoadFromDifferentZoom(Graphics2D g, int displayZoomLevel, List<Tile> missedTiles,
1639 int zoomOffset) {
1640
1641 int newzoom = displayZoomLevel + zoomOffset;
1642 if (newzoom < getMinZoomLvl() || newzoom > getMaxZoomLvl()) {
1643 return missedTiles;
1644 }
1645
1646 List<Tile> newlyMissedTiles = new LinkedList<>();
1647 for (Tile missed : missedTiles) {
1648 if (zoomOffset > 0 && "no-tile".equals(missed.getValue("tile-info"))) {
1649 // Don't try to paint from higher zoom levels when tile is overzoomed
1650 newlyMissedTiles.add(missed);
1651 continue;
1652 }
1653 TileSet ts2 = new TileSet(tileSource.getCoveringTileRange(missed, newzoom));
1654 // Instantiating large TileSets is expensive. If there are no loaded tiles, don't bother even trying.
1655 if (ts2.allLoadedTiles().isEmpty()) {
1656 if (zoomOffset > 0) {
1657 newlyMissedTiles.add(missed);
1658 continue;
1659 } else {
1660 /*
1661 * We have negative zoom offset. Try to load tiles from lower zoom levels, as they may be not present
1662 * in tile cache (e.g. when user panned the map or opened layer above zoom level, for which tiles are present.
1663 * This will ensure, that tileCache is populated with tiles from lower zoom levels so it will be possible to
1664 * use them to paint overzoomed tiles.
1665 * See: #14562
1666 */
1667 ts2.loadAllTiles(false);
1668 }
1669 }
1670 if (ts2.tooLarge()) {
1671 continue;
1672 }
1673 newlyMissedTiles.addAll(this.paintTileImages(g, ts2, newzoom, missed));
1674 }
1675 return newlyMissedTiles;
1676 }
1677
1678 /**
1679 * Returns tile for a pixel position.<p>
1680 * This isn't very efficient, but it is only used when the user right-clicks on the map.
1681 * @param px pixel X coordinate
1682 * @param py pixel Y coordinate
1683 * @return Tile at pixel position
1684 */
1685 private Tile getTileForPixelpos(int px, int py) {
1686 Logging.debug("getTileForPixelpos({0}, {1})", px, py);
1687 TileXY xy = coordinateConverter.getTileforPixel(px, py, currentZoomLevel);
1688 return getTile(xy.getXIndex(), xy.getYIndex(), currentZoomLevel);
1689 }
1690
1691 /**
1692 * Class to store a menu action and the class it belongs to.
1693 */
1694 private static class MenuAddition {
1695 final Action addition;
1696 @SuppressWarnings("rawtypes")
1697 final Class<? extends AbstractTileSourceLayer> clazz;
1698
1699 @SuppressWarnings("rawtypes")
1700 MenuAddition(Action addition, Class<? extends AbstractTileSourceLayer> clazz) {
1701 this.addition = addition;
1702 this.clazz = clazz;
1703 }
1704 }
1705
1706 /**
1707 * Register an additional layer context menu entry.
1708 *
1709 * @param addition additional menu action
1710 * @since 11197
1711 */
1712 public static void registerMenuAddition(Action addition) {
1713 menuAdditions.add(new MenuAddition(addition, AbstractTileSourceLayer.class));
1714 }
1715
1716 /**
1717 * Register an additional layer context menu entry for a imagery layer
1718 * class. The menu entry is valid for the specified class and subclasses
1719 * thereof only.
1720 * <p>
1721 * Example:
1722 * <pre>
1723 * TMSLayer.registerMenuAddition(new TMSSpecificAction(), TMSLayer.class);
1724 * </pre>
1725 *
1726 * @param addition additional menu action
1727 * @param clazz class the menu action is registered for
1728 * @since 11197
1729 */
1730 public static void registerMenuAddition(Action addition,
1731 Class<? extends AbstractTileSourceLayer<?>> clazz) {
1732 menuAdditions.add(new MenuAddition(addition, clazz));
1733 }
1734
1735 /**
1736 * Prepare list of additional layer context menu entries. The list is
1737 * empty if there are no additional menu entries.
1738 *
1739 * @return list of additional layer context menu entries
1740 */
1741 private List<Action> getMenuAdditions() {
1742 final LinkedList<Action> menuAdds = menuAdditions.stream()
1743 .filter(menuAdd -> menuAdd.clazz.isInstance(this))
1744 .map(menuAdd -> menuAdd.addition)
1745 .collect(Collectors.toCollection(LinkedList::new));
1746 if (!menuAdds.isEmpty()) {
1747 menuAdds.addFirst(SeparatorLayerAction.INSTANCE);
1748 }
1749 return menuAdds;
1750 }
1751
1752 @Override
1753 public Action[] getMenuEntries() {
1754 ArrayList<Action> actions = new ArrayList<>();
1755 actions.addAll(Arrays.asList(getLayerListEntries()));
1756 actions.addAll(Arrays.asList(getCommonEntries()));
1757 actions.addAll(getMenuAdditions());
1758 actions.add(SeparatorLayerAction.INSTANCE);
1759 actions.add(new LayerListPopup.InfoAction(this));
1760 return actions.toArray(new Action[0]);
1761 }
1762
1763 /**
1764 * Returns the contextual menu entries in layer list dialog.
1765 * @return the contextual menu entries in layer list dialog
1766 */
1767 public Action[] getLayerListEntries() {
1768 return new Action[] {
1769 LayerListDialog.getInstance().createActivateLayerAction(this),
1770 LayerListDialog.getInstance().createShowHideLayerAction(),
1771 MainApplication.getMenu().autoScaleActions.get(AutoScaleAction.AutoScaleMode.LAYER),
1772 LayerListDialog.getInstance().createDeleteLayerAction(),
1773 SeparatorLayerAction.INSTANCE,
1774 // color,
1775 new OffsetAction(),
1776 new RenameLayerAction(this.getAssociatedFile(), this),
1777 SeparatorLayerAction.INSTANCE
1778 };
1779 }
1780
1781 /**
1782 * Returns the common menu entries.
1783 * @return the common menu entries
1784 */
1785 public Action[] getCommonEntries() {
1786 return new Action[] {
1787 new AutoLoadTilesAction(this),
1788 new AutoZoomAction(this),
1789 new ShowErrorsAction(this),
1790 new IncreaseZoomAction(this),
1791 new DecreaseZoomAction(this),
1792 new ZoomToBestAction(this),
1793 new ZoomToNativeLevelAction(this),
1794 new FlushTileCacheAction(this),
1795 new LoadErroneousTilesAction(this),
1796 new LoadAllTilesAction(this)
1797 };
1798 }
1799
1800 @Override
1801 public String getToolTipText() {
1802 if (getDisplaySettings().isAutoLoad()) {
1803 return tr("{0} ({1}), automatically downloading in zoom {2}", this.getClass().getSimpleName(), getName(), currentZoomLevel);
1804 } else {
1805 return tr("{0} ({1}), downloading in zoom {2}", this.getClass().getSimpleName(), getName(), currentZoomLevel);
1806 }
1807 }
1808
1809 @Override
1810 public void visitBoundingBox(BoundingXYVisitor v) {
1811 }
1812
1813 /**
1814 * Task responsible for precaching imagery along the gpx track
1815 *
1816 */
1817 public class PrecacheTask implements TileLoaderListener {
1818 private final ProgressMonitor progressMonitor;
1819 private final int totalCount;
1820 private final AtomicInteger processedCount = new AtomicInteger(0);
1821 private final TileLoader tileLoader;
1822 private final Set<Tile> requestedTiles;
1823
1824 /**
1825 * Constructs a new {@code PrecacheTask}.
1826 * @param progressMonitor that will be notified about progess of the task
1827 * @param bufferY buffer Y in degrees around which to download tiles
1828 * @param bufferX buffer X in degrees around which to download tiles
1829 * @param points list of points along which to download
1830 */
1831 public PrecacheTask(ProgressMonitor progressMonitor, List<LatLon> points, double bufferX, double bufferY) {
1832 this.progressMonitor = progressMonitor;
1833 this.tileLoader = getTileLoaderFactory().makeTileLoader(this, getHeaders(tileSource), minimumTileExpire);
1834 if (this.tileLoader instanceof TMSCachedTileLoader) {
1835 ((TMSCachedTileLoader) this.tileLoader).setDownloadExecutor(
1836 TMSCachedTileLoader.getNewThreadPoolExecutor("Precache downloader"));
1837 }
1838 requestedTiles = new ConcurrentSkipListSet<>(
1839 (o1, o2) -> String.CASE_INSENSITIVE_ORDER.compare(o1.getKey(), o2.getKey()));
1840 for (LatLon point: points) {
1841 TileXY minTile = tileSource.latLonToTileXY(point.lat() - bufferY, point.lon() - bufferX, currentZoomLevel);
1842 TileXY curTile = tileSource.latLonToTileXY(CoordinateConversion.llToCoor(point), currentZoomLevel);
1843 TileXY maxTile = tileSource.latLonToTileXY(point.lat() + bufferY, point.lon() + bufferX, currentZoomLevel);
1844
1845 // take at least one tile of buffer
1846 int minY = Math.min(curTile.getYIndex() - 1, minTile.getYIndex());
1847 int maxY = Math.max(curTile.getYIndex() + 1, maxTile.getYIndex());
1848 int minX = Math.min(curTile.getXIndex() - 1, minTile.getXIndex());
1849 int maxX = Math.max(curTile.getXIndex() + 1, maxTile.getXIndex());
1850
1851 for (int x = minX; x <= maxX; x++) {
1852 for (int y = minY; y <= maxY; y++) {
1853 requestedTiles.add(new Tile(tileSource, x, y, currentZoomLevel));
1854 }
1855 }
1856 }
1857
1858 this.totalCount = requestedTiles.size();
1859 this.progressMonitor.setTicksCount(requestedTiles.size());
1860
1861 }
1862
1863 /**
1864 * Determines if the task is finished.
1865 * @return true, if all is done
1866 */
1867 public boolean isFinished() {
1868 return processedCount.get() >= totalCount;
1869 }
1870
1871 /**
1872 * Returns total number of tiles to download.
1873 * @return total number of tiles to download
1874 */
1875 public int getTotalCount() {
1876 return totalCount;
1877 }
1878
1879 /**
1880 * cancel the task
1881 */
1882 public void cancel() {
1883 if (tileLoader instanceof TMSCachedTileLoader) {
1884 TMSCachedTileLoader cachedTileLoader = (TMSCachedTileLoader) tileLoader;
1885 cachedTileLoader.cancelOutstandingTasks();
1886 cachedTileLoader.shutdown();
1887 }
1888 }
1889
1890 @Override
1891 public void tileLoadingFinished(Tile tile, boolean success) {
1892 int processed = this.processedCount.incrementAndGet();
1893 if (success) {
1894 synchronized (progressMonitor) {
1895 if (!this.progressMonitor.isCanceled()) {
1896 this.progressMonitor.worked(1);
1897 this.progressMonitor.setCustomText(tr("Downloaded {0}/{1} tiles", processed, totalCount));
1898 }
1899 }
1900 } else {
1901 Logging.warn("Tile loading failure: " + tile + " - " + tile.getErrorMessage());
1902 }
1903 if (tileLoader instanceof TMSCachedTileLoader) {
1904 TMSCachedTileLoader cachedTileLoader = (TMSCachedTileLoader) tileLoader;
1905 cachedTileLoader.cancelOutstandingTasks();
1906 cachedTileLoader.shutdown();
1907 }
1908 }
1909
1910 /**
1911 * Returns tile loader that is used to load the tiles.
1912 * @return tile loader that is used to load the tiles
1913 */
1914 public TileLoader getTileLoader() {
1915 return tileLoader;
1916 }
1917
1918 /**
1919 * Execute the download
1920 */
1921 public void run() {
1922 TileLoader loader = getTileLoader();
1923 for (Tile t: requestedTiles) {
1924 if (!progressMonitor.isCanceled()) {
1925 loader.createTileLoaderJob(t).submit();
1926 }
1927 }
1928
1929 }
1930 }
1931
1932 /**
1933 * Calculates tiles, that needs to be downloaded to cache, gets a current tile loader and creates a task to download
1934 * all of the tiles. Buffer contains at least one tile.
1935 *
1936 * To prevent accidental clear of the queue, new download executor is created with separate queue
1937 *
1938 * @param progressMonitor progress monitor for download task
1939 * @param points lat/lon coordinates to download
1940 * @param bufferX how many units in current Coordinate Reference System to cover in X axis in both sides
1941 * @param bufferY how many units in current Coordinate Reference System to cover in Y axis in both sides
1942 * @return precache task representing download task
1943 */
1944 public AbstractTileSourceLayer<T>.PrecacheTask getDownloadAreaToCacheTask(final ProgressMonitor progressMonitor, List<LatLon> points,
1945 double bufferX, double bufferY) {
1946 return new PrecacheTask(progressMonitor, points, bufferX, bufferY);
1947 }
1948
1949 @Override
1950 public boolean isSavable() {
1951 return true; // With WMSLayerExporter
1952 }
1953
1954 @Override
1955 public File createAndOpenSaveFileChooser() {
1956 return SaveActionBase.createAndOpenSaveFileChooser(tr("Save WMS file"), WMSLayerImporter.FILE_FILTER);
1957 }
1958
1959 @Override
1960 public synchronized void destroy() {
1961 super.destroy();
1962 MapView.removeZoomChangeListener(this);
1963 adjustAction.destroy();
1964 if (tileLoader instanceof TMSCachedTileLoader) {
1965 TMSCachedTileLoader cachedTileLoader = (TMSCachedTileLoader) tileLoader;
1966 cachedTileLoader.shutdown();
1967 }
1968 }
1969
1970 private class TileSourcePainter extends CompatibilityModeLayerPainter {
1971 /** The memory handle that will hold our tile source. */
1972 private MemoryHandle<?> memory;
1973
1974 @Override
1975 public void paint(MapViewGraphics graphics) {
1976 allocateCacheMemory();
1977 if (memory != null) {
1978 doPaint(graphics);
1979 }
1980 }
1981
1982 private void doPaint(MapViewGraphics graphics) {
1983 try {
1984 drawInViewArea(graphics.getDefaultGraphics(), graphics.getMapView(), graphics.getClipBounds().getProjectionBounds());
1985 } catch (IllegalArgumentException | IllegalStateException e) {
1986 throw BugReport.intercept(e)
1987 .put("graphics", graphics).put("tileSource", tileSource).put("currentZoomLevel", currentZoomLevel);
1988 }
1989 }
1990
1991 private void allocateCacheMemory() {
1992 if (memory == null) {
1993 MemoryManager manager = MemoryManager.getInstance();
1994 if (manager.isAvailable(getEstimatedCacheSize())) {
1995 try {
1996 memory = manager.allocateMemory("tile source layer", getEstimatedCacheSize(), Object::new);
1997 } catch (NotEnoughMemoryException e) {
1998 Logging.warn("Could not allocate tile source memory", e);
1999 }
2000 }
2001 }
2002 }
2003
2004 protected long getEstimatedCacheSize() {
2005 return 4L * tileSource.getTileSize() * tileSource.getTileSize() * estimateTileCacheSize();
2006 }
2007
2008 @Override
2009 public void detachFromMapView(MapViewEvent event) {
2010 event.getMapView().removeMouseListener(adapter);
2011 MapView.removeZoomChangeListener(AbstractTileSourceLayer.this);
2012 super.detachFromMapView(event);
2013 if (memory != null) {
2014 memory.free();
2015 }
2016 }
2017 }
2018
2019 @Override
2020 public void projectionChanged(Projection oldValue, Projection newValue) {
2021 super.projectionChanged(oldValue, newValue);
2022 displaySettings.setOffsetBookmark(displaySettings.getOffsetBookmark());
2023 if (tileCache != null) {
2024 tileCache.clear();
2025 }
2026 }
2027
2028 @Override
2029 protected List<OffsetMenuEntry> getOffsetMenuEntries() {
2030 return OffsetBookmark.getBookmarks()
2031 .stream()
2032 .filter(b -> b.isUsable(this))
2033 .map(OffsetMenuBookmarkEntry::new)
2034 .collect(Collectors.toList());
2035 }
2036
2037 /**
2038 * An entry for a bookmark in the offset menu.
2039 * @author Michael Zangl
2040 */
2041 private class OffsetMenuBookmarkEntry implements OffsetMenuEntry {
2042 private final OffsetBookmark bookmark;
2043
2044 OffsetMenuBookmarkEntry(OffsetBookmark bookmark) {
2045 this.bookmark = bookmark;
2046
2047 }
2048
2049 @Override
2050 public String getLabel() {
2051 return bookmark.getName();
2052 }
2053
2054 @Override
2055 public boolean isActive() {
2056 EastNorth offset = bookmark.getDisplacement(ProjectionRegistry.getProjection());
2057 EastNorth active = getDisplaySettings().getDisplacement();
2058 return Utils.equalsEpsilon(offset.east(), active.east()) && Utils.equalsEpsilon(offset.north(), active.north());
2059 }
2060
2061 @Override
2062 public void actionPerformed() {
2063 getDisplaySettings().setOffsetBookmark(bookmark);
2064 }
2065 }
2066}
Note: See TracBrowser for help on using the repository browser.