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

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

see #7427 - repaint after clear tile cache action

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