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.AWTKeyStroke;
007import java.awt.BorderLayout;
008import java.awt.Color;
009import java.awt.FlowLayout;
010import java.awt.Graphics;
011import java.awt.GridBagConstraints;
012import java.awt.GridBagLayout;
013import java.awt.Insets;
014import java.awt.KeyboardFocusManager;
015import java.awt.Point;
016import java.awt.event.ActionEvent;
017import java.awt.event.ActionListener;
018import java.awt.event.FocusEvent;
019import java.awt.event.FocusListener;
020import java.awt.event.KeyEvent;
021import java.beans.PropertyChangeEvent;
022import java.beans.PropertyChangeListener;
023import java.util.ArrayList;
024import java.util.HashSet;
025import java.util.List;
026import java.util.Set;
027import java.util.regex.Matcher;
028import java.util.regex.Pattern;
029
030import javax.swing.AbstractAction;
031import javax.swing.BorderFactory;
032import javax.swing.JButton;
033import javax.swing.JLabel;
034import javax.swing.JPanel;
035import javax.swing.JSpinner;
036import javax.swing.KeyStroke;
037import javax.swing.SpinnerNumberModel;
038import javax.swing.event.ChangeEvent;
039import javax.swing.event.ChangeListener;
040import javax.swing.text.JTextComponent;
041
042import org.openstreetmap.gui.jmapviewer.MapMarkerDot;
043import org.openstreetmap.gui.jmapviewer.interfaces.MapMarker;
044import org.openstreetmap.josm.data.Bounds;
045import org.openstreetmap.josm.data.coor.LatLon;
046import org.openstreetmap.josm.gui.widgets.AbstractTextComponentValidator;
047import org.openstreetmap.josm.gui.widgets.HtmlPanel;
048import org.openstreetmap.josm.gui.widgets.JosmTextField;
049import org.openstreetmap.josm.gui.widgets.SelectAllOnFocusGainedDecorator;
050import org.openstreetmap.josm.tools.ImageProvider;
051import org.openstreetmap.josm.tools.Utils;
052
053/**
054 * TileSelectionBBoxChooser allows to select a bounding box (i.e. for downloading) based
055 * on OSM tile numbers.
056 *
057 * TileSelectionBBoxChooser can be embedded as component in a Swing container. Example:
058 * <pre>
059 *    JFrame f = new JFrame(....);
060 *    f.getContentPane().setLayout(new BorderLayout()));
061 *    TileSelectionBBoxChooser chooser = new TileSelectionBBoxChooser();
062 *    f.add(chooser, BorderLayout.CENTER);
063 *    chooser.addPropertyChangeListener(new PropertyChangeListener() {
064 *        public void propertyChange(PropertyChangeEvent evt) {
065 *            // listen for BBOX events
066 *            if (evt.getPropertyName().equals(BBoxChooser.BBOX_PROP)) {
067 *               Logging.info("new bbox based on OSM tiles selected: " + (Bounds)evt.getNewValue());
068 *            }
069 *        }
070 *    });
071 *
072 *    // init the chooser with a bounding box
073 *    chooser.setBoundingBox(....);
074 *
075 *    f.setVisible(true);
076 * </pre>
077 */
078public class TileSelectionBBoxChooser extends JPanel implements BBoxChooser {
079
080    /** the current bounding box */
081    private transient Bounds bbox;
082    /** the map viewer showing the selected bounding box */
083    private final TileBoundsMapView mapViewer = new TileBoundsMapView();
084    /** a panel for entering a bounding box given by a  tile grid and a zoom level */
085    private final TileGridInputPanel pnlTileGrid = new TileGridInputPanel();
086    /** a panel for entering a bounding box given by the address of an individual OSM tile at a given zoom level */
087    private final TileAddressInputPanel pnlTileAddress = new TileAddressInputPanel();
088
089    /**
090     * builds the UI
091     */
092    protected final void build() {
093        setLayout(new GridBagLayout());
094
095        GridBagConstraints gc = new GridBagConstraints();
096        gc.weightx = 0.5;
097        gc.fill = GridBagConstraints.HORIZONTAL;
098        gc.anchor = GridBagConstraints.NORTHWEST;
099        add(pnlTileGrid, gc);
100
101        gc.gridx = 1;
102        add(pnlTileAddress, gc);
103
104        gc.gridx = 0;
105        gc.gridy = 1;
106        gc.gridwidth = 2;
107        gc.weightx = 1.0;
108        gc.weighty = 1.0;
109        gc.fill = GridBagConstraints.BOTH;
110        gc.insets = new Insets(2, 2, 2, 2);
111        add(mapViewer, gc);
112        mapViewer.setFocusable(false);
113        mapViewer.setZoomControlsVisible(false);
114        mapViewer.setMapMarkerVisible(false);
115
116        pnlTileAddress.addPropertyChangeListener(pnlTileGrid);
117        pnlTileGrid.addPropertyChangeListener(new TileBoundsChangeListener());
118    }
119
120    /**
121     * Constructs a new {@code TileSelectionBBoxChooser}.
122     */
123    public TileSelectionBBoxChooser() {
124        build();
125    }
126
127    /**
128     * Replies the current bounding box. null, if no valid bounding box is currently selected.
129     *
130     */
131    @Override
132    public Bounds getBoundingBox() {
133        return bbox;
134    }
135
136    /**
137     * Sets the current bounding box.
138     *
139     * @param bbox the bounding box. null, if this widget isn't initialized with a bounding box
140     */
141    @Override
142    public void setBoundingBox(Bounds bbox) {
143        pnlTileGrid.initFromBoundingBox(bbox);
144    }
145
146    protected void refreshMapView() {
147        if (bbox == null) return;
148
149        // calc the screen coordinates for the new selection rectangle
150        List<MapMarker> marker = new ArrayList<>(2);
151        marker.add(new MapMarkerDot(bbox.getMinLat(), bbox.getMinLon()));
152        marker.add(new MapMarkerDot(bbox.getMaxLat(), bbox.getMaxLon()));
153        mapViewer.setBoundingBox(bbox);
154        mapViewer.setMapMarkerList(marker);
155        mapViewer.setDisplayToFitMapMarkers();
156        mapViewer.zoomOut();
157    }
158
159    /**
160     * Computes the bounding box given a tile grid.
161     *
162     * @param tb the description of the tile grid
163     * @return the bounding box
164     */
165    protected Bounds convertTileBoundsToBoundingBox(TileBounds tb) {
166        LatLon min = getNorthWestLatLonOfTile(tb.min, tb.zoomLevel);
167        Point p = new Point(tb.max);
168        p.x++;
169        p.y++;
170        LatLon max = getNorthWestLatLonOfTile(p, tb.zoomLevel);
171        return new Bounds(max.lat(), min.lon(), min.lat(), max.lon());
172    }
173
174    /**
175     * Replies lat/lon of the north/west-corner of a tile at a specific zoom level
176     *
177     * @param tile  the tile address (x,y)
178     * @param zoom the zoom level
179     * @return lat/lon of the north/west-corner of a tile at a specific zoom level
180     */
181    protected LatLon getNorthWestLatLonOfTile(Point tile, int zoom) {
182        double lon = tile.x / Math.pow(2.0, zoom) * 360.0 - 180;
183        double lat = Utils.toDegrees(Math.atan(Math.sinh(Math.PI - (2.0 * Math.PI * tile.y) / Math.pow(2.0, zoom))));
184        return new LatLon(lat, lon);
185    }
186
187    /**
188     * Listens to changes in the selected tile bounds, refreshes the map view and emits
189     * property change events for {@link BBoxChooser#BBOX_PROP}
190     */
191    class TileBoundsChangeListener implements PropertyChangeListener {
192        @Override
193        public void propertyChange(PropertyChangeEvent evt) {
194            if (!evt.getPropertyName().equals(TileGridInputPanel.TILE_BOUNDS_PROP)) return;
195            TileBounds tb = (TileBounds) evt.getNewValue();
196            Bounds oldValue = TileSelectionBBoxChooser.this.bbox;
197            TileSelectionBBoxChooser.this.bbox = convertTileBoundsToBoundingBox(tb);
198            firePropertyChange(BBOX_PROP, oldValue, TileSelectionBBoxChooser.this.bbox);
199            refreshMapView();
200        }
201    }
202
203    /**
204     * A panel for describing a rectangular area of OSM tiles at a given zoom level.
205     *
206     * The panel emits PropertyChangeEvents for the property {@link TileGridInputPanel#TILE_BOUNDS_PROP}
207     * when the user successfully enters a valid tile grid specification.
208     *
209     */
210    private static class TileGridInputPanel extends JPanel implements PropertyChangeListener {
211        public static final String TILE_BOUNDS_PROP = TileGridInputPanel.class.getName() + ".tileBounds";
212
213        private final JosmTextField tfMaxY = new JosmTextField();
214        private final JosmTextField tfMinY = new JosmTextField();
215        private final JosmTextField tfMaxX = new JosmTextField();
216        private final JosmTextField tfMinX = new JosmTextField();
217        private transient TileCoordinateValidator valMaxY;
218        private transient TileCoordinateValidator valMinY;
219        private transient TileCoordinateValidator valMaxX;
220        private transient TileCoordinateValidator valMinX;
221        private final JSpinner spZoomLevel = new JSpinner(new SpinnerNumberModel(0, 0, 18, 1));
222        private final transient TileBoundsBuilder tileBoundsBuilder = new TileBoundsBuilder();
223        private boolean doFireTileBoundChanged = true;
224
225        protected JPanel buildTextPanel() {
226            JPanel pnl = new JPanel(new BorderLayout());
227            HtmlPanel msg = new HtmlPanel();
228            msg.setText(tr("<html>Please select a <strong>range of OSM tiles</strong> at a given zoom level.</html>"));
229            pnl.add(msg);
230            return pnl;
231        }
232
233        protected JPanel buildZoomLevelPanel() {
234            JPanel pnl = new JPanel(new FlowLayout(FlowLayout.LEFT));
235            pnl.add(new JLabel(tr("Zoom level:")));
236            pnl.add(spZoomLevel);
237            spZoomLevel.addChangeListener(new ZomeLevelChangeHandler());
238            spZoomLevel.addChangeListener(tileBoundsBuilder);
239            return pnl;
240        }
241
242        protected JPanel buildTileGridInputPanel() {
243            JPanel pnl = new JPanel(new GridBagLayout());
244            pnl.setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));
245            GridBagConstraints gc = new GridBagConstraints();
246            gc.anchor = GridBagConstraints.NORTHWEST;
247            gc.insets = new Insets(0, 0, 2, 2);
248
249            gc.gridwidth = 2;
250            gc.gridx = 1;
251            gc.fill = GridBagConstraints.HORIZONTAL;
252            pnl.add(buildZoomLevelPanel(), gc);
253
254            gc.gridwidth = 1;
255            gc.gridy = 1;
256            gc.gridx = 1;
257            pnl.add(new JLabel(tr("from tile")), gc);
258
259            gc.gridx = 2;
260            pnl.add(new JLabel(tr("up to tile")), gc);
261
262            gc.gridx = 0;
263            gc.gridy = 2;
264            gc.weightx = 0.0;
265            pnl.add(new JLabel("X:"), gc);
266
267
268            gc.gridx = 1;
269            gc.weightx = 0.5;
270            pnl.add(tfMinX, gc);
271            valMinX = new TileCoordinateValidator(tfMinX);
272            SelectAllOnFocusGainedDecorator.decorate(tfMinX);
273            tfMinX.addActionListener(tileBoundsBuilder);
274            tfMinX.addFocusListener(tileBoundsBuilder);
275
276            gc.gridx = 2;
277            gc.weightx = 0.5;
278            pnl.add(tfMaxX, gc);
279            valMaxX = new TileCoordinateValidator(tfMaxX);
280            SelectAllOnFocusGainedDecorator.decorate(tfMaxX);
281            tfMaxX.addActionListener(tileBoundsBuilder);
282            tfMaxX.addFocusListener(tileBoundsBuilder);
283
284            gc.gridx = 0;
285            gc.gridy = 3;
286            gc.weightx = 0.0;
287            pnl.add(new JLabel("Y:"), gc);
288
289            gc.gridx = 1;
290            gc.weightx = 0.5;
291            pnl.add(tfMinY, gc);
292            valMinY = new TileCoordinateValidator(tfMinY);
293            SelectAllOnFocusGainedDecorator.decorate(tfMinY);
294            tfMinY.addActionListener(tileBoundsBuilder);
295            tfMinY.addFocusListener(tileBoundsBuilder);
296
297            gc.gridx = 2;
298            gc.weightx = 0.5;
299            pnl.add(tfMaxY, gc);
300            valMaxY = new TileCoordinateValidator(tfMaxY);
301            SelectAllOnFocusGainedDecorator.decorate(tfMaxY);
302            tfMaxY.addActionListener(tileBoundsBuilder);
303            tfMaxY.addFocusListener(tileBoundsBuilder);
304
305            gc.gridy = 4;
306            gc.gridx = 0;
307            gc.gridwidth = 3;
308            gc.weightx = 1.0;
309            gc.weighty = 1.0;
310            gc.fill = GridBagConstraints.BOTH;
311            pnl.add(new JPanel(), gc);
312            return pnl;
313        }
314
315        protected void build() {
316            setLayout(new BorderLayout());
317            setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
318            add(buildTextPanel(), BorderLayout.NORTH);
319            add(buildTileGridInputPanel(), BorderLayout.CENTER);
320
321            Set<AWTKeyStroke> forwardKeys = new HashSet<>(getFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS));
322            forwardKeys.add(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0));
323            setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, forwardKeys);
324        }
325
326        TileGridInputPanel() {
327            build();
328        }
329
330        public void initFromBoundingBox(Bounds bbox) {
331            if (bbox == null)
332                return;
333            TileBounds tb = new TileBounds();
334            tb.zoomLevel = (Integer) spZoomLevel.getValue();
335            tb.min = new Point(
336                    Math.max(0, lonToTileX(tb.zoomLevel, bbox.getMinLon())),
337                    Math.max(0, latToTileY(tb.zoomLevel, bbox.getMaxLat() - 0.00001))
338            );
339            tb.max = new Point(
340                    Math.max(0, lonToTileX(tb.zoomLevel, bbox.getMaxLon())),
341                    Math.max(0, latToTileY(tb.zoomLevel, bbox.getMinLat() - 0.00001))
342            );
343            doFireTileBoundChanged = false;
344            setTileBounds(tb);
345            doFireTileBoundChanged = true;
346        }
347
348        public static int latToTileY(int zoom, double lat) {
349            if ((zoom < 3) || (zoom > 18)) return -1;
350            double l = lat / 180 * Math.PI;
351            double pf = Math.log(Math.tan(l) + (1/Math.cos(l)));
352            return (int) ((1 << (zoom-1)) * (Math.PI - pf) / Math.PI);
353        }
354
355        public static int lonToTileX(int zoom, double lon) {
356            if ((zoom < 3) || (zoom > 18)) return -1;
357            return (int) ((1 << (zoom-3)) * (lon + 180.0) / 45.0);
358        }
359
360        public void setTileBounds(TileBounds tileBounds) {
361            tfMinX.setText(Integer.toString(tileBounds.min.x));
362            tfMinY.setText(Integer.toString(tileBounds.min.y));
363            tfMaxX.setText(Integer.toString(tileBounds.max.x));
364            tfMaxY.setText(Integer.toString(tileBounds.max.y));
365            spZoomLevel.setValue(tileBounds.zoomLevel);
366        }
367
368        @Override
369        public void propertyChange(PropertyChangeEvent evt) {
370            if (evt.getPropertyName().equals(TileAddressInputPanel.TILE_BOUNDS_PROP)) {
371                TileBounds tb = (TileBounds) evt.getNewValue();
372                setTileBounds(tb);
373                fireTileBoundsChanged(tb);
374            }
375        }
376
377        protected void fireTileBoundsChanged(TileBounds tb) {
378            if (!doFireTileBoundChanged) return;
379            firePropertyChange(TILE_BOUNDS_PROP, null, tb);
380        }
381
382        class ZomeLevelChangeHandler implements ChangeListener {
383            @Override
384            public void stateChanged(ChangeEvent e) {
385                int zoomLevel = (Integer) spZoomLevel.getValue();
386                valMaxX.setZoomLevel(zoomLevel);
387                valMaxY.setZoomLevel(zoomLevel);
388                valMinX.setZoomLevel(zoomLevel);
389                valMinY.setZoomLevel(zoomLevel);
390            }
391        }
392
393        class TileBoundsBuilder implements ActionListener, FocusListener, ChangeListener {
394            protected void buildTileBounds() {
395                if (!valMaxX.isValid()) return;
396                if (!valMaxY.isValid()) return;
397                if (!valMinX.isValid()) return;
398                if (!valMinY.isValid()) return;
399                Point min = new Point(valMinX.getTileIndex(), valMinY.getTileIndex());
400                Point max = new Point(valMaxX.getTileIndex(), valMaxY.getTileIndex());
401                int zoomlevel = (Integer) spZoomLevel.getValue();
402                TileBounds tb = new TileBounds(min, max, zoomlevel);
403                fireTileBoundsChanged(tb);
404            }
405
406            @Override
407            public void focusGained(FocusEvent e) {
408                /* irrelevant */
409            }
410
411            @Override
412            public void focusLost(FocusEvent e) {
413                buildTileBounds();
414            }
415
416            @Override
417            public void actionPerformed(ActionEvent e) {
418                buildTileBounds();
419            }
420
421            @Override
422            public void stateChanged(ChangeEvent e) {
423                buildTileBounds();
424            }
425        }
426    }
427
428    /**
429     * A panel for entering the address of a single OSM tile at a given zoom level.
430     *
431     */
432    private static class TileAddressInputPanel extends JPanel {
433
434        public static final String TILE_BOUNDS_PROP = TileAddressInputPanel.class.getName() + ".tileBounds";
435
436        private transient TileAddressValidator valTileAddress;
437
438        protected JPanel buildTextPanel() {
439            JPanel pnl = new JPanel(new BorderLayout());
440            HtmlPanel msg = new HtmlPanel();
441            msg.setText(tr("<html>Alternatively you may enter a <strong>tile address</strong> for a single tile "
442                    + "in the format <i>zoomlevel/x/y</i>, e.g. <i>15/256/223</i>. Tile addresses "
443                    + "in the format <i>zoom,x,y</i> or <i>zoom;x;y</i> are valid too.</html>"));
444            pnl.add(msg);
445            return pnl;
446        }
447
448        protected JPanel buildTileAddressInputPanel() {
449            JPanel pnl = new JPanel(new GridBagLayout());
450            GridBagConstraints gc = new GridBagConstraints();
451            gc.anchor = GridBagConstraints.NORTHWEST;
452            gc.fill = GridBagConstraints.HORIZONTAL;
453            gc.weightx = 0.0;
454            gc.insets = new Insets(0, 0, 2, 2);
455            pnl.add(new JLabel(tr("Tile address:")), gc);
456
457            gc.weightx = 1.0;
458            gc.gridx = 1;
459            JosmTextField tfTileAddress = new JosmTextField();
460            pnl.add(tfTileAddress, gc);
461            valTileAddress = new TileAddressValidator(tfTileAddress);
462            SelectAllOnFocusGainedDecorator.decorate(tfTileAddress);
463
464            gc.weightx = 0.0;
465            gc.gridx = 2;
466            ApplyTileAddressAction applyTileAddressAction = new ApplyTileAddressAction();
467            JButton btn = new JButton(applyTileAddressAction);
468            btn.setBorder(BorderFactory.createEmptyBorder(1, 1, 1, 1));
469            pnl.add(btn, gc);
470            tfTileAddress.addActionListener(applyTileAddressAction);
471            return pnl;
472        }
473
474        protected void build() {
475            setLayout(new GridBagLayout());
476            GridBagConstraints gc = new GridBagConstraints();
477            gc.anchor = GridBagConstraints.NORTHWEST;
478            gc.fill = GridBagConstraints.HORIZONTAL;
479            gc.weightx = 1.0;
480            gc.insets = new Insets(0, 0, 5, 0);
481            add(buildTextPanel(), gc);
482
483            gc.gridy = 1;
484            add(buildTileAddressInputPanel(), gc);
485
486            // filler - grab remaining space
487            gc.gridy = 2;
488            gc.fill = GridBagConstraints.BOTH;
489            gc.weighty = 1.0;
490            add(new JPanel(), gc);
491        }
492
493        TileAddressInputPanel() {
494            setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
495            build();
496        }
497
498        protected void fireTileBoundsChanged(TileBounds tb) {
499            firePropertyChange(TILE_BOUNDS_PROP, null, tb);
500        }
501
502        class ApplyTileAddressAction extends AbstractAction {
503            ApplyTileAddressAction() {
504                new ImageProvider("apply").getResource().attachImageIcon(this, true);
505                putValue(SHORT_DESCRIPTION, tr("Apply the tile address"));
506            }
507
508            @Override
509            public void actionPerformed(ActionEvent e) {
510                TileBounds tb = valTileAddress.getTileBounds();
511                if (tb != null) {
512                    fireTileBoundsChanged(tb);
513                }
514            }
515        }
516    }
517
518    /**
519     * Validates a tile address
520     */
521    private static class TileAddressValidator extends AbstractTextComponentValidator {
522
523        private TileBounds tileBounds;
524
525        TileAddressValidator(JTextComponent tc) {
526            super(tc);
527        }
528
529        @Override
530        public boolean isValid() {
531            String value = getComponent().getText().trim();
532            Matcher m = Pattern.compile("(\\d+)[^\\d]+(\\d+)[^\\d]+(\\d+)").matcher(value);
533            tileBounds = null;
534            if (!m.matches()) return false;
535            int zoom;
536            try {
537                zoom = Integer.parseInt(m.group(1));
538            } catch (NumberFormatException e) {
539                return false;
540            }
541            if (zoom < 0 || zoom > 18) return false;
542
543            int x;
544            try {
545                x = Integer.parseInt(m.group(2));
546            } catch (NumberFormatException e) {
547                return false;
548            }
549            if (x < 0 || x >= Math.pow(2, zoom)) return false;
550            int y;
551            try {
552                y = Integer.parseInt(m.group(3));
553            } catch (NumberFormatException e) {
554                return false;
555            }
556            if (y < 0 || y >= Math.pow(2, zoom)) return false;
557
558            tileBounds = new TileBounds(new Point(x, y), new Point(x, y), zoom);
559            return true;
560        }
561
562        @Override
563        public void validate() {
564            if (isValid()) {
565                feedbackValid(tr("Please enter a tile address"));
566            } else {
567                feedbackInvalid(tr("The current value isn''t a valid tile address", getComponent().getText()));
568            }
569        }
570
571        public TileBounds getTileBounds() {
572            return tileBounds;
573        }
574    }
575
576    /**
577     * Validates the x- or y-coordinate of a tile at a given zoom level.
578     *
579     */
580    private static class TileCoordinateValidator extends AbstractTextComponentValidator {
581        private int zoomLevel;
582        private int tileIndex;
583
584        TileCoordinateValidator(JTextComponent tc) {
585            super(tc);
586        }
587
588        public void setZoomLevel(int zoomLevel) {
589            this.zoomLevel = zoomLevel;
590            validate();
591        }
592
593        @Override
594        public boolean isValid() {
595            String value = getComponent().getText().trim();
596            try {
597                if (value.isEmpty()) {
598                    tileIndex = 0;
599                } else {
600                    tileIndex = Integer.parseInt(value);
601                }
602            } catch (NumberFormatException e) {
603                return false;
604            }
605            return tileIndex >= 0 && tileIndex < Math.pow(2, zoomLevel);
606        }
607
608        @Override
609        public void validate() {
610            if (isValid()) {
611                feedbackValid(tr("Please enter a tile index"));
612            } else {
613                feedbackInvalid(tr("The current value isn''t a valid tile index for the given zoom level", getComponent().getText()));
614            }
615        }
616
617        public int getTileIndex() {
618            return tileIndex;
619        }
620    }
621
622    /**
623     * Represents a rectangular area of tiles at a given zoom level.
624     */
625    private static final class TileBounds {
626        private Point min;
627        private Point max;
628        private int zoomLevel;
629
630        private TileBounds() {
631            zoomLevel = 0;
632            min = new Point(0, 0);
633            max = new Point(0, 0);
634        }
635
636        private TileBounds(Point min, Point max, int zoomLevel) {
637            this.min = min;
638            this.max = max;
639            this.zoomLevel = zoomLevel;
640        }
641
642        @Override
643        public String toString() {
644            StringBuilder sb = new StringBuilder(24);
645            sb.append("min=").append(min.x).append(',').append(min.y)
646              .append(",max=").append(max.x).append(',').append(max.y)
647              .append(",zoom=").append(zoomLevel);
648            return sb.toString();
649        }
650    }
651
652    /**
653     * The map view used in this bounding box chooser
654     */
655    private static final class TileBoundsMapView extends JosmMapViewer {
656        private Point min;
657        private Point max;
658
659        private TileBoundsMapView() {
660            setBorder(BorderFactory.createLineBorder(Color.DARK_GRAY));
661        }
662
663        public void setBoundingBox(Bounds bbox) {
664            if (bbox == null) {
665                min = null;
666                max = null;
667            } else {
668                Point p1 = tileSource.latLonToXY(bbox.getMinLat(), bbox.getMinLon(), MAX_ZOOM);
669                Point p2 = tileSource.latLonToXY(bbox.getMaxLat(), bbox.getMaxLon(), MAX_ZOOM);
670
671                min = new Point(Math.min(p1.x, p2.x), Math.min(p1.y, p2.y));
672                max = new Point(Math.max(p1.x, p2.x), Math.max(p1.y, p2.y));
673            }
674            repaint();
675        }
676
677        private Point getTopLeftCoordinates() {
678            return new Point(center.x - (getWidth() / 2), center.y - (getHeight() / 2));
679        }
680
681        /**
682         * Draw the map.
683         */
684        @Override
685        public void paint(Graphics g) {
686            super.paint(g);
687            if (min == null || max == null) return;
688            int zoomDiff = MAX_ZOOM - zoom;
689            Point tlc = getTopLeftCoordinates();
690            int xMin = (min.x >> zoomDiff) - tlc.x;
691            int yMin = (min.y >> zoomDiff) - tlc.y;
692            int xMax = (max.x >> zoomDiff) - tlc.x;
693            int yMax = (max.y >> zoomDiff) - tlc.y;
694
695            int w = xMax - xMin;
696            int h = yMax - yMin;
697            g.setColor(new Color(0.9f, 0.7f, 0.7f, 0.6f));
698            g.fillRect(xMin, yMin, w, h);
699
700            g.setColor(Color.BLACK);
701            g.drawRect(xMin, yMin, w, h);
702        }
703    }
704}