source: josm/trunk/src/org/openstreetmap/josm/gui/bbox/SlippyMapBBoxChooser.java@ 15145

Last change on this file since 15145 was 15145, checked in by Don-vip, 5 years ago

see #17772 - make sure all JMapViewer instances in JOSM behave the same

  • Property svn:eol-style set to native
File size: 13.6 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.bbox;
3
4import java.awt.Color;
5import java.awt.Dimension;
6import java.awt.Graphics;
7import java.awt.Graphics2D;
8import java.awt.Point;
9import java.awt.Rectangle;
10import java.awt.geom.Area;
11import java.awt.geom.Path2D;
12import java.util.ArrayList;
13import java.util.LinkedHashMap;
14import java.util.List;
15import java.util.concurrent.CopyOnWriteArrayList;
16import java.util.stream.Collectors;
17
18import javax.swing.ButtonModel;
19import javax.swing.JToggleButton;
20import javax.swing.SpringLayout;
21import javax.swing.event.ChangeEvent;
22import javax.swing.event.ChangeListener;
23
24import org.openstreetmap.gui.jmapviewer.Coordinate;
25import org.openstreetmap.gui.jmapviewer.MapMarkerDot;
26import org.openstreetmap.gui.jmapviewer.MemoryTileCache;
27import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
28import org.openstreetmap.gui.jmapviewer.interfaces.MapMarker;
29import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
30import org.openstreetmap.josm.data.Bounds;
31import org.openstreetmap.josm.data.coor.LatLon;
32import org.openstreetmap.josm.data.osm.BBox;
33import org.openstreetmap.josm.data.osm.DataSet;
34import org.openstreetmap.josm.data.preferences.BooleanProperty;
35import org.openstreetmap.josm.data.preferences.StringProperty;
36import org.openstreetmap.josm.gui.MainApplication;
37import org.openstreetmap.josm.gui.layer.ImageryLayer;
38import org.openstreetmap.josm.gui.layer.MainLayerManager;
39import org.openstreetmap.josm.spi.preferences.Config;
40import org.openstreetmap.josm.tools.Logging;
41
42/**
43 * This panel displays a map and lets the user chose a {@link BBox}.
44 */
45public class SlippyMapBBoxChooser extends JosmMapViewer implements BBoxChooser, ChangeListener,
46 MainLayerManager.ActiveLayerChangeListener, MainLayerManager.LayerChangeListener {
47
48 /**
49 * Plugins that wish to add custom tile sources to slippy map choose should call this method
50 * @param tileSourceProvider new tile source provider
51 */
52 public static void addTileSourceProvider(TileSourceProvider tileSourceProvider) {
53 providers.addIfAbsent(tileSourceProvider);
54 }
55
56 private static CopyOnWriteArrayList<TileSourceProvider> providers = new CopyOnWriteArrayList<>();
57 static {
58 addTileSourceProvider(new DefaultOsmTileSourceProvider());
59 addTileSourceProvider(new TMSTileSourceProvider());
60 addTileSourceProvider(new CurrentLayersTileSourceProvider());
61 }
62
63 private static final StringProperty PROP_MAPSTYLE = new StringProperty("slippy_map_chooser.mapstyle", "Mapnik");
64 private static final BooleanProperty PROP_SHOWDLAREA = new BooleanProperty("slippy_map_chooser.show_downloaded_area", true);
65 /**
66 * The property name used for the resize button.
67 * @see #addPropertyChangeListener(java.beans.PropertyChangeListener)
68 */
69 public static final String RESIZE_PROP = SlippyMapBBoxChooser.class.getName() + ".resize";
70
71 private final SizeButton iSizeButton;
72 private final ButtonModel showDownloadAreaButtonModel;
73 private final SourceButton iSourceButton;
74 private transient Bounds bbox;
75
76 // upper left and lower right corners of the selection rectangle (x/y on ZOOM_MAX)
77 private transient ICoordinate iSelectionRectStart;
78 private transient ICoordinate iSelectionRectEnd;
79
80 /**
81 * Constructs a new {@code SlippyMapBBoxChooser}.
82 */
83 public SlippyMapBBoxChooser() {
84 debug = Logging.isDebugEnabled();
85 SpringLayout springLayout = new SpringLayout();
86 setLayout(springLayout);
87
88 setZoomControlsVisible(Config.getPref().getBoolean("slippy_map_chooser.zoomcontrols", false));
89 setMapMarkerVisible(false);
90 setMinimumSize(new Dimension(350, 350 / 2));
91 // We need to set an initial size - this prevents a wrong zoom selection
92 // for the area before the component has been displayed the first time
93 setBounds(new Rectangle(getMinimumSize()));
94 if (cachedLoader == null) {
95 setFileCacheEnabled(false);
96 } else {
97 setFileCacheEnabled(Config.getPref().getBoolean("slippy_map_chooser.file_cache", true));
98 }
99 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}
Note: See TracBrowser for help on using the repository browser.