001// License: GPL. See LICENSE file for details.
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.Graphics2D;
010import java.awt.Image;
011import java.awt.Point;
012import java.awt.Rectangle;
013import java.io.IOException;
014import java.util.ArrayList;
015import java.util.Arrays;
016import java.util.Collections;
017import java.util.HashSet;
018import java.util.List;
019import java.util.Set;
020import java.util.concurrent.CopyOnWriteArrayList;
021
022import javax.swing.JOptionPane;
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.OsmMercator;
029import org.openstreetmap.gui.jmapviewer.OsmTileLoader;
030import org.openstreetmap.gui.jmapviewer.interfaces.MapMarker;
031import org.openstreetmap.gui.jmapviewer.interfaces.TileSource;
032import org.openstreetmap.gui.jmapviewer.tilesources.MapQuestOpenAerialTileSource;
033import org.openstreetmap.gui.jmapviewer.tilesources.MapQuestOsmTileSource;
034import org.openstreetmap.gui.jmapviewer.tilesources.OsmTileSource;
035import org.openstreetmap.josm.Main;
036import org.openstreetmap.josm.data.Bounds;
037import org.openstreetmap.josm.data.Version;
038import org.openstreetmap.josm.data.coor.LatLon;
039import org.openstreetmap.josm.data.imagery.ImageryInfo;
040import org.openstreetmap.josm.data.imagery.ImageryLayerInfo;
041import org.openstreetmap.josm.data.preferences.StringProperty;
042import org.openstreetmap.josm.gui.layer.TMSLayer;
043
044public class SlippyMapBBoxChooser extends JMapViewer implements BBoxChooser {
045
046    public interface TileSourceProvider {
047        List<TileSource> getTileSources();
048    }
049
050    public static class RenamedSourceDecorator implements TileSource {
051
052        private final TileSource source;
053        private final String name;
054
055        public RenamedSourceDecorator(TileSource source, String name) {
056            this.source = source;
057            this.name = name;
058        }
059
060        @Override public String getName() {
061            return name;
062        }
063
064        @Override public int getMaxZoom() { return source.getMaxZoom(); }
065
066        @Override public int getMinZoom() { return source.getMinZoom(); }
067
068        @Override public int getTileSize() { return source.getTileSize(); }
069
070        @Override public String getTileType() { return source.getTileType(); }
071
072        @Override public TileUpdate getTileUpdate() { return source.getTileUpdate(); }
073
074        @Override public String getTileUrl(int zoom, int tilex, int tiley) throws IOException { return source.getTileUrl(zoom, tilex, tiley); }
075
076        @Override public boolean requiresAttribution() { return source.requiresAttribution(); }
077
078        @Override public String getAttributionText(int zoom, Coordinate topLeft, Coordinate botRight) { return source.getAttributionText(zoom, topLeft, botRight); }
079
080        @Override public String getAttributionLinkURL() { return source.getAttributionLinkURL(); }
081
082        @Override public Image getAttributionImage() { return source.getAttributionImage(); }
083
084        @Override public String getAttributionImageURL() { return source.getAttributionImageURL(); }
085
086        @Override public String getTermsOfUseText() { return source.getTermsOfUseText(); }
087
088        @Override public String getTermsOfUseURL() { return source.getTermsOfUseURL(); }
089
090        @Override public double latToTileY(double lat, int zoom) { return source.latToTileY(lat,zoom); }
091
092        @Override public double lonToTileX(double lon, int zoom) { return source.lonToTileX(lon,zoom); }
093
094        @Override public double tileYToLat(int y, int zoom) { return source.tileYToLat(y, zoom); }
095
096        @Override public double tileXToLon(int x, int zoom) { return source.tileXToLon(x, zoom); }
097    }
098
099    /**
100     * TMS TileSource provider for the slippymap chooser
101     */
102    public static class TMSTileSourceProvider implements TileSourceProvider {
103        static final Set<String> existingSlippyMapUrls = new HashSet<String>();
104        static {
105            // Urls that already exist in the slippymap chooser and shouldn't be copied from TMS layer list
106            existingSlippyMapUrls.add("http://tile.openstreetmap.org/{zoom}/{x}/{y}.png");      // Mapnik
107            existingSlippyMapUrls.add("http://tile.opencyclemap.org/cycle/{zoom}/{x}/{y}.png"); // Cyclemap
108            existingSlippyMapUrls.add("http://otile{switch:1,2,3,4}.mqcdn.com/tiles/1.0.0/osm/{zoom}/{x}/{y}.png"); // MapQuest-OSM
109            existingSlippyMapUrls.add("http://oatile{switch:1,2,3,4}.mqcdn.com/tiles/1.0.0/sat/{zoom}/{x}/{y}.png"); // MapQuest Open Aerial
110        }
111
112        @Override
113        public List<TileSource> getTileSources() {
114            if (!TMSLayer.PROP_ADD_TO_SLIPPYMAP_CHOOSER.get()) return Collections.<TileSource>emptyList();
115            List<TileSource> sources = new ArrayList<TileSource>();
116            for (ImageryInfo info : ImageryLayerInfo.instance.getLayers()) {
117                if (existingSlippyMapUrls.contains(info.getUrl())) {
118                    continue;
119                }
120                try {
121                    TileSource source = TMSLayer.getTileSource(info);
122                    if (source != null) {
123                        sources.add(source);
124                    }
125                } catch (IllegalArgumentException ex) {
126                    if (ex.getMessage() != null && !ex.getMessage().isEmpty()) {
127                        JOptionPane.showMessageDialog(Main.parent,
128                                ex.getMessage(), tr("Warning"),
129                                JOptionPane.WARNING_MESSAGE);
130                    }
131                }
132            }
133            return sources;
134        }
135
136        public static void addExistingSlippyMapUrl(String url) {
137            existingSlippyMapUrls.add(url);
138        }
139    }
140
141    /**
142     * Plugins that wish to add custom tile sources to slippy map choose should call this method
143     * @param tileSourceProvider
144     */
145    public static void addTileSourceProvider(TileSourceProvider tileSourceProvider) {
146        providers.addIfAbsent(tileSourceProvider);
147    }
148
149    private static CopyOnWriteArrayList<TileSourceProvider> providers = new CopyOnWriteArrayList<TileSourceProvider>();
150
151    static {
152        addTileSourceProvider(new TileSourceProvider() {
153            @Override
154            public List<TileSource> getTileSources() {
155                return Arrays.<TileSource>asList(
156                        new RenamedSourceDecorator(new OsmTileSource.Mapnik(), "Mapnik"),
157                        new RenamedSourceDecorator(new OsmTileSource.CycleMap(), "Cyclemap"),
158                        new RenamedSourceDecorator(new MapQuestOsmTileSource(), "MapQuest-OSM"),
159                        new RenamedSourceDecorator(new MapQuestOpenAerialTileSource(), "MapQuest Open Aerial")
160                        );
161            }
162        });
163        addTileSourceProvider(new TMSTileSourceProvider());
164    }
165
166    private static final StringProperty PROP_MAPSTYLE = new StringProperty("slippy_map_chooser.mapstyle", "Mapnik");
167    public static final String RESIZE_PROP = SlippyMapBBoxChooser.class.getName() + ".resize";
168
169    private OsmTileLoader cachedLoader;
170    private OsmTileLoader uncachedLoader;
171
172    private final SizeButton iSizeButton = new SizeButton();
173    private final SourceButton iSourceButton;
174    private Bounds bbox;
175
176    // upper left and lower right corners of the selection rectangle (x/y on ZOOM_MAX)
177    Point iSelectionRectStart;
178    Point iSelectionRectEnd;
179
180    /**
181     * Constructs a new {@code SlippyMapBBoxChooser}.
182     */
183    public SlippyMapBBoxChooser() {
184        TMSLayer.setMaxWorkers();
185        cachedLoader = TMSLayer.loaderFactory.makeTileLoader(this);
186
187        uncachedLoader = new OsmTileLoader(this);
188        uncachedLoader.headers.put("User-Agent", Version.getInstance().getFullAgentString());
189        setZoomContolsVisible(Main.pref.getBoolean("slippy_map_chooser.zoomcontrols",false));
190        setMapMarkerVisible(false);
191        setMinimumSize(new Dimension(350, 350 / 2));
192        // We need to set an initial size - this prevents a wrong zoom selection
193        // for the area before the component has been displayed the first time
194        setBounds(new Rectangle(getMinimumSize()));
195        if (cachedLoader == null) {
196            setFileCacheEnabled(false);
197        } else {
198            setFileCacheEnabled(Main.pref.getBoolean("slippy_map_chooser.file_cache", true));
199        }
200        setMaxTilesInMemory(Main.pref.getInteger("slippy_map_chooser.max_tiles", 1000));
201
202        List<TileSource> tileSources = getAllTileSources();
203
204        iSourceButton = new SourceButton(tileSources);
205
206        String mapStyle = PROP_MAPSTYLE.get();
207        boolean foundSource = false;
208        for (TileSource source: tileSources) {
209            if (source.getName().equals(mapStyle)) {
210                this.setTileSource(source);
211                iSourceButton.setCurrentMap(source);
212                foundSource = true;
213                break;
214            }
215        }
216        if (!foundSource) {
217            setTileSource(tileSources.get(0));
218            iSourceButton.setCurrentMap(tileSources.get(0));
219        }
220
221        new SlippyMapControler(this, this, iSizeButton, iSourceButton);
222    }
223    
224    private List<TileSource> getAllTileSources() {
225        List<TileSource> tileSources = new ArrayList<TileSource>();
226        for (TileSourceProvider provider: providers) {
227            tileSources.addAll(provider.getTileSources());
228        }
229        return tileSources;
230    }
231
232    public boolean handleAttribution(Point p, boolean click) {
233        return attribution.handleAttribution(p, click);
234    }
235
236    protected Point getTopLeftCoordinates() {
237        return new Point(center.x - (getWidth() / 2), center.y - (getHeight() / 2));
238    }
239
240    /**
241     * Draw the map.
242     */
243    @Override
244    public void paint(Graphics g) {
245        try {
246            super.paint(g);
247
248            // draw selection rectangle
249            if (iSelectionRectStart != null && iSelectionRectEnd != null) {
250
251                int zoomDiff = MAX_ZOOM - zoom;
252                Point tlc = getTopLeftCoordinates();
253                int x_min = (iSelectionRectStart.x >> zoomDiff) - tlc.x;
254                int y_min = (iSelectionRectStart.y >> zoomDiff) - tlc.y;
255                int x_max = (iSelectionRectEnd.x >> zoomDiff) - tlc.x;
256                int y_max = (iSelectionRectEnd.y >> zoomDiff) - tlc.y;
257
258                int w = x_max - x_min;
259                int h = y_max - y_min;
260                g.setColor(new Color(0.9f, 0.7f, 0.7f, 0.6f));
261                g.fillRect(x_min, y_min, w, h);
262
263                g.setColor(Color.BLACK);
264                g.drawRect(x_min, y_min, w, h);
265            }
266
267            iSizeButton.paint(g);
268            iSourceButton.paint((Graphics2D)g);
269        } catch (Exception e) {
270            e.printStackTrace();
271        }
272    }
273
274    public void setFileCacheEnabled(boolean enabled) {
275        if (enabled) {
276            setTileLoader(cachedLoader);
277        } else {
278            setTileLoader(uncachedLoader);
279        }
280    }
281
282    public void setMaxTilesInMemory(int tiles) {
283        ((MemoryTileCache) getTileCache()).setCacheSize(tiles);
284    }
285
286
287    /**
288     * Callback for the OsmMapControl. (Re-)Sets the start and end point of the
289     * selection rectangle.
290     *
291     * @param aStart
292     * @param aEnd
293     */
294    public void setSelection(Point aStart, Point aEnd) {
295        if (aStart == null || aEnd == null || aStart.x == aEnd.x || aStart.y == aEnd.y)
296            return;
297
298        Point p_max = new Point(Math.max(aEnd.x, aStart.x), Math.max(aEnd.y, aStart.y));
299        Point p_min = new Point(Math.min(aEnd.x, aStart.x), Math.min(aEnd.y, aStart.y));
300
301        Point tlc = getTopLeftCoordinates();
302        int zoomDiff = MAX_ZOOM - zoom;
303        Point pEnd = new Point(p_max.x + tlc.x, p_max.y + tlc.y);
304        Point pStart = new Point(p_min.x + tlc.x, p_min.y + tlc.y);
305
306        pEnd.x <<= zoomDiff;
307        pEnd.y <<= zoomDiff;
308        pStart.x <<= zoomDiff;
309        pStart.y <<= zoomDiff;
310
311        iSelectionRectStart = pStart;
312        iSelectionRectEnd = pEnd;
313
314        Coordinate l1 = getPosition(p_max); // lon may be outside [-180,180]
315        Coordinate l2 = getPosition(p_min); // lon may be outside [-180,180]
316        Bounds b = new Bounds(
317                new LatLon(
318                        Math.min(l2.getLat(), l1.getLat()),
319                        LatLon.toIntervalLon(Math.min(l1.getLon(), l2.getLon()))
320                        ),
321                        new LatLon(
322                                Math.max(l2.getLat(), l1.getLat()),
323                                LatLon.toIntervalLon(Math.max(l1.getLon(), l2.getLon())))
324                );
325        Bounds oldValue = this.bbox;
326        this.bbox = b;
327        repaint();
328        firePropertyChange(BBOX_PROP, oldValue, this.bbox);
329    }
330
331    /**
332     * Performs resizing of the DownloadDialog in order to enlarge or shrink the
333     * map.
334     */
335    public void resizeSlippyMap() {
336        boolean large = iSizeButton.isEnlarged();
337        firePropertyChange(RESIZE_PROP, !large, large);
338    }
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.0 && bbox.getMinLon() == 0.0
360                && bbox.getMaxLat() == 0.0 && bbox.getMaxLon() == 0.0)) {
361            this.bbox = null;
362            iSelectionRectStart = null;
363            iSelectionRectEnd = null;
364            repaint();
365            return;
366        }
367
368        this.bbox = bbox;
369        double minLon = bbox.getMinLon();
370        double maxLon = bbox.getMaxLon();
371
372        if (bbox.crosses180thMeridian()) {
373            minLon -= 360.0;
374        }
375
376        int y1 = OsmMercator.LatToY(bbox.getMinLat(), MAX_ZOOM);
377        int y2 = OsmMercator.LatToY(bbox.getMaxLat(), MAX_ZOOM);
378        int x1 = OsmMercator.LonToX(minLon, MAX_ZOOM);
379        int x2 = OsmMercator.LonToX(maxLon, MAX_ZOOM);
380
381        iSelectionRectStart = new Point(Math.min(x1, x2), Math.min(y1, y2));
382        iSelectionRectEnd = new Point(Math.max(x1, x2), Math.max(y1, y2));
383
384        // calc the screen coordinates for the new selection rectangle
385        MapMarkerDot xmin_ymin = new MapMarkerDot(bbox.getMinLat(), bbox.getMinLon());
386        MapMarkerDot xmax_ymax = new MapMarkerDot(bbox.getMaxLat(), bbox.getMaxLon());
387
388        List<MapMarker> marker = new ArrayList<MapMarker>(2);
389        marker.add(xmin_ymin);
390        marker.add(xmax_ymax);
391        setMapMarkerList(marker);
392        setDisplayToFitMapMarkers();
393        zoomOut();
394        repaint();
395    }
396    
397    /**
398     * Refreshes the tile sources
399     * @since 6364
400     */
401    public final void refreshTileSources() {
402        iSourceButton.setSources(getAllTileSources());
403    }
404}