source: josm/trunk/src/org/openstreetmap/josm/gui/layer/TMSLayer.java@ 8470

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

javadoc fixes. Removed one duplicated method in exception handling

  • Property svn:eol-style set to native
File size: 57.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.Font;
8import java.awt.Graphics;
9import java.awt.Graphics2D;
10import java.awt.GridBagLayout;
11import java.awt.Image;
12import java.awt.Point;
13import java.awt.Rectangle;
14import java.awt.Toolkit;
15import java.awt.event.ActionEvent;
16import java.awt.event.MouseAdapter;
17import java.awt.event.MouseEvent;
18import java.awt.image.ImageObserver;
19import java.io.File;
20import java.io.IOException;
21import java.io.StringReader;
22import java.net.URL;
23import java.text.SimpleDateFormat;
24import java.util.ArrayList;
25import java.util.Collections;
26import java.util.Comparator;
27import java.util.Date;
28import java.util.HashMap;
29import java.util.LinkedList;
30import java.util.List;
31import java.util.Map;
32import java.util.Map.Entry;
33import java.util.Scanner;
34import java.util.concurrent.Callable;
35import java.util.regex.Matcher;
36import java.util.regex.Pattern;
37
38import javax.swing.AbstractAction;
39import javax.swing.Action;
40import javax.swing.BorderFactory;
41import javax.swing.JCheckBoxMenuItem;
42import javax.swing.JLabel;
43import javax.swing.JMenuItem;
44import javax.swing.JOptionPane;
45import javax.swing.JPanel;
46import javax.swing.JPopupMenu;
47import javax.swing.JTextField;
48
49import org.openstreetmap.gui.jmapviewer.AttributionSupport;
50import org.openstreetmap.gui.jmapviewer.Coordinate;
51import org.openstreetmap.gui.jmapviewer.MemoryTileCache;
52import org.openstreetmap.gui.jmapviewer.OsmTileLoader;
53import org.openstreetmap.gui.jmapviewer.Tile;
54import org.openstreetmap.gui.jmapviewer.interfaces.CachedTileLoader;
55import org.openstreetmap.gui.jmapviewer.interfaces.TileCache;
56import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
57import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
58import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
59import org.openstreetmap.gui.jmapviewer.tilesources.BingAerialTileSource;
60import org.openstreetmap.gui.jmapviewer.tilesources.ScanexTileSource;
61import org.openstreetmap.gui.jmapviewer.tilesources.TMSTileSource;
62import org.openstreetmap.gui.jmapviewer.tilesources.TemplatedTMSTileSource;
63import org.openstreetmap.josm.Main;
64import org.openstreetmap.josm.actions.RenameLayerAction;
65import org.openstreetmap.josm.data.Bounds;
66import org.openstreetmap.josm.data.Version;
67import org.openstreetmap.josm.data.coor.EastNorth;
68import org.openstreetmap.josm.data.coor.LatLon;
69import org.openstreetmap.josm.data.imagery.ImageryInfo;
70import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
71import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader;
72import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
73import org.openstreetmap.josm.data.preferences.BooleanProperty;
74import org.openstreetmap.josm.data.preferences.IntegerProperty;
75import org.openstreetmap.josm.data.preferences.StringProperty;
76import org.openstreetmap.josm.data.projection.Projection;
77import org.openstreetmap.josm.gui.ExtendedDialog;
78import org.openstreetmap.josm.gui.MapFrame;
79import org.openstreetmap.josm.gui.MapView;
80import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
81import org.openstreetmap.josm.gui.NavigatableComponent.ZoomChangeListener;
82import org.openstreetmap.josm.gui.PleaseWaitRunnable;
83import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
84import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
85import org.openstreetmap.josm.gui.progress.ProgressMonitor;
86import org.openstreetmap.josm.io.CacheCustomContent;
87import org.openstreetmap.josm.io.OsmTransferException;
88import org.openstreetmap.josm.io.UTFInputStreamReader;
89import org.openstreetmap.josm.tools.CheckParameterUtil;
90import org.openstreetmap.josm.tools.GBC;
91import org.openstreetmap.josm.tools.Utils;
92import org.xml.sax.InputSource;
93import org.xml.sax.SAXException;
94
95/**
96 * Class that displays a slippy map layer.
97 *
98 * @author Frederik Ramm
99 * @author LuVar <lubomir.varga@freemap.sk>
100 * @author Dave Hansen <dave@sr71.net>
101 * @author Upliner <upliner@gmail.com>
102 *
103 */
104public class TMSLayer extends ImageryLayer implements ImageObserver, TileLoaderListener, ZoomChangeListener {
105 public static final String PREFERENCE_PREFIX = "imagery.tms";
106
107 public static final int MAX_ZOOM = 30;
108 public static final int MIN_ZOOM = 2;
109 public static final int DEFAULT_MAX_ZOOM = 20;
110 public static final int DEFAULT_MIN_ZOOM = 2;
111
112 public static final BooleanProperty PROP_DEFAULT_AUTOZOOM = new BooleanProperty(PREFERENCE_PREFIX + ".default_autozoom", true);
113 public static final BooleanProperty PROP_DEFAULT_AUTOLOAD = new BooleanProperty(PREFERENCE_PREFIX + ".default_autoload", true);
114 public static final BooleanProperty PROP_DEFAULT_SHOWERRORS = new BooleanProperty(PREFERENCE_PREFIX + ".default_showerrors", true);
115 public static final IntegerProperty PROP_MIN_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".min_zoom_lvl", DEFAULT_MIN_ZOOM);
116 public static final IntegerProperty PROP_MAX_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".max_zoom_lvl", DEFAULT_MAX_ZOOM);
117 public static final BooleanProperty PROP_ADD_TO_SLIPPYMAP_CHOOSER = new BooleanProperty(PREFERENCE_PREFIX +
118 ".add_to_slippymap_chooser", true);
119 public static final StringProperty PROP_TILECACHE_DIR;
120
121 static {
122 String defPath = null;
123 try {
124 defPath = new File(Main.pref.getCacheDirectory(), "tms").getAbsolutePath();
125 } catch (SecurityException e) {
126 Main.warn(e);
127 }
128 PROP_TILECACHE_DIR = new StringProperty(PREFERENCE_PREFIX + ".tilecache", defPath);
129 }
130
131 /**
132 * Interface for creating TileLoaders, ie. classes responsible for loading tiles on map
133 *
134 */
135 public interface TileLoaderFactory {
136 /**
137 * @param listener object that will be notified, when tile has finished loading
138 * @return TileLoader that will notify the listener
139 */
140 TileLoader makeTileLoader(TileLoaderListener listener);
141
142 /**
143 * @param listener object that will be notified, when tile has finished loading
144 * @param headers HTTP headers that should be sent by TileLoader to tile server
145 * @return TileLoader that will notify the listener
146 */
147 TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> headers);
148 }
149
150 protected TileCache tileCache;
151 protected TileSource tileSource;
152 protected TileLoader tileLoader;
153
154
155 public static TileLoaderFactory loaderFactory = new TileLoaderFactory() {
156 @Override
157 public TileLoader makeTileLoader(TileLoaderListener listener, Map<String, String> inputHeaders) {
158 Map<String, String> headers = new HashMap<>();
159 headers.put("User-Agent", Version.getInstance().getFullAgentString());
160 headers.put("Accept", "text/html, image/png, image/jpeg, image/gif, */*");
161 if (inputHeaders != null)
162 headers.putAll(inputHeaders);
163
164 try {
165 return new TMSCachedTileLoader(listener, "TMS",
166 Main.pref.getInteger("socket.timeout.connect",15) * 1000,
167 Main.pref.getInteger("socket.timeout.read", 30) * 1000,
168 headers,
169 PROP_TILECACHE_DIR.get());
170 } catch (IOException e) {
171 Main.warn(e);
172 }
173 return null;
174 }
175
176 @Override
177 public TileLoader makeTileLoader(TileLoaderListener listener) {
178 return makeTileLoader(listener, null);
179 }
180 };
181
182 /**
183 * Plugins that wish to set custom tile loader should call this method
184 */
185
186 public static void setCustomTileLoaderFactory(TileLoaderFactory loaderFactory) {
187 TMSLayer.loaderFactory = loaderFactory;
188 }
189
190 @Override
191 public synchronized void tileLoadingFinished(Tile tile, boolean success) {
192 if (tile.hasError()) {
193 success = false;
194 tile.setImage(null);
195 }
196 if (sharpenLevel != 0 && success) {
197 tile.setImage(sharpenImage(tile.getImage()));
198 }
199 tile.setLoaded(success);
200 needRedraw = true;
201 if (Main.map != null) {
202 Main.map.repaint(100);
203 }
204 if (Main.isDebugEnabled()) {
205 Main.debug("tileLoadingFinished() tile: " + tile + " success: " + success);
206 }
207 }
208
209 /**
210 * Clears the tile cache.
211 *
212 * If the current tileLoader is an instance of OsmTileLoader, a new
213 * TmsTileClearController is created and passed to the according clearCache
214 * method.
215 *
216 * @param monitor not used in this implementation - as cache clear is instaneus
217 */
218 public void clearTileCache(ProgressMonitor monitor) {
219 tileCache.clear();
220 if (tileLoader instanceof CachedTileLoader) {
221 ((CachedTileLoader)tileLoader).clearCache(tileSource);
222 }
223 redraw();
224 }
225
226 /**
227 * Zoomlevel at which tiles is currently downloaded.
228 * Initial zoom lvl is set to bestZoom
229 */
230 public int currentZoomLevel;
231
232 private Tile clickedTile;
233 private boolean needRedraw;
234 private JPopupMenu tileOptionMenu;
235 private Tile showMetadataTile;
236 private AttributionSupport attribution = new AttributionSupport();
237 private static final Font InfoFont = new Font("sansserif", Font.BOLD, 13);
238
239 protected boolean autoZoom;
240 protected boolean autoLoad;
241 protected boolean showErrors;
242
243 /**
244 * Initiates a repaint of Main.map
245 *
246 * @see Main#map
247 * @see MapFrame#repaint()
248 */
249 protected void redraw() {
250 needRedraw = true;
251 Main.map.repaint();
252 }
253
254 protected static int checkMaxZoomLvl(int maxZoomLvl, TileSource ts) {
255 if(maxZoomLvl > MAX_ZOOM) {
256 maxZoomLvl = MAX_ZOOM;
257 }
258 if(maxZoomLvl < PROP_MIN_ZOOM_LVL.get()) {
259 maxZoomLvl = PROP_MIN_ZOOM_LVL.get();
260 }
261 if (ts != null && ts.getMaxZoom() != 0 && ts.getMaxZoom() < maxZoomLvl) {
262 maxZoomLvl = ts.getMaxZoom();
263 }
264 return maxZoomLvl;
265 }
266
267 public static int getMaxZoomLvl(TileSource ts) {
268 return checkMaxZoomLvl(PROP_MAX_ZOOM_LVL.get(), ts);
269 }
270
271 public static void setMaxZoomLvl(int maxZoomLvl) {
272 Integer newMaxZoom = Integer.valueOf(checkMaxZoomLvl(maxZoomLvl, null));
273 PROP_MAX_ZOOM_LVL.put(newMaxZoom);
274 }
275
276 static int checkMinZoomLvl(int minZoomLvl, TileSource ts) {
277 if(minZoomLvl < MIN_ZOOM) {
278 /*Main.debug("Min. zoom level should not be less than "+MIN_ZOOM+"! Setting to that.");*/
279 minZoomLvl = MIN_ZOOM;
280 }
281 if(minZoomLvl > PROP_MAX_ZOOM_LVL.get()) {
282 /*Main.debug("Min. zoom level should not be more than Max. zoom level! Setting to Max.");*/
283 minZoomLvl = getMaxZoomLvl(ts);
284 }
285 if (ts != null && ts.getMinZoom() > minZoomLvl) {
286 /*Main.debug("Increasing min. zoom level to match tile source");*/
287 minZoomLvl = ts.getMinZoom();
288 }
289 return minZoomLvl;
290 }
291
292 public static int getMinZoomLvl(TileSource ts) {
293 return checkMinZoomLvl(PROP_MIN_ZOOM_LVL.get(), ts);
294 }
295
296 public static void setMinZoomLvl(int minZoomLvl) {
297 minZoomLvl = checkMinZoomLvl(minZoomLvl, null);
298 PROP_MIN_ZOOM_LVL.put(minZoomLvl);
299 }
300
301 private static class CachedAttributionBingAerialTileSource extends BingAerialTileSource {
302
303 public CachedAttributionBingAerialTileSource(ImageryInfo info) {
304 super(info);
305 }
306
307 class BingAttributionData extends CacheCustomContent<IOException> {
308
309 public BingAttributionData() {
310 super("bing.attribution.xml", CacheCustomContent.INTERVAL_HOURLY);
311 }
312
313 @Override
314 protected byte[] updateData() throws IOException {
315 URL u = getAttributionUrl();
316 try (Scanner scanner = new Scanner(UTFInputStreamReader.create(Utils.openURL(u)))) {
317 String r = scanner.useDelimiter("\\A").next();
318 Main.info("Successfully loaded Bing attribution data.");
319 return r.getBytes("UTF-8");
320 }
321 }
322 }
323
324 @Override
325 protected Callable<List<Attribution>> getAttributionLoaderCallable() {
326 return new Callable<List<Attribution>>() {
327
328 @Override
329 public List<Attribution> call() throws Exception {
330 BingAttributionData attributionLoader = new BingAttributionData();
331 int waitTimeSec = 1;
332 while (true) {
333 try {
334 String xml = attributionLoader.updateIfRequiredString();
335 return parseAttributionText(new InputSource(new StringReader(xml)));
336 } catch (IOException ex) {
337 Main.warn("Could not connect to Bing API. Will retry in " + waitTimeSec + " seconds.");
338 Thread.sleep(waitTimeSec * 1000L);
339 waitTimeSec *= 2;
340 }
341 }
342 }
343 };
344 }
345 }
346
347 /**
348 * Creates and returns a new TileSource instance depending on the {@link ImageryType}
349 * of the passed ImageryInfo object.
350 *
351 * If no appropriate TileSource is found, null is returned.
352 * Currently supported ImageryType are {@link ImageryType#TMS},
353 * {@link ImageryType#BING}, {@link ImageryType#SCANEX}.
354 *
355 * @param info imagery info
356 * @return a new TileSource instance or null if no TileSource for the ImageryInfo/ImageryType could be found.
357 * @throws IllegalArgumentException if url from imagery info is null or invalid
358 */
359 public static TileSource getTileSource(ImageryInfo info) {
360 if (info.getImageryType() == ImageryType.TMS) {
361 checkUrl(info.getUrl());
362 TMSTileSource t = new TemplatedTMSTileSource(info);
363 info.setAttribution(t);
364 return t;
365 } else if (info.getImageryType() == ImageryType.BING) {
366 return new CachedAttributionBingAerialTileSource(info);
367 } else if (info.getImageryType() == ImageryType.SCANEX) {
368 return new ScanexTileSource(info);
369 }
370 return null;
371 }
372
373 /**
374 * Checks validity of given URL.
375 * @param url URL to check
376 * @throws IllegalArgumentException if url is null or invalid
377 */
378 public static void checkUrl(String url) {
379 CheckParameterUtil.ensureParameterNotNull(url, "url");
380 Matcher m = Pattern.compile("\\{[^}]*\\}").matcher(url);
381 while (m.find()) {
382 boolean isSupportedPattern = false;
383 for (String pattern : TemplatedTMSTileSource.ALL_PATTERNS) {
384 if (m.group().matches(pattern)) {
385 isSupportedPattern = true;
386 break;
387 }
388 }
389 if (!isSupportedPattern) {
390 throw new IllegalArgumentException(
391 tr("{0} is not a valid TMS argument. Please check this server URL:\n{1}", m.group(), url));
392 }
393 }
394 }
395
396 private void initTileSource(TileSource tileSource) {
397 this.tileSource = tileSource;
398 attribution.initialize(tileSource);
399
400 currentZoomLevel = getBestZoom();
401
402 Map<String, String> headers = null;
403 if (tileSource instanceof TemplatedTMSTileSource) {
404 headers = (((TemplatedTMSTileSource)tileSource).getHeaders());
405 }
406
407 tileLoader = loaderFactory.makeTileLoader(this, headers);
408 if (tileLoader instanceof TMSCachedTileLoader) {
409 tileCache = (TileCache) tileLoader;
410 } else {
411 tileCache = new MemoryTileCache();
412 }
413 if (tileLoader == null)
414 tileLoader = new OsmTileLoader(this);
415 }
416
417 /**
418 * Marks layer as needing redraw on offset change
419 */
420 @Override
421 public void setOffset(double dx, double dy) {
422 super.setOffset(dx, dy);
423 needRedraw = true;
424 }
425 /**
426 * Returns average number of screen pixels per tile pixel for current mapview
427 */
428 private double getScaleFactor(int zoom) {
429 if (!Main.isDisplayingMapView()) return 1;
430 MapView mv = Main.map.mapView;
431 LatLon topLeft = mv.getLatLon(0, 0);
432 LatLon botRight = mv.getLatLon(mv.getWidth(), mv.getHeight());
433 double x1 = tileSource.lonToTileX(topLeft.lon(), zoom);
434 double y1 = tileSource.latToTileY(topLeft.lat(), zoom);
435 double x2 = tileSource.lonToTileX(botRight.lon(), zoom);
436 double y2 = tileSource.latToTileY(botRight.lat(), zoom);
437
438 int screenPixels = mv.getWidth()*mv.getHeight();
439 double tilePixels = Math.abs((y2-y1)*(x2-x1)*tileSource.getTileSize()*tileSource.getTileSize());
440 if (screenPixels == 0 || tilePixels == 0) return 1;
441 return screenPixels/tilePixels;
442 }
443
444 private final int getBestZoom() {
445 double factor = getScaleFactor(1); // check the ratio between area of tilesize at zoom 1 to current view
446 double result = Math.log(factor)/Math.log(2)/2+1;
447 /*
448 * Math.log(factor)/Math.log(2) - gives log base 2 of factor
449 * We divide result by 2, as factor contains ratio between areas. We could do Math.sqrt before log, or just divide log by 2
450 * In general, smaller zoom levels are more readable. We prefer big,
451 * block, pixelated (but readable) map text to small, smeared,
452 * unreadable underzoomed text. So, use .floor() instead of rounding
453 * to skew things a bit toward the lower zooms.
454 * Remember, that result here, should correspond to TMSLayer.paint(...)
455 * getScaleFactor(...) is supposed to be between 0.75 and 3
456 */
457 int intResult = (int)Math.floor(result);
458 if (intResult > getMaxZoomLvl())
459 return getMaxZoomLvl();
460 if (intResult < getMinZoomLvl())
461 return getMinZoomLvl();
462 return intResult;
463 }
464
465 @SuppressWarnings("serial")
466 public TMSLayer(ImageryInfo info) {
467 super(info);
468
469 if(!isProjectionSupported(Main.getProjection())) {
470 JOptionPane.showMessageDialog(Main.parent,
471 tr("TMS layers do not support the projection {0}.\n{1}\n"
472 + "Change the projection or remove the layer.",
473 Main.getProjection().toCode(), nameSupportedProjections()),
474 tr("Warning"),
475 JOptionPane.WARNING_MESSAGE);
476 }
477
478 setBackgroundLayer(true);
479 this.setVisible(true);
480
481 TileSource source = getTileSource(info);
482 if (source == null)
483 throw new IllegalStateException("Cannot create TMSLayer with non-TMS ImageryInfo");
484 initTileSource(source);
485
486 MapView.addZoomChangeListener(this);
487 }
488
489 /**
490 * Adds a context menu to the mapView.
491 */
492 @Override
493 public void hookUpMapView() {
494 tileOptionMenu = new JPopupMenu();
495
496 autoZoom = PROP_DEFAULT_AUTOZOOM.get();
497 JCheckBoxMenuItem autoZoomPopup = new JCheckBoxMenuItem();
498 autoZoomPopup.setAction(new AbstractAction(tr("Auto Zoom")) {
499 @Override
500 public void actionPerformed(ActionEvent ae) {
501 autoZoom = !autoZoom;
502 }
503 });
504 autoZoomPopup.setSelected(autoZoom);
505 tileOptionMenu.add(autoZoomPopup);
506
507 autoLoad = PROP_DEFAULT_AUTOLOAD.get();
508 JCheckBoxMenuItem autoLoadPopup = new JCheckBoxMenuItem();
509 autoLoadPopup.setAction(new AbstractAction(tr("Auto load tiles")) {
510 @Override
511 public void actionPerformed(ActionEvent ae) {
512 autoLoad= !autoLoad;
513 }
514 });
515 autoLoadPopup.setSelected(autoLoad);
516 tileOptionMenu.add(autoLoadPopup);
517
518 showErrors = PROP_DEFAULT_SHOWERRORS.get();
519 JCheckBoxMenuItem showErrorsPopup = new JCheckBoxMenuItem();
520 showErrorsPopup.setAction(new AbstractAction(tr("Show Errors")) {
521 @Override
522 public void actionPerformed(ActionEvent ae) {
523 showErrors = !showErrors;
524 }
525 });
526 showErrorsPopup.setSelected(showErrors);
527 tileOptionMenu.add(showErrorsPopup);
528
529 tileOptionMenu.add(new JMenuItem(new AbstractAction(tr("Load Tile")) {
530 @Override
531 public void actionPerformed(ActionEvent ae) {
532 if (clickedTile != null) {
533 loadTile(clickedTile, true);
534 redraw();
535 }
536 }
537 }));
538
539 tileOptionMenu.add(new JMenuItem(new AbstractAction(
540 tr("Show Tile Info")) {
541 private String getSizeString(int size) {
542 StringBuilder ret = new StringBuilder();
543 return ret.append(size).append("x").append(size).toString();
544 }
545
546 private JTextField createTextField(String text) {
547 JTextField ret = new JTextField(text);
548 ret.setEditable(false);
549 ret.setBorder(BorderFactory.createEmptyBorder());
550 return ret;
551 }
552 @Override
553 public void actionPerformed(ActionEvent ae) {
554 if (clickedTile != null) {
555 ExtendedDialog ed = new ExtendedDialog(Main.parent, tr("Tile Info"), new String[]{tr("OK")});
556 JPanel panel = new JPanel(new GridBagLayout());
557 Rectangle displaySize = tileToRect(clickedTile);
558 String url = "";
559 try {
560 url = clickedTile.getUrl();
561 } catch (IOException e) {
562 // silence exceptions
563 }
564
565 String[][] content = {
566 {"Tile name", clickedTile.getKey()},
567 {"Tile url", url},
568 {"Tile size", getSizeString(clickedTile.getTileSource().getTileSize()) },
569 {"Tile display size", new StringBuilder().append(displaySize.width).append("x").append(displaySize.height).toString()},
570 };
571
572 for (String[] entry: content) {
573 panel.add(new JLabel(tr(entry[0]) + ":"), GBC.std());
574 panel.add(GBC.glue(5,0), GBC.std());
575 panel.add(createTextField(entry[1]), GBC.eol().fill(GBC.HORIZONTAL));
576 }
577
578 for (Entry<String, String> e: clickedTile.getMetadata().entrySet()) {
579 panel.add(new JLabel(tr("Metadata ") + tr(e.getKey()) + ":"), GBC.std());
580 panel.add(GBC.glue(5,0), GBC.std());
581 String value = e.getValue();
582 if ("lastModification".equals(e.getKey()) || "expirationTime".equals(e.getKey())) {
583 value = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(Long.parseLong(value)));
584 }
585 panel.add(createTextField(value), GBC.eol().fill(GBC.HORIZONTAL));
586
587 }
588 ed.setIcon(JOptionPane.INFORMATION_MESSAGE);
589 ed.setContent(panel);
590 ed.showDialog();
591 }
592 }
593 }));
594
595 tileOptionMenu.add(new JMenuItem(new AbstractAction(
596 tr("Request Update")) {
597 @Override
598 public void actionPerformed(ActionEvent ae) {
599 if (clickedTile != null) {
600 clickedTile.setLoaded(false);
601 tileLoader.createTileLoaderJob(clickedTile).submit(true);
602 }
603 }
604 }));
605
606 tileOptionMenu.add(new JMenuItem(new AbstractAction(
607 tr("Load All Tiles")) {
608 @Override
609 public void actionPerformed(ActionEvent ae) {
610 loadAllTiles(true);
611 redraw();
612 }
613 }));
614
615 tileOptionMenu.add(new JMenuItem(new AbstractAction(
616 tr("Load All Error Tiles")) {
617 @Override
618 public void actionPerformed(ActionEvent ae) {
619 loadAllErrorTiles(true);
620 redraw();
621 }
622 }));
623
624 // increase and decrease commands
625 tileOptionMenu.add(new JMenuItem(new AbstractAction(
626 tr("Increase zoom")) {
627 @Override
628 public void actionPerformed(ActionEvent ae) {
629 increaseZoomLevel();
630 redraw();
631 }
632 }));
633
634 tileOptionMenu.add(new JMenuItem(new AbstractAction(
635 tr("Decrease zoom")) {
636 @Override
637 public void actionPerformed(ActionEvent ae) {
638 decreaseZoomLevel();
639 redraw();
640 }
641 }));
642
643 tileOptionMenu.add(new JMenuItem(new AbstractAction(
644 tr("Snap to tile size")) {
645 @Override
646 public void actionPerformed(ActionEvent ae) {
647 double newFactor = Math.sqrt(getScaleFactor(currentZoomLevel));
648 Main.map.mapView.zoomToFactor(newFactor);
649 redraw();
650 }
651 }));
652
653 tileOptionMenu.add(new JMenuItem(new AbstractAction(
654 tr("Flush Tile Cache")) {
655 @Override
656 public void actionPerformed(ActionEvent ae) {
657 new PleaseWaitRunnable(tr("Flush Tile Cache")) {
658 @Override
659 protected void realRun() throws SAXException, IOException,
660 OsmTransferException {
661 clearTileCache(getProgressMonitor());
662 }
663
664 @Override
665 protected void finish() {
666 }
667
668 @Override
669 protected void cancel() {
670 }
671 }.run();
672 }
673 }));
674
675 final MouseAdapter adapter = new MouseAdapter() {
676 @Override
677 public void mouseClicked(MouseEvent e) {
678 if (!isVisible()) return;
679 if (e.getButton() == MouseEvent.BUTTON3) {
680 clickedTile = getTileForPixelpos(e.getX(), e.getY());
681 tileOptionMenu.show(e.getComponent(), e.getX(), e.getY());
682 } else if (e.getButton() == MouseEvent.BUTTON1) {
683 attribution.handleAttribution(e.getPoint(), true);
684 }
685 }
686 };
687 Main.map.mapView.addMouseListener(adapter);
688
689 MapView.addLayerChangeListener(new LayerChangeListener() {
690 @Override
691 public void activeLayerChange(Layer oldLayer, Layer newLayer) {
692 //
693 }
694
695 @Override
696 public void layerAdded(Layer newLayer) {
697 //
698 }
699
700 @Override
701 public void layerRemoved(Layer oldLayer) {
702 if (oldLayer == TMSLayer.this) {
703 Main.map.mapView.removeMouseListener(adapter);
704 MapView.removeZoomChangeListener(TMSLayer.this);
705 MapView.removeLayerChangeListener(this);
706 }
707 }
708 });
709 }
710
711 /**
712 * This fires every time the user changes the zoom, but also (due to ZoomChangeListener) - on all
713 * changes to visible map (panning/zooming)
714 */
715 @Override
716 public void zoomChanged() {
717 if (Main.isDebugEnabled()) {
718 Main.debug("zoomChanged(): " + currentZoomLevel);
719 }
720 needRedraw = true;
721 if (tileLoader instanceof TMSCachedTileLoader) {
722 ((TMSCachedTileLoader) tileLoader).cancelOutstandingTasks();
723 }
724 }
725
726 protected int getMaxZoomLvl() {
727 if (info.getMaxZoom() != 0)
728 return checkMaxZoomLvl(info.getMaxZoom(), tileSource);
729 else
730 return getMaxZoomLvl(tileSource);
731 }
732
733 protected int getMinZoomLvl() {
734 return getMinZoomLvl(tileSource);
735 }
736
737 /**
738 * Zoom in, go closer to map.
739 *
740 * @return true, if zoom increasing was successful, false otherwise
741 */
742 public boolean zoomIncreaseAllowed() {
743 boolean zia = currentZoomLevel < this.getMaxZoomLvl();
744 if (Main.isDebugEnabled()) {
745 Main.debug("zoomIncreaseAllowed(): " + zia + " " + currentZoomLevel + " vs. " + this.getMaxZoomLvl());
746 }
747 return zia;
748 }
749
750 public boolean increaseZoomLevel() {
751 if (zoomIncreaseAllowed()) {
752 currentZoomLevel++;
753 if (Main.isDebugEnabled()) {
754 Main.debug("increasing zoom level to: " + currentZoomLevel);
755 }
756 zoomChanged();
757 } else {
758 Main.warn("Current zoom level ("+currentZoomLevel+") could not be increased. "+
759 "Max.zZoom Level "+this.getMaxZoomLvl()+" reached.");
760 return false;
761 }
762 return true;
763 }
764
765 public boolean setZoomLevel(int zoom) {
766 if (zoom == currentZoomLevel) return true;
767 if (zoom > this.getMaxZoomLvl()) return false;
768 if (zoom < this.getMinZoomLvl()) return false;
769 currentZoomLevel = zoom;
770 zoomChanged();
771 return true;
772 }
773
774 /**
775 * Check if zooming out is allowed
776 *
777 * @return true, if zooming out is allowed (currentZoomLevel &gt; minZoomLevel)
778 */
779 public boolean zoomDecreaseAllowed() {
780 return currentZoomLevel > this.getMinZoomLvl();
781 }
782
783 /**
784 * Zoom out from map.
785 *
786 * @return true, if zoom increasing was successfull, false othervise
787 */
788 public boolean decreaseZoomLevel() {
789 //int minZoom = this.getMinZoomLvl();
790 if (zoomDecreaseAllowed()) {
791 if (Main.isDebugEnabled()) {
792 Main.debug("decreasing zoom level to: " + currentZoomLevel);
793 }
794 currentZoomLevel--;
795 zoomChanged();
796 } else {
797 /*Main.debug("Current zoom level could not be decreased. Min. zoom level "+minZoom+" reached.");*/
798 return false;
799 }
800 return true;
801 }
802
803 /*
804 * We use these for quick, hackish calculations. They
805 * are temporary only and intentionally not inserted
806 * into the tileCache.
807 */
808 private Tile tempCornerTile(Tile t) {
809 int x = t.getXtile() + 1;
810 int y = t.getYtile() + 1;
811 int zoom = t.getZoom();
812 Tile tile = getTile(x, y, zoom);
813 if (tile != null)
814 return tile;
815 return new Tile(tileSource, x, y, zoom);
816 }
817
818 private Tile getOrCreateTile(int x, int y, int zoom) {
819 Tile tile = getTile(x, y, zoom);
820 if (tile == null) {
821 tile = new Tile(tileSource, x, y, zoom);
822 tileCache.addTile(tile);
823 tile.loadPlaceholderFromCache(tileCache);
824 }
825 return tile;
826 }
827
828 /*
829 * This can and will return null for tiles that are not
830 * already in the cache.
831 */
832 private Tile getTile(int x, int y, int zoom) {
833 int max = 1 << zoom;
834 if (x < 0 || x >= max || y < 0 || y >= max)
835 return null;
836 return tileCache.getTile(tileSource, x, y, zoom);
837 }
838
839 private boolean loadTile(Tile tile, boolean force) {
840 if (tile == null)
841 return false;
842 if (!force && (tile.isLoaded() || tile.hasError()))
843 return false;
844 if (tile.isLoading())
845 return false;
846 tileLoader.createTileLoaderJob(tile).submit(force);
847 return true;
848 }
849
850 private void loadAllTiles(boolean force) {
851 MapView mv = Main.map.mapView;
852 EastNorth topLeft = mv.getEastNorth(0, 0);
853 EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
854
855 TileSet ts = new TileSet(topLeft, botRight, currentZoomLevel);
856
857 // if there is more than 18 tiles on screen in any direction, do not
858 // load all tiles!
859 if (ts.tooLarge()) {
860 Main.warn("Not downloading all tiles because there is more than 18 tiles on an axis!");
861 return;
862 }
863 ts.loadAllTiles(force);
864 }
865
866 private void loadAllErrorTiles(boolean force) {
867 MapView mv = Main.map.mapView;
868 EastNorth topLeft = mv.getEastNorth(0, 0);
869 EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
870
871 TileSet ts = new TileSet(topLeft, botRight, currentZoomLevel);
872
873 ts.loadAllErrorTiles(force);
874 }
875
876 @Override
877 public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
878 boolean done = (infoflags & (ERROR | FRAMEBITS | ALLBITS)) != 0;
879 needRedraw = true;
880 if (Main.isDebugEnabled()) {
881 Main.debug("imageUpdate() done: " + done + " calling repaint");
882 }
883 Main.map.repaint(done ? 0 : 100);
884 return !done;
885 }
886
887 private boolean imageLoaded(Image i) {
888 if (i == null)
889 return false;
890 int status = Toolkit.getDefaultToolkit().checkImage(i, -1, -1, this);
891 if ((status & ALLBITS) != 0)
892 return true;
893 return false;
894 }
895
896 /**
897 * Returns the image for the given tile if both tile and image are loaded.
898 * Otherwise returns null.
899 *
900 * @param tile the Tile for which the image should be returned
901 * @return the image of the tile or null.
902 */
903 private Image getLoadedTileImage(Tile tile) {
904 if (!tile.isLoaded())
905 return null;
906 Image img = tile.getImage();
907 if (!imageLoaded(img))
908 return null;
909 return img;
910 }
911
912 private LatLon tileLatLon(Tile t) {
913 int zoom = t.getZoom();
914 return new LatLon(tileSource.tileYToLat(t.getYtile(), zoom),
915 tileSource.tileXToLon(t.getXtile(), zoom));
916 }
917
918 private Rectangle tileToRect(Tile t1) {
919 /*
920 * We need to get a box in which to draw, so advance by one tile in
921 * each direction to find the other corner of the box.
922 * Note: this somewhat pollutes the tile cache
923 */
924 Tile t2 = tempCornerTile(t1);
925 Rectangle rect = new Rectangle(pixelPos(t1));
926 rect.add(pixelPos(t2));
927 return rect;
928 }
929
930 // 'source' is the pixel coordinates for the area that
931 // the img is capable of filling in. However, we probably
932 // only want a portion of it.
933 //
934 // 'border' is the screen cordinates that need to be drawn.
935 // We must not draw outside of it.
936 private void drawImageInside(Graphics g, Image sourceImg, Rectangle source, Rectangle border) {
937 Rectangle target = source;
938
939 // If a border is specified, only draw the intersection
940 // if what we have combined with what we are supposed
941 // to draw.
942 if (border != null) {
943 target = source.intersection(border);
944 if (Main.isDebugEnabled()) {
945 Main.debug("source: " + source + "\nborder: " + border + "\nintersection: " + target);
946 }
947 }
948
949 // All of the rectangles are in screen coordinates. We need
950 // to how these correlate to the sourceImg pixels. We could
951 // avoid doing this by scaling the image up to the 'source' size,
952 // but this should be cheaper.
953 //
954 // In some projections, x any y are scaled differently enough to
955 // cause a pixel or two of fudge. Calculate them separately.
956 double imageYScaling = sourceImg.getHeight(this) / source.getHeight();
957 double imageXScaling = sourceImg.getWidth(this) / source.getWidth();
958
959 // How many pixels into the 'source' rectangle are we drawing?
960 int screen_x_offset = target.x - source.x;
961 int screen_y_offset = target.y - source.y;
962 // And how many pixels into the image itself does that
963 // correlate to?
964 int img_x_offset = (int)(screen_x_offset * imageXScaling + 0.5);
965 int img_y_offset = (int)(screen_y_offset * imageYScaling + 0.5);
966 // Now calculate the other corner of the image that we need
967 // by scaling the 'target' rectangle's dimensions.
968 int img_x_end = img_x_offset + (int)(target.getWidth() * imageXScaling + 0.5);
969 int img_y_end = img_y_offset + (int)(target.getHeight() * imageYScaling + 0.5);
970
971 if (Main.isDebugEnabled()) {
972 Main.debug("drawing image into target rect: " + target);
973 }
974 g.drawImage(sourceImg,
975 target.x, target.y,
976 target.x + target.width, target.y + target.height,
977 img_x_offset, img_y_offset,
978 img_x_end, img_y_end,
979 this);
980 if (PROP_FADE_AMOUNT.get() != 0) {
981 // dimm by painting opaque rect...
982 g.setColor(getFadeColorWithAlpha());
983 g.fillRect(target.x, target.y,
984 target.width, target.height);
985 }
986 }
987
988 // This function is called for several zoom levels, not just
989 // the current one. It should not trigger any tiles to be
990 // downloaded. It should also avoid polluting the tile cache
991 // with any tiles since these tiles are not mandatory.
992 //
993 // The "border" tile tells us the boundaries of where we may
994 // draw. It will not be from the zoom level that is being
995 // drawn currently. If drawing the displayZoomLevel,
996 // border is null and we draw the entire tile set.
997 private List<Tile> paintTileImages(Graphics g, TileSet ts, int zoom, Tile border) {
998 if (zoom <= 0) return Collections.emptyList();
999 Rectangle borderRect = null;
1000 if (border != null) {
1001 borderRect = tileToRect(border);
1002 }
1003 List<Tile> missedTiles = new LinkedList<>();
1004 // The callers of this code *require* that we return any tiles
1005 // that we do not draw in missedTiles. ts.allExistingTiles() by
1006 // default will only return already-existing tiles. However, we
1007 // need to return *all* tiles to the callers, so force creation
1008 // here.
1009 //boolean forceTileCreation = true;
1010 for (Tile tile : ts.allTilesCreate()) {
1011 Image img = getLoadedTileImage(tile);
1012 if (img == null || tile.hasError()) {
1013 if (Main.isDebugEnabled()) {
1014 Main.debug("missed tile: " + tile);
1015 }
1016 missedTiles.add(tile);
1017 continue;
1018 }
1019 Rectangle sourceRect = tileToRect(tile);
1020 if (borderRect != null && !sourceRect.intersects(borderRect)) {
1021 continue;
1022 }
1023 drawImageInside(g, img, sourceRect, borderRect);
1024 }
1025 return missedTiles;
1026 }
1027
1028 private void myDrawString(Graphics g, String text, int x, int y) {
1029 Color oldColor = g.getColor();
1030 g.setColor(Color.black);
1031 g.drawString(text,x+1,y+1);
1032 g.setColor(oldColor);
1033 g.drawString(text,x,y);
1034 }
1035
1036 private void paintTileText(TileSet ts, Tile tile, Graphics g, MapView mv, int zoom, Tile t) {
1037 int fontHeight = g.getFontMetrics().getHeight();
1038 if (tile == null)
1039 return;
1040 Point p = pixelPos(t);
1041 int texty = p.y + 2 + fontHeight;
1042
1043 /*if (PROP_DRAW_DEBUG.get()) {
1044 myDrawString(g, "x=" + t.getXtile() + " y=" + t.getYtile() + " z=" + zoom + "", p.x + 2, texty);
1045 texty += 1 + fontHeight;
1046 if ((t.getXtile() % 32 == 0) && (t.getYtile() % 32 == 0)) {
1047 myDrawString(g, "x=" + t.getXtile() / 32 + " y=" + t.getYtile() / 32 + " z=7", p.x + 2, texty);
1048 texty += 1 + fontHeight;
1049 }
1050 }*/
1051
1052 if (tile == showMetadataTile) {
1053 String md = tile.toString();
1054 if (md != null) {
1055 myDrawString(g, md, p.x + 2, texty);
1056 texty += 1 + fontHeight;
1057 }
1058 Map<String, String> meta = tile.getMetadata();
1059 if (meta != null) {
1060 for (Map.Entry<String, String> entry : meta.entrySet()) {
1061 myDrawString(g, entry.getKey() + ": " + entry.getValue(), p.x + 2, texty);
1062 texty += 1 + fontHeight;
1063 }
1064 }
1065 }
1066
1067 /*String tileStatus = tile.getStatus();
1068 if (!tile.isLoaded() && PROP_DRAW_DEBUG.get()) {
1069 myDrawString(g, tr("image " + tileStatus), p.x + 2, texty);
1070 texty += 1 + fontHeight;
1071 }*/
1072
1073 if (tile.hasError() && showErrors) {
1074 myDrawString(g, tr("Error") + ": " + tr(tile.getErrorMessage()), p.x + 2, texty);
1075 texty += 1 + fontHeight;
1076 }
1077
1078 /*int xCursor = -1;
1079 int yCursor = -1;
1080 if (PROP_DRAW_DEBUG.get()) {
1081 if (yCursor < t.getYtile()) {
1082 if (t.getYtile() % 32 == 31) {
1083 g.fillRect(0, p.y - 1, mv.getWidth(), 3);
1084 } else {
1085 g.drawLine(0, p.y, mv.getWidth(), p.y);
1086 }
1087 yCursor = t.getYtile();
1088 }
1089 // This draws the vertical lines for the entire
1090 // column. Only draw them for the top tile in
1091 // the column.
1092 if (xCursor < t.getXtile()) {
1093 if (t.getXtile() % 32 == 0) {
1094 // level 7 tile boundary
1095 g.fillRect(p.x - 1, 0, 3, mv.getHeight());
1096 } else {
1097 g.drawLine(p.x, 0, p.x, mv.getHeight());
1098 }
1099 xCursor = t.getXtile();
1100 }
1101 }*/
1102 }
1103
1104 private Point pixelPos(LatLon ll) {
1105 return Main.map.mapView.getPoint(Main.getProjection().latlon2eastNorth(ll).add(getDx(), getDy()));
1106 }
1107
1108 private Point pixelPos(Tile t) {
1109 double lon = tileSource.tileXToLon(t.getXtile(), t.getZoom());
1110 LatLon tmpLL = new LatLon(tileSource.tileYToLat(t.getYtile(), t.getZoom()), lon);
1111 return pixelPos(tmpLL);
1112 }
1113
1114 private LatLon getShiftedLatLon(EastNorth en) {
1115 return Main.getProjection().eastNorth2latlon(en.add(-getDx(), -getDy()));
1116 }
1117
1118 private Coordinate getShiftedCoord(EastNorth en) {
1119 LatLon ll = getShiftedLatLon(en);
1120 return new Coordinate(ll.lat(),ll.lon());
1121 }
1122
1123 private final TileSet nullTileSet = new TileSet((LatLon)null, (LatLon)null, 0);
1124 private final class TileSet {
1125 private int x0, x1, y0, y1;
1126 private int zoom;
1127 private int tileMax = -1;
1128
1129 /**
1130 * Create a TileSet by EastNorth bbox taking a layer shift in account
1131 */
1132 private TileSet(EastNorth topLeft, EastNorth botRight, int zoom) {
1133 this(getShiftedLatLon(topLeft), getShiftedLatLon(botRight),zoom);
1134 }
1135
1136 /**
1137 * Create a TileSet by known LatLon bbox without layer shift correction
1138 */
1139 private TileSet(LatLon topLeft, LatLon botRight, int zoom) {
1140 this.zoom = zoom;
1141 if (zoom == 0)
1142 return;
1143
1144 x0 = (int)tileSource.lonToTileX(topLeft.lon(), zoom);
1145 y0 = (int)tileSource.latToTileY(topLeft.lat(), zoom);
1146 x1 = (int)tileSource.lonToTileX(botRight.lon(), zoom);
1147 y1 = (int)tileSource.latToTileY(botRight.lat(), zoom);
1148 if (x0 > x1) {
1149 int tmp = x0;
1150 x0 = x1;
1151 x1 = tmp;
1152 }
1153 if (y0 > y1) {
1154 int tmp = y0;
1155 y0 = y1;
1156 y1 = tmp;
1157 }
1158 tileMax = (int)Math.pow(2.0, zoom);
1159 if (x0 < 0) {
1160 x0 = 0;
1161 }
1162 if (y0 < 0) {
1163 y0 = 0;
1164 }
1165 if (x1 > tileMax) {
1166 x1 = tileMax;
1167 }
1168 if (y1 > tileMax) {
1169 y1 = tileMax;
1170 }
1171 }
1172
1173 private boolean tooSmall() {
1174 return this.tilesSpanned() < 2.1;
1175 }
1176
1177 private boolean tooLarge() {
1178 return this.tilesSpanned() > 10;
1179 }
1180
1181 private boolean insane() {
1182 return this.tilesSpanned() > 100;
1183 }
1184
1185 private double tilesSpanned() {
1186 return Math.sqrt(1.0 * this.size());
1187 }
1188
1189 private int size() {
1190 int x_span = x1 - x0 + 1;
1191 int y_span = y1 - y0 + 1;
1192 return x_span * y_span;
1193 }
1194
1195 /*
1196 * Get all tiles represented by this TileSet that are
1197 * already in the tileCache.
1198 */
1199 private List<Tile> allExistingTiles() {
1200 return this.__allTiles(false);
1201 }
1202
1203 private List<Tile> allTilesCreate() {
1204 return this.__allTiles(true);
1205 }
1206
1207 private List<Tile> __allTiles(boolean create) {
1208 // Tileset is either empty or too large
1209 if (zoom == 0 || this.insane())
1210 return Collections.emptyList();
1211 List<Tile> ret = new ArrayList<>();
1212 for (int x = x0; x <= x1; x++) {
1213 for (int y = y0; y <= y1; y++) {
1214 Tile t;
1215 if (create) {
1216 t = getOrCreateTile(x % tileMax, y % tileMax, zoom);
1217 } else {
1218 t = getTile(x % tileMax, y % tileMax, zoom);
1219 }
1220 if (t != null) {
1221 ret.add(t);
1222 }
1223 }
1224 }
1225 return ret;
1226 }
1227
1228 private List<Tile> allLoadedTiles() {
1229 List<Tile> ret = new ArrayList<>();
1230 for (Tile t : this.allExistingTiles()) {
1231 if (t.isLoaded())
1232 ret.add(t);
1233 }
1234 return ret;
1235 }
1236
1237 private Comparator<Tile> getTileDistanceComparator() {
1238 final int centerX = (int) Math.ceil((x0 + x1) / 2);
1239 final int centerY = (int) Math.ceil((y0 + y1) / 2);
1240 return new Comparator<Tile>() {
1241 private int getDistance(Tile t) {
1242 return Math.abs(t.getXtile() - centerX) + Math.abs(t.getYtile() - centerY);
1243 }
1244 @Override
1245 public int compare(Tile o1, Tile o2) {
1246 int distance1 = getDistance(o1);
1247 int distance2 = getDistance(o2);
1248 return Integer.compare(distance1, distance2);
1249 }
1250 };
1251 }
1252
1253 private void loadAllTiles(boolean force) {
1254 if (!autoLoad && !force)
1255 return;
1256 List<Tile> allTiles = allTilesCreate();
1257 Collections.sort(allTiles, getTileDistanceComparator());
1258 for (Tile t : allTiles) {
1259 loadTile(t, force);
1260 }
1261 }
1262
1263 private void loadAllErrorTiles(boolean force) {
1264 if (!autoLoad && !force)
1265 return;
1266 for (Tile t : this.allTilesCreate()) {
1267 if (t.hasError()) {
1268 loadTile(t, true);
1269 }
1270 }
1271 }
1272 }
1273
1274
1275 private static class TileSetInfo {
1276 public boolean hasVisibleTiles = false;
1277 public boolean hasOverzoomedTiles = false;
1278 public boolean hasLoadingTiles = false;
1279 }
1280
1281 private static TileSetInfo getTileSetInfo(TileSet ts) {
1282 List<Tile> allTiles = ts.allExistingTiles();
1283 TileSetInfo result = new TileSetInfo();
1284 result.hasLoadingTiles = allTiles.size() < ts.size();
1285 for (Tile t : allTiles) {
1286 if (t.isLoaded()) {
1287 if (!t.hasError()) {
1288 result.hasVisibleTiles = true;
1289 }
1290 if ("no-tile".equals(t.getValue("tile-info"))) {
1291 result.hasOverzoomedTiles = true;
1292 }
1293 } else {
1294 result.hasLoadingTiles = true;
1295 }
1296 }
1297 return result;
1298 }
1299
1300 private class DeepTileSet {
1301 private final EastNorth topLeft, botRight;
1302 private final int minZoom, maxZoom;
1303 private final TileSet[] tileSets;
1304 private final TileSetInfo[] tileSetInfos;
1305 public DeepTileSet(EastNorth topLeft, EastNorth botRight, int minZoom, int maxZoom) {
1306 this.topLeft = topLeft;
1307 this.botRight = botRight;
1308 this.minZoom = minZoom;
1309 this.maxZoom = maxZoom;
1310 this.tileSets = new TileSet[maxZoom - minZoom + 1];
1311 this.tileSetInfos = new TileSetInfo[maxZoom - minZoom + 1];
1312 }
1313 public TileSet getTileSet(int zoom) {
1314 if (zoom < minZoom)
1315 return nullTileSet;
1316 synchronized (tileSets) {
1317 TileSet ts = tileSets[zoom-minZoom];
1318 if (ts == null) {
1319 ts = new TileSet(topLeft, botRight, zoom);
1320 tileSets[zoom-minZoom] = ts;
1321 }
1322 return ts;
1323 }
1324 }
1325
1326 public TileSetInfo getTileSetInfo(int zoom) {
1327 if (zoom < minZoom)
1328 return new TileSetInfo();
1329 synchronized (tileSetInfos) {
1330 TileSetInfo tsi = tileSetInfos[zoom-minZoom];
1331 if (tsi == null) {
1332 tsi = TMSLayer.getTileSetInfo(getTileSet(zoom));
1333 tileSetInfos[zoom-minZoom] = tsi;
1334 }
1335 return tsi;
1336 }
1337 }
1338 }
1339
1340 @Override
1341 public void paint(Graphics2D g, MapView mv, Bounds bounds) {
1342 EastNorth topLeft = mv.getEastNorth(0, 0);
1343 EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
1344
1345 if (botRight.east() == 0 || botRight.north() == 0) {
1346 /*Main.debug("still initializing??");*/
1347 // probably still initializing
1348 return;
1349 }
1350
1351 needRedraw = false;
1352
1353 int zoom = currentZoomLevel;
1354 if (autoZoom) {
1355 double pixelScaling = getScaleFactor(zoom);
1356 if (pixelScaling > 3 || pixelScaling < 0.7) {
1357 zoom = getBestZoom();
1358 }
1359 }
1360
1361 DeepTileSet dts = new DeepTileSet(topLeft, botRight, getMinZoomLvl(), zoom);
1362 TileSet ts = dts.getTileSet(zoom);
1363
1364 int displayZoomLevel = zoom;
1365
1366 boolean noTilesAtZoom = false;
1367 if (autoZoom && autoLoad) {
1368 // Auto-detection of tilesource maxzoom (currently fully works only for Bing)
1369 TileSetInfo tsi = dts.getTileSetInfo(zoom);
1370 if (!tsi.hasVisibleTiles && (!tsi.hasLoadingTiles || tsi.hasOverzoomedTiles)) {
1371 noTilesAtZoom = true;
1372 }
1373 // Find highest zoom level with at least one visible tile
1374 for (int tmpZoom = zoom; tmpZoom > dts.minZoom; tmpZoom--) {
1375 if (dts.getTileSetInfo(tmpZoom).hasVisibleTiles) {
1376 displayZoomLevel = tmpZoom;
1377 break;
1378 }
1379 }
1380 // Do binary search between currentZoomLevel and displayZoomLevel
1381 while (zoom > displayZoomLevel && !tsi.hasVisibleTiles && tsi.hasOverzoomedTiles){
1382 zoom = (zoom + displayZoomLevel)/2;
1383 tsi = dts.getTileSetInfo(zoom);
1384 }
1385
1386 setZoomLevel(zoom);
1387
1388 // If all tiles at displayZoomLevel is loaded, load all tiles at next zoom level
1389 // to make sure there're really no more zoom levels
1390 if (zoom == displayZoomLevel && !tsi.hasLoadingTiles && zoom < dts.maxZoom) {
1391 zoom++;
1392 tsi = dts.getTileSetInfo(zoom);
1393 }
1394 // When we have overzoomed tiles and all tiles at current zoomlevel is loaded,
1395 // load tiles at previovus zoomlevels until we have all tiles on screen is loaded.
1396 while (zoom > dts.minZoom && tsi.hasOverzoomedTiles && !tsi.hasLoadingTiles) {
1397 zoom--;
1398 tsi = dts.getTileSetInfo(zoom);
1399 }
1400 ts = dts.getTileSet(zoom);
1401 } else if (autoZoom) {
1402 setZoomLevel(zoom);
1403 }
1404
1405 // Too many tiles... refuse to download
1406 if (!ts.tooLarge()) {
1407 //Main.debug("size: " + ts.size() + " spanned: " + ts.tilesSpanned());
1408 ts.loadAllTiles(false);
1409 }
1410
1411 if (displayZoomLevel != zoom) {
1412 ts = dts.getTileSet(displayZoomLevel);
1413 }
1414
1415 g.setColor(Color.DARK_GRAY);
1416
1417 List<Tile> missedTiles = this.paintTileImages(g, ts, displayZoomLevel, null);
1418 int[] otherZooms = {-1, 1, -2, 2, -3, -4, -5};
1419 for (int zoomOffset : otherZooms) {
1420 if (!autoZoom) {
1421 break;
1422 }
1423 int newzoom = displayZoomLevel + zoomOffset;
1424 if (newzoom < MIN_ZOOM) {
1425 continue;
1426 }
1427 if (missedTiles.isEmpty()) {
1428 break;
1429 }
1430 List<Tile> newlyMissedTiles = new LinkedList<>();
1431 for (Tile missed : missedTiles) {
1432 if ("no-tile".equals(missed.getValue("tile-info")) && zoomOffset > 0) {
1433 // Don't try to paint from higher zoom levels when tile is overzoomed
1434 newlyMissedTiles.add(missed);
1435 continue;
1436 }
1437 Tile t2 = tempCornerTile(missed);
1438 LatLon topLeft2 = tileLatLon(missed);
1439 LatLon botRight2 = tileLatLon(t2);
1440 TileSet ts2 = new TileSet(topLeft2, botRight2, newzoom);
1441 // Instantiating large TileSets is expensive. If there
1442 // are no loaded tiles, don't bother even trying.
1443 if (ts2.allLoadedTiles().isEmpty()) {
1444 newlyMissedTiles.add(missed);
1445 continue;
1446 }
1447 if (ts2.tooLarge()) {
1448 continue;
1449 }
1450 newlyMissedTiles.addAll(this.paintTileImages(g, ts2, newzoom, missed));
1451 }
1452 missedTiles = newlyMissedTiles;
1453 }
1454 if (Main.isDebugEnabled() && !missedTiles.isEmpty()) {
1455 Main.debug("still missed "+missedTiles.size()+" in the end");
1456 }
1457 g.setColor(Color.red);
1458 g.setFont(InfoFont);
1459
1460 // The current zoom tileset should have all of its tiles
1461 // due to the loadAllTiles(), unless it to tooLarge()
1462 for (Tile t : ts.allExistingTiles()) {
1463 this.paintTileText(ts, t, g, mv, displayZoomLevel, t);
1464 }
1465
1466 attribution.paintAttribution(g, mv.getWidth(), mv.getHeight(), getShiftedCoord(topLeft), getShiftedCoord(botRight), displayZoomLevel, this);
1467
1468 //g.drawString("currentZoomLevel=" + currentZoomLevel, 120, 120);
1469 g.setColor(Color.lightGray);
1470 if (!autoZoom) {
1471 if (ts.insane()) {
1472 myDrawString(g, tr("zoom in to load any tiles"), 120, 120);
1473 } else if (ts.tooLarge()) {
1474 myDrawString(g, tr("zoom in to load more tiles"), 120, 120);
1475 } else if (ts.tooSmall()) {
1476 myDrawString(g, tr("increase zoom level to see more detail"), 120, 120);
1477 }
1478 }
1479 if (noTilesAtZoom) {
1480 myDrawString(g, tr("No tiles at this zoom level"), 120, 120);
1481 }
1482 if (Main.isDebugEnabled()) {
1483 myDrawString(g, tr("Current zoom: {0}", currentZoomLevel), 50, 140);
1484 myDrawString(g, tr("Display zoom: {0}", displayZoomLevel), 50, 155);
1485 myDrawString(g, tr("Pixel scale: {0}", getScaleFactor(currentZoomLevel)), 50, 170);
1486 myDrawString(g, tr("Best zoom: {0}", getBestZoom()), 50, 185);
1487 if(tileLoader instanceof TMSCachedTileLoader) {
1488 TMSCachedTileLoader cachedTileLoader = (TMSCachedTileLoader)tileLoader;
1489 int offset = 185;
1490 for(String part: cachedTileLoader.getStats().split("\n")) {
1491 myDrawString(g, tr("Cache stats: {0}", part), 50, offset+=15);
1492 }
1493
1494 }
1495 }
1496 }
1497
1498 /**
1499 * This isn't very efficient, but it is only used when the
1500 * user right-clicks on the map.
1501 */
1502 private Tile getTileForPixelpos(int px, int py) {
1503 if (Main.isDebugEnabled()) {
1504 Main.debug("getTileForPixelpos("+px+", "+py+")");
1505 }
1506 MapView mv = Main.map.mapView;
1507 Point clicked = new Point(px, py);
1508 EastNorth topLeft = mv.getEastNorth(0, 0);
1509 EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
1510 int z = currentZoomLevel;
1511 TileSet ts = new TileSet(topLeft, botRight, z);
1512
1513 if (!ts.tooLarge()) {
1514 ts.loadAllTiles(false); // make sure there are tile objects for all tiles
1515 }
1516 Tile clickedTile = null;
1517 for (Tile t1 : ts.allExistingTiles()) {
1518 Tile t2 = tempCornerTile(t1);
1519 Rectangle r = new Rectangle(pixelPos(t1));
1520 r.add(pixelPos(t2));
1521 if (Main.isDebugEnabled()) {
1522 Main.debug("r: " + r + " clicked: " + clicked);
1523 }
1524 if (!r.contains(clicked)) {
1525 continue;
1526 }
1527 clickedTile = t1;
1528 break;
1529 }
1530 if (clickedTile == null)
1531 return null;
1532 /*Main.debug("Clicked on tile: " + clickedTile.getXtile() + " " + clickedTile.getYtile() +
1533 " currentZoomLevel: " + currentZoomLevel);*/
1534 return clickedTile;
1535 }
1536
1537 @Override
1538 public Action[] getMenuEntries() {
1539 return new Action[] {
1540 LayerListDialog.getInstance().createShowHideLayerAction(),
1541 LayerListDialog.getInstance().createDeleteLayerAction(),
1542 SeparatorLayerAction.INSTANCE,
1543 // color,
1544 new OffsetAction(),
1545 new RenameLayerAction(this.getAssociatedFile(), this),
1546 SeparatorLayerAction.INSTANCE,
1547 new LayerListPopup.InfoAction(this) };
1548 }
1549
1550 @Override
1551 public String getToolTipText() {
1552 return tr("TMS layer ({0}), downloading in zoom {1}", getName(), currentZoomLevel);
1553 }
1554
1555 @Override
1556 public void visitBoundingBox(BoundingXYVisitor v) {
1557 }
1558
1559 @Override
1560 public boolean isChanged() {
1561 return needRedraw;
1562 }
1563
1564 @Override
1565 public final boolean isProjectionSupported(Projection proj) {
1566 return "EPSG:3857".equals(proj.toCode()) || "EPSG:4326".equals(proj.toCode());
1567 }
1568
1569 @Override
1570 public final String nameSupportedProjections() {
1571 return tr("EPSG:4326 and Mercator projection are supported");
1572 }
1573}
Note: See TracBrowser for help on using the repository browser.