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