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

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

see #15229 - deprecate all Main methods related to projections. New ProjectionRegistry class

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