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

Last change on this file since 16436 was 16436, checked in by simon04, 4 years ago

see #19251 - Java 8: use Stream

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