001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.bbox; 003 004import java.awt.Color; 005import java.awt.Dimension; 006import java.awt.Graphics; 007import java.awt.Graphics2D; 008import java.awt.Point; 009import java.awt.Rectangle; 010import java.awt.geom.Area; 011import java.awt.geom.Path2D; 012import java.util.ArrayList; 013import java.util.LinkedHashMap; 014import java.util.List; 015import java.util.concurrent.CopyOnWriteArrayList; 016import java.util.stream.Collectors; 017 018import javax.swing.ButtonModel; 019import javax.swing.JToggleButton; 020import javax.swing.SpringLayout; 021import javax.swing.event.ChangeEvent; 022import javax.swing.event.ChangeListener; 023 024import org.openstreetmap.gui.jmapviewer.Coordinate; 025import org.openstreetmap.gui.jmapviewer.MapMarkerDot; 026import org.openstreetmap.gui.jmapviewer.MemoryTileCache; 027import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate; 028import org.openstreetmap.gui.jmapviewer.interfaces.MapMarker; 029import org.openstreetmap.gui.jmapviewer.interfaces.TileSource; 030import org.openstreetmap.josm.data.Bounds; 031import org.openstreetmap.josm.data.coor.LatLon; 032import org.openstreetmap.josm.data.osm.BBox; 033import org.openstreetmap.josm.data.osm.DataSet; 034import org.openstreetmap.josm.data.preferences.BooleanProperty; 035import org.openstreetmap.josm.data.preferences.StringProperty; 036import org.openstreetmap.josm.gui.MainApplication; 037import org.openstreetmap.josm.gui.layer.ImageryLayer; 038import org.openstreetmap.josm.gui.layer.MainLayerManager; 039import org.openstreetmap.josm.spi.preferences.Config; 040import org.openstreetmap.josm.tools.Logging; 041 042/** 043 * This panel displays a map and lets the user chose a {@link BBox}. 044 */ 045public class SlippyMapBBoxChooser extends JosmMapViewer implements BBoxChooser, ChangeListener, 046 MainLayerManager.ActiveLayerChangeListener, MainLayerManager.LayerChangeListener { 047 048 /** 049 * Plugins that wish to add custom tile sources to slippy map choose should call this method 050 * @param tileSourceProvider new tile source provider 051 */ 052 public static void addTileSourceProvider(TileSourceProvider tileSourceProvider) { 053 providers.addIfAbsent(tileSourceProvider); 054 } 055 056 private static CopyOnWriteArrayList<TileSourceProvider> providers = new CopyOnWriteArrayList<>(); 057 static { 058 addTileSourceProvider(new DefaultOsmTileSourceProvider()); 059 addTileSourceProvider(new TMSTileSourceProvider()); 060 addTileSourceProvider(new CurrentLayersTileSourceProvider()); 061 } 062 063 private static final StringProperty PROP_MAPSTYLE = new StringProperty("slippy_map_chooser.mapstyle", "Mapnik"); 064 private static final BooleanProperty PROP_SHOWDLAREA = new BooleanProperty("slippy_map_chooser.show_downloaded_area", true); 065 /** 066 * The property name used for the resize button. 067 * @see #addPropertyChangeListener(java.beans.PropertyChangeListener) 068 */ 069 public static final String RESIZE_PROP = SlippyMapBBoxChooser.class.getName() + ".resize"; 070 071 private final SizeButton iSizeButton; 072 private final ButtonModel showDownloadAreaButtonModel; 073 private final SourceButton iSourceButton; 074 private transient Bounds bbox; 075 076 // upper left and lower right corners of the selection rectangle (x/y on ZOOM_MAX) 077 private transient ICoordinate iSelectionRectStart; 078 private transient ICoordinate iSelectionRectEnd; 079 080 /** 081 * Constructs a new {@code SlippyMapBBoxChooser}. 082 */ 083 public SlippyMapBBoxChooser() { 084 debug = Logging.isDebugEnabled(); 085 SpringLayout springLayout = new SpringLayout(); 086 setLayout(springLayout); 087 088 setZoomControlsVisible(Config.getPref().getBoolean("slippy_map_chooser.zoomcontrols", false)); 089 setMapMarkerVisible(false); 090 setMinimumSize(new Dimension(350, 350 / 2)); 091 // We need to set an initial size - this prevents a wrong zoom selection 092 // for the area before the component has been displayed the first time 093 setBounds(new Rectangle(getMinimumSize())); 094 if (cachedLoader == null) { 095 setFileCacheEnabled(false); 096 } else { 097 setFileCacheEnabled(Config.getPref().getBoolean("slippy_map_chooser.file_cache", true)); 098 } 099 setMaxTilesInMemory(Config.getPref().getInt("slippy_map_chooser.max_tiles", 1000)); 100 101 List<TileSource> tileSources = new ArrayList<>(getAllTileSources().values()); 102 103 this.showDownloadAreaButtonModel = new JToggleButton.ToggleButtonModel(); 104 this.showDownloadAreaButtonModel.setSelected(PROP_SHOWDLAREA.get()); 105 this.showDownloadAreaButtonModel.addChangeListener(this); 106 iSourceButton = new SourceButton(this, tileSources, this.showDownloadAreaButtonModel); 107 add(iSourceButton); 108 springLayout.putConstraint(SpringLayout.EAST, iSourceButton, -2, SpringLayout.EAST, this); 109 springLayout.putConstraint(SpringLayout.NORTH, iSourceButton, 2, SpringLayout.NORTH, this); 110 111 iSizeButton = new SizeButton(this); 112 add(iSizeButton); 113 114 String mapStyle = PROP_MAPSTYLE.get(); 115 boolean foundSource = false; 116 for (TileSource source: tileSources) { 117 if (source.getName().equals(mapStyle)) { 118 this.setTileSource(source); 119 iSourceButton.setCurrentMap(source); 120 foundSource = true; 121 break; 122 } 123 } 124 if (!foundSource) { 125 setTileSource(tileSources.get(0)); 126 iSourceButton.setCurrentMap(tileSources.get(0)); 127 } 128 129 MainApplication.getLayerManager().addActiveLayerChangeListener(this); 130 131 new SlippyMapControler(this, this); 132 } 133 134 private static LinkedHashMap<String, TileSource> getAllTileSources() { 135 // using a LinkedHashMap of <id, TileSource> to retain ordering but provide deduplication 136 return providers.stream().flatMap( 137 provider -> provider.getTileSources().stream() 138 ).collect(Collectors.toMap( 139 TileSource::getId, 140 ts -> ts, 141 (oldTs, newTs) -> oldTs, 142 LinkedHashMap::new 143 )); 144 } 145 146 /** 147 * Handles a click/move on the attribution 148 * @param p The point in the view 149 * @param click true if it was a click, false for hover 150 * @return if the attribution handled the event 151 */ 152 public boolean handleAttribution(Point p, boolean click) { 153 return attribution.handleAttribution(p, click); 154 } 155 156 /** 157 * Draw the map. 158 */ 159 @Override 160 public void paintComponent(Graphics g) { 161 super.paintComponent(g); 162 Graphics2D g2d = (Graphics2D) g; 163 164 // draw shaded area for non-downloaded region of current data set, but only if there *is* a current data set, 165 // and it has defined bounds. Routine is analogous to that in OsmDataLayer's paint routine (but just different 166 // enough to make sharing code impractical) 167 final DataSet ds = MainApplication.getLayerManager().getActiveDataSet(); 168 if (ds != null && this.showDownloadAreaButtonModel.isSelected() && !ds.getDataSources().isEmpty()) { 169 // initialize area with current viewport 170 Rectangle b = this.getBounds(); 171 // ensure we comfortably cover full area 172 b.grow(100, 100); 173 Path2D p = new Path2D.Float(); 174 175 // combine successively downloaded areas after converting to screen-space 176 for (Bounds bounds : ds.getDataSourceBounds()) { 177 if (bounds.isCollapsed()) { 178 continue; 179 } 180 Rectangle r = new Rectangle(this.getMapPosition(bounds.getMinLat(), bounds.getMinLon(), false)); 181 r.add(this.getMapPosition(bounds.getMaxLat(), bounds.getMaxLon(), false)); 182 p.append(r, false); 183 } 184 // subtract combined areas 185 Area a = new Area(b); 186 a.subtract(new Area(p)); 187 188 // paint remainder 189 g2d.setPaint(new Color(0, 0, 0, 32)); 190 g2d.fill(a); 191 } 192 193 // draw selection rectangle 194 if (iSelectionRectStart != null && iSelectionRectEnd != null) { 195 Rectangle box = new Rectangle(getMapPosition(iSelectionRectStart, false)); 196 box.add(getMapPosition(iSelectionRectEnd, false)); 197 198 g.setColor(new Color(0.9f, 0.7f, 0.7f, 0.6f)); 199 g.fillRect(box.x, box.y, box.width, box.height); 200 201 g.setColor(Color.BLACK); 202 g.drawRect(box.x, box.y, box.width, box.height); 203 } 204 } 205 206 @Override 207 public void activeOrEditLayerChanged(MainLayerManager.ActiveLayerChangeEvent e) { 208 this.repaint(); 209 } 210 211 @Override 212 public void stateChanged(ChangeEvent e) { 213 // fired for the stateChanged event of this.showDownloadAreaButtonModel 214 PROP_SHOWDLAREA.put(this.showDownloadAreaButtonModel.isSelected()); 215 this.repaint(); 216 } 217 218 /** 219 * Callback for the OsmMapControl. (Re-)Sets the start and end point of the selection rectangle. 220 * 221 * @param aStart selection start 222 * @param aEnd selection end 223 */ 224 public void setSelection(Point aStart, Point aEnd) { 225 if (aStart == null || aEnd == null || aStart.x == aEnd.x || aStart.y == aEnd.y) 226 return; 227 228 Point pMax = new Point(Math.max(aEnd.x, aStart.x), Math.max(aEnd.y, aStart.y)); 229 Point pMin = new Point(Math.min(aEnd.x, aStart.x), Math.min(aEnd.y, aStart.y)); 230 231 iSelectionRectStart = getPosition(pMin); 232 iSelectionRectEnd = getPosition(pMax); 233 234 Bounds b = new Bounds( 235 new LatLon( 236 Math.min(iSelectionRectStart.getLat(), iSelectionRectEnd.getLat()), 237 LatLon.toIntervalLon(Math.min(iSelectionRectStart.getLon(), iSelectionRectEnd.getLon())) 238 ), 239 new LatLon( 240 Math.max(iSelectionRectStart.getLat(), iSelectionRectEnd.getLat()), 241 LatLon.toIntervalLon(Math.max(iSelectionRectStart.getLon(), iSelectionRectEnd.getLon()))) 242 ); 243 Bounds oldValue = this.bbox; 244 this.bbox = b; 245 repaint(); 246 firePropertyChange(BBOX_PROP, oldValue, this.bbox); 247 } 248 249 /** 250 * Performs resizing of the DownloadDialog in order to enlarge or shrink the 251 * map. 252 */ 253 public void resizeSlippyMap() { 254 boolean large = iSizeButton.isEnlarged(); 255 firePropertyChange(RESIZE_PROP, !large, large); 256 } 257 258 /** 259 * Sets the active tile source 260 * @param tileSource The active tile source 261 */ 262 public void toggleMapSource(TileSource tileSource) { 263 this.tileController.setTileCache(new MemoryTileCache()); 264 this.setTileSource(tileSource); 265 PROP_MAPSTYLE.put(tileSource.getName()); // TODO Is name really unique? 266 267 // we need to refresh the tile sources in case the deselected source should no longer be present 268 // (and only remained there because its removal was deferred while the source was still the 269 // selected one). this should also have the effect of propagating the new selection to the 270 // iSourceButton & menu: it attempts to re-select the current source when rebuilding its menu. 271 this.refreshTileSources(); 272 } 273 274 @Override 275 public Bounds getBoundingBox() { 276 return bbox; 277 } 278 279 /** 280 * Sets the current bounding box in this bbox chooser without 281 * emitting a property change event. 282 * 283 * @param bbox the bounding box. null to reset the bounding box 284 */ 285 @Override 286 public void setBoundingBox(Bounds bbox) { 287 if (bbox == null || (bbox.getMinLat() == 0 && bbox.getMinLon() == 0 288 && bbox.getMaxLat() == 0 && bbox.getMaxLon() == 0)) { 289 this.bbox = null; 290 iSelectionRectStart = null; 291 iSelectionRectEnd = null; 292 repaint(); 293 return; 294 } 295 296 this.bbox = bbox; 297 iSelectionRectStart = new Coordinate(bbox.getMinLat(), bbox.getMinLon()); 298 iSelectionRectEnd = new Coordinate(bbox.getMaxLat(), bbox.getMaxLon()); 299 300 // calc the screen coordinates for the new selection rectangle 301 MapMarkerDot min = new MapMarkerDot(bbox.getMinLat(), bbox.getMinLon()); 302 MapMarkerDot max = new MapMarkerDot(bbox.getMaxLat(), bbox.getMaxLon()); 303 304 List<MapMarker> marker = new ArrayList<>(2); 305 marker.add(min); 306 marker.add(max); 307 setMapMarkerList(marker); 308 setDisplayToFitMapMarkers(); 309 zoomOut(); 310 repaint(); 311 } 312 313 /** 314 * Enables or disables painting of the shrink/enlarge button 315 * 316 * @param visible {@code true} to enable painting of the shrink/enlarge button 317 */ 318 public void setSizeButtonVisible(boolean visible) { 319 iSizeButton.setVisible(visible); 320 } 321 322 /** 323 * Refreshes the tile sources 324 * @since 6364 325 */ 326 public final void refreshTileSources() { 327 final LinkedHashMap<String, TileSource> newTileSources = getAllTileSources(); 328 final TileSource currentTileSource = this.getTileController().getTileSource(); 329 330 // re-add the currently active TileSource to prevent inconsistent display of menu 331 newTileSources.putIfAbsent(currentTileSource.getId(), currentTileSource); 332 333 this.iSourceButton.setSources(new ArrayList<>(newTileSources.values())); 334 } 335 336 @Override 337 public void layerAdded(MainLayerManager.LayerAddEvent e) { 338 if (e.getAddedLayer() instanceof ImageryLayer) { 339 this.refreshTileSources(); 340 } 341 } 342 343 @Override 344 public void layerRemoving(MainLayerManager.LayerRemoveEvent e) { 345 if (e.getRemovedLayer() instanceof ImageryLayer) { 346 this.refreshTileSources(); 347 } 348 } 349 350 @Override 351 public void layerOrderChanged(MainLayerManager.LayerOrderChangeEvent e) { 352 // Do nothing 353 } 354}