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        public static void addExistingSlippyMapUrl(String url) {
091            existingSlippyMapUrls.add(url);
092        }
093    }
094
095    /**
096     * Plugins that wish to add custom tile sources to slippy map choose should call this method
097     * @param tileSourceProvider new tile source provider
098     */
099    public static void addTileSourceProvider(TileSourceProvider tileSourceProvider) {
100        providers.addIfAbsent(tileSourceProvider);
101    }
102
103    private static CopyOnWriteArrayList<TileSourceProvider> providers = new CopyOnWriteArrayList<>();
104    static {
105        addTileSourceProvider(new TileSourceProvider() {
106            @Override
107            public List<TileSource> getTileSources() {
108                return Arrays.<TileSource>asList(
109                        new OsmTileSource.Mapnik(),
110                        new OsmTileSource.CycleMap(),
111                        new MapQuestOsmTileSource(),
112                        new MapQuestOpenAerialTileSource());
113            }
114        });
115        addTileSourceProvider(new TMSTileSourceProvider());
116    }
117
118    private static final StringProperty PROP_MAPSTYLE = new StringProperty("slippy_map_chooser.mapstyle", "Mapnik");
119    public static final String RESIZE_PROP = SlippyMapBBoxChooser.class.getName() + ".resize";
120
121    private transient TileLoader cachedLoader;
122    private transient OsmTileLoader uncachedLoader;
123
124    private final SizeButton iSizeButton;
125    private final SourceButton iSourceButton;
126    private transient Bounds bbox;
127
128    // upper left and lower right corners of the selection rectangle (x/y on ZOOM_MAX)
129    private ICoordinate iSelectionRectStart;
130    private ICoordinate iSelectionRectEnd;
131
132    /**
133     * Constructs a new {@code SlippyMapBBoxChooser}.
134     */
135    public SlippyMapBBoxChooser() {
136        debug = Main.isDebugEnabled();
137        SpringLayout springLayout = new SpringLayout();
138        setLayout(springLayout);
139
140        Map<String, String> headers = new HashMap<>();
141        headers.put("User-Agent", Version.getInstance().getFullAgentString());
142
143        cachedLoader = AbstractCachedTileSourceLayer.getTileLoaderFactory("TMS", TMSCachedTileLoader.class).makeTileLoader(this,  headers);
144
145        uncachedLoader = new OsmTileLoader(this);
146        uncachedLoader.headers.putAll(headers);
147        setZoomContolsVisible(Main.pref.getBoolean("slippy_map_chooser.zoomcontrols", false));
148        setMapMarkerVisible(false);
149        setMinimumSize(new Dimension(350, 350 / 2));
150        // We need to set an initial size - this prevents a wrong zoom selection
151        // for the area before the component has been displayed the first time
152        setBounds(new Rectangle(getMinimumSize()));
153        if (cachedLoader == null) {
154            setFileCacheEnabled(false);
155        } else {
156            setFileCacheEnabled(Main.pref.getBoolean("slippy_map_chooser.file_cache", true));
157        }
158        setMaxTilesInMemory(Main.pref.getInteger("slippy_map_chooser.max_tiles", 1000));
159
160        List<TileSource> tileSources = getAllTileSources();
161
162        iSourceButton = new SourceButton(this, tileSources);
163        add(iSourceButton);
164        springLayout.putConstraint(SpringLayout.EAST, iSourceButton, 0, SpringLayout.EAST, this);
165        springLayout.putConstraint(SpringLayout.NORTH, iSourceButton, 30, SpringLayout.NORTH, this);
166
167        iSizeButton = new SizeButton(this);
168        add(iSizeButton);
169
170        String mapStyle = PROP_MAPSTYLE.get();
171        boolean foundSource = false;
172        for (TileSource source: tileSources) {
173            if (source.getName().equals(mapStyle)) {
174                this.setTileSource(source);
175                iSourceButton.setCurrentMap(source);
176                foundSource = true;
177                break;
178            }
179        }
180        if (!foundSource) {
181            setTileSource(tileSources.get(0));
182            iSourceButton.setCurrentMap(tileSources.get(0));
183        }
184
185        new SlippyMapControler(this, this);
186    }
187
188    private List<TileSource> getAllTileSources() {
189        List<TileSource> tileSources = new ArrayList<>();
190        for (TileSourceProvider provider: providers) {
191            tileSources.addAll(provider.getTileSources());
192        }
193        return tileSources;
194    }
195
196    public boolean handleAttribution(Point p, boolean click) {
197        return attribution.handleAttribution(p, click);
198    }
199
200    /**
201     * Draw the map.
202     */
203    @Override
204    public void paint(Graphics g) {
205        try {
206            super.paint(g);
207
208            // draw selection rectangle
209            if (iSelectionRectStart != null && iSelectionRectEnd != null) {
210                Rectangle box = new Rectangle(getMapPosition(iSelectionRectStart, false));
211                box.add(getMapPosition(iSelectionRectEnd, false));
212
213                g.setColor(new Color(0.9f, 0.7f, 0.7f, 0.6f));
214                g.fillRect(box.x, box.y, box.width, box.height);
215
216                g.setColor(Color.BLACK);
217                g.drawRect(box.x, box.y, box.width, box.height);
218            }
219        } catch (Exception e) {
220            Main.error(e);
221        }
222    }
223
224    public final void setFileCacheEnabled(boolean enabled) {
225        if (enabled) {
226            setTileLoader(cachedLoader);
227        } else {
228            setTileLoader(uncachedLoader);
229        }
230    }
231
232    public final void setMaxTilesInMemory(int tiles) {
233        ((MemoryTileCache) getTileCache()).setCacheSize(tiles);
234    }
235
236    /**
237     * Callback for the OsmMapControl. (Re-)Sets the start and end point of the selection rectangle.
238     *
239     * @param aStart selection start
240     * @param aEnd selection end
241     */
242    public void setSelection(Point aStart, Point aEnd) {
243        if (aStart == null || aEnd == null || aStart.x == aEnd.x || aStart.y == aEnd.y)
244            return;
245
246        Point p_max = new Point(Math.max(aEnd.x, aStart.x), Math.max(aEnd.y, aStart.y));
247        Point p_min = new Point(Math.min(aEnd.x, aStart.x), Math.min(aEnd.y, aStart.y));
248
249        iSelectionRectStart = getPosition(p_min);
250        iSelectionRectEnd =   getPosition(p_max);
251
252        Bounds b = new Bounds(
253                new LatLon(
254                        Math.min(iSelectionRectStart.getLat(), iSelectionRectEnd.getLat()),
255                        LatLon.toIntervalLon(Math.min(iSelectionRectStart.getLon(), iSelectionRectEnd.getLon()))
256                        ),
257                        new LatLon(
258                                Math.max(iSelectionRectStart.getLat(), iSelectionRectEnd.getLat()),
259                                LatLon.toIntervalLon(Math.max(iSelectionRectStart.getLon(), iSelectionRectEnd.getLon())))
260                );
261        Bounds oldValue = this.bbox;
262        this.bbox = b;
263        repaint();
264        firePropertyChange(BBOX_PROP, oldValue, this.bbox);
265    }
266
267    /**
268     * Performs resizing of the DownloadDialog in order to enlarge or shrink the
269     * map.
270     */
271    public void resizeSlippyMap() {
272        boolean large = iSizeButton.isEnlarged();
273        firePropertyChange(RESIZE_PROP, !large, large);
274    }
275
276    public void toggleMapSource(TileSource tileSource) {
277        this.tileController.setTileCache(new MemoryTileCache());
278        this.setTileSource(tileSource);
279        PROP_MAPSTYLE.put(tileSource.getName()); // TODO Is name really unique?
280    }
281
282    @Override
283    public Bounds getBoundingBox() {
284        return bbox;
285    }
286
287    /**
288     * Sets the current bounding box in this bbox chooser without
289     * emiting a property change event.
290     *
291     * @param bbox the bounding box. null to reset the bounding box
292     */
293    @Override
294    public void setBoundingBox(Bounds bbox) {
295        if (bbox == null || (bbox.getMinLat() == 0 && bbox.getMinLon() == 0
296                && bbox.getMaxLat() == 0 && bbox.getMaxLon() == 0)) {
297            this.bbox = null;
298            iSelectionRectStart = null;
299            iSelectionRectEnd = null;
300            repaint();
301            return;
302        }
303
304        this.bbox = bbox;
305        iSelectionRectStart = new Coordinate(bbox.getMinLat(), bbox.getMinLon());
306        iSelectionRectEnd = new Coordinate(bbox.getMaxLat(), bbox.getMaxLon());
307
308        // calc the screen coordinates for the new selection rectangle
309        MapMarkerDot min = new MapMarkerDot(bbox.getMinLat(), bbox.getMinLon());
310        MapMarkerDot max = new MapMarkerDot(bbox.getMaxLat(), bbox.getMaxLon());
311
312        List<MapMarker> marker = new ArrayList<>(2);
313        marker.add(min);
314        marker.add(max);
315        setMapMarkerList(marker);
316        setDisplayToFitMapMarkers();
317        zoomOut();
318        repaint();
319    }
320
321    /**
322     * Enables or disables painting of the shrink/enlarge button
323     *
324     * @param visible {@code true} to enable painting of the shrink/enlarge button
325     */
326    public void setSizeButtonVisible(boolean visible) {
327        iSizeButton.setVisible(visible);
328    }
329
330    /**
331     * Refreshes the tile sources
332     * @since 6364
333     */
334    public final void refreshTileSources() {
335        iSourceButton.setSources(getAllTileSources());
336    }
337}