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