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

Last change on this file since 12927 was 12927, checked in by bastiK, 2 months ago

applied #15369 - download area display in SlippyMapBBoxChooser (patch by ris)

  • Property svn:eol-style set to native
File size: 15.0 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.bbox;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.Color;
7import java.awt.Dimension;
8import java.awt.Graphics;
9import java.awt.Graphics2D;
10import java.awt.Point;
11import java.awt.Rectangle;
12import java.awt.geom.Area;
13import java.awt.geom.Path2D;
14import java.util.ArrayList;
15import java.util.Arrays;
16import java.util.Collections;
17import java.util.HashMap;
18import java.util.HashSet;
19import java.util.List;
20import java.util.Map;
21import java.util.Set;
22import java.util.concurrent.CopyOnWriteArrayList;
23
24import javax.swing.JOptionPane;
25import javax.swing.SpringLayout;
26
27import org.openstreetmap.gui.jmapviewer.Coordinate;
28import org.openstreetmap.gui.jmapviewer.JMapViewer;
29import org.openstreetmap.gui.jmapviewer.MapMarkerDot;
30import org.openstreetmap.gui.jmapviewer.MemoryTileCache;
31import org.openstreetmap.gui.jmapviewer.OsmTileLoader;
32import org.openstreetmap.gui.jmapviewer.interfaces.ICoordinate;
33import org.openstreetmap.gui.jmapviewer.interfaces.MapMarker;
34import org.openstreetmap.gui.jmapviewer.interfaces.TileLoader;
35import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
36import org.openstreetmap.gui.jmapviewer.tilesources.OsmTileSource;
37import org.openstreetmap.josm.Main;
38import org.openstreetmap.josm.data.Bounds;
39import org.openstreetmap.josm.data.Version;
40import org.openstreetmap.josm.data.coor.LatLon;
41import org.openstreetmap.josm.data.imagery.ImageryInfo;
42import org.openstreetmap.josm.data.imagery.ImageryLayerInfo;
43import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader;
44import org.openstreetmap.josm.data.imagery.TileLoaderFactory;
45import org.openstreetmap.josm.data.osm.BBox;
46import org.openstreetmap.josm.data.preferences.StringProperty;
47import org.openstreetmap.josm.gui.MainApplication;
48import org.openstreetmap.josm.gui.layer.AbstractCachedTileSourceLayer;
49import org.openstreetmap.josm.gui.layer.MainLayerManager;
50import org.openstreetmap.josm.gui.layer.OsmDataLayer;
51import org.openstreetmap.josm.gui.layer.TMSLayer;
52import org.openstreetmap.josm.spi.preferences.Config;
53import org.openstreetmap.josm.tools.Logging;
54
55/**
56 * This panel displays a map and lets the user chose a {@link BBox}.
57 */
58public class SlippyMapBBoxChooser extends JMapViewer implements BBoxChooser, MainLayerManager.ActiveLayerChangeListener {
59
60    /**
61     * A list of tile sources that can be used for displaying the map.
62     */
63    @FunctionalInterface
64    public interface TileSourceProvider {
65        /**
66         * Gets the tile sources that can be displayed
67         * @return The tile sources
68         */
69        List<TileSource> getTileSources();
70    }
71
72    /**
73     * TMS TileSource provider for the slippymap chooser
74     */
75    public static class TMSTileSourceProvider implements TileSourceProvider {
76        private static final Set<String> existingSlippyMapUrls = new HashSet<>();
77        static {
78            // Urls that already exist in the slippymap chooser and shouldn't be copied from TMS layer list
79            existingSlippyMapUrls.add("https://{switch:a,b,c}.tile.openstreetmap.org/{zoom}/{x}/{y}.png");      // Mapnik
80        }
81
82        @Override
83        public List<TileSource> getTileSources() {
84            if (!TMSLayer.PROP_ADD_TO_SLIPPYMAP_CHOOSER.get()) return Collections.<TileSource>emptyList();
85            List<TileSource> sources = new ArrayList<>();
86            for (ImageryInfo info : ImageryLayerInfo.instance.getLayers()) {
87                if (existingSlippyMapUrls.contains(info.getUrl())) {
88                    continue;
89                }
90                try {
91                    TileSource source = TMSLayer.getTileSourceStatic(info);
92                    if (source != null) {
93                        sources.add(source);
94                    }
95                } catch (IllegalArgumentException ex) {
96                    Logging.warn(ex);
97                    if (ex.getMessage() != null && !ex.getMessage().isEmpty()) {
98                        JOptionPane.showMessageDialog(Main.parent,
99                                ex.getMessage(), tr("Warning"),
100                                JOptionPane.WARNING_MESSAGE);
101                    }
102                }
103            }
104            return sources;
105        }
106    }
107
108    /**
109     * Plugins that wish to add custom tile sources to slippy map choose should call this method
110     * @param tileSourceProvider new tile source provider
111     */
112    public static void addTileSourceProvider(TileSourceProvider tileSourceProvider) {
113        providers.addIfAbsent(tileSourceProvider);
114    }
115
116    private static CopyOnWriteArrayList<TileSourceProvider> providers = new CopyOnWriteArrayList<>();
117    static {
118        addTileSourceProvider(() -> Arrays.<TileSource>asList(new OsmTileSource.Mapnik()));
119        addTileSourceProvider(new TMSTileSourceProvider());
120    }
121
122    private static final StringProperty PROP_MAPSTYLE = new StringProperty("slippy_map_chooser.mapstyle", "Mapnik");
123    /**
124     * The property name used for the resize button.
125     * @see #addPropertyChangeListener(java.beans.PropertyChangeListener)
126     */
127    public static final String RESIZE_PROP = SlippyMapBBoxChooser.class.getName() + ".resize";
128
129    private final transient TileLoader cachedLoader;
130    private final transient OsmTileLoader uncachedLoader;
131
132    private final SizeButton iSizeButton;
133    private final SourceButton iSourceButton;
134    private transient Bounds bbox;
135
136    // upper left and lower right corners of the selection rectangle (x/y on ZOOM_MAX)
137    private transient ICoordinate iSelectionRectStart;
138    private transient ICoordinate iSelectionRectEnd;
139
140    /**
141     * Constructs a new {@code SlippyMapBBoxChooser}.
142     */
143    public SlippyMapBBoxChooser() {
144        debug = Logging.isDebugEnabled();
145        SpringLayout springLayout = new SpringLayout();
146        setLayout(springLayout);
147
148        Map<String, String> headers = new HashMap<>();
149        headers.put("User-Agent", Version.getInstance().getFullAgentString());
150
151        TileLoaderFactory cachedLoaderFactory = AbstractCachedTileSourceLayer.getTileLoaderFactory("TMS", TMSCachedTileLoader.class);
152        if (cachedLoaderFactory != null) {
153            cachedLoader = cachedLoaderFactory.makeTileLoader(this, headers);
154        } else {
155            cachedLoader = null;
156        }
157
158        uncachedLoader = new OsmTileLoader(this);
159        uncachedLoader.headers.putAll(headers);
160        setZoomContolsVisible(Config.getPref().getBoolean("slippy_map_chooser.zoomcontrols", false));
161        setMapMarkerVisible(false);
162        setMinimumSize(new Dimension(350, 350 / 2));
163        // We need to set an initial size - this prevents a wrong zoom selection
164        // for the area before the component has been displayed the first time
165        setBounds(new Rectangle(getMinimumSize()));
166        if (cachedLoader == null) {
167            setFileCacheEnabled(false);
168        } else {
169            setFileCacheEnabled(Config.getPref().getBoolean("slippy_map_chooser.file_cache", true));
170        }
171        setMaxTilesInMemory(Config.getPref().getInt("slippy_map_chooser.max_tiles", 1000));
172
173        List<TileSource> tileSources = getAllTileSources();
174
175        iSourceButton = new SourceButton(this, tileSources);
176        add(iSourceButton);
177        springLayout.putConstraint(SpringLayout.EAST, iSourceButton, 0, SpringLayout.EAST, this);
178        springLayout.putConstraint(SpringLayout.NORTH, iSourceButton, 30, SpringLayout.NORTH, this);
179
180        iSizeButton = new SizeButton(this);
181        add(iSizeButton);
182
183        String mapStyle = PROP_MAPSTYLE.get();
184        boolean foundSource = false;
185        for (TileSource source: tileSources) {
186            if (source.getName().equals(mapStyle)) {
187                this.setTileSource(source);
188                iSourceButton.setCurrentMap(source);
189                foundSource = true;
190                break;
191            }
192        }
193        if (!foundSource) {
194            setTileSource(tileSources.get(0));
195            iSourceButton.setCurrentMap(tileSources.get(0));
196        }
197
198        MainApplication.getLayerManager().addActiveLayerChangeListener(this);
199
200        new SlippyMapControler(this, this);
201    }
202
203    private static List<TileSource> getAllTileSources() {
204        List<TileSource> tileSources = new ArrayList<>();
205        for (TileSourceProvider provider: providers) {
206            tileSources.addAll(provider.getTileSources());
207        }
208        return tileSources;
209    }
210
211    /**
212     * Handles a click/move on the attribution
213     * @param p The point in the view
214     * @param click true if it was a click, false for hover
215     * @return if the attribution handled the event
216     */
217    public boolean handleAttribution(Point p, boolean click) {
218        return attribution.handleAttribution(p, click);
219    }
220
221    /**
222     * Draw the map.
223     */
224    @Override
225    public void paintComponent(Graphics g) {
226        super.paintComponent(g);
227        Graphics2D g2d = (Graphics2D)g;
228
229        // draw shaded area for non-downloaded region of current "edit layer", but only if there *is* a current "edit layer",
230        // and it has defined bounds. Routine is analogous to that in OsmDataLayer's paint routine (but just different
231        // enough to make sharing code impractical)
232        final OsmDataLayer editLayer = MainApplication.getLayerManager().getEditLayer();
233        if (editLayer != null && Config.getPref().getBoolean("draw.data.downloaded_area", true) && !editLayer.data.getDataSources().isEmpty()) {
234            // initialize area with current viewport
235            Rectangle b = this.getBounds();
236            // ensure we comfortably cover full area
237            b.grow(100, 100);
238            Path2D p = new Path2D.Float();
239
240            // combine successively downloaded areas after converting to screen-space
241            for (Bounds bounds : editLayer.data.getDataSourceBounds()) {
242                if (bounds.isCollapsed()) {
243                    continue;
244                }
245                Rectangle r = new Rectangle(this.getMapPosition(bounds.getMinLat(), bounds.getMinLon(), false));
246                r.add(this.getMapPosition(bounds.getMaxLat(), bounds.getMaxLon(), false));
247                p.append(r, false);
248            }
249            // subtract combined areas
250            Area a = new Area(b);
251            a.subtract(new Area(p));
252
253            // paint remainder
254            g2d.setPaint(new Color(0, 0, 0, 32));
255            g2d.fill(a);
256        }
257
258        // draw selection rectangle
259        if (iSelectionRectStart != null && iSelectionRectEnd != null) {
260            Rectangle box = new Rectangle(getMapPosition(iSelectionRectStart, false));
261            box.add(getMapPosition(iSelectionRectEnd, false));
262
263            g.setColor(new Color(0.9f, 0.7f, 0.7f, 0.6f));
264            g.fillRect(box.x, box.y, box.width, box.height);
265
266            g.setColor(Color.BLACK);
267            g.drawRect(box.x, box.y, box.width, box.height);
268        }
269    }
270
271    @Override
272    public void activeOrEditLayerChanged(MainLayerManager.ActiveLayerChangeEvent e) {
273        this.repaint();
274    }
275
276    /**
277     * Enables the disk tile cache.
278     * @param enabled true to enable, false to disable
279     */
280    public final void setFileCacheEnabled(boolean enabled) {
281        if (enabled && cachedLoader != null) {
282            setTileLoader(cachedLoader);
283        } else {
284            setTileLoader(uncachedLoader);
285        }
286    }
287
288    /**
289     * Sets the maximum number of tiles that may be held in memory
290     * @param tiles The maximum number of tiles.
291     */
292    public final void setMaxTilesInMemory(int tiles) {
293        ((MemoryTileCache) getTileCache()).setCacheSize(tiles);
294    }
295
296    /**
297     * Callback for the OsmMapControl. (Re-)Sets the start and end point of the selection rectangle.
298     *
299     * @param aStart selection start
300     * @param aEnd selection end
301     */
302    public void setSelection(Point aStart, Point aEnd) {
303        if (aStart == null || aEnd == null || aStart.x == aEnd.x || aStart.y == aEnd.y)
304            return;
305
306        Point pMax = new Point(Math.max(aEnd.x, aStart.x), Math.max(aEnd.y, aStart.y));
307        Point pMin = new Point(Math.min(aEnd.x, aStart.x), Math.min(aEnd.y, aStart.y));
308
309        iSelectionRectStart = getPosition(pMin);
310        iSelectionRectEnd = getPosition(pMax);
311
312        Bounds b = new Bounds(
313                new LatLon(
314                        Math.min(iSelectionRectStart.getLat(), iSelectionRectEnd.getLat()),
315                        LatLon.toIntervalLon(Math.min(iSelectionRectStart.getLon(), iSelectionRectEnd.getLon()))
316                        ),
317                        new LatLon(
318                                Math.max(iSelectionRectStart.getLat(), iSelectionRectEnd.getLat()),
319                                LatLon.toIntervalLon(Math.max(iSelectionRectStart.getLon(), iSelectionRectEnd.getLon())))
320                );
321        Bounds oldValue = this.bbox;
322        this.bbox = b;
323        repaint();
324        firePropertyChange(BBOX_PROP, oldValue, this.bbox);
325    }
326
327    /**
328     * Performs resizing of the DownloadDialog in order to enlarge or shrink the
329     * map.
330     */
331    public void resizeSlippyMap() {
332        boolean large = iSizeButton.isEnlarged();
333        firePropertyChange(RESIZE_PROP, !large, large);
334    }
335
336    /**
337     * Sets the active tile source
338     * @param tileSource The active tile source
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 && bbox.getMinLon() == 0
360                && bbox.getMaxLat() == 0 && bbox.getMaxLon() == 0)) {
361            this.bbox = null;
362            iSelectionRectStart = null;
363            iSelectionRectEnd = null;
364            repaint();
365            return;
366        }
367
368        this.bbox = bbox;
369        iSelectionRectStart = new Coordinate(bbox.getMinLat(), bbox.getMinLon());
370        iSelectionRectEnd = new Coordinate(bbox.getMaxLat(), bbox.getMaxLon());
371
372        // calc the screen coordinates for the new selection rectangle
373        MapMarkerDot min = new MapMarkerDot(bbox.getMinLat(), bbox.getMinLon());
374        MapMarkerDot max = new MapMarkerDot(bbox.getMaxLat(), bbox.getMaxLon());
375
376        List<MapMarker> marker = new ArrayList<>(2);
377        marker.add(min);
378        marker.add(max);
379        setMapMarkerList(marker);
380        setDisplayToFitMapMarkers();
381        zoomOut();
382        repaint();
383    }
384
385    /**
386     * Enables or disables painting of the shrink/enlarge button
387     *
388     * @param visible {@code true} to enable painting of the shrink/enlarge button
389     */
390    public void setSizeButtonVisible(boolean visible) {
391        iSizeButton.setVisible(visible);
392    }
393
394    /**
395     * Refreshes the tile sources
396     * @since 6364
397     */
398    public final void refreshTileSources() {
399        iSourceButton.setSources(getAllTileSources());
400    }
401}
Note: See TracBrowser for help on using the repository browser.