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

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

see #15880 - nicer exception

  • Property svn:eol-style set to native
File size: 74.3 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.concurrent.ConcurrentSkipListSet;
42import java.util.concurrent.atomic.AtomicInteger;
43import java.util.function.Consumer;
44import java.util.function.Function;
45import java.util.stream.Collectors;
46import java.util.stream.IntStream;
47import java.util.stream.Stream;
48
49import javax.swing.AbstractAction;
50import javax.swing.Action;
51import javax.swing.JLabel;
52import javax.swing.JMenuItem;
53import javax.swing.JOptionPane;
54import javax.swing.JPanel;
55import javax.swing.JPopupMenu;
56import javax.swing.JSeparator;
57import javax.swing.Timer;
58
59import org.openstreetmap.gui.jmapviewer.AttributionSupport;
60import org.openstreetmap.gui.jmapviewer.MemoryTileCache;
61import org.openstreetmap.gui.jmapviewer.OsmTileLoader;
62import org.openstreetmap.gui.jmapviewer.Tile;
63import org.openstreetmap.gui.jmapviewer.TileRange;
64import org.openstreetmap.gui.jmapviewer.TileXY;
65import org.openstreetmap.gui.jmapviewer.interfaces.CachedTileLoader;
66import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
67import org.openstreetmap.gui.jmapviewer.interfaces.IProjected;
68import org.openstreetmap.gui.jmapviewer.interfaces.TemplatedTileSource;
69import org.openstreetmap.gui.jmapviewer.interfaces.TileCache;
70import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
71import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
72import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
73import org.openstreetmap.gui.jmapviewer.tilesources.AbstractTMSTileSource;
74import org.openstreetmap.josm.Main;
75import org.openstreetmap.josm.actions.ExpertToggleAction;
76import org.openstreetmap.josm.actions.ImageryAdjustAction;
77import org.openstreetmap.josm.actions.RenameLayerAction;
78import org.openstreetmap.josm.actions.SaveActionBase;
79import org.openstreetmap.josm.data.Bounds;
80import org.openstreetmap.josm.data.ProjectionBounds;
81import org.openstreetmap.josm.data.coor.EastNorth;
82import org.openstreetmap.josm.data.coor.LatLon;
83import org.openstreetmap.josm.data.imagery.CoordinateConversion;
84import org.openstreetmap.josm.data.imagery.ImageryInfo;
85import org.openstreetmap.josm.data.imagery.OffsetBookmark;
86import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader;
87import org.openstreetmap.josm.data.imagery.TileLoaderFactory;
88import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
89import org.openstreetmap.josm.data.preferences.IntegerProperty;
90import org.openstreetmap.josm.data.projection.Projection;
91import org.openstreetmap.josm.data.projection.Projections;
92import org.openstreetmap.josm.gui.ExtendedDialog;
93import org.openstreetmap.josm.gui.MainApplication;
94import org.openstreetmap.josm.gui.MapView;
95import org.openstreetmap.josm.gui.NavigatableComponent.ZoomChangeListener;
96import org.openstreetmap.josm.gui.Notification;
97import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
98import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
99import org.openstreetmap.josm.gui.io.importexport.WMSLayerImporter;
100import org.openstreetmap.josm.gui.layer.imagery.AutoLoadTilesAction;
101import org.openstreetmap.josm.gui.layer.imagery.AutoZoomAction;
102import org.openstreetmap.josm.gui.layer.imagery.DecreaseZoomAction;
103import org.openstreetmap.josm.gui.layer.imagery.FlushTileCacheAction;
104import org.openstreetmap.josm.gui.layer.imagery.ImageryFilterSettings.FilterChangeListener;
105import org.openstreetmap.josm.gui.layer.imagery.IncreaseZoomAction;
106import org.openstreetmap.josm.gui.layer.imagery.LoadAllTilesAction;
107import org.openstreetmap.josm.gui.layer.imagery.LoadErroneousTilesAction;
108import org.openstreetmap.josm.gui.layer.imagery.ReprojectionTile;
109import org.openstreetmap.josm.gui.layer.imagery.ShowErrorsAction;
110import org.openstreetmap.josm.gui.layer.imagery.TileAnchor;
111import org.openstreetmap.josm.gui.layer.imagery.TileCoordinateConverter;
112import org.openstreetmap.josm.gui.layer.imagery.TilePosition;
113import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings;
114import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings.DisplaySettingsChangeEvent;
115import org.openstreetmap.josm.gui.layer.imagery.TileSourceDisplaySettings.DisplaySettingsChangeListener;
116import org.openstreetmap.josm.gui.layer.imagery.ZoomToBestAction;
117import org.openstreetmap.josm.gui.layer.imagery.ZoomToNativeLevelAction;
118import org.openstreetmap.josm.gui.progress.ProgressMonitor;
119import org.openstreetmap.josm.gui.util.GuiHelper;
120import org.openstreetmap.josm.tools.GBC;
121import org.openstreetmap.josm.tools.HttpClient;
122import org.openstreetmap.josm.tools.Logging;
123import org.openstreetmap.josm.tools.MemoryManager;
124import org.openstreetmap.josm.tools.MemoryManager.MemoryHandle;
125import org.openstreetmap.josm.tools.MemoryManager.NotEnoughMemoryException;
126import org.openstreetmap.josm.tools.Utils;
127import org.openstreetmap.josm.tools.bugreport.BugReport;
128
129/**
130 * Base abstract class that supports displaying images provided by TileSource. It might be TMS source, WMS or WMTS
131 * It implements all standard functions of tilesource based layers: autozoom, tile reloads, layer saving, loading,etc.
132 *
133 * @author Upliner
134 * @author Wiktor Niesiobędzki
135 * @param <T> Tile Source class used for this layer
136 * @since 3715
137 * @since 8526 (copied from TMSLayer)
138 */
139public abstract class AbstractTileSourceLayer<T extends AbstractTMSTileSource> extends ImageryLayer
140implements ImageObserver, TileLoaderListener, ZoomChangeListener, FilterChangeListener, DisplaySettingsChangeListener {
141 private static final String PREFERENCE_PREFIX = "imagery.generic";
142 static { // Registers all setting properties
143 new TileSourceDisplaySettings();
144 }
145
146 /** maximum zoom level supported */
147 public static final int MAX_ZOOM = 30;
148 /** minium zoom level supported */
149 public static final int MIN_ZOOM = 2;
150 private static final Font InfoFont = new Font("sansserif", Font.BOLD, 13);
151
152 /** additional layer menu actions */
153 private static List<MenuAddition> menuAdditions = new LinkedList<>();
154
155 /** minimum zoom level to show to user */
156 public static final IntegerProperty PROP_MIN_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".min_zoom_lvl", 2);
157 /** maximum zoom level to show to user */
158 public static final IntegerProperty PROP_MAX_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".max_zoom_lvl", 20);
159
160 //public static final BooleanProperty PROP_DRAW_DEBUG = new BooleanProperty(PREFERENCE_PREFIX + ".draw_debug", false);
161 /** Zoomlevel at which tiles is currently downloaded. Initial zoom lvl is set to bestZoom */
162 private int currentZoomLevel;
163
164 private final AttributionSupport attribution = new AttributionSupport();
165 private final TileHolder clickedTileHolder = new TileHolder();
166
167 /**
168 * Offset between calculated zoom level and zoom level used to download and show tiles. Negative values will result in
169 * lower resolution of imagery useful in "retina" displays, positive values will result in higher resolution
170 */
171 public static final IntegerProperty ZOOM_OFFSET = new IntegerProperty(PREFERENCE_PREFIX + ".zoom_offset", 0);
172
173 /*
174 * use MemoryTileCache instead of tileLoader JCS cache, as tileLoader caches only content (byte[] of image)
175 * and MemoryTileCache caches whole Tile. This gives huge performance improvement when a lot of tiles are visible
176 * in MapView (for example - when limiting min zoom in imagery)
177 *
178 * Use per-layer tileCache instance, as the more layers there are, the more tiles needs to be cached
179 */
180 protected TileCache tileCache; // initialized together with tileSource
181 protected T tileSource;
182 protected TileLoader tileLoader;
183
184 /** A timer that is used to delay invalidation events if required. */
185 private final Timer invalidateLaterTimer = new Timer(100, e -> this.invalidate());
186
187 private final MouseAdapter adapter = new MouseAdapter() {
188 @Override
189 public void mouseClicked(MouseEvent e) {
190 if (!isVisible()) return;
191 if (e.getButton() == MouseEvent.BUTTON3) {
192 clickedTileHolder.setTile(getTileForPixelpos(e.getX(), e.getY()));
193 new TileSourceLayerPopup().show(e.getComponent(), e.getX(), e.getY());
194 } else if (e.getButton() == MouseEvent.BUTTON1) {
195 attribution.handleAttribution(e.getPoint(), true);
196 }
197 }
198 };
199
200 private final TileSourceDisplaySettings displaySettings = createDisplaySettings();
201
202 private final ImageryAdjustAction adjustAction = new ImageryAdjustAction(this);
203 // prepared to be moved to the painter
204 protected TileCoordinateConverter coordinateConverter;
205
206 /**
207 * Creates Tile Source based Imagery Layer based on Imagery Info
208 * @param info imagery info
209 */
210 public AbstractTileSourceLayer(ImageryInfo info) {
211 super(info);
212 setBackgroundLayer(true);
213 this.setVisible(true);
214 getFilterSettings().addFilterChangeListener(this);
215 getDisplaySettings().addSettingsChangeListener(this);
216 }
217
218 /**
219 * This method creates the {@link TileSourceDisplaySettings} object. Subclasses may implement it to e.g. change the prefix.
220 * @return The object.
221 * @since 10568
222 */
223 protected TileSourceDisplaySettings createDisplaySettings() {
224 return new TileSourceDisplaySettings();
225 }
226
227 /**
228 * Gets the {@link TileSourceDisplaySettings} instance associated with this tile source.
229 * @return The tile source display settings
230 * @since 10568
231 */
232 public TileSourceDisplaySettings getDisplaySettings() {
233 return displaySettings;
234 }
235
236 @Override
237 public void filterChanged() {
238 invalidate();
239 }
240
241 protected abstract TileLoaderFactory getTileLoaderFactory();
242
243 /**
244 * Get projections this imagery layer supports natively.
245 *
246 * For example projection of tiles that are downloaded from a server. Layer
247 * may support even more projections (by reprojecting the tiles), but with a
248 * certain loss in image quality and performance.
249 * @return projections this imagery layer supports natively; null if layer is projection agnostic.
250 */
251 public abstract Collection<String> getNativeProjections();
252
253 /**
254 * Creates and returns a new {@link TileSource} instance depending on {@link #info} specified in the constructor.
255 *
256 * @return TileSource for specified ImageryInfo
257 * @throws IllegalArgumentException when Imagery is not supported by layer
258 */
259 protected abstract T getTileSource();
260
261 protected Map<String, String> getHeaders(T tileSource) {
262 if (tileSource instanceof TemplatedTileSource) {
263 return ((TemplatedTileSource) tileSource).getHeaders();
264 }
265 return null;
266 }
267
268 protected void initTileSource(T tileSource) {
269 coordinateConverter = new TileCoordinateConverter(MainApplication.getMap().mapView, tileSource, getDisplaySettings());
270 attribution.initialize(tileSource);
271
272 currentZoomLevel = getBestZoom();
273
274 Map<String, String> headers = getHeaders(tileSource);
275
276 tileLoader = getTileLoaderFactory().makeTileLoader(this, headers);
277
278 try {
279 if ("file".equalsIgnoreCase(new URL(tileSource.getBaseUrl()).getProtocol())) {
280 tileLoader = new OsmTileLoader(this);
281 }
282 } catch (MalformedURLException e) {
283 // ignore, assume that this is not a file
284 Logging.log(Logging.LEVEL_DEBUG, e);
285 }
286
287 if (tileLoader == null)
288 tileLoader = new OsmTileLoader(this, headers);
289
290 tileCache = new MemoryTileCache(estimateTileCacheSize());
291 }
292
293 @Override
294 public synchronized void tileLoadingFinished(Tile tile, boolean success) {
295 if (tile.hasError()) {
296 success = false;
297 tile.setImage(null);
298 }
299 tile.setLoaded(success);
300 invalidateLater();
301 Logging.debug("tileLoadingFinished() tile: {0} success: {1}", tile, success);
302 }
303
304 /**
305 * Clears the tile cache.
306 */
307 public void clearTileCache() {
308 if (tileLoader instanceof CachedTileLoader) {
309 ((CachedTileLoader) tileLoader).clearCache(tileSource);
310 }
311 tileCache.clear();
312 }
313
314 /**
315 * {@inheritDoc}
316 * @deprecated Use {@link TileSourceDisplaySettings#getDx()}
317 */
318 @Override
319 @Deprecated
320 public double getDx() {
321 return getDisplaySettings().getDx();
322 }
323
324 /**
325 * {@inheritDoc}
326 * @deprecated Use {@link TileSourceDisplaySettings#getDy()}
327 */
328 @Override
329 @Deprecated
330 public double getDy() {
331 return getDisplaySettings().getDy();
332 }
333
334 /**
335 * {@inheritDoc}
336 * @deprecated Use {@link TileSourceDisplaySettings}
337 */
338 @Override
339 @Deprecated
340 public void setOffset(OffsetBookmark offset) {
341 getDisplaySettings().setOffsetBookmark(offset);
342 }
343
344 @Override
345 public Object getInfoComponent() {
346 JPanel panel = (JPanel) super.getInfoComponent();
347 List<List<String>> content = new ArrayList<>();
348 Collection<String> nativeProjections = getNativeProjections();
349 if (nativeProjections != null) {
350 content.add(Arrays.asList(tr("Native projections"), Utils.join(", ", getNativeProjections())));
351 }
352 EastNorth offset = getDisplaySettings().getDisplacement();
353 if (offset.distanceSq(0, 0) > 1e-10) {
354 content.add(Arrays.asList(tr("Offset"), offset.east() + ";" + offset.north()));
355 }
356 if (coordinateConverter.requiresReprojection()) {
357 content.add(Arrays.asList(tr("Tile download projection"), tileSource.getServerCRS()));
358 content.add(Arrays.asList(tr("Tile display projection"), Main.getProjection().toCode()));
359 }
360 content.add(Arrays.asList(tr("Current zoom"), Integer.toString(currentZoomLevel)));
361 for (List<String> entry: content) {
362 panel.add(new JLabel(entry.get(0) + ':'), GBC.std());
363 panel.add(GBC.glue(5, 0), GBC.std());
364 panel.add(createTextField(entry.get(1)), GBC.eol().fill(GBC.HORIZONTAL));
365 }
366 return panel;
367 }
368
369 @Override
370 protected Action getAdjustAction() {
371 return adjustAction;
372 }
373
374 /**
375 * Returns average number of screen pixels per tile pixel for current mapview
376 * @param zoom zoom level
377 * @return average number of screen pixels per tile pixel
378 */
379 public double getScaleFactor(int zoom) {
380 if (coordinateConverter != null) {
381 return coordinateConverter.getScaleFactor(zoom);
382 } else {
383 return 1;
384 }
385 }
386
387 public int getBestZoom() {
388 double factor = getScaleFactor(1); // check the ratio between area of tilesize at zoom 1 to current view
389 double result = Math.log(factor)/Math.log(2)/2;
390 /*
391 * Math.log(factor)/Math.log(2) - gives log base 2 of factor
392 * We divide result by 2, as factor contains ratio between areas. We could do Math.sqrt before log, or just divide log by 2
393 *
394 * ZOOM_OFFSET controls, whether we work with overzoomed or underzoomed tiles. Positive ZOOM_OFFSET
395 * is for working with underzoomed tiles (higher quality when working with aerial imagery), negative ZOOM_OFFSET
396 * is for working with overzoomed tiles (big, pixelated), which is good when working with high-dpi screens and/or
397 * maps as a imagery layer
398 */
399 int intResult = (int) Math.round(result + 1 + ZOOM_OFFSET.get() / 1.9);
400 int minZoom = getMinZoomLvl();
401 int maxZoom = getMaxZoomLvl();
402 if (minZoom <= maxZoom) {
403 intResult = Utils.clamp(intResult, minZoom, maxZoom);
404 } else if (intResult > maxZoom) {
405 intResult = maxZoom;
406 }
407 return intResult;
408 }
409
410 /**
411 * Default implementation of {@link org.openstreetmap.josm.gui.layer.Layer.LayerAction#supportLayers(List)}.
412 * @param layers layers
413 * @return {@code true} is layers contains only a {@code TMSLayer}
414 */
415 public static boolean actionSupportLayers(List<Layer> layers) {
416 return layers.size() == 1 && layers.get(0) instanceof TMSLayer;
417 }
418
419 private final class ShowTileInfoAction extends AbstractAction {
420
421 private ShowTileInfoAction() {
422 super(tr("Show tile info"));
423 setEnabled(clickedTileHolder.getTile() != null);
424 }
425
426 private String getSizeString(int size) {
427 return new StringBuilder().append(size).append('x').append(size).toString();
428 }
429
430 @Override
431 public void actionPerformed(ActionEvent ae) {
432 Tile clickedTile = clickedTileHolder.getTile();
433 if (clickedTile != null) {
434 ExtendedDialog ed = new ExtendedDialog(Main.parent, tr("Tile Info"), tr("OK"));
435 JPanel panel = new JPanel(new GridBagLayout());
436 Rectangle2D displaySize = coordinateConverter.getRectangleForTile(clickedTile);
437 String url = "";
438 try {
439 url = clickedTile.getUrl();
440 } catch (IOException e) {
441 // silence exceptions
442 Logging.trace(e);
443 }
444
445 List<List<String>> content = new ArrayList<>();
446 content.add(Arrays.asList(tr("Tile name"), clickedTile.getKey()));
447 content.add(Arrays.asList(tr("Tile URL"), url));
448 content.add(Arrays.asList(tr("Tile size"),
449 getSizeString(clickedTile.getTileSource().getTileSize())));
450 content.add(Arrays.asList(tr("Tile display size"),
451 new StringBuilder().append(displaySize.getWidth())
452 .append('x')
453 .append(displaySize.getHeight()).toString()));
454 if (coordinateConverter.requiresReprojection()) {
455 content.add(Arrays.asList(tr("Reprojection"),
456 clickedTile.getTileSource().getServerCRS() +
457 " -> " + Main.getProjection().toCode()));
458 BufferedImage img = clickedTile.getImage();
459 if (img != null) {
460 content.add(Arrays.asList(tr("Reprojected tile size"),
461 img.getWidth() + "x" + img.getHeight()));
462
463 }
464 }
465 for (List<String> entry: content) {
466 panel.add(new JLabel(entry.get(0) + ':'), GBC.std());
467 panel.add(GBC.glue(5, 0), GBC.std());
468 panel.add(createTextField(entry.get(1)), GBC.eol().fill(GBC.HORIZONTAL));
469 }
470
471 for (Entry<String, String> e: clickedTile.getMetadata().entrySet()) {
472 panel.add(new JLabel(tr("Metadata ") + tr(e.getKey()) + ':'), GBC.std());
473 panel.add(GBC.glue(5, 0), GBC.std());
474 String value = e.getValue();
475 if ("lastModification".equals(e.getKey()) || "expirationTime".equals(e.getKey())) {
476 value = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(Long.parseLong(value)));
477 }
478 panel.add(createTextField(value), GBC.eol().fill(GBC.HORIZONTAL));
479
480 }
481 ed.setIcon(JOptionPane.INFORMATION_MESSAGE);
482 ed.setContent(panel);
483 ed.showDialog();
484 }
485 }
486 }
487
488 private final class LoadTileAction extends AbstractAction {
489
490 private LoadTileAction() {
491 super(tr("Load tile"));
492 setEnabled(clickedTileHolder.getTile() != null);
493 }
494
495 @Override
496 public void actionPerformed(ActionEvent ae) {
497 Tile clickedTile = clickedTileHolder.getTile();
498 if (clickedTile != null) {
499 loadTile(clickedTile, true);
500 invalidate();
501 }
502 }
503 }
504
505 private void sendOsmTileRequest(String request) {
506 Tile clickedTile = clickedTileHolder.getTile();
507 if (clickedTile != null) {
508 try {
509 new Notification(HttpClient.create(new URL(clickedTile.getUrl() + '/' + request))
510 .connect().fetchContent()).show();
511 } catch (IOException ex) {
512 Logging.error(ex);
513 }
514 }
515 }
516
517 private final class GetOsmTileStatusAction extends AbstractAction {
518 private GetOsmTileStatusAction() {
519 super(tr("Get tile status"));
520 setEnabled(clickedTileHolder.getTile() != null);
521 }
522
523 @Override
524 public void actionPerformed(ActionEvent e) {
525 sendOsmTileRequest("status");
526 }
527 }
528
529 private final class MarkOsmTileDirtyAction extends AbstractAction {
530 private MarkOsmTileDirtyAction() {
531 super(tr("Force tile rendering"));
532 setEnabled(clickedTileHolder.getTile() != null);
533 }
534
535 @Override
536 public void actionPerformed(ActionEvent e) {
537 sendOsmTileRequest("dirty");
538 }
539 }
540
541 /**
542 * Simple class to keep clickedTile within hookUpMapView
543 */
544 private static final class TileHolder {
545 private Tile t;
546
547 public Tile getTile() {
548 return t;
549 }
550
551 public void setTile(Tile t) {
552 this.t = t;
553 }
554 }
555
556 /**
557 * Creates popup menu items and binds to mouse actions
558 */
559 @Override
560 public void hookUpMapView() {
561 // this needs to be here and not in constructor to allow empty TileSource class construction using SessionWriter
562 initializeIfRequired();
563 super.hookUpMapView();
564 }
565
566 @Override
567 public LayerPainter attachToMapView(MapViewEvent event) {
568 initializeIfRequired();
569
570 event.getMapView().addMouseListener(adapter);
571 MapView.addZoomChangeListener(this);
572
573 if (this instanceof NativeScaleLayer) {
574 event.getMapView().setNativeScaleLayer((NativeScaleLayer) this);
575 }
576
577 // 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.
578 // FIXME: Check if this is still required.
579 event.getMapView().repaint(500);
580
581 return super.attachToMapView(event);
582 }
583
584 private void initializeIfRequired() {
585 if (tileSource == null) {
586 tileSource = getTileSource();
587 if (tileSource == null) {
588 throw new IllegalArgumentException(tr("Failed to create tile source"));
589 }
590 // check if projection is supported
591 projectionChanged(null, Main.getProjection());
592 initTileSource(this.tileSource);
593 }
594 }
595
596 @Override
597 protected LayerPainter createMapViewPainter(MapViewEvent event) {
598 return new TileSourcePainter();
599 }
600
601 /**
602 * Tile source layer popup menu.
603 */
604 public class TileSourceLayerPopup extends JPopupMenu {
605 /**
606 * Constructs a new {@code TileSourceLayerPopup}.
607 */
608 public TileSourceLayerPopup() {
609 for (Action a : getCommonEntries()) {
610 if (a instanceof LayerAction) {
611 add(((LayerAction) a).createMenuComponent());
612 } else {
613 add(new JMenuItem(a));
614 }
615 }
616 add(new JSeparator());
617 add(new JMenuItem(new LoadTileAction()));
618 add(new JMenuItem(new ShowTileInfoAction()));
619 if (ExpertToggleAction.isExpert() && tileSource != null && tileSource.isModTileFeatures()) {
620 add(new JMenuItem(new GetOsmTileStatusAction()));
621 add(new JMenuItem(new MarkOsmTileDirtyAction()));
622 }
623 }
624 }
625
626 protected int estimateTileCacheSize() {
627 Dimension screenSize = GuiHelper.getMaximumScreenSize();
628 int height = screenSize.height;
629 int width = screenSize.width;
630 int tileSize = 256; // default tile size
631 if (tileSource != null) {
632 tileSize = tileSource.getTileSize();
633 }
634 // as we can see part of the tile at the top and at the bottom, use Math.ceil(...) + 1 to accommodate for that
635 int visibileTiles = (int) (Math.ceil((double) height / tileSize + 1) * Math.ceil((double) width / tileSize + 1));
636 // add 10% for tiles from different zoom levels
637 int ret = (int) Math.ceil(
638 Math.pow(2d, ZOOM_OFFSET.get()) * visibileTiles // use offset to decide, how many tiles are visible
639 * 4);
640 Logging.info("AbstractTileSourceLayer: estimated visible tiles: {0}, estimated cache size: {1}", visibileTiles, ret);
641 return ret;
642 }
643
644 @Override
645 public void displaySettingsChanged(DisplaySettingsChangeEvent e) {
646 if (tileSource == null) {
647 return;
648 }
649 switch (e.getChangedSetting()) {
650 case TileSourceDisplaySettings.AUTO_ZOOM:
651 if (getDisplaySettings().isAutoZoom() && getBestZoom() != currentZoomLevel) {
652 setZoomLevel(getBestZoom());
653 invalidate();
654 }
655 break;
656 case TileSourceDisplaySettings.AUTO_LOAD:
657 if (getDisplaySettings().isAutoLoad()) {
658 invalidate();
659 }
660 break;
661 default:
662 // e.g. displacement
663 // trigger a redraw in every case
664 invalidate();
665 }
666 }
667
668 /**
669 * Checks zoom level against settings
670 * @param maxZoomLvl zoom level to check
671 * @param ts tile source to crosscheck with
672 * @return maximum zoom level, not higher than supported by tilesource nor set by the user
673 */
674 public static int checkMaxZoomLvl(int maxZoomLvl, TileSource ts) {
675 if (maxZoomLvl > MAX_ZOOM) {
676 maxZoomLvl = MAX_ZOOM;
677 }
678 if (maxZoomLvl < PROP_MIN_ZOOM_LVL.get()) {
679 maxZoomLvl = PROP_MIN_ZOOM_LVL.get();
680 }
681 if (ts != null && ts.getMaxZoom() != 0 && ts.getMaxZoom() < maxZoomLvl) {
682 maxZoomLvl = ts.getMaxZoom();
683 }
684 return maxZoomLvl;
685 }
686
687 /**
688 * Checks zoom level against settings
689 * @param minZoomLvl zoom level to check
690 * @param ts tile source to crosscheck with
691 * @return minimum zoom level, not higher than supported by tilesource nor set by the user
692 */
693 public static int checkMinZoomLvl(int minZoomLvl, TileSource ts) {
694 if (minZoomLvl < MIN_ZOOM) {
695 minZoomLvl = MIN_ZOOM;
696 }
697 if (minZoomLvl > PROP_MAX_ZOOM_LVL.get()) {
698 minZoomLvl = getMaxZoomLvl(ts);
699 }
700 if (ts != null && ts.getMinZoom() > minZoomLvl) {
701 minZoomLvl = ts.getMinZoom();
702 }
703 return minZoomLvl;
704 }
705
706 /**
707 * @param ts TileSource for which we want to know maximum zoom level
708 * @return maximum max zoom level, that will be shown on layer
709 */
710 public static int getMaxZoomLvl(TileSource ts) {
711 return checkMaxZoomLvl(PROP_MAX_ZOOM_LVL.get(), ts);
712 }
713
714 /**
715 * @param ts TileSource for which we want to know minimum zoom level
716 * @return minimum zoom level, that will be shown on layer
717 */
718 public static int getMinZoomLvl(TileSource ts) {
719 return checkMinZoomLvl(PROP_MIN_ZOOM_LVL.get(), ts);
720 }
721
722 /**
723 * Sets maximum zoom level, that layer will attempt show
724 * @param maxZoomLvl maximum zoom level
725 */
726 public static void setMaxZoomLvl(int maxZoomLvl) {
727 PROP_MAX_ZOOM_LVL.put(checkMaxZoomLvl(maxZoomLvl, null));
728 }
729
730 /**
731 * Sets minimum zoom level, that layer will attempt show
732 * @param minZoomLvl minimum zoom level
733 */
734 public static void setMinZoomLvl(int minZoomLvl) {
735 PROP_MIN_ZOOM_LVL.put(checkMinZoomLvl(minZoomLvl, null));
736 }
737
738 /**
739 * This fires every time the user changes the zoom, but also (due to ZoomChangeListener) - on all
740 * changes to visible map (panning/zooming)
741 */
742 @Override
743 public void zoomChanged() {
744 zoomChanged(true);
745 }
746
747 private void zoomChanged(boolean invalidate) {
748 Logging.debug("zoomChanged(): {0}", currentZoomLevel);
749 if (tileLoader instanceof TMSCachedTileLoader) {
750 ((TMSCachedTileLoader) tileLoader).cancelOutstandingTasks();
751 }
752 if (invalidate) {
753 invalidate();
754 }
755 }
756
757 protected int getMaxZoomLvl() {
758 if (info.getMaxZoom() != 0)
759 return checkMaxZoomLvl(info.getMaxZoom(), tileSource);
760 else
761 return getMaxZoomLvl(tileSource);
762 }
763
764 protected int getMinZoomLvl() {
765 if (info.getMinZoom() != 0)
766 return checkMinZoomLvl(info.getMinZoom(), tileSource);
767 else
768 return getMinZoomLvl(tileSource);
769 }
770
771 /**
772 *
773 * @return if its allowed to zoom in
774 */
775 public boolean zoomIncreaseAllowed() {
776 boolean zia = currentZoomLevel < this.getMaxZoomLvl();
777 Logging.debug("zoomIncreaseAllowed(): {0} {1} vs. {2}", zia, currentZoomLevel, this.getMaxZoomLvl());
778 return zia;
779 }
780
781 /**
782 * Zoom in, go closer to map.
783 *
784 * @return true, if zoom increasing was successful, false otherwise
785 */
786 public boolean increaseZoomLevel() {
787 if (zoomIncreaseAllowed()) {
788 currentZoomLevel++;
789 Logging.debug("increasing zoom level to: {0}", currentZoomLevel);
790 zoomChanged();
791 } else {
792 Logging.warn("Current zoom level ("+currentZoomLevel+") could not be increased. "+
793 "Max.zZoom Level "+this.getMaxZoomLvl()+" reached.");
794 return false;
795 }
796 return true;
797 }
798
799 /**
800 * Get the current zoom level of the layer
801 * @return the current zoom level
802 * @since 12603
803 */
804 public int getZoomLevel() {
805 return currentZoomLevel;
806 }
807
808 /**
809 * Sets the zoom level of the layer
810 * @param zoom zoom level
811 * @return true, when zoom has changed to desired value, false if it was outside supported zoom levels
812 */
813 public boolean setZoomLevel(int zoom) {
814 return setZoomLevel(zoom, true);
815 }
816
817 private boolean setZoomLevel(int zoom, boolean invalidate) {
818 if (zoom == currentZoomLevel) return true;
819 if (zoom > this.getMaxZoomLvl()) return false;
820 if (zoom < this.getMinZoomLvl()) return false;
821 currentZoomLevel = zoom;
822 zoomChanged(invalidate);
823 return true;
824 }
825
826 /**
827 * Check if zooming out is allowed
828 *
829 * @return true, if zooming out is allowed (currentZoomLevel &gt; minZoomLevel)
830 */
831 public boolean zoomDecreaseAllowed() {
832 boolean zda = currentZoomLevel > this.getMinZoomLvl();
833 Logging.debug("zoomDecreaseAllowed(): {0} {1} vs. {2}", zda, currentZoomLevel, this.getMinZoomLvl());
834 return zda;
835 }
836
837 /**
838 * Zoom out from map.
839 *
840 * @return true, if zoom increasing was successfull, false othervise
841 */
842 public boolean decreaseZoomLevel() {
843 if (zoomDecreaseAllowed()) {
844 Logging.debug("decreasing zoom level to: {0}", currentZoomLevel);
845 currentZoomLevel--;
846 zoomChanged();
847 } else {
848 return false;
849 }
850 return true;
851 }
852
853 private Tile getOrCreateTile(TilePosition tilePosition) {
854 return getOrCreateTile(tilePosition.getX(), tilePosition.getY(), tilePosition.getZoom());
855 }
856
857 private Tile getOrCreateTile(int x, int y, int zoom) {
858 Tile tile = getTile(x, y, zoom);
859 if (tile == null) {
860 if (coordinateConverter.requiresReprojection()) {
861 tile = new ReprojectionTile(tileSource, x, y, zoom);
862 } else {
863 tile = new Tile(tileSource, x, y, zoom);
864 }
865 tileCache.addTile(tile);
866 }
867 return tile;
868 }
869
870 private Tile getTile(TilePosition tilePosition) {
871 return getTile(tilePosition.getX(), tilePosition.getY(), tilePosition.getZoom());
872 }
873
874 /**
875 * Returns tile at given position.
876 * This can and will return null for tiles that are not already in the cache.
877 * @param x tile number on the x axis of the tile to be retrieved
878 * @param y tile number on the y axis of the tile to be retrieved
879 * @param zoom zoom level of the tile to be retrieved
880 * @return tile at given position
881 */
882 private Tile getTile(int x, int y, int zoom) {
883 if (x < tileSource.getTileXMin(zoom) || x > tileSource.getTileXMax(zoom)
884 || y < tileSource.getTileYMin(zoom) || y > tileSource.getTileYMax(zoom))
885 return null;
886 return tileCache.getTile(tileSource, x, y, zoom);
887 }
888
889 private boolean loadTile(Tile tile, boolean force) {
890 if (tile == null)
891 return false;
892 if (!force && (tile.isLoaded() || tile.hasError()))
893 return false;
894 if (tile.isLoading())
895 return false;
896 tileLoader.createTileLoaderJob(tile).submit(force);
897 return true;
898 }
899
900 private TileSet getVisibleTileSet() {
901 ProjectionBounds bounds = MainApplication.getMap().mapView.getState().getViewArea().getProjectionBounds();
902 return getTileSet(bounds, currentZoomLevel);
903 }
904
905 /**
906 * Load all visible tiles.
907 * @param force {@code true} to force loading if auto-load is disabled
908 * @since 11950
909 */
910 public void loadAllTiles(boolean force) {
911 TileSet ts = getVisibleTileSet();
912
913 // if there is more than 18 tiles on screen in any direction, do not load all tiles!
914 if (ts.tooLarge()) {
915 Logging.warn("Not downloading all tiles because there is more than 18 tiles on an axis!");
916 return;
917 }
918 ts.loadAllTiles(force);
919 invalidate();
920 }
921
922 /**
923 * Load all visible tiles in error.
924 * @param force {@code true} to force loading if auto-load is disabled
925 * @since 11950
926 */
927 public void loadAllErrorTiles(boolean force) {
928 TileSet ts = getVisibleTileSet();
929 ts.loadAllErrorTiles(force);
930 invalidate();
931 }
932
933 @Override
934 public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
935 boolean done = (infoflags & (ERROR | FRAMEBITS | ALLBITS)) != 0;
936 Logging.debug("imageUpdate() done: {0} calling repaint", done);
937
938 if (done) {
939 invalidate();
940 } else {
941 invalidateLater();
942 }
943 return !done;
944 }
945
946 /**
947 * Invalidate the layer at a time in the future so that the user still sees the interface responsive.
948 */
949 private void invalidateLater() {
950 GuiHelper.runInEDT(() -> {
951 if (!invalidateLaterTimer.isRunning()) {
952 invalidateLaterTimer.setRepeats(false);
953 invalidateLaterTimer.start();
954 }
955 });
956 }
957
958 private boolean imageLoaded(Image i) {
959 if (i == null)
960 return false;
961 int status = Toolkit.getDefaultToolkit().checkImage(i, -1, -1, this);
962 return (status & ALLBITS) != 0;
963 }
964
965 /**
966 * Returns the image for the given tile image is loaded.
967 * Otherwise returns null.
968 *
969 * @param tile the Tile for which the image should be returned
970 * @return the image of the tile or null.
971 */
972 private BufferedImage getLoadedTileImage(Tile tile) {
973 BufferedImage img = tile.getImage();
974 if (!imageLoaded(img))
975 return null;
976 return img;
977 }
978
979 /**
980 * Draw a tile image on screen.
981 * @param g the Graphics2D
982 * @param toDrawImg tile image
983 * @param anchorImage tile anchor in image coordinates
984 * @param anchorScreen tile anchor in screen coordinates
985 * @param clip clipping region in screen coordinates (can be null)
986 */
987 private void drawImageInside(Graphics2D g, BufferedImage toDrawImg, TileAnchor anchorImage, TileAnchor anchorScreen, Shape clip) {
988 AffineTransform imageToScreen = anchorImage.convert(anchorScreen);
989 Point2D screen0 = imageToScreen.transform(new Point.Double(0, 0), null);
990 Point2D screen1 = imageToScreen.transform(new Point.Double(
991 toDrawImg.getWidth(), toDrawImg.getHeight()), null);
992
993 Shape oldClip = null;
994 if (clip != null) {
995 oldClip = g.getClip();
996 g.clip(clip);
997 }
998 g.drawImage(toDrawImg, (int) Math.round(screen0.getX()), (int) Math.round(screen0.getY()),
999 (int) Math.round(screen1.getX()) - (int) Math.round(screen0.getX()),
1000 (int) Math.round(screen1.getY()) - (int) Math.round(screen0.getY()), this);
1001 if (clip != null) {
1002 g.setClip(oldClip);
1003 }
1004 }
1005
1006 private List<Tile> paintTileImages(Graphics2D g, TileSet ts) {
1007 Object paintMutex = new Object();
1008 List<TilePosition> missed = Collections.synchronizedList(new ArrayList<>());
1009 ts.visitTiles(tile -> {
1010 boolean miss = false;
1011 BufferedImage img = null;
1012 TileAnchor anchorImage = null;
1013 if (!tile.isLoaded() || tile.hasError()) {
1014 miss = true;
1015 } else {
1016 synchronized (tile) {
1017 img = getLoadedTileImage(tile);
1018 anchorImage = getAnchor(tile, img);
1019 }
1020 if (img == null || anchorImage == null) {
1021 miss = true;
1022 }
1023 }
1024 if (miss) {
1025 missed.add(new TilePosition(tile));
1026 return;
1027 }
1028
1029 img = applyImageProcessors(img);
1030
1031 TileAnchor anchorScreen = coordinateConverter.getScreenAnchorForTile(tile);
1032 synchronized (paintMutex) {
1033 //cannot paint in parallel
1034 drawImageInside(g, img, anchorImage, anchorScreen, null);
1035 }
1036 MapView mapView = MainApplication.getMap().mapView;
1037 if (tile instanceof ReprojectionTile && ((ReprojectionTile) tile).needsUpdate(mapView.getScale())) {
1038 // This means we have a reprojected tile in memory cache, but not at
1039 // current scale. Generally, the positioning of the tile will still
1040 // be correct, but for best image quality, the tile should be
1041 // reprojected to the target scale. The original tile image should
1042 // still be in disk cache, so this is fairly cheap.
1043 ((ReprojectionTile) tile).invalidate();
1044 loadTile(tile, false);
1045 }
1046
1047 }, missed::add);
1048
1049 return missed.stream().map(this::getOrCreateTile).collect(Collectors.toList());
1050 }
1051
1052 // This function is called for several zoom levels, not just the current one.
1053 // It should not trigger any tiles to be downloaded.
1054 // It should also avoid polluting the tile cache with any tiles since these tiles are not mandatory.
1055 //
1056 // The "border" tile tells us the boundaries of where we may drawn.
1057 // It will not be from the zoom level that is being drawn currently.
1058 // If drawing the displayZoomLevel, border is null and we draw the entire tile set.
1059 private List<Tile> paintTileImages(Graphics2D g, TileSet ts, int zoom, Tile border) {
1060 if (zoom <= 0) return Collections.emptyList();
1061 Shape borderClip = coordinateConverter.getTileShapeScreen(border);
1062 List<Tile> missedTiles = new LinkedList<>();
1063 // The callers of this code *require* that we return any tiles that we do not draw in missedTiles.
1064 // ts.allExistingTiles() by default will only return already-existing tiles.
1065 // However, we need to return *all* tiles to the callers, so force creation here.
1066 for (Tile tile : ts.allTilesCreate()) {
1067 boolean miss = false;
1068 BufferedImage img = null;
1069 TileAnchor anchorImage = null;
1070 if (!tile.isLoaded() || tile.hasError()) {
1071 miss = true;
1072 } else {
1073 synchronized (tile) {
1074 img = getLoadedTileImage(tile);
1075 anchorImage = getAnchor(tile, img);
1076 }
1077
1078 if (img == null || anchorImage == null) {
1079 miss = true;
1080 }
1081 }
1082 if (miss) {
1083 missedTiles.add(tile);
1084 continue;
1085 }
1086
1087 // applying all filters to this layer
1088 img = applyImageProcessors(img);
1089
1090 Shape clip;
1091 if (tileSource.isInside(tile, border)) {
1092 clip = null;
1093 } else if (tileSource.isInside(border, tile)) {
1094 clip = borderClip;
1095 } else {
1096 continue;
1097 }
1098 TileAnchor anchorScreen = coordinateConverter.getScreenAnchorForTile(tile);
1099 drawImageInside(g, img, anchorImage, anchorScreen, clip);
1100 }
1101 return missedTiles;
1102 }
1103
1104 private static TileAnchor getAnchor(Tile tile, BufferedImage image) {
1105 if (tile instanceof ReprojectionTile) {
1106 return ((ReprojectionTile) tile).getAnchor();
1107 } else if (image != null) {
1108 return new TileAnchor(new Point.Double(0, 0), new Point.Double(image.getWidth(), image.getHeight()));
1109 } else {
1110 return null;
1111 }
1112 }
1113
1114 private void myDrawString(Graphics g, String text, int x, int y) {
1115 Color oldColor = g.getColor();
1116 String textToDraw = text;
1117 if (g.getFontMetrics().stringWidth(text) > tileSource.getTileSize()) {
1118 // text longer than tile size, split it
1119 StringBuilder line = new StringBuilder();
1120 StringBuilder ret = new StringBuilder();
1121 for (String s: text.split(" ")) {
1122 if (g.getFontMetrics().stringWidth(line.toString() + s) > tileSource.getTileSize()) {
1123 ret.append(line).append('\n');
1124 line.setLength(0);
1125 }
1126 line.append(s).append(' ');
1127 }
1128 ret.append(line);
1129 textToDraw = ret.toString();
1130 }
1131 int offset = 0;
1132 for (String s: textToDraw.split("\n")) {
1133 g.setColor(Color.black);
1134 g.drawString(s, x + 1, y + offset + 1);
1135 g.setColor(oldColor);
1136 g.drawString(s, x, y + offset);
1137 offset += g.getFontMetrics().getHeight() + 3;
1138 }
1139 }
1140
1141 private void paintTileText(Tile tile, Graphics2D g) {
1142 if (tile == null) {
1143 return;
1144 }
1145 Point2D p = coordinateConverter.getPixelForTile(tile);
1146 int fontHeight = g.getFontMetrics().getHeight();
1147 int x = (int) p.getX();
1148 int y = (int) p.getY();
1149 int texty = y + 2 + fontHeight;
1150
1151 /*if (PROP_DRAW_DEBUG.get()) {
1152 myDrawString(g, "x=" + t.getXtile() + " y=" + t.getYtile() + " z=" + zoom + "", p.x + 2, texty);
1153 texty += 1 + fontHeight;
1154 if ((t.getXtile() % 32 == 0) && (t.getYtile() % 32 == 0)) {
1155 myDrawString(g, "x=" + t.getXtile() / 32 + " y=" + t.getYtile() / 32 + " z=7", p.x + 2, texty);
1156 texty += 1 + fontHeight;
1157 }
1158 }
1159
1160 String tileStatus = tile.getStatus();
1161 if (!tile.isLoaded() && PROP_DRAW_DEBUG.get()) {
1162 myDrawString(g, tr("image " + tileStatus), p.x + 2, texty);
1163 texty += 1 + fontHeight;
1164 }*/
1165
1166 if (tile.hasError() && getDisplaySettings().isShowErrors()) {
1167 myDrawString(g, tr("Error") + ": " + tr(tile.getErrorMessage()), x + 2, texty);
1168 //texty += 1 + fontHeight;
1169 }
1170
1171 if (Logging.isDebugEnabled()) {
1172 // draw tile outline in semi-transparent red
1173 g.setColor(new Color(255, 0, 0, 50));
1174 g.draw(coordinateConverter.getTileShapeScreen(tile));
1175 }
1176 }
1177
1178 private LatLon getShiftedLatLon(EastNorth en) {
1179 return coordinateConverter.getProjecting().eastNorth2latlonClamped(en);
1180 }
1181
1182 private ICoordinate getShiftedCoord(EastNorth en) {
1183 return CoordinateConversion.llToCoor(getShiftedLatLon(en));
1184 }
1185
1186 private final TileSet nullTileSet = new TileSet();
1187
1188 protected class TileSet extends TileRange {
1189
1190 private volatile TileSetInfo info;
1191
1192 protected TileSet(TileXY t1, TileXY t2, int zoom) {
1193 super(t1, t2, zoom);
1194 sanitize();
1195 }
1196
1197 protected TileSet(TileRange range) {
1198 super(range);
1199 sanitize();
1200 }
1201
1202 /**
1203 * null tile set
1204 */
1205 private TileSet() {
1206 // default
1207 }
1208
1209 protected void sanitize() {
1210 if (minX < tileSource.getTileXMin(zoom)) {
1211 minX = tileSource.getTileXMin(zoom);
1212 }
1213 if (minY < tileSource.getTileYMin(zoom)) {
1214 minY = tileSource.getTileYMin(zoom);
1215 }
1216 if (maxX > tileSource.getTileXMax(zoom)) {
1217 maxX = tileSource.getTileXMax(zoom);
1218 }
1219 if (maxY > tileSource.getTileYMax(zoom)) {
1220 maxY = tileSource.getTileYMax(zoom);
1221 }
1222 }
1223
1224 private boolean tooSmall() {
1225 return this.tilesSpanned() < 2.1;
1226 }
1227
1228 private boolean tooLarge() {
1229 return insane() || this.tilesSpanned() > 20;
1230 }
1231
1232 private boolean insane() {
1233 return tileCache == null || size() > tileCache.getCacheSize();
1234 }
1235
1236 /**
1237 * Get all tiles represented by this TileSet that are already in the tileCache.
1238 * @return all tiles represented by this TileSet that are already in the tileCache
1239 */
1240 private List<Tile> allExistingTiles() {
1241 return allTiles(AbstractTileSourceLayer.this::getTile);
1242 }
1243
1244 private List<Tile> allTilesCreate() {
1245 return allTiles(AbstractTileSourceLayer.this::getOrCreateTile);
1246 }
1247
1248 private List<Tile> allTiles(Function<TilePosition, Tile> mapper) {
1249 return tilePositions().map(mapper).filter(Objects::nonNull).collect(Collectors.toList());
1250 }
1251
1252 /**
1253 * Gets a stream of all tile positions in this set
1254 * @return A stream of all positions
1255 */
1256 public Stream<TilePosition> tilePositions() {
1257 if (zoom == 0 || this.insane()) {
1258 return Stream.empty(); // Tileset is either empty or too large
1259 } else {
1260 return IntStream.rangeClosed(minX, maxX).mapToObj(
1261 x -> IntStream.rangeClosed(minY, maxY).mapToObj(y -> new TilePosition(x, y, zoom))
1262 ).flatMap(Function.identity());
1263 }
1264 }
1265
1266 private List<Tile> allLoadedTiles() {
1267 return allExistingTiles().stream().filter(Tile::isLoaded).collect(Collectors.toList());
1268 }
1269
1270 /**
1271 * @return comparator, that sorts the tiles from the center to the edge of the current screen
1272 */
1273 private Comparator<Tile> getTileDistanceComparator() {
1274 final int centerX = (int) Math.ceil((minX + maxX) / 2d);
1275 final int centerY = (int) Math.ceil((minY + maxY) / 2d);
1276 return Comparator.comparingInt(t -> Math.abs(t.getXtile() - centerX) + Math.abs(t.getYtile() - centerY));
1277 }
1278
1279 private void loadAllTiles(boolean force) {
1280 if (!getDisplaySettings().isAutoLoad() && !force)
1281 return;
1282 List<Tile> allTiles = allTilesCreate();
1283 allTiles.sort(getTileDistanceComparator());
1284 for (Tile t : allTiles) {
1285 loadTile(t, force);
1286 }
1287 }
1288
1289 private void loadAllErrorTiles(boolean force) {
1290 if (!getDisplaySettings().isAutoLoad() && !force)
1291 return;
1292 for (Tile t : this.allTilesCreate()) {
1293 if (t.hasError()) {
1294 tileLoader.createTileLoaderJob(t).submit(force);
1295 }
1296 }
1297 }
1298
1299 /**
1300 * Call the given paint method for all tiles in this tile set.<p>
1301 * Uses a parallel stream.
1302 * @param visitor A visitor to call for each tile.
1303 * @param missed a consumer to call for each missed tile.
1304 */
1305 public void visitTiles(Consumer<Tile> visitor, Consumer<TilePosition> missed) {
1306 tilePositions().parallel().forEach(tp -> visitTilePosition(visitor, tp, missed));
1307 }
1308
1309 private void visitTilePosition(Consumer<Tile> visitor, TilePosition tp, Consumer<TilePosition> missed) {
1310 Tile tile = getTile(tp);
1311 if (tile == null) {
1312 missed.accept(tp);
1313 } else {
1314 visitor.accept(tile);
1315 }
1316 }
1317
1318 /**
1319 * Check if there is any tile fully loaded without error.
1320 * @return true if there is any tile fully loaded without error
1321 */
1322 public boolean hasVisibleTiles() {
1323 return getTileSetInfo().hasVisibleTiles;
1324 }
1325
1326 /**
1327 * Check if there there is a tile that is overzoomed.
1328 * <p>
1329 * I.e. the server response for one tile was "there is no tile here".
1330 * This usually happens when zoomed in too much. The limit depends on
1331 * the region, so at the edge of such a region, some tiles may be
1332 * available and some not.
1333 * @return true if there there is a tile that is overzoomed
1334 */
1335 public boolean hasOverzoomedTiles() {
1336 return getTileSetInfo().hasOverzoomedTiles;
1337 }
1338
1339 /**
1340 * Check if there are tiles still loading.
1341 * <p>
1342 * This is the case if there is a tile not yet in the cache, or in the
1343 * cache but marked as loading ({@link Tile#isLoading()}.
1344 * @return true if there are tiles still loading
1345 */
1346 public boolean hasLoadingTiles() {
1347 return getTileSetInfo().hasLoadingTiles;
1348 }
1349
1350 /**
1351 * Check if all tiles in the range are fully loaded.
1352 * <p>
1353 * A tile is considered to be fully loaded even if the result of loading
1354 * the tile was an error.
1355 * @return true if all tiles in the range are fully loaded
1356 */
1357 public boolean hasAllLoadedTiles() {
1358 return getTileSetInfo().hasAllLoadedTiles;
1359 }
1360
1361 private TileSetInfo getTileSetInfo() {
1362 if (info == null) {
1363 synchronized (this) {
1364 if (info == null) {
1365 List<Tile> allTiles = this.allExistingTiles();
1366 info = new TileSetInfo();
1367 info.hasLoadingTiles = allTiles.size() < this.size();
1368 info.hasAllLoadedTiles = true;
1369 for (Tile t : allTiles) {
1370 if ("no-tile".equals(t.getValue("tile-info"))) {
1371 info.hasOverzoomedTiles = true;
1372 }
1373 if (t.isLoaded()) {
1374 if (!t.hasError()) {
1375 info.hasVisibleTiles = true;
1376 }
1377 } else {
1378 info.hasAllLoadedTiles = false;
1379 if (t.isLoading()) {
1380 info.hasLoadingTiles = true;
1381 }
1382 }
1383 }
1384 }
1385 }
1386 }
1387 return info;
1388 }
1389
1390 @Override
1391 public String toString() {
1392 return getClass().getName() + ": zoom: " + zoom + " X(" + minX + ", " + maxX + ") Y(" + minY + ", " + maxY + ") size: " + size();
1393 }
1394 }
1395
1396 /**
1397 * Data container to hold information about a {@code TileSet} class.
1398 */
1399 private static class TileSetInfo {
1400 boolean hasVisibleTiles;
1401 boolean hasOverzoomedTiles;
1402 boolean hasLoadingTiles;
1403 boolean hasAllLoadedTiles;
1404 }
1405
1406 /**
1407 * Create a TileSet by EastNorth bbox taking a layer shift in account
1408 * @param bounds the EastNorth bounds
1409 * @param zoom zoom level
1410 * @return the tile set
1411 */
1412 protected TileSet getTileSet(ProjectionBounds bounds, int zoom) {
1413 if (zoom == 0)
1414 return new TileSet();
1415 TileXY t1, t2;
1416 IProjected topLeftUnshifted = coordinateConverter.shiftDisplayToServer(bounds.getMin());
1417 IProjected botRightUnshifted = coordinateConverter.shiftDisplayToServer(bounds.getMax());
1418 if (coordinateConverter.requiresReprojection()) {
1419 Projection projServer = Projections.getProjectionByCode(tileSource.getServerCRS());
1420 if (projServer == null) {
1421 throw new IllegalStateException(tileSource.toString());
1422 }
1423 ProjectionBounds projBounds = new ProjectionBounds(
1424 CoordinateConversion.projToEn(topLeftUnshifted),
1425 CoordinateConversion.projToEn(botRightUnshifted));
1426 ProjectionBounds bbox = projServer.getEastNorthBoundsBox(projBounds, Main.getProjection());
1427 t1 = tileSource.projectedToTileXY(CoordinateConversion.enToProj(bbox.getMin()), zoom);
1428 t2 = tileSource.projectedToTileXY(CoordinateConversion.enToProj(bbox.getMax()), zoom);
1429 } else {
1430 t1 = tileSource.projectedToTileXY(topLeftUnshifted, zoom);
1431 t2 = tileSource.projectedToTileXY(botRightUnshifted, zoom);
1432 }
1433 return new TileSet(t1, t2, zoom);
1434 }
1435
1436 private class DeepTileSet {
1437 private final ProjectionBounds bounds;
1438 private final int minZoom, maxZoom;
1439 private final TileSet[] tileSets;
1440
1441 @SuppressWarnings("unchecked")
1442 DeepTileSet(ProjectionBounds bounds, int minZoom, int maxZoom) {
1443 this.bounds = bounds;
1444 this.minZoom = minZoom;
1445 this.maxZoom = maxZoom;
1446 if (minZoom > maxZoom) {
1447 throw new IllegalArgumentException(minZoom + " > " + maxZoom);
1448 }
1449 this.tileSets = new AbstractTileSourceLayer.TileSet[maxZoom - minZoom + 1];
1450 }
1451
1452 public TileSet getTileSet(int zoom) {
1453 if (zoom < minZoom)
1454 return nullTileSet;
1455 synchronized (tileSets) {
1456 TileSet ts = tileSets[zoom-minZoom];
1457 if (ts == null) {
1458 ts = AbstractTileSourceLayer.this.getTileSet(bounds, zoom);
1459 tileSets[zoom-minZoom] = ts;
1460 }
1461 return ts;
1462 }
1463 }
1464 }
1465
1466 @Override
1467 public void paint(Graphics2D g, MapView mv, Bounds bounds) {
1468 // old and unused.
1469 }
1470
1471 private void drawInViewArea(Graphics2D g, MapView mv, ProjectionBounds pb) {
1472 int zoom = currentZoomLevel;
1473 if (getDisplaySettings().isAutoZoom()) {
1474 zoom = getBestZoom();
1475 }
1476
1477 DeepTileSet dts = new DeepTileSet(pb, getMinZoomLvl(), zoom);
1478
1479 int displayZoomLevel = zoom;
1480
1481 boolean noTilesAtZoom = false;
1482 if (getDisplaySettings().isAutoZoom() && getDisplaySettings().isAutoLoad()) {
1483 // Auto-detection of tilesource maxzoom (currently fully works only for Bing)
1484 TileSet ts0 = dts.getTileSet(zoom);
1485 if (!ts0.hasVisibleTiles() && (!ts0.hasLoadingTiles() || ts0.hasOverzoomedTiles())) {
1486 noTilesAtZoom = true;
1487 }
1488 // Find highest zoom level with at least one visible tile
1489 for (int tmpZoom = zoom; tmpZoom > dts.minZoom; tmpZoom--) {
1490 if (dts.getTileSet(tmpZoom).hasVisibleTiles()) {
1491 displayZoomLevel = tmpZoom;
1492 break;
1493 }
1494 }
1495 // Do binary search between currentZoomLevel and displayZoomLevel
1496 while (zoom > displayZoomLevel && !ts0.hasVisibleTiles() && ts0.hasOverzoomedTiles()) {
1497 zoom = (zoom + displayZoomLevel)/2;
1498 ts0 = dts.getTileSet(zoom);
1499 }
1500
1501 setZoomLevel(zoom, false);
1502
1503 // If all tiles at displayZoomLevel is loaded, load all tiles at next zoom level
1504 // to make sure there're really no more zoom levels
1505 // loading is done in the next if section
1506 if (zoom == displayZoomLevel && !ts0.hasLoadingTiles() && zoom < dts.maxZoom) {
1507 zoom++;
1508 ts0 = dts.getTileSet(zoom);
1509 }
1510 // When we have overzoomed tiles and all tiles at current zoomlevel is loaded,
1511 // load tiles at previovus zoomlevels until we have all tiles on screen is loaded.
1512 // loading is done in the next if section
1513 while (zoom > dts.minZoom && ts0.hasOverzoomedTiles() && !ts0.hasLoadingTiles()) {
1514 zoom--;
1515 ts0 = dts.getTileSet(zoom);
1516 }
1517 } else if (getDisplaySettings().isAutoZoom()) {
1518 setZoomLevel(zoom, false);
1519 }
1520 TileSet ts = dts.getTileSet(zoom);
1521
1522 // Too many tiles... refuse to download
1523 if (!ts.tooLarge()) {
1524 // try to load tiles from desired zoom level, no matter what we will show (for example, tiles from previous zoom level
1525 // on zoom in)
1526 ts.loadAllTiles(false);
1527 }
1528
1529 if (displayZoomLevel != zoom) {
1530 ts = dts.getTileSet(displayZoomLevel);
1531 if (!dts.getTileSet(displayZoomLevel).hasAllLoadedTiles() && displayZoomLevel < zoom) {
1532 // if we are showing tiles from lower zoom level, ensure that all tiles are loaded as they are few,
1533 // and should not trash the tile cache
1534 // This is especially needed when dts.getTileSet(zoom).tooLarge() is true and we are not loading tiles
1535 ts.loadAllTiles(false);
1536 }
1537 }
1538
1539 g.setColor(Color.DARK_GRAY);
1540
1541 List<Tile> missedTiles = this.paintTileImages(g, ts);
1542 int[] otherZooms = {1, 2, -1, -2, -3, -4, -5};
1543 for (int zoomOffset : otherZooms) {
1544 if (!getDisplaySettings().isAutoZoom()) {
1545 break;
1546 }
1547 int newzoom = displayZoomLevel + zoomOffset;
1548 if (newzoom < getMinZoomLvl() || newzoom > getMaxZoomLvl()) {
1549 continue;
1550 }
1551 if (missedTiles.isEmpty()) {
1552 break;
1553 }
1554 List<Tile> newlyMissedTiles = new LinkedList<>();
1555 for (Tile missed : missedTiles) {
1556 if (zoomOffset > 0 && "no-tile".equals(missed.getValue("tile-info"))) {
1557 // Don't try to paint from higher zoom levels when tile is overzoomed
1558 newlyMissedTiles.add(missed);
1559 continue;
1560 }
1561 TileSet ts2 = new TileSet(tileSource.getCoveringTileRange(missed, newzoom));
1562 // Instantiating large TileSets is expensive. If there are no loaded tiles, don't bother even trying.
1563 if (ts2.allLoadedTiles().isEmpty()) {
1564 newlyMissedTiles.add(missed);
1565 continue;
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));
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(Main.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.