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

Last change on this file since 16669 was 16669, checked in by stoecker, 4 years ago

fix #18193 - reduce flickering when paning map by loading a little bit more tiles than strictly necessary

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