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