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

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

fix #13222 - Use visitor pattern for painting tiles (patch by michael2402) - gsoc-core

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