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

Last change on this file since 11831 was 11831, checked in by bastiK, 7 years ago

see #7427 - use integer operations on tile index when possible (instead of EastNorth or LatLon rectangles)

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