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.MapQuestOpenAerialTileSource;
034import org.openstreetmap.gui.jmapviewer.tilesources.MapQuestOsmTileSource;
035import org.openstreetmap.gui.jmapviewer.tilesources.OsmTileSource;
036import org.openstreetmap.josm.Main;
037import org.openstreetmap.josm.data.Bounds;
038import org.openstreetmap.josm.data.Version;
039import org.openstreetmap.josm.data.coor.LatLon;
040import org.openstreetmap.josm.data.imagery.ImageryInfo;
041import org.openstreetmap.josm.data.imagery.ImageryLayerInfo;
042import org.openstreetmap.josm.data.imagery.TMSCachedTileLoader;
043import org.openstreetmap.josm.data.preferences.StringProperty;
044import org.openstreetmap.josm.gui.layer.AbstractCachedTileSourceLayer;
045import org.openstreetmap.josm.gui.layer.TMSLayer;
046
047public class SlippyMapBBoxChooser extends JMapViewer implements BBoxChooser {
048
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            existingSlippyMapUrls.add("http://otile{switch:1,2,3,4}.mqcdn.com/tiles/1.0.0/osm/{zoom}/{x}/{y}.png"); // MapQuest-OSM
063            existingSlippyMapUrls.add("http://oatile{switch:1,2,3,4}.mqcdn.com/tiles/1.0.0/sat/{zoom}/{x}/{y}.png"); // MapQuest Open Aerial
064        }
065
066        @Override
067        public List<TileSource> getTileSources() {
068            if (!TMSLayer.PROP_ADD_TO_SLIPPYMAP_CHOOSER.get()) return Collections.<TileSource>emptyList();
069            List<TileSource> sources = new ArrayList<>();
070            for (ImageryInfo info : ImageryLayerInfo.instance.getLayers()) {
071                if (existingSlippyMapUrls.contains(info.getUrl())) {
072                    continue;
073                }
074                try {
075                    TileSource source = TMSLayer.getTileSourceStatic(info);
076                    if (source != null) {
077                        sources.add(source);
078                    }
079                } catch (IllegalArgumentException ex) {
080                    if (ex.getMessage() != null && !ex.getMessage().isEmpty()) {
081                        JOptionPane.showMessageDialog(Main.parent,
082                                ex.getMessage(), tr("Warning"),
083                                JOptionPane.WARNING_MESSAGE);
084                    }
085                }
086            }
087            return sources;
088        }
089    }
090
091    /**
092     * Plugins that wish to add custom tile sources to slippy map choose should call this method
093     * @param tileSourceProvider new tile source provider
094     */
095    public static void addTileSourceProvider(TileSourceProvider tileSourceProvider) {
096        providers.addIfAbsent(tileSourceProvider);
097    }
098
099    private static CopyOnWriteArrayList<TileSourceProvider> providers = new CopyOnWriteArrayList<>();
100    static {
101        addTileSourceProvider(new TileSourceProvider() {
102            @Override
103            public List<TileSource> getTileSources() {
104                return Arrays.<TileSource>asList(
105                        new OsmTileSource.Mapnik(),
106                        new OsmTileSource.CycleMap(),
107                        new MapQuestOsmTileSource(),
108                        new MapQuestOpenAerialTileSource());
109            }
110        });
111        addTileSourceProvider(new TMSTileSourceProvider());
112    }
113
114    private static final StringProperty PROP_MAPSTYLE = new StringProperty("slippy_map_chooser.mapstyle", "Mapnik");
115    public static final String RESIZE_PROP = SlippyMapBBoxChooser.class.getName() + ".resize";
116
117    private final transient TileLoader cachedLoader;
118    private final transient OsmTileLoader uncachedLoader;
119
120    private final SizeButton iSizeButton;
121    private final SourceButton iSourceButton;
122    private transient Bounds bbox;
123
124    // upper left and lower right corners of the selection rectangle (x/y on ZOOM_MAX)
125    private transient ICoordinate iSelectionRectStart;
126    private transient ICoordinate iSelectionRectEnd;
127
128    /**
129     * Constructs a new {@code SlippyMapBBoxChooser}.
130     */
131    public SlippyMapBBoxChooser() {
132        debug = Main.isDebugEnabled();
133        SpringLayout springLayout = new SpringLayout();
134        setLayout(springLayout);
135
136        Map<String, String> headers = new HashMap<>();
137        headers.put("User-Agent", Version.getInstance().getFullAgentString());
138
139        cachedLoader = AbstractCachedTileSourceLayer.getTileLoaderFactory("TMS", TMSCachedTileLoader.class).makeTileLoader(this,  headers);
140
141        uncachedLoader = new OsmTileLoader(this);
142        uncachedLoader.headers.putAll(headers);
143        setZoomContolsVisible(Main.pref.getBoolean("slippy_map_chooser.zoomcontrols", false));
144        setMapMarkerVisible(false);
145        setMinimumSize(new Dimension(350, 350 / 2));
146        // We need to set an initial size - this prevents a wrong zoom selection
147        // for the area before the component has been displayed the first time
148        setBounds(new Rectangle(getMinimumSize()));
149        if (cachedLoader == null) {
150            setFileCacheEnabled(false);
151        } else {
152            setFileCacheEnabled(Main.pref.getBoolean("slippy_map_chooser.file_cache", true));
153        }
154        setMaxTilesInMemory(Main.pref.getInteger("slippy_map_chooser.max_tiles", 1000));
155
156        List<TileSource> tileSources = getAllTileSources();
157
158        iSourceButton = new SourceButton(this, tileSources);
159        add(iSourceButton);
160        springLayout.putConstraint(SpringLayout.EAST, iSourceButton, 0, SpringLayout.EAST, this);
161        springLayout.putConstraint(SpringLayout.NORTH, iSourceButton, 30, SpringLayout.NORTH, this);
162
163        iSizeButton = new SizeButton(this);
164        add(iSizeButton);
165
166        String mapStyle = PROP_MAPSTYLE.get();
167        boolean foundSource = false;
168        for (TileSource source: tileSources) {
169            if (source.getName().equals(mapStyle)) {
170                this.setTileSource(source);
171                iSourceButton.setCurrentMap(source);
172                foundSource = true;
173                break;
174            }
175        }
176        if (!foundSource) {
177            setTileSource(tileSources.get(0));
178            iSourceButton.setCurrentMap(tileSources.get(0));
179        }
180
181        new SlippyMapControler(this, this);
182    }
183
184    private List<TileSource> getAllTileSources() {
185        List<TileSource> tileSources = new ArrayList<>();
186        for (TileSourceProvider provider: providers) {
187            tileSources.addAll(provider.getTileSources());
188        }
189        return tileSources;
190    }
191
192    public boolean handleAttribution(Point p, boolean click) {
193        return attribution.handleAttribution(p, click);
194    }
195
196    /**
197     * Draw the map.
198     */
199    @Override
200    public void paint(Graphics g) {
201        super.paint(g);
202
203        // draw selection rectangle
204        if (iSelectionRectStart != null && iSelectionRectEnd != null) {
205            Rectangle box = new Rectangle(getMapPosition(iSelectionRectStart, false));
206            box.add(getMapPosition(iSelectionRectEnd, false));
207
208            g.setColor(new Color(0.9f, 0.7f, 0.7f, 0.6f));
209            g.fillRect(box.x, box.y, box.width, box.height);
210
211            g.setColor(Color.BLACK);
212            g.drawRect(box.x, box.y, box.width, box.height);
213        }
214    }
215
216    public final void setFileCacheEnabled(boolean enabled) {
217        if (enabled) {
218            setTileLoader(cachedLoader);
219        } else {
220            setTileLoader(uncachedLoader);
221        }
222    }
223
224    public final void setMaxTilesInMemory(int tiles) {
225        ((MemoryTileCache) getTileCache()).setCacheSize(tiles);
226    }
227
228    /**
229     * Callback for the OsmMapControl. (Re-)Sets the start and end point of the selection rectangle.
230     *
231     * @param aStart selection start
232     * @param aEnd selection end
233     */
234    public void setSelection(Point aStart, Point aEnd) {
235        if (aStart == null || aEnd == null || aStart.x == aEnd.x || aStart.y == aEnd.y)
236            return;
237
238        Point pMax = new Point(Math.max(aEnd.x, aStart.x), Math.max(aEnd.y, aStart.y));
239        Point pMin = new Point(Math.min(aEnd.x, aStart.x), Math.min(aEnd.y, aStart.y));
240
241        iSelectionRectStart = getPosition(pMin);
242        iSelectionRectEnd =   getPosition(pMax);
243
244        Bounds b = new Bounds(
245                new LatLon(
246                        Math.min(iSelectionRectStart.getLat(), iSelectionRectEnd.getLat()),
247                        LatLon.toIntervalLon(Math.min(iSelectionRectStart.getLon(), iSelectionRectEnd.getLon()))
248                        ),
249                        new LatLon(
250                                Math.max(iSelectionRectStart.getLat(), iSelectionRectEnd.getLat()),
251                                LatLon.toIntervalLon(Math.max(iSelectionRectStart.getLon(), iSelectionRectEnd.getLon())))
252                );
253        Bounds oldValue = this.bbox;
254        this.bbox = b;
255        repaint();
256        firePropertyChange(BBOX_PROP, oldValue, this.bbox);
257    }
258
259    /**
260     * Performs resizing of the DownloadDialog in order to enlarge or shrink the
261     * map.
262     */
263    public void resizeSlippyMap() {
264        boolean large = iSizeButton.isEnlarged();
265        firePropertyChange(RESIZE_PROP, !large, large);
266    }
267
268    public void toggleMapSource(TileSource tileSource) {
269        this.tileController.setTileCache(new MemoryTileCache());
270        this.setTileSource(tileSource);
271        PROP_MAPSTYLE.put(tileSource.getName()); // TODO Is name really unique?
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     * emiting 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        iSourceButton.setSources(getAllTileSources());
328    }
329}