Ignore:
Timestamp:
2013-03-19T20:46:10+01:00 (11 years ago)
Author:
zverik
Message:

another iteration

Location:
applications/editors/josm/plugins/imagery_offset_db
Files:
4 added
9 edited

Legend:

Unmodified
Added
Removed
  • applications/editors/josm/plugins/imagery_offset_db/src/iodb/CalibrationObject.java

    r29371 r29376  
    22
    33import java.util.Map;
    4 import org.openstreetmap.josm.data.osm.*;
     4import org.openstreetmap.josm.data.coor.LatLon;
     5import org.openstreetmap.josm.data.osm.Node;
     6import org.openstreetmap.josm.data.osm.OsmPrimitive;
     7import org.openstreetmap.josm.data.osm.Way;
    58
    69/**
     
    912 */
    1013public class CalibrationObject extends ImageryOffsetBase {
    11     private OsmPrimitive object;
    12     private long lastUserId;
     14    private LatLon[] geometry;
    1315
    14     public CalibrationObject(OsmPrimitive object, long lastUserId) {
    15         this.object = object;
    16         this.lastUserId = lastUserId;
     16    public CalibrationObject(LatLon[] geometry) {
     17        this.geometry = geometry;
    1718    }
    1819
    19     public CalibrationObject(OsmPrimitive object) {
    20         this(object, 0);
     20    public CalibrationObject( OsmPrimitive p ) {
     21        if( p instanceof Node )
     22            geometry = new LatLon[] { ((Node)p).getCoor() };
     23        else if( p instanceof Way ) {
     24            geometry = new LatLon[((Way)p).getNodesCount()];
     25            for( int i = 0; i < geometry.length; i++ )
     26                geometry[i] = ((Way)p).getNode(i).getCoor();
     27        } else
     28            throw new IllegalArgumentException("Calibration Object can be created either from node or a way");
    2129    }
    2230
    23     public long getLastUserId() {
    24         return lastUserId;
     31    public LatLon[] getGeometry() {
     32        return geometry;
    2533    }
    2634
    27     public OsmPrimitive getObject() {
    28         return object;
    29     }
    30    
    3135    @Override
    3236    public void putServerParams( Map<String, String> map ) {
    3337        super.putServerParams(map);
    34         map.put("object", object instanceof Node ? "node" : "way");
    35         map.put("id", String.valueOf(object.getId()));
     38        StringBuilder sb = new StringBuilder();
     39        for( int i = 0; i < geometry.length; i++ ) {
     40            if( i > 0 )
     41                sb.append(',');
     42            sb.append(geometry[i].lon()).append(' ').append(geometry[i].lat());
     43        }
     44        map.put("geometry", sb.toString());
    3645    }
    3746
    3847    @Override
    3948    public String toString() {
    40         return "CalibrationObject{" + "object=" + object + ", lastUserId=" + lastUserId + "position=" + position + ", date=" + date + ", author=" + author + ", description=" + description + ", abandonDate=" + abandonDate + '}';
     49        return "CalibrationObject{" + geometry.length + "nodes; position=" + position + ", date=" + date + ", author=" + author + ", description=" + description + ", abandonDate=" + abandonDate + '}';
    4150    }
    4251}
  • applications/editors/josm/plugins/imagery_offset_db/src/iodb/GetImageryOffsetAction.java

    r29371 r29376  
    99import javax.swing.JOptionPane;
    1010import org.openstreetmap.josm.Main;
    11 import org.openstreetmap.josm.actions.AutoScaleAction;
    12 import org.openstreetmap.josm.actions.DownloadPrimitiveAction;
    1311import org.openstreetmap.josm.actions.JosmAction;
    1412import org.openstreetmap.josm.data.coor.LatLon;
    15 import org.openstreetmap.josm.data.osm.*;
    1613import org.openstreetmap.josm.data.projection.Projection;
    1714import org.openstreetmap.josm.gui.layer.ImageryLayer;
     
    7067                Main.map.repaint();
    7168            } else if( offset instanceof CalibrationObject ) {
    72                 OsmPrimitive obj = ((CalibrationObject)offset).getObject();
    73                 final List<PrimitiveId> ids = new ArrayList<PrimitiveId>(1);
    74                 ids.add(obj);
    75                 DownloadPrimitiveAction.processItems(false, ids, false, true);
    76                 Main.worker.submit(new AfterCalibrationDownloadTask((CalibrationObject)offset));
     69                CalibrationLayer clayer = new CalibrationLayer((CalibrationObject)offset);
     70                Main.map.mapView.addLayer(clayer);
     71                clayer.panToCenter();
     72                if( !Main.pref.getBoolean("iodb.calibration.message", false) ) {
     73                    JOptionPane.showMessageDialog(Main.parent, // todo: update text
     74                            tr("A layer has been added with a calibration geometry. Hide data layers,\n"
     75                            + "find the corresponding feature on the imagery layer and move it accordingly."),
     76                            ImageryOffsetTools.DIALOG_TITLE, JOptionPane.INFORMATION_MESSAGE);
     77                    Main.pref.put("iodb.calibration.message", true);
     78                }
    7779            }
    7880        }
    7981    }
    8082
    81     class AfterCalibrationDownloadTask implements Runnable {
    82         private CalibrationObject offset;
    83 
    84         public AfterCalibrationDownloadTask( CalibrationObject offset ) {
    85             this.offset = offset;
    86         }
    87 
    88         @Override
    89         public void run() {
    90             OsmPrimitive p = getCurrentDataSet().getPrimitiveById(offset.getObject());
    91             if( p == null ) {
    92                 return;
    93             }
    94             // check for last user
    95             if( offset.getLastUserId() > 0 ) {
    96                 long uid = p.getUser().getId();
    97                 Date ts = p.getTimestamp();
    98                 if( p instanceof Way ) {
    99                     for( Node n : ((Way)p).getNodes() ) {
    100                         if( n.getTimestamp().after(ts) ) {
    101                             ts = n.getTimestamp();
    102                             uid = n.getUser().getId();
    103                         }
    104                     }
    105                 }
    106                 if( uid != offset.getLastUserId() ) {
    107                     int result = JOptionPane.showConfirmDialog(Main.parent,
    108                             tr("The calibration object has been changed in unknown way.\n"
    109                              + "It may be moved or extended, thus ceasing to be a reliable mark\n"
    110                              + "for imagery calibration. Do you want to notify the server of this?"),
    111                             ImageryOffsetTools.DIALOG_TITLE, JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
    112                     if( result == JOptionPane.YES_OPTION ) {
    113                         DeprecateOffsetAction.deprecateOffset(offset);
    114                         return;
    115                     }
    116                 }
    117             }
    118             Main.main.getCurrentDataSet().setSelected(p);
    119             AutoScaleAction.zoomTo(Collections.singleton(p));
    120             if( !Main.pref.getBoolean("iodb.calibration.message", false) ) {
    121                 JOptionPane.showMessageDialog(Main.parent,
    122                         tr("An object has been selected on the map. Find the corresponding feature\n"
    123                          + "on the imagery layer and move that layer accordingly.\n"
    124                          + "DO NOT touch the selected object, so it can be used by others later."),
    125                         ImageryOffsetTools.DIALOG_TITLE, JOptionPane.INFORMATION_MESSAGE);
    126                 Main.pref.put("iodb.calibration.message", true);
    127             }
    128         }
    129     }
    130    
    13183    class DownloadOffsetsTask extends SimpleOffsetQueryTask {
    13284        private ImageryLayer layer;
     
    13890                String query = "get?lat=" + center.lat() + "&lon=" + center.lon()
    13991                        + "&imagery=" + URLEncoder.encode(imagery, "UTF8");
     92                int radius = Main.pref.getInteger("iodb.radius", -1);
     93                if( radius > 0 )
     94                    query = query + "?radius=" + radius;
    14095                setQuery(query);
    14196            } catch( UnsupportedEncodingException e ) {
  • applications/editors/josm/plugins/imagery_offset_db/src/iodb/IODBReader.java

    r29371 r29376  
    1111import javax.xml.parsers.SAXParserFactory;
    1212import org.openstreetmap.josm.data.coor.LatLon;
    13 import org.openstreetmap.josm.data.osm.Node;
    14 import org.openstreetmap.josm.data.osm.OsmPrimitive;
    15 import org.openstreetmap.josm.data.osm.Way;
    1613import org.openstreetmap.josm.io.UTFInputStreamReader;
    1714import org.xml.sax.Attributes;
     
    3330        private IOFields fields;
    3431        private boolean parsingOffset;
     32        private boolean parsingDeprecate;
    3533        private SimpleDateFormat dateParser = new SimpleDateFormat("yyyy-MM-dd");
    3634
     
    5149        public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
    5250            if( !parsingOffset ) {
    53                 if( qName.equals("offset") || qName.equals("calibration-object") ) {
     51                if( qName.equals("offset") || qName.equals("calibration") ) {
    5452                    parsingOffset = true;
     53                    parsingDeprecate = false;
    5554                    fields.clear();
    5655                    fields.position = parseLatLon(attributes);
     56                    fields.id = Integer.parseInt(attributes.getValue("id"));
    5757                }
    5858            } else {
    59                 if( qName.equals("object") ) {
    60                     fields.isNode = attributes.getValue("type").equals("node");
    61                 } else if( qName.equals("last-user") ) {
    62                     fields.lastUserId = Integer.parseInt(attributes.getValue("id"));
     59                if( qName.equals("node") ) {
     60                    fields.geometry.add(parseLatLon(attributes));
    6361                } else if( qName.equals("imagery-position") ) {
    6462                    fields.imageryPos = parseLatLon(attributes);
     
    7068                    if( maxZoom != null )
    7169                        fields.maxZoom = Integer.parseInt(maxZoom);
     70                } else if( qName.equals("deprecated") ) {
     71                    parsingDeprecate = true;
    7272                }
    7373            }
     
    8585            if( parsingOffset ) {
    8686                if( qName.equals("author") ) {
    87                     fields.author = accumulator.toString();
     87                    if( !parsingDeprecate )
     88                        fields.author = accumulator.toString();
     89                    else
     90                        fields.abandonAuthor = accumulator.toString();
    8891                } else if( qName.equals("description") ) {
    8992                    fields.description = accumulator.toString();
     93                } else if( qName.equals("reason") && parsingDeprecate ) {
     94                    fields.abandonReason = accumulator.toString();
    9095                } else if( qName.equals("date") ) {
    9196                    try {
    92                         fields.date = dateParser.parse(accumulator.toString());
     97                        if( !parsingDeprecate )
     98                            fields.date = dateParser.parse(accumulator.toString());
     99                        else
     100                            fields.abandonDate = dateParser.parse(accumulator.toString());
    93101                    } catch (ParseException ex) {
    94102                        throw new SAXException(ex);
    95103                    }
    96104                } else if( qName.equals("deprecated") ) {
    97                     try {
    98                         fields.abandonDate = dateParser.parse(accumulator.toString());
    99                     } catch (ParseException ex) {
    100                         throw new SAXException(ex);
    101                     }
     105                    parsingDeprecate = false;
    102106                } else if( qName.equals("imagery") ) {
    103107                    fields.imagery = accumulator.toString();
    104                 } else if( qName.equals("object") ) {
    105                     fields.objectId = Integer.parseInt(accumulator.toString());
    106                 } else if( qName.equals("offset") || qName.equals("calibration-object") ) {
     108                } else if( qName.equals("offset") || qName.equals("calibration") ) {
    107109                    // store offset
    108110                    try {
     
    136138   
    137139    private class IOFields {
     140        public int id;
    138141        public LatLon position;
    139142        public Date date;
     
    141144        public String description;
    142145        public Date abandonDate;
     146        public String abandonAuthor;
     147        public String abandonReason;
    143148        public LatLon imageryPos;
    144149        public String imagery;
    145150        public int minZoom, maxZoom;
    146         public boolean isNode;
    147         public long objectId;
    148         public long lastUserId;
     151        public List<LatLon> geometry;
    149152
    150153        public IOFields() {
     
    153156       
    154157        public void clear() {
     158            id = -1;
    155159            position = null;
    156160            date = null;
     
    158162            description = null;
    159163            abandonDate = null;
     164            abandonAuthor = null;
     165            abandonReason = null;
    160166            imageryPos = null;
    161167            imagery = null;
    162168            minZoom = -1;
    163169            maxZoom = -1;
    164             isNode = false;
    165             objectId = -1;
    166             lastUserId = -1;
     170            geometry = new ArrayList<LatLon>();
    167171        }
    168172
     
    170174            if( author == null || description == null || position == null || date == null )
    171175                throw new IllegalArgumentException("Not enought arguments to build an object");
    172             if( objectId < 0 ) {
     176            ImageryOffsetBase result;
     177            if( geometry.isEmpty() ) {
    173178                if( imagery == null || imageryPos == null )
    174179                    throw new IllegalArgumentException("Both imagery and imageryPos should be sepcified for the offset");
    175                 ImageryOffset result = new ImageryOffset(imagery, imageryPos);
     180                result = new ImageryOffset(imagery, imageryPos);
    176181                if( minZoom >= 0 )
    177                     result.setMinZoom(minZoom);
     182                    ((ImageryOffset)result).setMinZoom(minZoom);
    178183                if( maxZoom >= 0 )
    179                     result.setMaxZoom(maxZoom);
    180                 result.setBasicInfo(position, author, description, date);
    181                 result.setAbandonDate(abandonDate);
    182                 return result;
     184                    ((ImageryOffset)result).setMaxZoom(maxZoom);
    183185            } else {
    184                 OsmPrimitive p = isNode ? new Node(objectId) : new Way(objectId);
    185                 CalibrationObject result = new CalibrationObject(p, lastUserId);
    186                 result.setBasicInfo(position, author, description, date);
    187                 result.setAbandonDate(abandonDate);
    188                 return result;
     186                result = new CalibrationObject(geometry.toArray(new LatLon[0]));
    189187            }
     188            if( id >= 0 )
     189                result.setId(id);
     190            result.setBasicInfo(position, author, description, date);
     191            result.setDeprecated(abandonDate, abandonAuthor, abandonReason);
     192            return result;
    190193        }
    191194    }
  • applications/editors/josm/plugins/imagery_offset_db/src/iodb/ImageryOffsetBase.java

    r29371 r29376  
    3737    }
    3838
    39     public void setAbandonDate(Date abandonDate) {
     39    public void setDeprecated(Date abandonDate, String author, String reason) {
    4040        this.abandonDate = abandonDate;
     41        this.abandonAuthor = author;
     42        this.abandonReason = reason;
    4143    }
    4244
  • applications/editors/josm/plugins/imagery_offset_db/src/iodb/ImageryOffsetTools.java

    r29371 r29376  
    11package iodb;
    22
     3import java.text.SimpleDateFormat;
    34import java.util.*;
    45import org.openstreetmap.josm.Main;
     
    1819public class ImageryOffsetTools {
    1920    public static final String DIALOG_TITLE = tr("Imagery Offset");
     21    public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");
    2022   
    2123    public static ImageryLayer getTopImageryLayer() {
  • applications/editors/josm/plugins/imagery_offset_db/src/iodb/OffsetDialog.java

    r29371 r29376  
    11package iodb;
    22
     3import java.awt.FlowLayout;
    34import java.awt.GridLayout;
     5import java.awt.Insets;
    46import java.awt.event.ActionEvent;
    57import java.awt.event.ActionListener;
    6 import java.util.List;
     8import java.awt.event.KeyEvent;
     9import java.util.*;
    710import javax.swing.*;
     11import javax.swing.border.CompoundBorder;
     12import javax.swing.border.EmptyBorder;
    813import org.openstreetmap.josm.Main;
    914import static org.openstreetmap.josm.tools.I18n.tr;
     
    1520 */
    1621public class OffsetDialog extends JDialog implements ActionListener {
     22    protected static final String PREF_CALIBRATION = "iodb.show.calibration";
     23    protected static final String PREF_DEPRECATED = "iodb.show.deprecated";
     24
    1725    private List<ImageryOffsetBase> offsets;
    1826    private ImageryOffsetBase selectedOffset;
     27    private JPanel buttonPanel;
    1928
    2029    public OffsetDialog( List<ImageryOffsetBase> offsets ) {
    21         super(JOptionPane.getFrameForComponent(Main.parent), tr("Imagery Offset"), ModalityType.DOCUMENT_MODAL);
     30        super(JOptionPane.getFrameForComponent(Main.parent), ImageryOffsetTools.DIALOG_TITLE, ModalityType.DOCUMENT_MODAL);
    2231        setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
     32        setResizable(false);
    2333        this.offsets = offsets;
     34
     35        // make this dialog close on "escape"
     36        getRootPane().registerKeyboardAction(this,
     37                KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0),
     38                JComponent.WHEN_IN_FOCUSED_WINDOW);
    2439    }
    2540   
    2641    private void prepareDialog() {
    27         JPanel buttonPanel = new JPanel(new GridLayout(offsets.size() + 1, 1));
    28         for( ImageryOffsetBase offset : offsets ) {
     42        Box dialog = new Box(BoxLayout.Y_AXIS);
     43        updateButtonPanel();
     44        // todo: calibration objects and deprecated offsets button
     45        final JCheckBox calibrationBox = new JCheckBox(tr("Hide calibration geometries"));
     46        calibrationBox.setSelected(Main.pref.getBoolean(PREF_CALIBRATION, true));
     47        calibrationBox.addActionListener(new ActionListener() {
     48            public void actionPerformed( ActionEvent e ) {
     49                Main.pref.put(PREF_CALIBRATION, calibrationBox.isSelected());
     50                updateButtonPanel();
     51            }
     52        });
     53        final JCheckBox deprecatedBox = new JCheckBox(tr("Show deprecated offsets"));
     54        deprecatedBox.setSelected(Main.pref.getBoolean(PREF_DEPRECATED, false));
     55        deprecatedBox.addActionListener(new ActionListener() {
     56            public void actionPerformed( ActionEvent e ) {
     57                Main.pref.put(PREF_DEPRECATED, deprecatedBox.isSelected());
     58                updateButtonPanel();
     59            }
     60        });
     61        Box checkBoxPanel = new Box(BoxLayout.X_AXIS);
     62        checkBoxPanel.add(calibrationBox);
     63        checkBoxPanel.add(deprecatedBox);
     64        JButton cancelButton = new JButton("Cancel");
     65        cancelButton.addActionListener(this);
     66        cancelButton.setAlignmentX(CENTER_ALIGNMENT);
     67
     68        dialog.add(buttonPanel);
     69        dialog.add(checkBoxPanel);
     70        dialog.add(cancelButton);
     71
     72        dialog.setBorder(new CompoundBorder(dialog.getBorder(), new EmptyBorder(5, 5, 5, 5)));
     73        setContentPane(dialog);
     74        pack();
     75        setLocationRelativeTo(Main.parent);
     76    }
     77
     78    private void updateButtonPanel() {
     79        List<ImageryOffsetBase> filteredOffsets = filterOffsets();
     80        if( buttonPanel == null )
     81            buttonPanel = new JPanel();
     82        buttonPanel.removeAll();
     83        buttonPanel.setLayout(new GridLayout(filteredOffsets.size(), 1, 0, 5));
     84        for( ImageryOffsetBase offset : filteredOffsets ) {
    2985            OffsetDialogButton button = new OffsetDialogButton(offset);
    3086            button.addActionListener(this);
    31 /*            JPopupMenu popupMenu = new JPopupMenu();
     87            JPopupMenu popupMenu = new JPopupMenu();
    3288            popupMenu.add(new OffsetInfoAction(offset));
    3389            if( !offset.isDeprecated() )
    3490                popupMenu.add(new DeprecateOffsetAction(offset));
    35             button.add(popupMenu);*/
     91            button.setComponentPopupMenu(popupMenu);
    3692            buttonPanel.add(button);
    3793        }
    38         // todo: calibration objects and deprecated offsets button
    39         JButton cancelButton = new JButton("Cancel");
    40         cancelButton.addActionListener(this);
    41         buttonPanel.add(cancelButton); // todo: proper button
    42         setContentPane(buttonPanel);
    4394        pack();
    44         setLocationRelativeTo(Main.parent);
     95    }
     96
     97    private List<ImageryOffsetBase> filterOffsets() {
     98        boolean showCalibration = Main.pref.getBoolean(PREF_CALIBRATION, true);
     99        boolean showDeprecated = Main.pref.getBoolean(PREF_DEPRECATED, false);
     100        List<ImageryOffsetBase> filteredOffsets = new ArrayList<ImageryOffsetBase>();
     101        for( ImageryOffsetBase offset : offsets ) {
     102            if( offset.isDeprecated() && !showDeprecated )
     103                continue;
     104            if( offset instanceof CalibrationObject && !showCalibration )
     105                continue;
     106            filteredOffsets.add(offset);
     107        }
     108        return filteredOffsets;
    45109    }
    46110   
  • applications/editors/josm/plugins/imagery_offset_db/src/iodb/StoreImageryOffsetAction.java

    r29371 r29376  
    4646        if( selectedObjects.size() == 1 ) {
    4747            OsmPrimitive selection = selectedObjects.iterator().next();
    48             if( selection instanceof Node || selection instanceof Way ) {
    49                 boolean suitable = !selection.isNewOrUndeleted() && !selection.isDeleted() && !selection.isModified();
    50                 if( selection instanceof Way ) {
    51                     for( Node n : ((Way)selection).getNodes() )
    52                         if( n.isNewOrUndeleted() || n.isDeleted() || n.isModified() )
    53                             suitable = false;
    54                 } else if( selection.isReferredByWays(1) ) {
    55                     suitable = false;
    56                 }
    57                 if( suitable ) {
    58                     String[] options = new String[] {tr("Store calibration object"), tr("Store imagery offset"), tr("Cancel")};
    59                     int result = JOptionPane.showOptionDialog(Main.parent,
    60                             tr("The selected object can be used as a calibration object. What do you intend to do?"), ImageryOffsetTools.DIALOG_TITLE, JOptionPane.DEFAULT_OPTION, JOptionPane.QUESTION_MESSAGE,
    61                             null, options, options[0]);
    62                     if( result == 2 || result == JOptionPane.CLOSED_OPTION )
    63                         return;
    64                     if( result == 0 )
    65                         calibration = selection;
    66                 } else {
    67                     String[] options = new String[] {tr("Store imagery offset"), tr("Cancel")};
    68                     int result = JOptionPane.showOptionDialog(Main.parent,
    69                             tr("You have an object selected and might want to use it as a calibration object.\n"
    70                              + "But in this case it should be uploaded to OSM server first."), ImageryOffsetTools.DIALOG_TITLE, JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE,
    71                             null, options, options[1]);
    72                     if( result == 1 || result == JOptionPane.CLOSED_OPTION )
    73                         return;
    74                 }
     48            if( (selection instanceof Node || selection instanceof Way) && !selection.isIncomplete() && !selection.isReferredByWays(1) ) {
     49                String[] options = new String[] {tr("Store calibration geometry"), tr("Store imagery offset"), tr("Cancel")};
     50                int result = JOptionPane.showOptionDialog(Main.parent,
     51                        tr("The selected object can be used as a calibration geometry. What do you intend to do?"),
     52                        ImageryOffsetTools.DIALOG_TITLE, JOptionPane.DEFAULT_OPTION, JOptionPane.QUESTION_MESSAGE,
     53                        null, options, options[0]);
     54                if( result == 2 || result == JOptionPane.CLOSED_OPTION )
     55                    return;
     56                if( result == 0 )
     57                    calibration = selection;
    7558            }
    7659        }
     
    8871            LatLon offset = ImageryOffsetTools.getLayerOffset(layer, center);
    8972            offsetObj = new ImageryOffset(ImageryOffsetTools.getImageryID(layer), offset);
    90             message = "You are registering an imagery offset.\n"
    91                     + "Other users in this area will be able to use it for mapping.\n"
    92                     + "Please make sure it is as precise as possible, and\n"
    93                     + "describe a region this offset is applicable to.";
     73            message = "You are registering an imagery offset. Other users in this area will be able to use it for mapping.\n"
     74                    + "Please make sure it is as precise as possible, and describe a region this offset is applicable to.";
    9475        } else {
    9576            // register calibration object
    9677            offsetObj = new CalibrationObject(calibration);
    97             message = "You are registering calibration object.\n"
    98                     + "It should be the most precisely positioned object,\n"
     78            message = "You are registering a calibration geometry. It should be the most precisely positioned object,\n"
    9979                    + "with clearly visible boundaries on various satellite imagery.\n"
    10080                    + "Please describe a region where this object is located.";
Note: See TracChangeset for help on using the changeset viewer.