001// License: GPL. See LICENSE file for details. 002package org.openstreetmap.josm.gui.bbox; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.Color; 007import java.awt.Dimension; 008import java.awt.Graphics; 009import java.awt.Graphics2D; 010import java.awt.Image; 011import java.awt.Point; 012import java.awt.Rectangle; 013import java.io.IOException; 014import java.util.ArrayList; 015import java.util.Arrays; 016import java.util.Collections; 017import java.util.HashSet; 018import java.util.List; 019import java.util.Set; 020import java.util.concurrent.CopyOnWriteArrayList; 021 022import javax.swing.JOptionPane; 023 024import org.openstreetmap.gui.jmapviewer.Coordinate; 025import org.openstreetmap.gui.jmapviewer.JMapViewer; 026import org.openstreetmap.gui.jmapviewer.MapMarkerDot; 027import org.openstreetmap.gui.jmapviewer.MemoryTileCache; 028import org.openstreetmap.gui.jmapviewer.OsmMercator; 029import org.openstreetmap.gui.jmapviewer.OsmTileLoader; 030import org.openstreetmap.gui.jmapviewer.interfaces.MapMarker; 031import org.openstreetmap.gui.jmapviewer.interfaces.TileSource; 032import org.openstreetmap.gui.jmapviewer.tilesources.MapQuestOpenAerialTileSource; 033import org.openstreetmap.gui.jmapviewer.tilesources.MapQuestOsmTileSource; 034import org.openstreetmap.gui.jmapviewer.tilesources.OsmTileSource; 035import org.openstreetmap.josm.Main; 036import org.openstreetmap.josm.data.Bounds; 037import org.openstreetmap.josm.data.Version; 038import org.openstreetmap.josm.data.coor.LatLon; 039import org.openstreetmap.josm.data.imagery.ImageryInfo; 040import org.openstreetmap.josm.data.imagery.ImageryLayerInfo; 041import org.openstreetmap.josm.data.preferences.StringProperty; 042import org.openstreetmap.josm.gui.layer.TMSLayer; 043 044public class SlippyMapBBoxChooser extends JMapViewer implements BBoxChooser { 045 046 public interface TileSourceProvider { 047 List<TileSource> getTileSources(); 048 } 049 050 public static class RenamedSourceDecorator implements TileSource { 051 052 private final TileSource source; 053 private final String name; 054 055 public RenamedSourceDecorator(TileSource source, String name) { 056 this.source = source; 057 this.name = name; 058 } 059 060 @Override public String getName() { 061 return name; 062 } 063 064 @Override public int getMaxZoom() { return source.getMaxZoom(); } 065 066 @Override public int getMinZoom() { return source.getMinZoom(); } 067 068 @Override public int getTileSize() { return source.getTileSize(); } 069 070 @Override public String getTileType() { return source.getTileType(); } 071 072 @Override public TileUpdate getTileUpdate() { return source.getTileUpdate(); } 073 074 @Override public String getTileUrl(int zoom, int tilex, int tiley) throws IOException { return source.getTileUrl(zoom, tilex, tiley); } 075 076 @Override public boolean requiresAttribution() { return source.requiresAttribution(); } 077 078 @Override public String getAttributionText(int zoom, Coordinate topLeft, Coordinate botRight) { return source.getAttributionText(zoom, topLeft, botRight); } 079 080 @Override public String getAttributionLinkURL() { return source.getAttributionLinkURL(); } 081 082 @Override public Image getAttributionImage() { return source.getAttributionImage(); } 083 084 @Override public String getAttributionImageURL() { return source.getAttributionImageURL(); } 085 086 @Override public String getTermsOfUseText() { return source.getTermsOfUseText(); } 087 088 @Override public String getTermsOfUseURL() { return source.getTermsOfUseURL(); } 089 090 @Override public double latToTileY(double lat, int zoom) { return source.latToTileY(lat,zoom); } 091 092 @Override public double lonToTileX(double lon, int zoom) { return source.lonToTileX(lon,zoom); } 093 094 @Override public double tileYToLat(int y, int zoom) { return source.tileYToLat(y, zoom); } 095 096 @Override public double tileXToLon(int x, int zoom) { return source.tileXToLon(x, zoom); } 097 } 098 099 /** 100 * TMS TileSource provider for the slippymap chooser 101 */ 102 public static class TMSTileSourceProvider implements TileSourceProvider { 103 static final Set<String> existingSlippyMapUrls = new HashSet<String>(); 104 static { 105 // Urls that already exist in the slippymap chooser and shouldn't be copied from TMS layer list 106 existingSlippyMapUrls.add("http://tile.openstreetmap.org/{zoom}/{x}/{y}.png"); // Mapnik 107 existingSlippyMapUrls.add("http://tile.opencyclemap.org/cycle/{zoom}/{x}/{y}.png"); // Cyclemap 108 existingSlippyMapUrls.add("http://otile{switch:1,2,3,4}.mqcdn.com/tiles/1.0.0/osm/{zoom}/{x}/{y}.png"); // MapQuest-OSM 109 existingSlippyMapUrls.add("http://oatile{switch:1,2,3,4}.mqcdn.com/tiles/1.0.0/sat/{zoom}/{x}/{y}.png"); // MapQuest Open Aerial 110 } 111 112 @Override 113 public List<TileSource> getTileSources() { 114 if (!TMSLayer.PROP_ADD_TO_SLIPPYMAP_CHOOSER.get()) return Collections.<TileSource>emptyList(); 115 List<TileSource> sources = new ArrayList<TileSource>(); 116 for (ImageryInfo info : ImageryLayerInfo.instance.getLayers()) { 117 if (existingSlippyMapUrls.contains(info.getUrl())) { 118 continue; 119 } 120 try { 121 TileSource source = TMSLayer.getTileSource(info); 122 if (source != null) { 123 sources.add(source); 124 } 125 } catch (IllegalArgumentException ex) { 126 if (ex.getMessage() != null && !ex.getMessage().isEmpty()) { 127 JOptionPane.showMessageDialog(Main.parent, 128 ex.getMessage(), tr("Warning"), 129 JOptionPane.WARNING_MESSAGE); 130 } 131 } 132 } 133 return sources; 134 } 135 136 public static void addExistingSlippyMapUrl(String url) { 137 existingSlippyMapUrls.add(url); 138 } 139 } 140 141 /** 142 * Plugins that wish to add custom tile sources to slippy map choose should call this method 143 * @param tileSourceProvider 144 */ 145 public static void addTileSourceProvider(TileSourceProvider tileSourceProvider) { 146 providers.addIfAbsent(tileSourceProvider); 147 } 148 149 private static CopyOnWriteArrayList<TileSourceProvider> providers = new CopyOnWriteArrayList<TileSourceProvider>(); 150 151 static { 152 addTileSourceProvider(new TileSourceProvider() { 153 @Override 154 public List<TileSource> getTileSources() { 155 return Arrays.<TileSource>asList( 156 new RenamedSourceDecorator(new OsmTileSource.Mapnik(), "Mapnik"), 157 new RenamedSourceDecorator(new OsmTileSource.CycleMap(), "Cyclemap"), 158 new RenamedSourceDecorator(new MapQuestOsmTileSource(), "MapQuest-OSM"), 159 new RenamedSourceDecorator(new MapQuestOpenAerialTileSource(), "MapQuest Open Aerial") 160 ); 161 } 162 }); 163 addTileSourceProvider(new TMSTileSourceProvider()); 164 } 165 166 private static final StringProperty PROP_MAPSTYLE = new StringProperty("slippy_map_chooser.mapstyle", "Mapnik"); 167 public static final String RESIZE_PROP = SlippyMapBBoxChooser.class.getName() + ".resize"; 168 169 private OsmTileLoader cachedLoader; 170 private OsmTileLoader uncachedLoader; 171 172 private final SizeButton iSizeButton = new SizeButton(); 173 private final SourceButton iSourceButton; 174 private Bounds bbox; 175 176 // upper left and lower right corners of the selection rectangle (x/y on ZOOM_MAX) 177 Point iSelectionRectStart; 178 Point iSelectionRectEnd; 179 180 /** 181 * Constructs a new {@code SlippyMapBBoxChooser}. 182 */ 183 public SlippyMapBBoxChooser() { 184 TMSLayer.setMaxWorkers(); 185 cachedLoader = TMSLayer.loaderFactory.makeTileLoader(this); 186 187 uncachedLoader = new OsmTileLoader(this); 188 uncachedLoader.headers.put("User-Agent", Version.getInstance().getFullAgentString()); 189 setZoomContolsVisible(Main.pref.getBoolean("slippy_map_chooser.zoomcontrols",false)); 190 setMapMarkerVisible(false); 191 setMinimumSize(new Dimension(350, 350 / 2)); 192 // We need to set an initial size - this prevents a wrong zoom selection 193 // for the area before the component has been displayed the first time 194 setBounds(new Rectangle(getMinimumSize())); 195 if (cachedLoader == null) { 196 setFileCacheEnabled(false); 197 } else { 198 setFileCacheEnabled(Main.pref.getBoolean("slippy_map_chooser.file_cache", true)); 199 } 200 setMaxTilesInMemory(Main.pref.getInteger("slippy_map_chooser.max_tiles", 1000)); 201 202 List<TileSource> tileSources = getAllTileSources(); 203 204 iSourceButton = new SourceButton(tileSources); 205 206 String mapStyle = PROP_MAPSTYLE.get(); 207 boolean foundSource = false; 208 for (TileSource source: tileSources) { 209 if (source.getName().equals(mapStyle)) { 210 this.setTileSource(source); 211 iSourceButton.setCurrentMap(source); 212 foundSource = true; 213 break; 214 } 215 } 216 if (!foundSource) { 217 setTileSource(tileSources.get(0)); 218 iSourceButton.setCurrentMap(tileSources.get(0)); 219 } 220 221 new SlippyMapControler(this, this, iSizeButton, iSourceButton); 222 } 223 224 private List<TileSource> getAllTileSources() { 225 List<TileSource> tileSources = new ArrayList<TileSource>(); 226 for (TileSourceProvider provider: providers) { 227 tileSources.addAll(provider.getTileSources()); 228 } 229 return tileSources; 230 } 231 232 public boolean handleAttribution(Point p, boolean click) { 233 return attribution.handleAttribution(p, click); 234 } 235 236 protected Point getTopLeftCoordinates() { 237 return new Point(center.x - (getWidth() / 2), center.y - (getHeight() / 2)); 238 } 239 240 /** 241 * Draw the map. 242 */ 243 @Override 244 public void paint(Graphics g) { 245 try { 246 super.paint(g); 247 248 // draw selection rectangle 249 if (iSelectionRectStart != null && iSelectionRectEnd != null) { 250 251 int zoomDiff = MAX_ZOOM - zoom; 252 Point tlc = getTopLeftCoordinates(); 253 int x_min = (iSelectionRectStart.x >> zoomDiff) - tlc.x; 254 int y_min = (iSelectionRectStart.y >> zoomDiff) - tlc.y; 255 int x_max = (iSelectionRectEnd.x >> zoomDiff) - tlc.x; 256 int y_max = (iSelectionRectEnd.y >> zoomDiff) - tlc.y; 257 258 int w = x_max - x_min; 259 int h = y_max - y_min; 260 g.setColor(new Color(0.9f, 0.7f, 0.7f, 0.6f)); 261 g.fillRect(x_min, y_min, w, h); 262 263 g.setColor(Color.BLACK); 264 g.drawRect(x_min, y_min, w, h); 265 } 266 267 iSizeButton.paint(g); 268 iSourceButton.paint((Graphics2D)g); 269 } catch (Exception e) { 270 e.printStackTrace(); 271 } 272 } 273 274 public void setFileCacheEnabled(boolean enabled) { 275 if (enabled) { 276 setTileLoader(cachedLoader); 277 } else { 278 setTileLoader(uncachedLoader); 279 } 280 } 281 282 public void setMaxTilesInMemory(int tiles) { 283 ((MemoryTileCache) getTileCache()).setCacheSize(tiles); 284 } 285 286 287 /** 288 * Callback for the OsmMapControl. (Re-)Sets the start and end point of the 289 * selection rectangle. 290 * 291 * @param aStart 292 * @param aEnd 293 */ 294 public void setSelection(Point aStart, Point aEnd) { 295 if (aStart == null || aEnd == null || aStart.x == aEnd.x || aStart.y == aEnd.y) 296 return; 297 298 Point p_max = new Point(Math.max(aEnd.x, aStart.x), Math.max(aEnd.y, aStart.y)); 299 Point p_min = new Point(Math.min(aEnd.x, aStart.x), Math.min(aEnd.y, aStart.y)); 300 301 Point tlc = getTopLeftCoordinates(); 302 int zoomDiff = MAX_ZOOM - zoom; 303 Point pEnd = new Point(p_max.x + tlc.x, p_max.y + tlc.y); 304 Point pStart = new Point(p_min.x + tlc.x, p_min.y + tlc.y); 305 306 pEnd.x <<= zoomDiff; 307 pEnd.y <<= zoomDiff; 308 pStart.x <<= zoomDiff; 309 pStart.y <<= zoomDiff; 310 311 iSelectionRectStart = pStart; 312 iSelectionRectEnd = pEnd; 313 314 Coordinate l1 = getPosition(p_max); // lon may be outside [-180,180] 315 Coordinate l2 = getPosition(p_min); // lon may be outside [-180,180] 316 Bounds b = new Bounds( 317 new LatLon( 318 Math.min(l2.getLat(), l1.getLat()), 319 LatLon.toIntervalLon(Math.min(l1.getLon(), l2.getLon())) 320 ), 321 new LatLon( 322 Math.max(l2.getLat(), l1.getLat()), 323 LatLon.toIntervalLon(Math.max(l1.getLon(), l2.getLon()))) 324 ); 325 Bounds oldValue = this.bbox; 326 this.bbox = b; 327 repaint(); 328 firePropertyChange(BBOX_PROP, oldValue, this.bbox); 329 } 330 331 /** 332 * Performs resizing of the DownloadDialog in order to enlarge or shrink the 333 * map. 334 */ 335 public void resizeSlippyMap() { 336 boolean large = iSizeButton.isEnlarged(); 337 firePropertyChange(RESIZE_PROP, !large, large); 338 } 339 340 public void toggleMapSource(TileSource tileSource) { 341 this.tileController.setTileCache(new MemoryTileCache()); 342 this.setTileSource(tileSource); 343 PROP_MAPSTYLE.put(tileSource.getName()); // TODO Is name really unique? 344 } 345 346 @Override 347 public Bounds getBoundingBox() { 348 return bbox; 349 } 350 351 /** 352 * Sets the current bounding box in this bbox chooser without 353 * emiting a property change event. 354 * 355 * @param bbox the bounding box. null to reset the bounding box 356 */ 357 @Override 358 public void setBoundingBox(Bounds bbox) { 359 if (bbox == null || (bbox.getMinLat() == 0.0 && bbox.getMinLon() == 0.0 360 && bbox.getMaxLat() == 0.0 && bbox.getMaxLon() == 0.0)) { 361 this.bbox = null; 362 iSelectionRectStart = null; 363 iSelectionRectEnd = null; 364 repaint(); 365 return; 366 } 367 368 this.bbox = bbox; 369 double minLon = bbox.getMinLon(); 370 double maxLon = bbox.getMaxLon(); 371 372 if (bbox.crosses180thMeridian()) { 373 minLon -= 360.0; 374 } 375 376 int y1 = OsmMercator.LatToY(bbox.getMinLat(), MAX_ZOOM); 377 int y2 = OsmMercator.LatToY(bbox.getMaxLat(), MAX_ZOOM); 378 int x1 = OsmMercator.LonToX(minLon, MAX_ZOOM); 379 int x2 = OsmMercator.LonToX(maxLon, MAX_ZOOM); 380 381 iSelectionRectStart = new Point(Math.min(x1, x2), Math.min(y1, y2)); 382 iSelectionRectEnd = new Point(Math.max(x1, x2), Math.max(y1, y2)); 383 384 // calc the screen coordinates for the new selection rectangle 385 MapMarkerDot xmin_ymin = new MapMarkerDot(bbox.getMinLat(), bbox.getMinLon()); 386 MapMarkerDot xmax_ymax = new MapMarkerDot(bbox.getMaxLat(), bbox.getMaxLon()); 387 388 List<MapMarker> marker = new ArrayList<MapMarker>(2); 389 marker.add(xmin_ymin); 390 marker.add(xmax_ymax); 391 setMapMarkerList(marker); 392 setDisplayToFitMapMarkers(); 393 zoomOut(); 394 repaint(); 395 } 396 397 /** 398 * Refreshes the tile sources 399 * @since 6364 400 */ 401 public final void refreshTileSources() { 402 iSourceButton.setSources(getAllTileSources()); 403 } 404}