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