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

Last change on this file since 3787 was 3787, checked in by Upliner, 13 years ago

Another rework of autozoom algorithm, fixes flicker when zooming in.

Also uses binary search of neccecary zoomlevel when downloading instead of linear one.
Has less code, more comments and more comprehensible at all.

  • Property svn:eol-style set to native
File size: 46.9 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.font.TextAttribute;
18import java.awt.geom.Rectangle2D;
19import java.awt.image.ImageObserver;
20import java.io.File;
21import java.io.IOException;
22import java.net.URI;
23import java.net.URISyntaxException;
24import java.util.ArrayList;
25import java.util.Collections;
26import java.util.HashMap;
27import java.util.HashSet;
28import java.util.LinkedList;
29import java.util.List;
30import java.util.Map;
31
32import javax.swing.AbstractAction;
33import javax.swing.Action;
34import javax.swing.JCheckBoxMenuItem;
35import javax.swing.JMenuItem;
36import javax.swing.JPopupMenu;
37import javax.swing.SwingUtilities;
38
39import org.openstreetmap.gui.jmapviewer.BingAerialTileSource;
40import org.openstreetmap.gui.jmapviewer.Coordinate;
41import org.openstreetmap.gui.jmapviewer.JobDispatcher;
42import org.openstreetmap.gui.jmapviewer.MemoryTileCache;
43import org.openstreetmap.gui.jmapviewer.OsmFileCacheTileLoader;
44import org.openstreetmap.gui.jmapviewer.OsmTileLoader;
45import org.openstreetmap.gui.jmapviewer.TMSTileSource;
46import org.openstreetmap.gui.jmapviewer.TemplatedTMSTileSource;
47import org.openstreetmap.gui.jmapviewer.Tile;
48import org.openstreetmap.gui.jmapviewer.interfaces.TileCache;
49import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
50import org.openstreetmap.gui.jmapviewer.interfaces.TileLoaderListener;
51import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
52import org.openstreetmap.josm.Main;
53import org.openstreetmap.josm.actions.RenameLayerAction;
54import org.openstreetmap.josm.data.Bounds;
55import org.openstreetmap.josm.data.coor.EastNorth;
56import org.openstreetmap.josm.data.coor.LatLon;
57import org.openstreetmap.josm.data.imagery.ImageryInfo;
58import org.openstreetmap.josm.data.imagery.ImageryInfo.ImageryType;
59import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
60import org.openstreetmap.josm.data.preferences.BooleanProperty;
61import org.openstreetmap.josm.data.preferences.IntegerProperty;
62import org.openstreetmap.josm.data.preferences.StringProperty;
63import org.openstreetmap.josm.gui.MapView;
64import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
65import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
66import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
67
68/**
69 * Class that displays a slippy map layer.
70 *
71 * @author Frederik Ramm <frederik@remote.org>
72 * @author LuVar <lubomir.varga@freemap.sk>
73 * @author Dave Hansen <dave@sr71.net>
74 * @author Upliner <upliner@gmail.com>
75 *
76 */
77public class TMSLayer extends ImageryLayer implements ImageObserver, TileLoaderListener {
78 public static final String PREFERENCE_PREFIX = "imagery.tms";
79
80 public static final int MAX_ZOOM = 30;
81 public static final int MIN_ZOOM = 2;
82 public static final int DEFAULT_MAX_ZOOM = 20;
83 public static final int DEFAULT_MIN_ZOOM = 2;
84
85 public static final BooleanProperty PROP_DEFAULT_AUTOZOOM = new BooleanProperty(PREFERENCE_PREFIX + ".default_autozoom", true);
86 public static final BooleanProperty PROP_DEFAULT_AUTOLOAD = new BooleanProperty(PREFERENCE_PREFIX + ".default_autoload", true);
87 public static final IntegerProperty PROP_MIN_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".min_zoom_lvl", DEFAULT_MIN_ZOOM);
88 public static final IntegerProperty PROP_MAX_ZOOM_LVL = new IntegerProperty(PREFERENCE_PREFIX + ".max_zoom_lvl", DEFAULT_MAX_ZOOM);
89 public static final BooleanProperty PROP_DRAW_DEBUG = new BooleanProperty(PREFERENCE_PREFIX + ".draw_debug", false);
90 public static final BooleanProperty PROP_ADD_TO_SLIPPYMAP_CHOOSER = new BooleanProperty(PREFERENCE_PREFIX + ".add_to_slippymap_chooser", true);
91 public static final StringProperty PROP_TILECACHE_DIR;
92
93 static {
94 String defPath = null;
95 try {
96 defPath = OsmFileCacheTileLoader.getDefaultCacheDir().getAbsolutePath();
97 } catch (SecurityException e) {
98 }
99 PROP_TILECACHE_DIR = new StringProperty(PREFERENCE_PREFIX + ".tilecache_path", defPath);
100 }
101
102 boolean debug = false;
103 void out(String s)
104 {
105 Main.debug(s);
106 }
107
108 protected MemoryTileCache tileCache;
109 protected TileSource tileSource;
110 protected TileLoader tileLoader;
111 JobDispatcher jobDispatcher = JobDispatcher.getInstance();
112
113 HashSet<Tile> tileRequestsOutstanding = new HashSet<Tile>();
114 @Override
115 public synchronized void tileLoadingFinished(Tile tile, boolean success)
116 {
117 if (tile.hasError()) {
118 success = false;
119 tile.setImage(null);
120 }
121 if (sharpenLevel != 0 && success) {
122 tile.setImage(sharpenImage(tile.getImage()));
123 }
124 tile.setLoaded(true);
125 needRedraw = true;
126 Main.map.repaint(100);
127 tileRequestsOutstanding.remove(tile);
128 if (debug) {
129 out("tileLoadingFinished() tile: " + tile + " success: " + success);
130 }
131 }
132 @Override
133 public TileCache getTileCache()
134 {
135 return tileCache;
136 }
137 void clearTileCache()
138 {
139 if (debug) {
140 out("clearing tile storage");
141 }
142 tileCache = new MemoryTileCache();
143 tileCache.setCacheSize(200);
144 }
145
146 /**
147 * Zoomlevel at which tiles is currently downloaded.
148 * Initial zoom lvl is set to bestZoom
149 */
150 public int currentZoomLevel;
151
152 private Tile clickedTile;
153 private boolean needRedraw;
154 private JPopupMenu tileOptionMenu;
155 JCheckBoxMenuItem autoZoomPopup;
156 JCheckBoxMenuItem autoLoadPopup;
157 Tile showMetadataTile;
158 private Image attrImage;
159 private String attrTermsUrl;
160 private Rectangle attrImageBounds, attrToUBounds;
161 private static final Font InfoFont = new Font("sansserif", Font.BOLD, 13);
162 private static final Font ATTR_FONT = new Font("Arial", Font.PLAIN, 10);
163 private static final Font ATTR_LINK_FONT;
164 static {
165 HashMap<TextAttribute, Integer> aUnderline = new HashMap<TextAttribute, Integer>();
166 aUnderline.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON);
167 ATTR_LINK_FONT = ATTR_FONT.deriveFont(aUnderline);
168 }
169
170 protected boolean autoZoom;
171 protected boolean autoLoad;
172
173 void redraw()
174 {
175 needRedraw = true;
176 Main.map.repaint();
177 }
178
179 static int checkMaxZoomLvl(int maxZoomLvl, TileSource ts)
180 {
181 if(maxZoomLvl > MAX_ZOOM) {
182 System.err.println("MaxZoomLvl shouldnt be more than 30! Setting to 30.");
183 maxZoomLvl = MAX_ZOOM;
184 }
185 if(maxZoomLvl < PROP_MIN_ZOOM_LVL.get()) {
186 System.err.println("maxZoomLvl shouldnt be more than minZoomLvl! Setting to minZoomLvl.");
187 maxZoomLvl = PROP_MIN_ZOOM_LVL.get();
188 }
189 if (ts != null && ts.getMaxZoom() != 0 && ts.getMaxZoom() < maxZoomLvl) {
190 maxZoomLvl = ts.getMaxZoom();
191 }
192 return maxZoomLvl;
193 }
194
195 public static int getMaxZoomLvl(TileSource ts)
196 {
197 return checkMaxZoomLvl(PROP_MAX_ZOOM_LVL.get(), ts);
198 }
199
200 public static void setMaxZoomLvl(int maxZoomLvl) {
201 maxZoomLvl = checkMaxZoomLvl(maxZoomLvl, null);
202 PROP_MAX_ZOOM_LVL.put(maxZoomLvl);
203 }
204
205 static int checkMinZoomLvl(int minZoomLvl, TileSource ts)
206 {
207 if(minZoomLvl < MIN_ZOOM) {
208 System.err.println("minZoomLvl shouldnt be lees than "+MIN_ZOOM+"! Setting to that.");
209 minZoomLvl = MIN_ZOOM;
210 }
211 if(minZoomLvl > PROP_MAX_ZOOM_LVL.get()) {
212 System.err.println("minZoomLvl shouldnt be more than maxZoomLvl! Setting to maxZoomLvl.");
213 minZoomLvl = getMaxZoomLvl(ts);
214 }
215 if (ts != null && ts.getMinZoom() > minZoomLvl) {
216 System.err.println("increasomg minZoomLvl to match tile source");
217 minZoomLvl = ts.getMinZoom();
218 }
219 return minZoomLvl;
220 }
221
222 public static int getMinZoomLvl(TileSource ts)
223 {
224 return checkMinZoomLvl(PROP_MIN_ZOOM_LVL.get(), ts);
225 }
226
227 public static void setMinZoomLvl(int minZoomLvl) {
228 minZoomLvl = checkMinZoomLvl(minZoomLvl, null);
229 PROP_MIN_ZOOM_LVL.put(minZoomLvl);
230 }
231
232 public static TileSource getTileSource(ImageryInfo info) {
233 if (info.getImageryType() == ImageryType.TMS) {
234 if(ImageryInfo.isUrlWithPatterns(info.getURL()))
235 return new TemplatedTMSTileSource(info.getName(), info.getURL(), info.getMaxZoom());
236 else
237 return new TMSTileSource(info.getName(),info.getURL(), info.getMaxZoom());
238 } else if (info.getImageryType() == ImageryType.BING)
239 return new BingAerialTileSource();
240 return null;
241 }
242
243 private void initTileSource(TileSource tileSource)
244 {
245 this.tileSource = tileSource;
246 boolean requireAttr = tileSource.requiresAttribution();
247 if(requireAttr) {
248 attrImage = tileSource.getAttributionImage();
249 if(attrImage == null) {
250 System.out.println("Attribution image was null.");
251 } else {
252 System.out.println("Got an attribution image " + attrImage.getHeight(this) + "x" + attrImage.getWidth(this));
253 }
254
255 attrTermsUrl = tileSource.getTermsOfUseURL();
256 }
257
258 currentZoomLevel = getBestZoom();
259
260 clearTileCache();
261 String cachePath = TMSLayer.PROP_TILECACHE_DIR.get();
262 tileLoader = null;
263 if (cachePath != null && !cachePath.isEmpty()) {
264 try {
265 tileLoader = new OsmFileCacheTileLoader(this, new File(cachePath));
266 } catch (IOException e) {
267 }
268 }
269 if (tileLoader == null) {
270 tileLoader = new OsmTileLoader(this);
271 }
272 }
273
274 @Override
275 public void setOffset(double dx, double dy) {
276 super.setOffset(dx, dy);
277 needRedraw = true;
278 }
279
280 /**
281 * Returns average number of screen pixels per tile pixel for current mapview
282 */
283 private double getScaleFactor(int zoom) {
284 if (Main.map == null || Main.map.mapView == null) return 1;
285 MapView mv = Main.map.mapView;
286 LatLon topLeft = mv.getLatLon(0, 0);
287 LatLon botRight = mv.getLatLon(mv.getWidth(), mv.getHeight());
288 double x1 = lonToTileX(topLeft.lon(), zoom);
289 double y1 = latToTileY(topLeft.lat(), zoom);
290 double x2 = lonToTileX(botRight.lon(), zoom);
291 double y2 = latToTileY(botRight.lat(), zoom);
292
293 int screenPixels = mv.getWidth()*mv.getHeight();
294 double tilePixels = Math.abs((y2-y1)*(x2-x1)*tileSource.getTileSize()*tileSource.getTileSize());
295 if (screenPixels == 0 || tilePixels == 0) return 1;
296 return screenPixels/tilePixels;
297 }
298
299 private int getBestZoom() {
300 double factor = getScaleFactor(1);
301 double result = Math.log(factor)/Math.log(2)/2+1;
302 int intResult = (int)Math.round(result);
303 if (intResult > getMaxZoomLvl())
304 return getMaxZoomLvl();
305 if (intResult < getMinZoomLvl())
306 return getMinZoomLvl();
307 return intResult;
308 }
309
310 @SuppressWarnings("serial")
311 public TMSLayer(ImageryInfo info) {
312 super(info);
313
314 setBackgroundLayer(true);
315 this.setVisible(true);
316
317 TileSource source = getTileSource(info);
318 if (source == null)
319 throw new IllegalStateException("cannot create TMSLayer with non-TMS ImageryInfo");
320 initTileSource(source);
321
322 tileOptionMenu = new JPopupMenu();
323
324 autoZoom = PROP_DEFAULT_AUTOZOOM.get();
325 autoZoomPopup = new JCheckBoxMenuItem();
326 autoZoomPopup.setAction(new AbstractAction(tr("Auto Zoom")) {
327 @Override
328 public void actionPerformed(ActionEvent ae) {
329 autoZoom = !autoZoom;
330 }
331 });
332 autoZoomPopup.setSelected(autoZoom);
333 tileOptionMenu.add(autoZoomPopup);
334
335 autoLoad = PROP_DEFAULT_AUTOLOAD.get();
336 autoLoadPopup = new JCheckBoxMenuItem();
337 autoLoadPopup.setAction(new AbstractAction(tr("Auto load tiles")) {
338 @Override
339 public void actionPerformed(ActionEvent ae) {
340 autoLoad= !autoLoad;
341 }
342 });
343 autoLoadPopup.setSelected(autoLoad);
344 tileOptionMenu.add(autoLoadPopup);
345
346 tileOptionMenu.add(new JMenuItem(new AbstractAction(tr("Load Tile")) {
347 @Override
348 public void actionPerformed(ActionEvent ae) {
349 if (clickedTile != null) {
350 loadTile(clickedTile);
351 redraw();
352 }
353 }
354 }));
355
356 tileOptionMenu.add(new JMenuItem(new AbstractAction(
357 tr("Show Tile Info")) {
358 @Override
359 public void actionPerformed(ActionEvent ae) {
360 out("info tile: " + clickedTile);
361 if (clickedTile != null) {
362 showMetadataTile = clickedTile;
363 redraw();
364 }
365 }
366 }));
367
368 /* FIXME
369 tileOptionMenu.add(new JMenuItem(new AbstractAction(
370 tr("Request Update")) {
371 public void actionPerformed(ActionEvent ae) {
372 if (clickedTile != null) {
373 clickedTile.requestUpdate();
374 redraw();
375 }
376 }
377 }));*/
378
379 tileOptionMenu.add(new JMenuItem(new AbstractAction(
380 tr("Load All Tiles")) {
381 @Override
382 public void actionPerformed(ActionEvent ae) {
383 loadAllTiles(true);
384 redraw();
385 }
386 }));
387
388 // increase and decrease commands
389 tileOptionMenu.add(new JMenuItem(
390 new AbstractAction(tr("Increase zoom")) {
391 @Override
392 public void actionPerformed(ActionEvent ae) {
393 increaseZoomLevel();
394 redraw();
395 }
396 }));
397
398 tileOptionMenu.add(new JMenuItem(
399 new AbstractAction(tr("Decrease zoom")) {
400 @Override
401 public void actionPerformed(ActionEvent ae) {
402 decreaseZoomLevel();
403 redraw();
404 }
405 }));
406
407 // FIXME: currently ran in errors
408
409 tileOptionMenu.add(new JMenuItem(
410 new AbstractAction(tr("Snap to tile size")) {
411 @Override
412 public void actionPerformed(ActionEvent ae) {
413 double new_factor = Math.sqrt(getScaleFactor(currentZoomLevel));
414 Main.map.mapView.zoomToFactor(new_factor);
415 redraw();
416 }
417 }));
418 // end of adding menu commands
419
420 tileOptionMenu.add(new JMenuItem(
421 new AbstractAction(tr("Flush Tile Cache")) {
422 @Override
423 public void actionPerformed(ActionEvent ae) {
424 System.out.print("flushing all tiles...");
425 clearTileCache();
426 System.out.println("done");
427 }
428 }));
429 // end of adding menu commands
430
431 SwingUtilities.invokeLater(new Runnable() {
432 @Override
433 public void run() {
434 Main.map.mapView.addMouseListener(new MouseAdapter() {
435 @Override
436 public void mouseClicked(MouseEvent e) {
437 if (e.getButton() == MouseEvent.BUTTON3) {
438 clickedTile = getTileForPixelpos(e.getX(), e.getY());
439 tileOptionMenu.show(e.getComponent(), e.getX(), e.getY());
440 } else if (e.getButton() == MouseEvent.BUTTON1) {
441 if(!tileSource.requiresAttribution())
442 return;
443
444 if(attrImageBounds.contains(e.getPoint())) {
445 try {
446 java.awt.Desktop desktop = java.awt.Desktop.getDesktop();
447 desktop.browse(new URI(tileSource.getAttributionLinkURL()));
448 } catch (IOException e1) {
449 e1.printStackTrace();
450 } catch (URISyntaxException e1) {
451 e1.printStackTrace();
452 }
453 } else if(attrToUBounds.contains(e.getPoint())) {
454 try {
455 java.awt.Desktop desktop = java.awt.Desktop.getDesktop();
456 desktop.browse(new URI(tileSource.getTermsOfUseURL()));
457 } catch (IOException e1) {
458 e1.printStackTrace();
459 } catch (URISyntaxException e1) {
460 e1.printStackTrace();
461 }
462 }
463 }
464 }
465 });
466
467 MapView.addLayerChangeListener(new LayerChangeListener() {
468 @Override
469 public void activeLayerChange(Layer oldLayer, Layer newLayer) {
470 //
471 }
472
473 @Override
474 public void layerAdded(Layer newLayer) {
475 //
476 }
477
478 @Override
479 public void layerRemoved(Layer oldLayer) {
480 MapView.removeLayerChangeListener(this);
481 }
482 });
483 }
484 });
485 }
486
487 void zoomChanged()
488 {
489 if (debug) {
490 out("zoomChanged(): " + currentZoomLevel);
491 }
492 needRedraw = true;
493 jobDispatcher.cancelOutstandingJobs();
494 tileRequestsOutstanding.clear();
495 }
496
497 int getMaxZoomLvl()
498 {
499 if (info.getMaxZoom() != 0)
500 return checkMaxZoomLvl(info.getMaxZoom(), tileSource);
501 else
502 return getMaxZoomLvl(tileSource);
503 }
504
505 int getMinZoomLvl()
506 {
507 return getMinZoomLvl(tileSource);
508 }
509
510 /**
511 * Zoom in, go closer to map.
512 *
513 * @return true, if zoom increasing was successfull, false othervise
514 */
515 public boolean zoomIncreaseAllowed()
516 {
517 boolean zia = currentZoomLevel < this.getMaxZoomLvl();
518 if (debug) {
519 out("zoomIncreaseAllowed(): " + zia + " " + currentZoomLevel + " vs. " + this.getMaxZoomLvl() );
520 }
521 return zia;
522 }
523 public boolean increaseZoomLevel()
524 {
525 if (zoomIncreaseAllowed()) {
526 currentZoomLevel++;
527 if (debug) {
528 out("increasing zoom level to: " + currentZoomLevel);
529 }
530 zoomChanged();
531 } else {
532 System.err.println("current zoom lvl ("+currentZoomLevel+") couldnt be increased. "+
533 "MaxZoomLvl ("+this.getMaxZoomLvl()+") reached.");
534 return false;
535 }
536 return true;
537 }
538
539 public boolean setZoomLevel(int zoom)
540 {
541 if (zoom == currentZoomLevel) return true;
542 if (zoom > this.getMaxZoomLvl()) return false;
543 if (zoom < this.getMinZoomLvl()) return false;
544 currentZoomLevel = zoom;
545 zoomChanged();
546 return true;
547 }
548
549 /**
550 * Zoom out from map.
551 *
552 * @return true, if zoom increasing was successfull, false othervise
553 */
554 public boolean zoomDecreaseAllowed()
555 {
556 return currentZoomLevel > this.getMinZoomLvl();
557 }
558 public boolean decreaseZoomLevel() {
559 int minZoom = this.getMinZoomLvl();
560 if (zoomDecreaseAllowed()) {
561 if (debug) {
562 out("decreasing zoom level to: " + currentZoomLevel);
563 }
564 currentZoomLevel--;
565 zoomChanged();
566 } else {
567 System.err.println("current zoom lvl couldnt be decreased. MinZoomLvl("+minZoom+") reached.");
568 return false;
569 }
570 return true;
571 }
572
573 /*
574 * We use these for quick, hackish calculations. They
575 * are temporary only and intentionally not inserted
576 * into the tileCache.
577 */
578 synchronized Tile tempCornerTile(Tile t) {
579 int x = t.getXtile() + 1;
580 int y = t.getYtile() + 1;
581 int zoom = t.getZoom();
582 Tile tile = getTile(x, y, zoom);
583 if (tile != null)
584 return tile;
585 return new Tile(tileSource, x, y, zoom);
586 }
587 synchronized Tile getOrCreateTile(int x, int y, int zoom) {
588 Tile tile = getTile(x, y, zoom);
589 if (tile == null) {
590 tile = new Tile(tileSource, x, y, zoom);
591 tileCache.addTile(tile);
592 tile.loadPlaceholderFromCache(tileCache);
593 }
594 return tile;
595 }
596
597 /*
598 * This can and will return null for tiles that are not
599 * already in the cache.
600 */
601 synchronized Tile getTile(int x, int y, int zoom) {
602 int max = (1 << zoom);
603 if (x < 0 || x >= max || y < 0 || y >= max)
604 return null;
605 Tile tile = tileCache.getTile(tileSource, x, y, zoom);
606 return tile;
607 }
608
609 synchronized boolean loadTile(Tile tile)
610 {
611 if (tile == null)
612 return false;
613 if (tile.hasError())
614 return false;
615 if (tile.isLoaded())
616 return false;
617 if (tile.isLoading())
618 return false;
619 if (tileRequestsOutstanding.contains(tile))
620 return false;
621 tileRequestsOutstanding.add(tile);
622 jobDispatcher.addJob(tileLoader.createTileLoaderJob(tileSource,
623 tile.getXtile(), tile.getYtile(), tile.getZoom()));
624 return true;
625 }
626
627 void loadAllTiles(boolean force) {
628 MapView mv = Main.map.mapView;
629 EastNorth topLeft = mv.getEastNorth(0, 0);
630 EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
631
632 TileSet ts = new TileSet(topLeft, botRight, currentZoomLevel);
633
634 // if there is more than 18 tiles on screen in any direction, do not
635 // load all tiles!
636 if (ts.tooLarge()) {
637 System.out.println("Not downloading all tiles because there is more than 18 tiles on an axis!");
638 return;
639 }
640 ts.loadAllTiles(force);
641 }
642
643 /*
644 * Attempt to approximate how much the image is being scaled. For instance,
645 * a 100x100 image being scaled to 50x50 would return 0.25.
646 */
647 Image lastScaledImage = null;
648 @Override
649 public boolean imageUpdate(Image img, int infoflags, int x, int y, int width, int height) {
650 boolean done = ((infoflags & (ERROR | FRAMEBITS | ALLBITS)) != 0);
651 needRedraw = true;
652 if (debug) {
653 out("imageUpdate() done: " + done + " calling repaint");
654 }
655 Main.map.repaint(done ? 0 : 100);
656 return !done;
657 }
658 boolean imageLoaded(Image i) {
659 if (i == null)
660 return false;
661 int status = Toolkit.getDefaultToolkit().checkImage(i, -1, -1, this);
662 if ((status & ALLBITS) != 0)
663 return true;
664 return false;
665 }
666 Image getLoadedTileImage(Tile tile)
667 {
668 if (!tile.isLoaded())
669 return null;
670 Image img = tile.getImage();
671 if (!imageLoaded(img))
672 return null;
673 return img;
674 }
675
676 LatLon tileLatLon(Tile t)
677 {
678 int zoom = t.getZoom();
679 return new LatLon(tileYToLat(t.getYtile(), zoom),
680 tileXToLon(t.getXtile(), zoom));
681 }
682
683 Rectangle tileToRect(Tile t1)
684 {
685 /*
686 * We need to get a box in which to draw, so advance by one tile in
687 * each direction to find the other corner of the box.
688 * Note: this somewhat pollutes the tile cache
689 */
690 Tile t2 = tempCornerTile(t1);
691 Rectangle rect = new Rectangle(pixelPos(t1));
692 rect.add(pixelPos(t2));
693 return rect;
694 }
695
696 // 'source' is the pixel coordinates for the area that
697 // the img is capable of filling in. However, we probably
698 // only want a portion of it.
699 //
700 // 'border' is the screen cordinates that need to be drawn.
701 // We must not draw outside of it.
702 void drawImageInside(Graphics g, Image sourceImg, Rectangle source, Rectangle border)
703 {
704 Rectangle target = source;
705
706 // If a border is specified, only draw the intersection
707 // if what we have combined with what we are supposed
708 // to draw.
709 if (border != null) {
710 target = source.intersection(border);
711 if (debug) {
712 out("source: " + source + "\nborder: " + border + "\nintersection: " + target);
713 }
714 }
715
716 // All of the rectangles are in screen coordinates. We need
717 // to how these correlate to the sourceImg pixels. We could
718 // avoid doing this by scaling the image up to the 'source' size,
719 // but this should be cheaper.
720 //
721 // In some projections, x any y are scaled differently enough to
722 // cause a pixel or two of fudge. Calculate them separately.
723 double imageYScaling = sourceImg.getHeight(this) / source.getHeight();
724 double imageXScaling = sourceImg.getWidth(this) / source.getWidth();
725
726 // How many pixels into the 'source' rectangle are we drawing?
727 int screen_x_offset = target.x - source.x;
728 int screen_y_offset = target.y - source.y;
729 // And how many pixels into the image itself does that
730 // correlate to?
731 int img_x_offset = (int)(screen_x_offset * imageXScaling);
732 int img_y_offset = (int)(screen_y_offset * imageYScaling);
733 // Now calculate the other corner of the image that we need
734 // by scaling the 'target' rectangle's dimensions.
735 int img_x_end = img_x_offset + (int)(target.getWidth() * imageXScaling);
736 int img_y_end = img_y_offset + (int)(target.getHeight() * imageYScaling);
737
738 if (debug) {
739 out("drawing image into target rect: " + target);
740 }
741 g.drawImage(sourceImg,
742 target.x, target.y,
743 target.x + target.width, target.y + target.height,
744 img_x_offset, img_y_offset,
745 img_x_end, img_y_end,
746 this);
747 if (PROP_FADE_AMOUNT.get() != 0) {
748 // dimm by painting opaque rect...
749 g.setColor(getFadeColorWithAlpha());
750 g.fillRect(target.x, target.y,
751 target.width, target.height);
752 }
753 }
754 // This function is called for several zoom levels, not just
755 // the current one. It should not trigger any tiles to be
756 // downloaded. It should also avoid polluting the tile cache
757 // with any tiles since these tiles are not mandatory.
758 //
759 // The "border" tile tells us the boundaries of where we may
760 // draw. It will not be from the zoom level that is being
761 // drawn currently. If drawing the displayZoomLevel,
762 // border is null and we draw the entire tile set.
763 List<Tile> paintTileImages(Graphics g, TileSet ts, int zoom, Tile border) {
764 if (zoom <= 0) return Collections.emptyList();
765 Rectangle borderRect = null;
766 if (border != null) {
767 borderRect = tileToRect(border);
768 }
769 List<Tile> missedTiles = new LinkedList<Tile>();
770 for (Tile tile : ts.allTiles()) {
771 Image img = getLoadedTileImage(tile);
772 if (img == null || tile.hasError()) {
773 if (debug) {
774 out("missed tile: " + tile);
775 }
776 missedTiles.add(tile);
777 continue;
778 }
779 Rectangle sourceRect = tileToRect(tile);
780 if (borderRect != null && !sourceRect.intersects(borderRect)) {
781 continue;
782 }
783 drawImageInside(g, img, sourceRect, borderRect);
784 }// end of for
785 return missedTiles;
786 }
787
788 void myDrawString(Graphics g, String text, int x, int y) {
789 Color oldColor = g.getColor();
790 g.setColor(Color.black);
791 g.drawString(text,x+1,y+1);
792 g.setColor(oldColor);
793 g.drawString(text,x,y);
794 }
795
796 void paintTileText(TileSet ts, Tile tile, Graphics g, MapView mv, int zoom, Tile t) {
797 int fontHeight = g.getFontMetrics().getHeight();
798 if (tile == null)
799 return;
800 Point p = pixelPos(t);
801 int texty = p.y + 2 + fontHeight;
802
803 if (PROP_DRAW_DEBUG.get()) {
804 myDrawString(g, "x=" + t.getXtile() + " y=" + t.getYtile() + " z=" + zoom + "", p.x + 2, texty);
805 texty += 1 + fontHeight;
806 if ((t.getXtile() % 32 == 0) && (t.getYtile() % 32 == 0)) {
807 myDrawString(g, "x=" + t.getXtile() / 32 + " y=" + t.getYtile() / 32 + " z=7", p.x + 2, texty);
808 texty += 1 + fontHeight;
809 }
810 }// end of if draw debug
811
812 if (tile == showMetadataTile) {
813 String md = tile.toString();
814 if (md != null) {
815 myDrawString(g, md, p.x + 2, texty);
816 texty += 1 + fontHeight;
817 }
818 Map<String, String> meta = tile.getMetadata();
819 if (meta != null) {
820 for (Map.Entry<String, String> entry : meta.entrySet()) {
821 myDrawString(g, entry.getKey() + ": " + entry.getValue(), p.x + 2, texty);
822 texty += 1 + fontHeight;
823 }
824 }
825 }
826
827 String tileStatus = tile.getStatus();
828 if (!tile.isLoaded() && PROP_DRAW_DEBUG.get()) {
829 myDrawString(g, tr("image " + tileStatus), p.x + 2, texty);
830 texty += 1 + fontHeight;
831 }
832
833 if (tile.hasError()) {
834 myDrawString(g, tr("Error") + ": " + tr(tile.getErrorMessage()), p.x + 2, texty);
835 texty += 1 + fontHeight;
836 }
837
838 int xCursor = -1;
839 int yCursor = -1;
840 if (PROP_DRAW_DEBUG.get()) {
841 if (yCursor < t.getYtile()) {
842 if (t.getYtile() % 32 == 31) {
843 g.fillRect(0, p.y - 1, mv.getWidth(), 3);
844 } else {
845 g.drawLine(0, p.y, mv.getWidth(), p.y);
846 }
847 yCursor = t.getYtile();
848 }
849 // This draws the vertical lines for the entire
850 // column. Only draw them for the top tile in
851 // the column.
852 if (xCursor < t.getXtile()) {
853 if (t.getXtile() % 32 == 0) {
854 // level 7 tile boundary
855 g.fillRect(p.x - 1, 0, 3, mv.getHeight());
856 } else {
857 g.drawLine(p.x, 0, p.x, mv.getHeight());
858 }
859 xCursor = t.getXtile();
860 }
861 }
862 }
863
864 private Point pixelPos(LatLon ll) {
865 return Main.map.mapView.getPoint(Main.proj.latlon2eastNorth(ll).add(getDx(), getDy()));
866 }
867 private Point pixelPos(Tile t) {
868 double lon = tileXToLon(t.getXtile(), t.getZoom());
869 LatLon tmpLL = new LatLon(tileYToLat(t.getYtile(), t.getZoom()), lon);
870 return pixelPos(tmpLL);
871 }
872 private LatLon getShiftedLatLon(EastNorth en) {
873 return Main.proj.eastNorth2latlon(en.add(-getDx(), -getDy()));
874 }
875 private Coordinate getShiftedCoord(EastNorth en) {
876 LatLon ll = getShiftedLatLon(en);
877 return new Coordinate(ll.lat(),ll.lon());
878 }
879 private final TileSet nullTileSet = new TileSet((LatLon)null, (LatLon)null, 0);
880 private class TileSet {
881 int x0, x1, y0, y1;
882 int zoom;
883 int tileMax = -1;
884
885 /**
886 * Create a TileSet by EastNorth bbox taking a layer shift in account
887 */
888 TileSet(EastNorth topLeft, EastNorth botRight, int zoom) {
889 this(getShiftedLatLon(topLeft), getShiftedLatLon(botRight),zoom);
890 }
891
892 /**
893 * Create a TileSet by known LatLon bbox without layer shift correction
894 */
895 TileSet(LatLon topLeft, LatLon botRight, int zoom) {
896 this.zoom = zoom;
897 if (zoom == 0)
898 return;
899
900 x0 = (int)lonToTileX(topLeft.lon(), zoom);
901 y0 = (int)latToTileY(topLeft.lat(), zoom);
902 x1 = (int)lonToTileX(botRight.lon(), zoom);
903 y1 = (int)latToTileY(botRight.lat(), zoom);
904 if (x0 > x1) {
905 int tmp = x0;
906 x0 = x1;
907 x1 = tmp;
908 }
909 if (y0 > y1) {
910 int tmp = y0;
911 y0 = y1;
912 y1 = tmp;
913 }
914 tileMax = (int)Math.pow(2.0, zoom);
915 if (x0 < 0) {
916 x0 = 0;
917 }
918 if (y0 < 0) {
919 y0 = 0;
920 }
921 if (x1 > tileMax) {
922 x1 = tileMax;
923 }
924 if (y1 > tileMax) {
925 y1 = tileMax;
926 }
927 }
928 boolean tooSmall() {
929 return this.tilesSpanned() < 2.1;
930 }
931 boolean tooLarge() {
932 return this.tilesSpanned() > 10;
933 }
934 boolean insane() {
935 return this.tilesSpanned() > 100;
936 }
937 double tilesSpanned() {
938 return Math.sqrt(1.0 * this.size());
939 }
940
941 int size() {
942 int x_span = x1 - x0 + 1;
943 int y_span = y1 - y0 + 1;
944 return x_span * y_span;
945 }
946
947 /*
948 * Get all tiles represented by this TileSet that are
949 * already in the tileCache.
950 */
951 List<Tile> allTiles()
952 {
953 return this.allTiles(false);
954 }
955 private List<Tile> allTiles(boolean create)
956 {
957 // Tileset is either empty or too large
958 if (zoom == 0 || this.insane())
959 return Collections.emptyList();
960 List<Tile> ret = new ArrayList<Tile>();
961 for (int x = x0; x <= x1; x++) {
962 for (int y = y0; y <= y1; y++) {
963 Tile t;
964 if (create) {
965 t = getOrCreateTile(x % tileMax, y % tileMax, zoom);
966 } else {
967 t = getTile(x % tileMax, y % tileMax, zoom);
968 }
969 if (t != null) {
970 ret.add(t);
971 }
972 }
973 }
974 return ret;
975 }
976
977 void loadAllTiles(boolean force)
978 {
979 List<Tile> tiles = this.allTiles(true);
980 if (!autoLoad && !force)
981 return;
982 int nr_queued = 0;
983 for (Tile t : tiles) {
984 if (loadTile(t)) {
985 nr_queued++;
986 }
987 }
988 if (debug)
989 if (nr_queued > 0) {
990 out("queued to load: " + nr_queued + "/" + tiles.size() + " tiles at zoom: " + zoom);
991 }
992 }
993 }
994
995
996 private static class TileSetInfo {
997 public boolean hasVisibleTiles = false;
998 public boolean hasOverzoomedTiles = false;
999 public boolean hasLoadingTiles = false;
1000 }
1001
1002 private static TileSetInfo getTileSetInfo(TileSet ts) {
1003 List<Tile> allTiles = ts.allTiles();
1004 TileSetInfo result = new TileSetInfo();
1005 result.hasLoadingTiles = allTiles.size() < ts.size();
1006 for (Tile t : allTiles) {
1007 if (t.isLoaded()) {
1008 if (!t.hasError()) {
1009 result.hasVisibleTiles = true;
1010 }
1011 if ("no-tile".equals(t.getValue("tile-info"))) {
1012 result.hasOverzoomedTiles = true;
1013 }
1014 } else {
1015 result.hasLoadingTiles = true;
1016 }
1017 }
1018 return result;
1019 }
1020
1021 private class DeepTileSet {
1022 final EastNorth topLeft, botRight;
1023 final int minZoom, maxZoom;
1024 private final TileSet[] tileSets;
1025 private final TileSetInfo[] tileSetInfos;
1026 public DeepTileSet(EastNorth topLeft, EastNorth botRight, int minZoom, int maxZoom) {
1027 this.topLeft = topLeft;
1028 this.botRight = botRight;
1029 this.minZoom = minZoom;
1030 this.maxZoom = maxZoom;
1031 this.tileSets = new TileSet[maxZoom - minZoom + 1];
1032 this.tileSetInfos = new TileSetInfo[maxZoom - minZoom + 1];
1033 }
1034 public TileSet getTileSet(int zoom) {
1035 if (zoom < minZoom)
1036 return nullTileSet;
1037 TileSet ts = tileSets[zoom-minZoom];
1038 if (ts == null) {
1039 ts = new TileSet(topLeft, botRight, zoom);
1040 tileSets[zoom-minZoom] = ts;
1041 }
1042 return ts;
1043 }
1044 public TileSetInfo getTileSetInfo(int zoom) {
1045 if (zoom < minZoom)
1046 return new TileSetInfo();
1047 TileSetInfo tsi = tileSetInfos[zoom-minZoom];
1048 if (tsi == null) {
1049 tsi = TMSLayer.getTileSetInfo(getTileSet(zoom));
1050 tileSetInfos[zoom-minZoom] = tsi;
1051 }
1052 return tsi;
1053 }
1054 }
1055
1056 /**
1057 */
1058 @Override
1059 public void paint(Graphics2D g, MapView mv, Bounds bounds) {
1060 //long start = System.currentTimeMillis();
1061 EastNorth topLeft = mv.getEastNorth(0, 0);
1062 EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
1063
1064 if (botRight.east() == 0.0 || botRight.north() == 0) {
1065 Main.debug("still initializing??");
1066 // probably still initializing
1067 return;
1068 }
1069
1070 needRedraw = false;
1071
1072 int zoom = currentZoomLevel;
1073 if (autoZoom) {
1074 double pixelScaling = getScaleFactor(zoom);
1075 if (pixelScaling > 3 || pixelScaling < 0.45) {
1076 zoom = getBestZoom();
1077 }
1078 }
1079
1080 DeepTileSet dts = new DeepTileSet(topLeft, botRight, getMinZoomLvl(), zoom);
1081 TileSet ts = dts.getTileSet(zoom);
1082
1083 int displayZoomLevel = zoom;
1084
1085 boolean noTilesAtZoom = false;
1086 if (autoZoom && autoLoad) {
1087 // Auto-detection of tilesource maxzoom (currently fully works only for Bing)
1088 TileSetInfo tsi = dts.getTileSetInfo(zoom);
1089 if (!tsi.hasVisibleTiles && (!tsi.hasLoadingTiles || tsi.hasOverzoomedTiles)) {
1090 noTilesAtZoom = true;
1091 }
1092 // Find highest zoom level with at least one visible tile
1093 while (displayZoomLevel > dts.minZoom &&
1094 !dts.getTileSetInfo(displayZoomLevel).hasVisibleTiles) {
1095 displayZoomLevel--;
1096 }
1097 // Do binary search between currentZoomLevel and displayZoomLevel
1098 while (zoom > displayZoomLevel && !tsi.hasVisibleTiles && tsi.hasOverzoomedTiles){
1099 zoom = (zoom + displayZoomLevel)/2;
1100 tsi = dts.getTileSetInfo(zoom);
1101 }
1102
1103 setZoomLevel(zoom);
1104
1105 // If all tiles at displayZoomLevel is loaded, load all tiles at next zoom level
1106 // to make sure there're really no more zoom levels
1107 if (zoom == displayZoomLevel && !tsi.hasLoadingTiles && zoom < dts.maxZoom) {
1108 zoom++;
1109 tsi = dts.getTileSetInfo(zoom);
1110 }
1111 // When we have overzoomed tiles and all tiles at current zoomlevel is loaded,
1112 // load tiles at previovus zoomlevels until we have all tiles on screen is loaded.
1113 while (zoom > dts.minZoom && tsi.hasOverzoomedTiles && !tsi.hasLoadingTiles) {
1114 zoom--;
1115 tsi = dts.getTileSetInfo(zoom);
1116 }
1117 ts = dts.getTileSet(zoom);
1118 } else if (autoZoom) {
1119 setZoomLevel(zoom);
1120 }
1121
1122 // Too many tiles... refuse to download
1123 if (!ts.tooLarge()) {
1124 //out("size: " + ts.size() + " spanned: " + ts.tilesSpanned());
1125 ts.loadAllTiles(false);
1126 }
1127
1128 if (displayZoomLevel != zoom) {
1129 ts = dts.getTileSet(displayZoomLevel);
1130 }
1131
1132 g.setColor(Color.DARK_GRAY);
1133
1134 List<Tile> missedTiles = this.paintTileImages(g, ts, displayZoomLevel, null);
1135 int otherZooms[] = { -1, 1, -2, 2, -3, -4, -5};
1136 for (int zoomOffset : otherZooms) {
1137 if (!autoZoom) {
1138 break;
1139 }
1140 if (!autoLoad) {
1141 break;
1142 }
1143 int newzoom = displayZoomLevel + zoomOffset;
1144 if (missedTiles.size() <= 0) {
1145 break;
1146 }
1147 List<Tile> newlyMissedTiles = new LinkedList<Tile>();
1148 for (Tile missed : missedTiles) {
1149 if ("no-tile".equals(missed.getValue("tile-info")) && zoomOffset > 0) {
1150 // Don't try to paint from higher zoom levels when tile is overzoomed
1151 newlyMissedTiles.add(missed);
1152 continue;
1153 }
1154 Tile t2 = tempCornerTile(missed);
1155 LatLon topLeft2 = tileLatLon(missed);
1156 LatLon botRight2 = tileLatLon(t2);
1157 TileSet ts2 = new TileSet(topLeft2, botRight2, newzoom);
1158 if (ts2.tooLarge()) {
1159 continue;
1160 }
1161 newlyMissedTiles.addAll(this.paintTileImages(g, ts2, newzoom, missed));
1162 }
1163 missedTiles = newlyMissedTiles;
1164 }
1165 if (debug && missedTiles.size() > 0) {
1166 out("still missed "+missedTiles.size()+" in the end");
1167 }
1168 g.setColor(Color.red);
1169 g.setFont(InfoFont);
1170
1171 // The current zoom tileset is guaranteed to have all of
1172 // its tiles
1173 for (Tile t : ts.allTiles()) {
1174 this.paintTileText(ts, t, g, mv, displayZoomLevel, t);
1175 }
1176
1177 if (tileSource.requiresAttribution()) {
1178 // Draw attribution
1179 Font font = g.getFont();
1180 g.setFont(ATTR_LINK_FONT);
1181 g.setColor(Color.white);
1182
1183 // Draw terms of use text
1184 Rectangle2D termsStringBounds = g.getFontMetrics().getStringBounds("Background Terms of Use", g);
1185 int textHeight = (int) termsStringBounds.getHeight() - 5;
1186 int textWidth = (int) termsStringBounds.getWidth();
1187 int termsTextY = mv.getHeight() - textHeight;
1188 if(attrTermsUrl != null) {
1189 int x = 2;
1190 int y = mv.getHeight() - textHeight;
1191 attrToUBounds = new Rectangle(x, y, textWidth, textHeight);
1192 myDrawString(g, "Background Terms of Use", x, y);
1193 }
1194
1195 // Draw attribution logo
1196 int imgWidth = attrImage.getWidth(this);
1197 if(attrImage != null) {
1198 int x = 2;
1199 int height = attrImage.getHeight(this);
1200 int y = termsTextY - height - textHeight - 5;
1201 attrImageBounds = new Rectangle(x, y, imgWidth, height);
1202 g.drawImage(attrImage, x, y, this);
1203 }
1204
1205 g.setFont(ATTR_FONT);
1206 String attributionText = tileSource.getAttributionText(displayZoomLevel,
1207 getShiftedCoord(topLeft), getShiftedCoord(botRight));
1208 Rectangle2D stringBounds = g.getFontMetrics().getStringBounds(attributionText, g);
1209 {
1210 int x = mv.getWidth() - (int) stringBounds.getWidth();
1211 int y = mv.getHeight() - textHeight;
1212 myDrawString(g, attributionText, x, y);
1213 }
1214
1215 g.setFont(font);
1216 }
1217
1218 //g.drawString("currentZoomLevel=" + currentZoomLevel, 120, 120);
1219 g.setColor(Color.lightGray);
1220 if (!autoZoom) {
1221 if (ts.insane()) {
1222 myDrawString(g, tr("zoom in to load any tiles"), 120, 120);
1223 } else if (ts.tooLarge()) {
1224 myDrawString(g, tr("zoom in to load more tiles"), 120, 120);
1225 } else if (ts.tooSmall()) {
1226 myDrawString(g, tr("increase zoom level to see more detail"), 120, 120);
1227 }
1228 }
1229 if (noTilesAtZoom) {
1230 myDrawString(g, tr("No tiles at this zoom level"), 120, 120);
1231 }
1232 if (debug) {
1233 myDrawString(g, tr("Current zoom: {0}", currentZoomLevel), 50, 140);
1234 myDrawString(g, tr("Display zoom: {0}", displayZoomLevel), 50, 155);
1235 myDrawString(g, tr("Pixel scale: {0}", getScaleFactor(currentZoomLevel)), 50, 170);
1236 myDrawString(g, tr("Best zoom: {0}", Math.log(getScaleFactor(1))/Math.log(2)/2+1), 50, 185);
1237 }
1238 }// end of paint method
1239
1240 /**
1241 * This isn't very efficient, but it is only used when the
1242 * user right-clicks on the map.
1243 */
1244 Tile getTileForPixelpos(int px, int py) {
1245 if (debug) {
1246 out("getTileForPixelpos("+px+", "+py+")");
1247 }
1248 MapView mv = Main.map.mapView;
1249 Point clicked = new Point(px, py);
1250 EastNorth topLeft = mv.getEastNorth(0, 0);
1251 EastNorth botRight = mv.getEastNorth(mv.getWidth(), mv.getHeight());
1252 int z = currentZoomLevel;
1253 TileSet ts = new TileSet(topLeft, botRight, z);
1254
1255 if (!ts.tooLarge()) {
1256 ts.loadAllTiles(false); // make sure there are tile objects for all tiles
1257 }
1258 Tile clickedTile = null;
1259 for (Tile t1 : ts.allTiles()) {
1260 Tile t2 = tempCornerTile(t1);
1261 Rectangle r = new Rectangle(pixelPos(t1));
1262 r.add(pixelPos(t2));
1263 if (debug) {
1264 out("r: " + r + " clicked: " + clicked);
1265 }
1266 if (!r.contains(clicked)) {
1267 continue;
1268 }
1269 clickedTile = t1;
1270 break;
1271 }
1272 if (clickedTile == null)
1273 return null;
1274 System.out.println("clicked on tile: " + clickedTile.getXtile() + " " + clickedTile.getYtile() +
1275 " currentZoomLevel: " + currentZoomLevel);
1276 return clickedTile;
1277 }
1278
1279 @Override
1280 public Action[] getMenuEntries() {
1281 return new Action[] {
1282 LayerListDialog.getInstance().createShowHideLayerAction(),
1283 LayerListDialog.getInstance().createDeleteLayerAction(),
1284 SeparatorLayerAction.INSTANCE,
1285 // color,
1286 new OffsetAction(),
1287 new RenameLayerAction(this.getAssociatedFile(), this),
1288 SeparatorLayerAction.INSTANCE,
1289 new LayerListPopup.InfoAction(this) };
1290 }
1291
1292 @Override
1293 public String getToolTipText() {
1294 return null;
1295 }
1296
1297 @Override
1298 public void visitBoundingBox(BoundingXYVisitor v) {
1299 }
1300
1301 @Override
1302 public boolean isChanged() {
1303 return needRedraw;
1304 }
1305
1306 private static double latToTileY(double lat, int zoom) {
1307 double l = lat / 180 * Math.PI;
1308 double pf = Math.log(Math.tan(l) + (1 / Math.cos(l)));
1309 return Math.pow(2.0, zoom - 1) * (Math.PI - pf) / Math.PI;
1310 }
1311
1312 private static double lonToTileX(double lon, int zoom) {
1313 return Math.pow(2.0, zoom - 3) * (lon + 180.0) / 45.0;
1314 }
1315
1316 private static double tileYToLat(int y, int zoom) {
1317 return Math.atan(Math.sinh(Math.PI
1318 - (Math.PI * y / Math.pow(2.0, zoom - 1))))
1319 * 180 / Math.PI;
1320 }
1321
1322 private static double tileXToLon(int x, int zoom) {
1323 return x * 45.0 / Math.pow(2.0, zoom - 3) - 180.0;
1324 }
1325}
Note: See TracBrowser for help on using the repository browser.