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