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