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

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

code style - Useless parentheses around expressions should be removed to prevent any misunderstanding

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