Ticket #15057: overpass-dialog-refactored.patch

File overpass-dialog-refactored.patch, 60.0 KB (added by bafonins, 7 years ago)

Here is the patch

  • src/org/openstreetmap/josm/actions/OverpassDownloadAction.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
     
    66
    77import java.awt.BorderLayout;
    88import java.awt.Component;
    9 import java.awt.GridLayout;
    10 import java.awt.Rectangle;
     9import java.awt.Dimension;
     10import java.awt.GridBagLayout;
    1111import java.awt.event.ActionEvent;
    12 import java.awt.event.ActionListener;
    1312import java.awt.event.FocusEvent;
    1413import java.awt.event.FocusListener;
    1514import java.awt.event.KeyEvent;
    1615import java.util.ArrayList;
     16import java.util.Arrays;
    1717import java.util.Collection;
    1818import java.util.Collections;
    19 import java.util.Deque;
    20 import java.util.LinkedList;
     19import java.util.Optional;
    2120import java.util.concurrent.Future;
     21import java.util.function.Consumer;
    2222
    2323import javax.swing.AbstractAction;
    2424import javax.swing.Action;
    2525import javax.swing.ActionMap;
    2626import javax.swing.JButton;
    27 import javax.swing.JComponent;
     27import javax.swing.JEditorPane;
    2828import javax.swing.JLabel;
    29 import javax.swing.JMenuItem;
    3029import javax.swing.JOptionPane;
    3130import javax.swing.JPanel;
    32 import javax.swing.JPopupMenu;
    3331import javax.swing.JScrollPane;
     32import javax.swing.event.HyperlinkEvent;
    3433import javax.swing.plaf.basic.BasicArrowButton;
     34import javax.swing.text.JTextComponent;
    3535
    3636import org.openstreetmap.josm.Main;
    3737import org.openstreetmap.josm.actions.downloadtasks.DownloadOsmTask;
    3838import org.openstreetmap.josm.actions.downloadtasks.PostDownloadHandler;
    3939import org.openstreetmap.josm.data.Bounds;
     40import org.openstreetmap.josm.data.preferences.BooleanProperty;
    4041import org.openstreetmap.josm.data.preferences.CollectionProperty;
    41 import org.openstreetmap.josm.data.preferences.IntegerProperty;
    42 import org.openstreetmap.josm.gui.HelpAwareOptionPane;
     42import org.openstreetmap.josm.gui.ExtendedDialog;
     43import org.openstreetmap.josm.gui.OverpassQueryList;
    4344import org.openstreetmap.josm.gui.download.DownloadDialog;
    4445import org.openstreetmap.josm.gui.preferences.server.OverpassServerPreference;
    4546import org.openstreetmap.josm.gui.util.GuiHelper;
     
    4748import org.openstreetmap.josm.gui.widgets.JosmTextArea;
    4849import org.openstreetmap.josm.io.OverpassDownloadReader;
    4950import org.openstreetmap.josm.tools.GBC;
    50 import org.openstreetmap.josm.tools.InputMapUtils;
     51import org.openstreetmap.josm.tools.OpenBrowser;
    5152import org.openstreetmap.josm.tools.OverpassTurboQueryWizard;
    5253import org.openstreetmap.josm.tools.Shortcut;
    5354import org.openstreetmap.josm.tools.UncheckedParseException;
     
    7677        OverpassDownloadDialog dialog = OverpassDownloadDialog.getInstance();
    7778        dialog.restoreSettings();
    7879        dialog.setVisible(true);
    79         if (!dialog.isCanceled()) {
    80             dialog.rememberSettings();
    81             Bounds area = dialog.getSelectedDownloadArea();
    82             DownloadOsmTask task = new DownloadOsmTask();
    83             task.setZoomAfterDownload(dialog.isZoomToDownloadedDataRequired());
    84             Future<?> future = task.download(
    85                     new OverpassDownloadReader(area, OverpassServerPreference.getOverpassServer(), dialog.getOverpassQuery()),
    86                     dialog.isNewLayerRequired(), area, null);
    87             Main.worker.submit(new PostDownloadHandler(task, future));
    88         }
     80
     81        if (dialog.isCanceled()) {
     82            return;
     83        }
     84
     85        dialog.rememberSettings();
     86        Optional<Bounds> selectedArea = dialog.getSelectedDownloadArea();
     87        String overpassQuery = dialog.getOverpassQuery();
     88
     89        /*
     90         * Absence of the selected area can be justified only if the overpass query
     91         * is not restricted to bbox.
     92         */
     93        if (!selectedArea.isPresent() && overpassQuery.contains("{{bbox}}")) {
     94            JOptionPane.showMessageDialog(
     95                    dialog,
     96                    tr("Please select a download area first."),
     97                    tr("Error"),
     98                    JOptionPane.ERROR_MESSAGE
     99            );
     100            return;
     101        }
     102
     103        /*
     104         * A callback that is passed to PostDownloadReporter that is called once the download task
     105         * has finished. According to the number of errors happened, their type we decide whether we
     106         * want to save the last query in OverpassQueryList.
     107         */
     108        Consumer<Collection> errorReporter = (errors) -> {
     109
     110            boolean onlyNoDataError = errors.size() == 1 &&
     111                    errors.contains("No data found in this area.");
     112
     113            if (errors.isEmpty() || onlyNoDataError) {
     114                dialog.saveHistoricItemOnSuccess();
     115            }
     116        };
     117
     118        /*
     119         * In order to support queries generated by the Overpass Turbo Query Wizard tool
     120         * which do not require the area to be specified.
     121         */
     122        Bounds area = selectedArea.orElseGet(() -> new Bounds(0, 0, 0, 0));
     123        DownloadOsmTask task = new DownloadOsmTask();
     124        task.setZoomAfterDownload(dialog.isZoomToDownloadedDataRequired());
     125        Future<?> future = task.download(
     126                new OverpassDownloadReader(area, OverpassServerPreference.getOverpassServer(), dialog.getOverpassQuery()),
     127                dialog.isNewLayerRequired(), area, null);
     128        Main.worker.submit(new PostDownloadHandler(task, future, errorReporter));
    89129    }
    90130
    91131    private static final class DisableActionsFocusListener implements FocusListener {
     
    121161
    122162    private static final class OverpassDownloadDialog extends DownloadDialog {
    123163
    124         private HistoryComboBox overpassWizard;
    125164        private JosmTextArea overpassQuery;
     165        private OverpassQueryList overpassQueryList;
    126166        private static OverpassDownloadDialog instance;
    127         private static final CollectionProperty OVERPASS_WIZARD_HISTORY = new CollectionProperty("download.overpass.wizard",
    128                 new ArrayList<String>());
     167        private static final BooleanProperty OVERPASS_QUERY_LIST_OPENED =
     168                new BooleanProperty("download.overpass.query-list.opened", false);
    129169
    130170        private OverpassDownloadDialog(Component parent) {
    131171            super(parent, ht("/Action/OverpassDownload"));
     
    145185
    146186        @Override
    147187        protected void buildMainPanelAboveDownloadSelections(JPanel pnl) {
     188            // needed for the invisible checkboxes cbDownloadGpxData, cbDownloadNotes
     189            pnl.add(new JLabel(), GBC.eol());
    148190
    149191            DisableActionsFocusListener disableActionsFocusListener =
    150192                    new DisableActionsFocusListener(slippyMapChooser.getNavigationComponentActionMap());
    151193
    152             pnl.add(new JLabel(), GBC.eol()); // needed for the invisible checkboxes cbDownloadGpxData, cbDownloadNotes
    153 
    154             final String tooltip = tr("Builds an Overpass query using the Overpass Turbo query wizard");
    155             overpassWizard = new HistoryComboBox();
    156             overpassWizard.setToolTipText(tooltip);
    157             overpassWizard.getEditorComponent().addFocusListener(disableActionsFocusListener);
    158             final JButton buildQuery = new JButton(tr("Build query"));
    159             final Action buildQueryAction = new AbstractAction() {
     194            String tooltip = tr("Build an Overpass query using the Overpass Turbo Query Wizard tool");
     195            Action queryWizardAction = new AbstractAction() {
    160196                @Override
    161197                public void actionPerformed(ActionEvent e) {
    162                     final String overpassWizardText = overpassWizard.getText();
    163                     try {
    164                         overpassQuery.setText(OverpassTurboQueryWizard.getInstance().constructQuery(overpassWizardText));
    165                     } catch (UncheckedParseException ex) {
    166                         Main.error(ex);
    167                         HelpAwareOptionPane.showOptionDialog(
    168                                 Main.parent,
    169                                 tr("<html>The Overpass wizard could not parse the following query:"
    170                                         + Utils.joinAsHtmlUnorderedList(Collections.singleton(overpassWizardText))),
    171                                 tr("Parse error"),
    172                                 JOptionPane.ERROR_MESSAGE,
    173                                 null
    174                         );
    175                     }
     198                    QueryWizardDialog.getInstance().showDialog();
    176199                }
    177200            };
    178             buildQuery.addActionListener(buildQueryAction);
    179             buildQuery.setToolTipText(tooltip);
    180             pnl.add(buildQuery, GBC.std().insets(5, 5, 5, 5));
    181             pnl.add(overpassWizard, GBC.eol().fill(GBC.HORIZONTAL));
    182             InputMapUtils.addEnterAction(overpassWizard.getEditorComponent(), buildQueryAction);
    183201
    184             overpassQuery = new JosmTextArea("", 8, 80);
    185             overpassQuery.setFont(GuiHelper.getMonospacedFont(overpassQuery));
    186             overpassQuery.addFocusListener(disableActionsFocusListener);
    187             JScrollPane scrollPane = new JScrollPane(overpassQuery);
    188             final JPanel pane = new JPanel(new BorderLayout());
    189             final BasicArrowButton arrowButton = new BasicArrowButton(BasicArrowButton.SOUTH);
    190             arrowButton.addActionListener(new AbstractAction() {
     202            JButton openQueryWizard = new JButton("Query Wizard");
     203            openQueryWizard.setToolTipText(tooltip);
     204            openQueryWizard.addActionListener(queryWizardAction);
     205
     206            // CHECKSTYLE.OFF: LineLength
     207            this.overpassQuery = new JosmTextArea(
     208                    "/*\n" +
     209                    tr("Place your Overpass query below or generate one using the Overpass Turbo Query Wizard")
     210                    + "\n*/",
     211                    8, 80);
     212            // CHECKSTYLE.ON: LineLength
     213            this.overpassQuery.setFont(GuiHelper.getMonospacedFont(overpassQuery));
     214            this.overpassQuery.addFocusListener(disableActionsFocusListener);
     215            this.overpassQuery.addFocusListener(new FocusListener() {
    191216                @Override
    192                 public void actionPerformed(ActionEvent e) {
    193                     OverpassQueryHistoryPopup.show(arrowButton, OverpassDownloadDialog.this);
     217                public void focusGained(FocusEvent e) {
     218                    overpassQuery.selectAll();
     219                }
     220
     221                @Override
     222                public void focusLost(FocusEvent e) {
     223
    194224                }
    195225            });
    196             pane.add(scrollPane, BorderLayout.CENTER);
    197             pane.add(arrowButton, BorderLayout.EAST);
    198             pnl.add(new JLabel(tr("Overpass query: ")), GBC.std().insets(5, 5, 5, 5));
    199             GBC gbc = GBC.eol().fill(GBC.HORIZONTAL);
    200             gbc.ipady = 200;
     226
     227            this.overpassQueryList = new OverpassQueryList(this, this.overpassQuery);
     228            overpassQueryList.setToolTipText(tr("Show/hide Overpass snippet list"));
     229            overpassQueryList.setVisible(OVERPASS_QUERY_LIST_OPENED.get());
     230            overpassQueryList.setPreferredSize(new Dimension(350, 300));
     231            JScrollPane scrollPane = new JScrollPane(overpassQuery);
     232            BasicArrowButton arrowButton = new BasicArrowButton(overpassQueryList.isVisible()
     233                ? BasicArrowButton.EAST
     234                : BasicArrowButton.WEST);
     235            arrowButton.addActionListener(e ->  {
     236                if (overpassQueryList.isVisible()) {
     237                    overpassQueryList.setVisible(false);
     238                    arrowButton.setDirection(BasicArrowButton.WEST);
     239                    OVERPASS_QUERY_LIST_OPENED.put(false);
     240                } else {
     241                    overpassQueryList.setVisible(true);
     242                    arrowButton.setDirection(BasicArrowButton.EAST);
     243                    OVERPASS_QUERY_LIST_OPENED.put(false);
     244                }
     245            });
     246
     247            JPanel innerPanel = new JPanel(new BorderLayout());
     248            innerPanel.add(scrollPane, BorderLayout.CENTER);
     249            innerPanel.add(arrowButton, BorderLayout.EAST);
     250
     251            JPanel pane = new JPanel(new BorderLayout());
     252            pane.add(innerPanel, BorderLayout.CENTER);
     253            pane.add(overpassQueryList, BorderLayout.EAST);
     254
     255            GBC gbc = GBC.eol().fill(GBC.HORIZONTAL); gbc.ipady = 200;
     256            pnl.add(openQueryWizard, GBC.std().insets(5, 5, 5, 5));
    201257            pnl.add(pane, gbc);
    202 
    203258        }
    204259
    205         public String getOverpassQuery() {
     260        String getOverpassQuery() {
    206261            return overpassQuery.getText();
    207262        }
    208263
    209         public void setOverpassQuery(String text) {
     264        void setOverpassQuery(String text) {
    210265            overpassQuery.setText(text);
    211266        }
    212267
    213         @Override
    214         public void restoreSettings() {
    215             super.restoreSettings();
    216             overpassWizard.setPossibleItems(OVERPASS_WIZARD_HISTORY.get());
    217         }
    218 
    219         @Override
    220         public void rememberSettings() {
    221             super.rememberSettings();
    222             overpassWizard.addCurrentItemToHistory();
    223             OVERPASS_WIZARD_HISTORY.put(overpassWizard.getHistory());
    224             OverpassQueryHistoryPopup.addToHistory(getOverpassQuery());
     268        /**
     269         * Adds the current query to {@link OverpassQueryList}.
     270         */
     271        void saveHistoricItemOnSuccess() {
     272            overpassQueryList.saveHistoricItem(overpassQuery.getText());
    225273        }
    226274
    227275        @Override
    228276        protected void updateSizeCheck() {
    229277            displaySizeCheckResult(false);
    230278        }
     279
     280        /**
     281         * Triggers the download action to fire.
     282         */
     283        private void triggerDownload() {
     284            super.btnDownload.doClick();
     285        }
    231286    }
    232287
    233     static class OverpassQueryHistoryPopup extends JPopupMenu {
     288    private static final class QueryWizardDialog extends ExtendedDialog {
    234289
    235         static final CollectionProperty OVERPASS_QUERY_HISTORY = new CollectionProperty("download.overpass.query", new ArrayList<String>());
    236         static final IntegerProperty OVERPASS_QUERY_HISTORY_SIZE = new IntegerProperty("download.overpass.query.size", 12);
     290        private static QueryWizardDialog dialog;
     291        private final HistoryComboBox queryWizard;
     292        private final OverpassTurboQueryWizard overpassQueryBuilder;
     293        private static final CollectionProperty OVERPASS_WIZARD_HISTORY =
     294                new CollectionProperty("download.overpass.wizard", new ArrayList<String>());
    237295
    238         OverpassQueryHistoryPopup(final OverpassDownloadDialog dialog) {
    239             final Collection<String> history = OVERPASS_QUERY_HISTORY.get();
    240             setLayout(new GridLayout((int) Math.ceil(history.size() / 2.), 2));
    241             for (final String i : history) {
    242                 add(new OverpassQueryHistoryItem(i, dialog));
    243             }
    244         }
     296        // dialog buttons
     297        private static final int BUILD_QUERY = 0;
     298        private static final int BUILD_AN_EXECUTE_QUERY = 1;
     299        private static final int CANCEL = 2;
    245300
    246         static void show(final JComponent parent, final OverpassDownloadDialog dialog) {
    247             final OverpassQueryHistoryPopup menu = new OverpassQueryHistoryPopup(dialog);
    248             final Rectangle r = parent.getBounds();
    249             menu.show(parent.getParent(), r.x + r.width - (int) menu.getPreferredSize().getWidth(), r.y + r.height);
    250         }
     301        /**
     302         * Get an instance of {@link QueryWizardDialog}.
     303         */
     304        public static QueryWizardDialog getInstance() {
     305            if (dialog == null) {
     306                dialog = new QueryWizardDialog();
     307            }
    251308
    252         static void addToHistory(final String query) {
    253             final Deque<String> history = new LinkedList<>(OVERPASS_QUERY_HISTORY.get());
    254             if (!history.contains(query)) {
    255                 history.add(query);
    256             }
    257             while (history.size() > OVERPASS_QUERY_HISTORY_SIZE.get()) {
    258                 history.removeFirst();
    259             }
    260             OVERPASS_QUERY_HISTORY.put(history);
     309            return dialog;
    261310        }
    262     }
    263311
    264     static class OverpassQueryHistoryItem extends JMenuItem implements ActionListener {
     312        private static final String DESCRIPTION_STYLE =
     313                "<style type=\"text/css\">\n"
     314                + "table { border-spacing: 0pt;}\n"
     315                + "h3 {text-align: center; padding: 8px;}\n"
     316                + "td {border: 1px solid #dddddd; text-align: left; padding: 8px;}\n"
     317                + "#desc {width: 350px;}"
     318                + "</style>\n";
    265319
    266         final String query;
    267         final OverpassDownloadDialog dialog;
     320        private QueryWizardDialog() {
     321            super(OverpassDownloadDialog.getInstance(), tr("Overpass Turbo Query Wizard"),
     322                    tr("Build query"), tr("Build query and execute"), tr("Cancel"));
    268323
    269         OverpassQueryHistoryItem(final String query, final OverpassDownloadDialog dialog) {
    270             this.query = query;
    271             this.dialog = dialog;
    272             setText("<html><pre style='width:300px;'>" +
    273                     Utils.escapeReservedCharactersHTML(Utils.restrictStringLines(query, 7)));
    274             addActionListener(this);
     324            this.queryWizard = new HistoryComboBox();
     325            this.overpassQueryBuilder = OverpassTurboQueryWizard.getInstance();
     326
     327            JPanel panel = new JPanel(new GridBagLayout());
     328
     329            JLabel searchLabel = new JLabel(tr("Search :"));
     330            JTextComponent descPane = this.buildDescriptionSection();
     331            JScrollPane scroll = GuiHelper.embedInVerticalScrollPane(descPane);
     332            scroll.getVerticalScrollBar().setUnitIncrement(10); // make scrolling smooth
     333
     334            panel.add(searchLabel, GBC.std().insets(0, 0, 0, 20).anchor(GBC.SOUTHEAST));
     335            panel.add(queryWizard, GBC.eol().insets(0, 0, 0, 15).fill(GBC.HORIZONTAL).anchor(GBC.SOUTH));
     336            panel.add(scroll, GBC.eol().fill(GBC.BOTH).anchor(GBC.CENTER));
     337
     338            queryWizard.setPossibleItems(OVERPASS_WIZARD_HISTORY.get());
     339
     340            setCancelButton(CANCEL);
     341            setDefaultButton(BUILD_AN_EXECUTE_QUERY + 1); // Build and execute button
     342            setContent(panel, false);
    275343        }
    276344
    277345        @Override
    278         public void actionPerformed(ActionEvent e) {
    279             dialog.setOverpassQuery(query);
    280         }
    281     }
     346        public void buttonAction(int buttonIndex, ActionEvent evt) {
     347            switch (buttonIndex) {
     348                case BUILD_QUERY:
     349                    if (this.buildQueryAction()) {
     350                        this.saveHistory();
     351                        super.buttonAction(BUILD_QUERY, evt);
     352                    }
     353                    break;
     354                case BUILD_AN_EXECUTE_QUERY:
     355                    if (this.buildQueryAction()) {
     356                        this.saveHistory();
     357                        super.buttonAction(BUILD_AN_EXECUTE_QUERY, evt);
     358
     359                        OverpassDownloadDialog.getInstance().triggerDownload();
     360                    }
     361                    break;
     362                default:
     363                    super.buttonAction(buttonIndex, evt);
    282364
    283 }
     365            }
     366        }
     367
     368        /**
     369         * Saves the latest, successfully parsed search term.
     370         */
     371        private void saveHistory() {
     372            queryWizard.addCurrentItemToHistory();
     373            OVERPASS_WIZARD_HISTORY.put(queryWizard.getHistory());
     374        }
     375
     376        /**
     377         * Tries to process a search term using {@link OverpassTurboQueryWizard}. If the term cannot
     378         * be parsed, the the corresponding dialog is shown.
     379         * @param searchTerm The search term to parse.
     380         * @return {@link Optional#empty()} if an exception was thrown when parsing, meaning
     381         * that the term cannot be processed, or non-empty {@link Optional} containing the result
     382         * of parsing.
     383         */
     384        private Optional<String> tryParseSearchTerm(String searchTerm) {
     385            try {
     386                String query = this.overpassQueryBuilder.constructQuery(searchTerm);
     387
     388                return Optional.of(query);
     389            } catch (UncheckedParseException ex) {
     390                Main.error(ex);
     391                JOptionPane.showMessageDialog(
     392                        OverpassDownloadDialog.getInstance(),
     393                        "<html>" +
     394                         tr("The Overpass wizard could not parse the following query:") +
     395                         Utils.joinAsHtmlUnorderedList(Collections.singleton(searchTerm)) +
     396                         "</html>",
     397                        tr("Parse error"),
     398                        JOptionPane.ERROR_MESSAGE
     399                );
     400
     401                return Optional.empty();
     402            }
     403        }
     404
     405        /**
     406         * Builds an Overpass query out from {@link QueryWizardDialog#queryWizard} contents.
     407         * @return {@code true} if the query successfully built, {@code false} otherwise.
     408         */
     409        private boolean buildQueryAction() {
     410            final String wizardSearchTerm = this.queryWizard.getText();
     411
     412            Optional<String> q = this.tryParseSearchTerm(wizardSearchTerm);
     413            if (q.isPresent()) {
     414                String query = q.get();
     415                OverpassDownloadDialog.getInstance().setOverpassQuery(query);
     416
     417                return true;
     418            }
     419
     420            return false;
     421        }
     422
     423        private JTextComponent buildDescriptionSection() {
     424            JEditorPane descriptionSection = new JEditorPane("text/html", this.getDescriptionContent());
     425            descriptionSection.setEditable(false);
     426            descriptionSection.addHyperlinkListener(e -> {
     427                if (HyperlinkEvent.EventType.ACTIVATED.equals(e.getEventType())) {
     428                    OpenBrowser.displayUrl(e.getURL().toString());
     429                }
     430            });
     431
     432            return descriptionSection;
     433        }
     434
     435        private String getDescriptionContent() {
     436            return new StringBuilder("<html>")
     437                    .append(DESCRIPTION_STYLE)
     438                    .append("<body>")
     439                    .append("<h3>")
     440                    .append(tr("Query Wizard"))
     441                    .append("</h3>")
     442                    .append("<p>")
     443                    .append(tr("Allows you to interact with <i>Overpass API</i> by writing declarative, human-readable terms."))
     444                    .append(tr("The <i>Query Wizard</i> tool will transform those to a valid overpass query."))
     445                    .append(tr("For more detailed description see "))
     446                    .append(tr("<a href=\"{0}\">OSM Wiki</a>.", Main.getOSMWebsite() + "/wiki/Overpass_turbo/Wizard"))
     447                    .append("</p>")
     448                    .append("<h3>").append(tr("Hints")).append("</h3>")
     449                    .append("<table>").append("<tr>").append("<td>")
     450                    .append(Utils.joinAsHtmlUnorderedList(Arrays.asList("<i>type:node</i>", "<i>type:relation</i>", "<i>type:way</i>")))
     451                    .append("</td>").append("<td>")
     452                    .append("<span>").append(tr("Download objects of a certain type.")).append("</span>")
     453                    .append("</td>").append("</tr>")
     454                    .append("<tr>").append("<td>")
     455                    .append(Utils.joinAsHtmlUnorderedList(
     456                            Arrays.asList("<i>key=value in <u>location</u></i>",
     457                                    "<i>key=value around <u>location</u></i>",
     458                                    "<i>key=value in bbox</i>")))
     459                    .append("</td>").append("<td>")
     460                    .append(tr("Download object by specifying a specific location. For example,"))
     461                    .append(Utils.joinAsHtmlUnorderedList(Arrays.asList(
     462                            tr("{0} all objects having {1} as attribute are downloaded.", "<i>tourism=hotel in Berlin</i> -", "'tourism=hotel'"),
     463                            tr("{0} all object with the corresponding key/value pair located around Berlin. Note, the default value for radius "+
     464                                    "is set to 1000m, but it can be changed in the generated query.", "<i>tourism=hotel around Berlin</i> -"),
     465                            tr("{0} all objects within the current selection that have {1} as attribute.", "<i>tourism=hotel in bbox</i> -",
     466                                    "'tourism=hotel'"))))
     467                    .append("<span>")
     468                    .append(tr("Instead of <i>location</i> any valid place name can be used like address, city, etc."))
     469                    .append("</span>")
     470                    .append("</td>").append("</tr>")
     471                    .append("<tr>").append("<td>")
     472                    .append(Utils.joinAsHtmlUnorderedList(Arrays.asList("<i>key=value</i>", "<i>key=*</i>", "<i>key~regex</i>",
     473                            "<i>key!=value</i>", "<i>key!~regex</i>", "<i>key=\"combined value\"</i>")))
     474                    .append("</td>").append("<td>")
     475                    .append(tr("<span>Download objects that have some concrete key/value pair, only the key with any contents for the value, " +
     476                            "the value matching some regular expression. 'Not equal' operators are supported as well.</span>"))
     477                    .append("</td>").append("</tr>")
     478                    .append("<tr>").append("<td>")
     479                    .append(Utils.joinAsHtmlUnorderedList(Arrays.asList(
     480                            tr("<i>expression1 {0} expression2</i>", "or"),
     481                            tr("<i>expression1 {0} expression2</i>", "and"))))
     482                    .append("</td>").append("<td>")
     483                    .append("<span>")
     484                    .append(tr("Basic logical operators can be used to create more sophisticated queries. Instead of 'or' - '|', '||' " +
     485                            "can be used, and instead of 'and' - '&', '&&'."))
     486                    .append("</span>")
     487                    .append("</td>").append("</tr>").append("</table>")
     488                    .append("</body>")
     489                    .append("</html>")
     490                    .toString();
     491        }
     492    }
     493}
     494 No newline at end of file
  • src/org/openstreetmap/josm/actions/downloadtasks/PostDownloadHandler.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
     
    1111import java.util.concurrent.CancellationException;
    1212import java.util.concurrent.ExecutionException;
    1313import java.util.concurrent.Future;
     14import java.util.function.Consumer;
    1415
    1516import javax.swing.JOptionPane;
    1617import javax.swing.SwingUtilities;
     
    2829public class PostDownloadHandler implements Runnable {
    2930    private final DownloadTask task;
    3031    private final Future<?> future;
     32    private Consumer<Collection> errorReporter;
    3133
    3234    /**
    3335     * constructor
     
    3941        this.future = future;
    4042    }
    4143
     44    /**
     45     * constructor
     46     * @param task the asynchronous download task
     47     * @param future the future on which the completion of the download task can be synchronized
     48     * @param errorReporter a callback to inform about the number errors happened during the download
     49     *                      task
     50     */
     51    public PostDownloadHandler(DownloadTask task, Future<?> future, Consumer<Collection> errorReporter) {
     52        this(task, future);
     53        this.errorReporter = errorReporter;
     54    }
     55
    4256    @Override
    4357    public void run() {
    4458        // wait for downloads task to finish (by waiting for the future to return a value)
     
    5367        // make sure errors are reported only once
    5468        //
    5569        Set<Object> errors = new LinkedHashSet<>(task.getErrorObjects());
    56         if (errors.isEmpty())
     70        if (this.errorReporter != null) {
     71            errorReporter.accept(errors);
     72        }
     73
     74        if (errors.isEmpty()) {
    5775            return;
     76        }
    5877
    5978        // just one error object?
    6079        //
     
    100119            return;
    101120        }
    102121    }
    103 }
     122}
     123 No newline at end of file
  • src/org/openstreetmap/josm/gui/download/DownloadDialog.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
     
    1919import java.awt.event.WindowEvent;
    2020import java.util.ArrayList;
    2121import java.util.List;
     22import java.util.Optional;
    2223
    2324import javax.swing.AbstractAction;
    2425import javax.swing.JButton;
     
    457458    }
    458459
    459460    /**
    460      * Replies the currently selected download area.
    461      * @return the currently selected download area. May be {@code null}, if no download area is selected yet.
     461     * Returns an {@link Optional} of the currently selected download area.
     462     * @return An {@link Optional} of the currently selected download area.
    462463     */
    463     public Bounds getSelectedDownloadArea() {
    464         return currentBounds;
     464    public Optional<Bounds> getSelectedDownloadArea() {
     465        return Optional.ofNullable(currentBounds);
    465466    }
    466467
    467468    @Override
     
    524525        }
    525526
    526527        public void run() {
    527             if (currentBounds == null) {
    528                 JOptionPane.showMessageDialog(
    529                         DownloadDialog.this,
    530                         tr("Please select a download area first."),
    531                         tr("Error"),
    532                         JOptionPane.ERROR_MESSAGE
    533                 );
    534                 return;
    535             }
     528            /*
     529             * Checks if the user selected the type of data to download. At least one the following
     530             * must be chosen : raw osm data, gpx data, notes.
     531             * If none of those are selected, then the corresponding dialog is shown to inform the user.
     532             */
    536533            if (!isDownloadOsmData() && !isDownloadGpxData() && !isDownloadNotes()) {
    537534                JOptionPane.showMessageDialog(
    538535                        DownloadDialog.this,
    539536                        tr("<html>Neither <strong>{0}</strong> nor <strong>{1}</strong> nor <strong>{2}</strong> is enabled.<br>"
    540                                 + "Please choose to either download OSM data, or GPX data, or Notes, or all.</html>",
     537                                        + "Please choose to either download OSM data, or GPX data, or Notes, or all.</html>",
    541538                                cbDownloadOsmData.getText(),
    542539                                cbDownloadGpxData.getText(),
    543540                                cbDownloadNotes.getText()
     
    547544                );
    548545                return;
    549546            }
     547
    550548            setCanceled(false);
    551549            setVisible(false);
    552550        }
     
    568566            btnDownload.requestFocusInWindow();
    569567        }
    570568    }
    571 }
     569}
     570 No newline at end of file
  • src/org/openstreetmap/josm/actions/DownloadAction.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
     
    88import java.awt.event.KeyEvent;
    99import java.util.ArrayList;
    1010import java.util.List;
     11import java.util.Optional;
    1112import java.util.concurrent.ExecutionException;
    1213import java.util.concurrent.Future;
    1314
     
    2526import org.openstreetmap.josm.tools.Pair;
    2627import org.openstreetmap.josm.tools.Shortcut;
    2728
     29import javax.swing.JOptionPane;
     30
    2831/**
    2932 * Action that opens a connection to the osm server and downloads map data.
    3033 *
     
    5053        DownloadDialog dialog = DownloadDialog.getInstance();
    5154        dialog.restoreSettings();
    5255        dialog.setVisible(true);
    53         if (!dialog.isCanceled()) {
    54             dialog.rememberSettings();
    55             final Bounds area = dialog.getSelectedDownloadArea();
    56             final boolean zoom = dialog.isZoomToDownloadedDataRequired();
    57             final List<Pair<AbstractDownloadTask<?>, Future<?>>> tasks = new ArrayList<>();
    58             if (dialog.isDownloadOsmData()) {
    59                 DownloadOsmTask task = new DownloadOsmTask();
    60                 task.setZoomAfterDownload(zoom && !dialog.isDownloadGpxData() && !dialog.isDownloadNotes());
    61                 Future<?> future = task.download(dialog.isNewLayerRequired(), area, null);
    62                 Main.worker.submit(new PostDownloadHandler(task, future));
    63                 if (zoom) {
    64                     tasks.add(new Pair<>(task, future));
    65                 }
    66             }
    67             if (dialog.isDownloadGpxData()) {
    68                 DownloadGpsTask task = new DownloadGpsTask();
    69                 task.setZoomAfterDownload(zoom && !dialog.isDownloadOsmData() && !dialog.isDownloadNotes());
    70                 Future<?> future = task.download(dialog.isNewLayerRequired(), area, null);
    71                 Main.worker.submit(new PostDownloadHandler(task, future));
    72                 if (zoom) {
    73                     tasks.add(new Pair<>(task, future));
    74                 }
    75             }
    76             if (dialog.isDownloadNotes()) {
    77                 DownloadNotesTask task = new DownloadNotesTask();
    78                 task.setZoomAfterDownload(zoom && !dialog.isDownloadOsmData() && !dialog.isDownloadGpxData());
    79                 Future<?> future = task.download(false, area, null);
    80                 Main.worker.submit(new PostDownloadHandler(task, future));
    81                 if (zoom) {
    82                     tasks.add(new Pair<>(task, future));
    83                 }
    84             }
    85             if (zoom && tasks.size() > 1) {
    86                 Main.worker.submit(() -> {
    87                     ProjectionBounds bounds = null;
    88                     // Wait for completion of download jobs
    89                     for (Pair<AbstractDownloadTask<?>, Future<?>> p : tasks) {
    90                         try {
    91                             p.b.get();
    92                             ProjectionBounds b = p.a.getDownloadProjectionBounds();
    93                             if (bounds == null) {
    94                                 bounds = b;
    95                             } else if (b != null) {
    96                                 bounds.extend(b);
    97                             }
    98                         } catch (InterruptedException | ExecutionException ex) {
    99                             Main.warn(ex);
    100                         }
    101                     }
    102                     // Zoom to the larger download bounds
    103                     if (Main.map != null && bounds != null) {
    104                         final ProjectionBounds pb = bounds;
    105                         GuiHelper.runInEDTAndWait(() -> Main.map.mapView.zoomTo(new ViewportData(pb)));
    106                     }
    107                 });
    108             }
     56
     57        if (dialog.isCanceled()) {
     58            return;
     59        }
     60
     61        dialog.rememberSettings();
     62
     63        Optional<Bounds> selectedArea = dialog.getSelectedDownloadArea();
     64        if (!selectedArea.isPresent()) {
     65            JOptionPane.showMessageDialog(
     66                    dialog,
     67                    tr("Please select a download area first."),
     68                    tr("Error"),
     69                    JOptionPane.ERROR_MESSAGE
     70            );
     71            return;
     72        }
     73
     74        final Bounds area = selectedArea.get();
     75        final boolean zoom = dialog.isZoomToDownloadedDataRequired();
     76        final List<Pair<AbstractDownloadTask<?>, Future<?>>> tasks = new ArrayList<>();
     77
     78        if (dialog.isDownloadOsmData()) {
     79            DownloadOsmTask task = new DownloadOsmTask();
     80            task.setZoomAfterDownload(zoom && !dialog.isDownloadGpxData() && !dialog.isDownloadNotes());
     81            Future<?> future = task.download(dialog.isNewLayerRequired(), area, null);
     82            Main.worker.submit(new PostDownloadHandler(task, future));
     83            if (zoom) {
     84                tasks.add(new Pair<>(task, future));
     85            }
     86        }
     87
     88        if (dialog.isDownloadGpxData()) {
     89            DownloadGpsTask task = new DownloadGpsTask();
     90            task.setZoomAfterDownload(zoom && !dialog.isDownloadOsmData() && !dialog.isDownloadNotes());
     91            Future<?> future = task.download(dialog.isNewLayerRequired(), area, null);
     92            Main.worker.submit(new PostDownloadHandler(task, future));
     93            if (zoom) {
     94                tasks.add(new Pair<>(task, future));
     95            }
     96        }
     97
     98        if (dialog.isDownloadNotes()) {
     99            DownloadNotesTask task = new DownloadNotesTask();
     100            task.setZoomAfterDownload(zoom && !dialog.isDownloadOsmData() && !dialog.isDownloadGpxData());
     101            Future<?> future = task.download(false, area, null);
     102            Main.worker.submit(new PostDownloadHandler(task, future));
     103            if (zoom) {
     104                tasks.add(new Pair<>(task, future));
     105            }
     106        }
     107
     108        if (zoom && tasks.size() > 1) {
     109            Main.worker.submit(() -> {
     110                ProjectionBounds bounds = null;
     111                // Wait for completion of download jobs
     112                for (Pair<AbstractDownloadTask<?>, Future<?>> p : tasks) {
     113                    try {
     114                        p.b.get();
     115                        ProjectionBounds b = p.a.getDownloadProjectionBounds();
     116                        if (bounds == null) {
     117                            bounds = b;
     118                        } else if (b != null) {
     119                            bounds.extend(b);
     120                        }
     121                    } catch (InterruptedException | ExecutionException ex) {
     122                        Main.warn(ex);
     123                    }
     124                }
     125                // Zoom to the larger download bounds
     126                if (Main.map != null && bounds != null) {
     127                    final ProjectionBounds pb = bounds;
     128                    GuiHelper.runInEDTAndWait(() -> Main.map.mapView.zoomTo(new ViewportData(pb)));
     129                }
     130            });
    109131        }
    110132    }
    111 }
     133}
     134 No newline at end of file
  • src/org/openstreetmap/josm/gui/OverpassQueryList.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.gui;
     3
     4import org.openstreetmap.josm.Main;
     5import org.openstreetmap.josm.gui.util.GuiHelper;
     6import org.openstreetmap.josm.gui.widgets.AbstractTextComponentValidator;
     7import org.openstreetmap.josm.gui.widgets.DefaultTextComponentValidator;
     8import org.openstreetmap.josm.gui.widgets.JosmTextArea;
     9import org.openstreetmap.josm.gui.widgets.SearchTextResultListPanel;
     10import org.openstreetmap.josm.tools.GBC;
     11import org.openstreetmap.josm.tools.Utils;
     12
     13import javax.swing.AbstractAction;
     14import javax.swing.BorderFactory;
     15import javax.swing.JLabel;
     16import javax.swing.JList;
     17import javax.swing.JOptionPane;
     18import javax.swing.JPanel;
     19import javax.swing.JPopupMenu;
     20import javax.swing.JScrollPane;
     21import javax.swing.JTextField;
     22import javax.swing.ListCellRenderer;
     23import javax.swing.SwingUtilities;
     24import javax.swing.border.CompoundBorder;
     25import javax.swing.text.JTextComponent;
     26import java.awt.Color;
     27import java.awt.Component;
     28import java.awt.Dimension;
     29import java.awt.Font;
     30import java.awt.GridBagLayout;
     31import java.awt.Point;
     32import java.awt.event.ActionEvent;
     33import java.awt.event.ActionListener;
     34import java.awt.event.MouseAdapter;
     35import java.awt.event.MouseEvent;
     36import java.time.LocalDateTime;
     37import java.time.format.DateTimeFormatter;
     38import java.util.ArrayList;
     39import java.util.Collection;
     40import java.util.Collections;
     41import java.util.HashMap;
     42import java.util.Locale;
     43import java.util.Map;
     44import java.util.Objects;
     45import java.util.Optional;
     46import java.util.stream.Collectors;
     47
     48import static org.openstreetmap.josm.tools.I18n.tr;
     49
     50/**
     51 * A component to select user saved Overpass queries.
     52 */
     53public final class OverpassQueryList extends SearchTextResultListPanel<OverpassQueryList.SelectorItem> {
     54
     55    private final DateTimeFormatter format = DateTimeFormatter.ofPattern("HH:mm:ss, dd-MM-yyyy");
     56
     57    /*
     58     * GUI elements
     59     */
     60    private final JTextComponent target;
     61    private final Component componentParent;
     62
     63    /*
     64     * All loaded elements within the list.
     65     */
     66    private final transient Map<String, SelectorItem> items;
     67
     68    /*
     69     * Preferences
     70     */
     71    private static final String KEY_KEY = "key";
     72    private static final String QUERY_KEY = "query";
     73    private static final String USE_COUNT_KEY = "useCount";
     74    private static final String PREFERENCE_ITEMS = "download.overpass.query";
     75
     76    /**
     77     * Constructs a new {@code OverpassQueryList}.
     78     * @param parent The parent of this component.
     79     * @param target The text component to which the queries must be added.
     80     */
     81    public OverpassQueryList(Component parent, JTextComponent target) {
     82        this.target = target;
     83        this.componentParent = parent;
     84        this.items = this.restorePreferences();
     85
     86        OverpassQueryListMouseAdapter mouseHandler = new OverpassQueryListMouseAdapter(lsResult, lsResultModel);
     87        super.lsResult.setCellRenderer(new OverpassQueryCellRendered());
     88        super.setDblClickListener(this::getDblClickListener);
     89        super.lsResult.addMouseListener(mouseHandler);
     90        super.lsResult.addMouseMotionListener(mouseHandler);
     91
     92        filterItems();
     93    }
     94
     95    /**
     96     * Returns currently selected element from the list.
     97     * @return An {@link Optional#empty()} if nothing is selected, otherwise
     98     * the idem is returned.
     99     */
     100    public synchronized Optional<SelectorItem> getSelectedItem() {
     101        int idx = lsResult.getSelectedIndex();
     102        if (lsResultModel.getSize() == 0 || idx == -1) {
     103            return Optional.empty();
     104        }
     105
     106        SelectorItem item = lsResultModel.getElementAt(idx);
     107        item.increaseUsageCount();
     108
     109        this.items.values().stream()
     110                .filter(it -> !it.getKey().equals(item.getKey()))
     111                .forEach(SelectorItem::decreaseUsageCount);
     112
     113        filterItems();
     114
     115        return Optional.of(item);
     116    }
     117
     118    /**
     119     * Adds a new historic item to the list. The key has form 'history {current date}'.
     120     * Note, the item is not saved if there is already a historic item with the same query.
     121     * @param query The query of the item.
     122     * @exception IllegalArgumentException if the query is empty.
     123     * @exception NullPointerException if the query is {@code null}.
     124     */
     125    public synchronized void saveHistoricItem(String query) {
     126        boolean historicExist = this.items.values().stream()
     127                .filter(it -> it.getKey().contains("history"))
     128                .map(SelectorItem::getQuery)
     129                .anyMatch(q -> q.equals(query));
     130
     131        if (!historicExist) {
     132            SelectorItem item = new SelectorItem(
     133                    "history " + LocalDateTime.now().format(this.format),
     134                    query);
     135
     136            this.items.put(item.getKey(), item);
     137
     138            savePreferences();
     139            filterItems();
     140        }
     141    }
     142
     143    /**
     144     * Removes currently selected item, saves the current state to preferences and
     145     * updates the view.
     146     */
     147    private synchronized void removeSelectedItem() {
     148        Optional<SelectorItem> it = this.getSelectedItem();
     149
     150        if (!it.isPresent()) {
     151            JOptionPane.showMessageDialog(
     152                    componentParent,
     153                    tr("Please select an item first"));
     154            return;
     155        }
     156
     157        SelectorItem item = it.get();
     158        if (this.items.remove(item.getKey(), item)) {
     159            savePreferences();
     160            filterItems();
     161        }
     162    }
     163
     164    /**
     165     * Opens {@link EditItemDialog} for the selected item, saves the current state
     166     * to preferences and updates the view.
     167     */
     168    private synchronized void editSelectedItem() {
     169        Optional<SelectorItem> it = this.getSelectedItem();
     170
     171        if (!it.isPresent()) {
     172            JOptionPane.showMessageDialog(
     173                    componentParent,
     174                    tr("Please select an item first"));
     175            return;
     176        }
     177
     178        SelectorItem item = it.get();
     179
     180        EditItemDialog dialog = new EditItemDialog(
     181                componentParent,
     182                tr("Edit item"),
     183                item.getKey(),
     184                item.getQuery(),
     185                new String[] {tr("Save")});
     186        dialog.showDialog();
     187
     188        Optional<SelectorItem> editedItem = dialog.getOutputItem();
     189        editedItem.ifPresent(i -> {
     190            this.items.remove(item.getKey(), item);
     191            this.items.put(i.getKey(), i);
     192
     193            savePreferences();
     194            filterItems();
     195        });
     196    }
     197
     198    /**
     199     * Opens {@link EditItemDialog}, saves the state to preferences if a new item is added
     200     * and updates the view.
     201     */
     202    private synchronized void createNewItem() {
     203        EditItemDialog dialog = new EditItemDialog(componentParent, tr("Add snippet"), tr("Add"));
     204        dialog.showDialog();
     205
     206        Optional<SelectorItem> newItem = dialog.getOutputItem();
     207        newItem.ifPresent(i -> {
     208            items.put(i.getKey(), new SelectorItem(i.getKey(), i.getQuery()));
     209            savePreferences();
     210            filterItems();
     211        });
     212    }
     213
     214    @Override
     215    public void setDblClickListener(ActionListener dblClickListener) {
     216        // this listener is already set within this class
     217    }
     218
     219    @Override
     220    protected void filterItems() {
     221        String text = edSearchText.getText().toLowerCase(Locale.ENGLISH);
     222
     223        super.lsResultModel.setItems(this.items.values().stream()
     224                .filter(item -> item.getKey().contains(text))
     225                .collect(Collectors.toList()));
     226    }
     227
     228    private void getDblClickListener(ActionEvent e) {
     229        Optional<SelectorItem> selectedItem = this.getSelectedItem();
     230
     231        if (!selectedItem.isPresent()) {
     232            return;
     233        }
     234
     235        SelectorItem item = selectedItem.get();
     236        this.target.setText(item.getQuery());
     237    }
     238
     239    /**
     240     * Saves all elements from the list to {@link Main#pref}.
     241     */
     242    private void savePreferences() {
     243        Collection<Map<String, String>> toSave = new ArrayList<>(this.items.size());
     244        for (SelectorItem item : this.items.values()) {
     245            Map<String, String> it = new HashMap<>();
     246            it.put(KEY_KEY, item.getKey());
     247            it.put(QUERY_KEY, item.getQuery());
     248            it.put(USE_COUNT_KEY, Integer.toString(item.getUsageCount()));
     249
     250            toSave.add(it);
     251        }
     252
     253        Main.pref.putListOfStructs(PREFERENCE_ITEMS, toSave);
     254    }
     255
     256    /**
     257     * Loads the user saved items from {@link Main#pref}.
     258     * @return A set of the user saved items.
     259     */
     260    private Map<String, SelectorItem> restorePreferences() {
     261        Collection<Map<String, String>> toRetrieve =
     262                Main.pref.getListOfStructs(PREFERENCE_ITEMS, Collections.emptyList());
     263        Map<String, SelectorItem> result = new HashMap<>();
     264
     265        for (Map<String, String> entry : toRetrieve) {
     266            String key = entry.get(KEY_KEY);
     267            String query = entry.get(QUERY_KEY);
     268            int usageCount = Integer.parseInt(entry.get(USE_COUNT_KEY));
     269
     270            result.put(key, new SelectorItem(key, query, usageCount));
     271        }
     272
     273        return result;
     274    }
     275
     276    private class OverpassQueryListMouseAdapter extends MouseAdapter {
     277
     278        private final JList list;
     279        private final ResultListModel model;
     280        private final JPopupMenu emptySelectionPopup = new JPopupMenu();
     281        private final JPopupMenu elementPopup = new JPopupMenu();
     282        private final JPopupMenu queryLookup = new JPopupMenu();
     283
     284        OverpassQueryListMouseAdapter(JList list, ResultListModel listModel) {
     285            this.list = list;
     286            this.model = listModel;
     287
     288            this.initPopupMenus();
     289        }
     290
     291        /*
     292         * Do not select the closest element if the user clicked on
     293         * an empty area within the list.
     294         */
     295        private int locationToIndex(Point p) {
     296            int idx = list.locationToIndex(p);
     297
     298            if (idx != -1 && !list.getCellBounds(idx, idx).contains(p)) {
     299                return -1;
     300            } else {
     301                return idx;
     302            }
     303        }
     304
     305        @Override
     306        public void mouseClicked(MouseEvent e) {
     307            super.mouseClicked(e);
     308            if (SwingUtilities.isRightMouseButton(e)) {
     309                int index = locationToIndex(e.getPoint());
     310
     311                if (model.getSize() == 0 || index == -1) {
     312                    list.clearSelection();
     313                    emptySelectionPopup.show(list, e.getX(), e.getY());
     314                } else {
     315                    list.setSelectedIndex(index);
     316                    list.ensureIndexIsVisible(index);
     317                    elementPopup.show(list, e.getX(), e.getY());
     318                }
     319            }
     320        }
     321
     322        @Override
     323        public void mouseMoved(MouseEvent e) {
     324            super.mouseMoved(e);
     325            int idx = locationToIndex(e.getPoint());
     326            if (idx == -1) {
     327                return;
     328            }
     329
     330            SelectorItem item = (SelectorItem) model.getElementAt(idx);
     331            list.setToolTipText("<html><pre style='width:300px;'>" +
     332                    Utils.escapeReservedCharactersHTML(Utils.restrictStringLines(item.getQuery(), 9)));
     333        }
     334
     335        private void initPopupMenus() {
     336            AbstractAction add = new AbstractAction(tr("Add")) {
     337                @Override
     338                public void actionPerformed(ActionEvent e) {
     339                    createNewItem();
     340                }
     341            };
     342            AbstractAction edit = new AbstractAction(tr("Edit")) {
     343                @Override
     344                public void actionPerformed(ActionEvent e) {
     345                    editSelectedItem();
     346                }
     347            };
     348            AbstractAction remove = new AbstractAction(tr("Remove")) {
     349                @Override
     350                public void actionPerformed(ActionEvent e) {
     351                    removeSelectedItem();
     352                }
     353            };
     354            this.emptySelectionPopup.add(add);
     355            this.elementPopup.add(add);
     356            this.elementPopup.add(edit);
     357            this.elementPopup.add(remove);
     358        }
     359    }
     360
     361    /**
     362     * This class defines the way each element is rendered in the list.
     363     */
     364    private static class OverpassQueryCellRendered extends JLabel implements ListCellRenderer<SelectorItem> {
     365
     366        OverpassQueryCellRendered() {
     367            setOpaque(true);
     368        }
     369
     370        @Override
     371        public Component getListCellRendererComponent(
     372                JList<? extends SelectorItem> list,
     373                SelectorItem value,
     374                int index,
     375                boolean isSelected,
     376                boolean cellHasFocus) {
     377
     378            Font font = list.getFont();
     379            if (isSelected) {
     380                setFont(new Font(font.getFontName(), Font.BOLD, font.getSize() + 2));
     381                setBackground(list.getSelectionBackground());
     382                setForeground(list.getSelectionForeground());
     383            } else {
     384                setFont(new Font(font.getFontName(), Font.PLAIN, font.getSize() + 2));
     385                setBackground(list.getBackground());
     386                setForeground(list.getForeground());
     387            }
     388
     389            setEnabled(list.isEnabled());
     390            setText(value.getKey());
     391
     392            if (isSelected && cellHasFocus) {
     393                setBorder(new CompoundBorder(
     394                        BorderFactory.createLineBorder(Color.BLACK, 1),
     395                        BorderFactory.createEmptyBorder(2, 0, 2, 0)));
     396            } else {
     397                setBorder(new CompoundBorder(
     398                        null,
     399                        BorderFactory.createEmptyBorder(2, 0, 2, 0)));
     400            }
     401
     402            return this;
     403        }
     404    }
     405
     406    /**
     407     * Dialog that provides functionality to add/edit an item from the list.
     408     */
     409    private final class EditItemDialog extends ExtendedDialog {
     410
     411        private final JTextField name;
     412        private final JosmTextArea query;
     413        private final int initialNameHash;
     414
     415        private final transient AbstractTextComponentValidator queryValidator;
     416        private final transient AbstractTextComponentValidator nameValidator;
     417
     418        private static final int SUCCESS_BTN = 0;
     419        private static final int CANCEL_BTN = 1;
     420
     421        /**
     422         * Added/Edited object to be returned. If {@link Optional#empty()} then probably
     423         * the user closed the dialog, otherwise {@link SelectorItem} is present.
     424         */
     425        private transient Optional<SelectorItem> outputItem = Optional.empty();
     426
     427        EditItemDialog(Component parent, String title, String... buttonTexts) {
     428            this(parent, title, "", "", buttonTexts);
     429        }
     430
     431        EditItemDialog(
     432                Component parent,
     433                String title,
     434                String nameToEdit,
     435                String queryToEdit,
     436                String... buttonTexts) {
     437            super(parent, title, buttonTexts);
     438
     439            this.initialNameHash = nameToEdit.hashCode();
     440
     441            this.name = new JTextField(nameToEdit);
     442            this.query = new JosmTextArea(queryToEdit);
     443
     444            this.queryValidator = new DefaultTextComponentValidator(this.query, "", tr("Query cannot be empty"));
     445            this.nameValidator = new AbstractTextComponentValidator(this.name) {
     446                @Override
     447                public void validate() {
     448                    if (isValid()) {
     449                        feedbackValid(tr("This name can be used for the item"));
     450                    } else {
     451                        feedbackInvalid(tr("Item with this name already exists"));
     452                    }
     453                }
     454
     455                @Override
     456                public boolean isValid() {
     457                    String currentName = name.getText();
     458                    int currentHash = currentName.hashCode();
     459
     460                    return !Utils.isStripEmpty(currentName) &&
     461                            !(currentHash != initialNameHash &&
     462                                    items.containsKey(currentName));
     463                }
     464            };
     465
     466            this.name.getDocument().addDocumentListener(this.nameValidator);
     467            this.query.getDocument().addDocumentListener(this.queryValidator);
     468
     469            JPanel panel = new JPanel(new GridBagLayout());
     470            JScrollPane queryScrollPane = GuiHelper.embedInVerticalScrollPane(this.query);
     471            queryScrollPane.getVerticalScrollBar().setUnitIncrement(10); // make scrolling smooth
     472
     473            GBC constraint = GBC.eol().insets(8, 0, 8, 8).anchor(GBC.CENTER).fill(GBC.HORIZONTAL);
     474            constraint.ipady = 250;
     475            panel.add(this.name, GBC.eol().insets(5).anchor(GBC.SOUTHEAST).fill(GBC.HORIZONTAL));
     476            panel.add(queryScrollPane, constraint);
     477
     478            setDefaultButton(SUCCESS_BTN);
     479            setCancelButton(CANCEL_BTN);
     480            setPreferredSize(new Dimension(400, 400));
     481            setContent(panel, false);
     482        }
     483
     484        /**
     485         * Gets a new {@link SelectorItem} if one was created/modified.
     486         * @return A {@link SelectorItem} object created out of the fields of the dialog.
     487         */
     488        public Optional<SelectorItem> getOutputItem() {
     489            return this.outputItem;
     490        }
     491
     492        @Override
     493        protected void buttonAction(int buttonIndex, ActionEvent evt) {
     494            if (buttonIndex == SUCCESS_BTN) {
     495                if (!this.nameValidator.isValid()) {
     496                    JOptionPane.showMessageDialog(
     497                            componentParent,
     498                            tr("The item cannot be created with provided name"),
     499                            tr("Warning"),
     500                            JOptionPane.WARNING_MESSAGE);
     501                } else if (!this.queryValidator.isValid()) {
     502                    JOptionPane.showMessageDialog(
     503                            componentParent,
     504                            tr("The item cannot be created with an empty query"),
     505                            tr("Warning"),
     506                            JOptionPane.WARNING_MESSAGE);
     507                } else {
     508                    this.outputItem = Optional.of(new SelectorItem(this.name.getText(), this.query.getText()));
     509                    super.buttonAction(buttonIndex, evt);
     510                }
     511            } else {
     512                super.buttonAction(buttonIndex, evt);
     513            }
     514        }
     515    }
     516
     517    /**
     518     * This class represents an Overpass query used by the user that can be
     519     * shown within {@link OverpassQueryList}.
     520     */
     521    public static class SelectorItem {
     522        private final String itemKey;
     523        private final String query;
     524        private int usageCount;
     525
     526        /**
     527         * Constructs a new {@code SelectorItem}.
     528         * @param key The key of this item.
     529         * @param query The query of the item.
     530         * @exception NullPointerException if any parameter is {@code null}.
     531         * @exception IllegalArgumentException if any parameter is empty.
     532         */
     533        public SelectorItem(String key, String query) {
     534            this(key, query, 1);
     535        }
     536
     537        /**
     538         * Constructs a new {@code SelectorItem}.
     539         * @param key The key of this item.
     540         * @param query The query of the item.
     541         * @param usageCount The number of times this query was used.
     542         * @exception NullPointerException if any parameter is {@code null}.
     543         * @exception IllegalArgumentException if any parameter is empty.
     544         */
     545        public SelectorItem(String key, String query, int usageCount) {
     546            Objects.requireNonNull(key);
     547            Objects.requireNonNull(query);
     548
     549            if (Utils.isStripEmpty(key)) {
     550                throw new IllegalArgumentException("The key of the item cannot be empty");
     551            }
     552            if (Utils.isStripEmpty(query)) {
     553                throw new IllegalArgumentException("The query cannot be empty");
     554            }
     555
     556            this.itemKey = key;
     557            this.query = query;
     558            this.usageCount = usageCount;
     559        }
     560
     561        /**
     562         * Gets the key (a string that is displayed in the selector) of this item.
     563         * @return A string representing the key of this item.
     564         */
     565        public String getKey() {
     566            return this.itemKey;
     567        }
     568
     569        /**
     570         * Gets the overpass query of this item.
     571         * @return A string representing the overpass query of this item.
     572         */
     573        public String getQuery() {
     574            return this.query;
     575        }
     576
     577        /**
     578         * Gets the number of times the query was used by the user.
     579         * @return The usage count of this item.
     580         */
     581        public int getUsageCount() {
     582            return this.usageCount;
     583        }
     584
     585        /**
     586         * Increments the {@link SelectorItem#usageCount} by one till
     587         * it reaches {@link Integer#MAX_VALUE}.
     588         */
     589        public void increaseUsageCount() {
     590            if (this.usageCount < Integer.MAX_VALUE) {
     591                this.usageCount++;
     592            }
     593        }
     594
     595        /**
     596         * Decrements the {@link SelectorItem#usageCount} ny one till
     597         * it reaches 0.
     598         */
     599        public void decreaseUsageCount() {
     600            if (this.usageCount > 0) {
     601                this.usageCount--;
     602            }
     603        }
     604
     605        @Override
     606        public boolean equals(Object o) {
     607            if (this == o) return true;
     608            if (!(o instanceof SelectorItem)) return false;
     609
     610            SelectorItem that = (SelectorItem) o;
     611
     612            return itemKey.equals(that.itemKey) &&
     613                    query.equals(that.getKey());
     614        }
     615
     616        @Override
     617        public int hashCode() {
     618            return itemKey.hashCode();
     619        }
     620    }
     621}