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