From d7a889711ae3a92940f484704c072fc7ffae2ae6 Mon Sep 17 00:00:00 2001
From: Simon Legner <Simon.Legner@gmail.com>
Date: Thu, 11 Jun 2015 20:46:06 +0200
Subject: [PATCH] Add Overpass download dialog to core, provide Overpass Turbo
 wizard

---
 images/download-overpass.png                       | Bin 0 -> 1516 bytes
 .../josm/actions/OverpassDownloadAction.java       | 225 +++++++++++++++++++++
 .../josm/actions/OverpassTurboQueryWizard.java     |  91 +++++++++
 src/org/openstreetmap/josm/gui/MainMenu.java       |   4 +
 4 files changed, 320 insertions(+)
 create mode 100644 images/download-overpass.png
 create mode 100644 src/org/openstreetmap/josm/actions/OverpassDownloadAction.java
 create mode 100644 src/org/openstreetmap/josm/actions/OverpassTurboQueryWizard.java

diff --git a/images/download-overpass.png b/images/download-overpass.png
new file mode 100644
index 0000000000000000000000000000000000000000..179567687ba96428c80763cd34e115454fba04f5
GIT binary patch
literal 1516
zcmV<I1rz#-P)<h;3K|Lk000e1NJLTq000;O000;W1^@s6;CDUv00006VoOIv0RI60
z0RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru-vki~7AEnVC!7EP1$jwC
zK~zY`m6l&@6xS8Te|L8F|IVy;*BkG8Sv$7Z4#wDP92197CuyL6LR(d7<5r3)UQk3;
z5tK@46jf@=dlf3Rl3)7J@K&i+ksn$mD9}(y06`#XWQ>iSVy|O^|1Mtd&d$v4+&p-3
za6_VS-_DuQ`R={veBXBkU%kA51;~4e7iEhEyat>Wh*61C_`A4eeOUqJD~cz9I$#Ho
zx|eheSrUk}K%52sD^UJ0BwK?I1JF>*KMLFdrhp=_7ijxO;8qnlRXbTDkpG+br1&rZ
zJ|F^I0Xl#(PyyZt)`7iC7mbU+0`L*=%8T;RP9AbS^*+?U0ngXsXMo3n_FCMz|HIS^
z07J6%?2v4o`o2ti9tJ=R!Vl)*U=TWM&E5|9f#>dR0uV#8rE5sG8bh-6&krnge{lTx
zaX-R&&$7+_wbi_`_k)Z4r)eAEt{tqbtfK20E|-gig+-Fd1e23fWHN2!p1Z(O&p4fL
zz2Jn)@k=he-#mKss0pCn4`5fzhqK+;p25Lq6e%UScTTa-C~;0ocK`ec*RPL}&Gs-q
zKaZ-axZQ4w#UjG{V}7k3pzC0h;yIP-y)kj)5HS3OCVM^JeVI(VuTrrQLQs9IgY{&R
zY}sUSagju#fy<XKW7{@UQ&WV)VJyp{v3?u7tKVi+T%ftF$?s9!`yMz&4o+8HU1am-
zT3T9|@CS&DjL;a5(|G<oPfkzc^?I3`o5L^+gb);qMY6`T%(!lVUt-y2cWoMdn*hg-
z9aAcHWm_Py!I_e=G1^i}jNknh^~4EeG>WXRr}3ZvVuw@V%)d{wuylv<Km8Hy?O6)0
zRZ1V6C7)kw1`<GG6CfCLHHSi)ttbKn1pFI7mSy5<Xh6ndOsCUKr_&INqHW)fq9_O<
zkli~eX%U1Y@rWwA7IOAKzds6;Hvw$v>P)3lj&0jGj`ILYDj)<%364W5nc6&|8I58^
zBS=MH4h#^9#;WO5V`fVagp{3$hNRE7EmoG7Hr37J@lwsrA{G{C&gE#%<zO|>^5`i3
zx&X^-3)r@e&4%6;^96KW_o^;;=awFLz3#qveZ5*Pmnj-Wa<jLnR;v^W1zf#ZDlfl`
zSXx5l^UQtk02kKQ_@DL>#o!uEO=*_(906OWE)=1oy+dtoZfXT?e||-FWsk0hDVB<y
zKX;Dx^>t34IYlg9$Ly!G%)I(bW;LBl=`^NkqG=j?x_gP19$_WF!p-{2c;_N0wn8G2
zKvmU&Ej^G@rb3|*h4nRd_3a|8hcU`!OtZ|Hk&{@-IPcwE0;S5t<QT!Y!ulv!6^l}(
zz+~(SkJ)|b{wR@141s9c$^(IL9EV-mex7{d8~A-8zVodE_&gyFWgjPCOJ4fv&)73C
zNK11&PxpV9%9@`a{qQHuJ$((2@bOaS2<>ehNGXxE+M8PRi&u~QK9xwk=28TX<B*%l
zk=m9*-kf4zZkBfjpF-Dl#>Q@t$+U6%_8oTa>>?*`GInr;)F(S>80p_2tmDXu@yX%0
z{&M2T{bJYK)BnV^upVS??l!@YhIA^tzPQM6G>Q`l;P-jy-`&TDSFV!n?cw9=*GUfF
zrR2Ly{!kNr-B~VOxysCRZgF~Q@-T4nezDU5%0`)ZJce6Uu`H95?HK}Y6_49Z#kQH5
z$<dwd-tg2n*D+J8G`zW&REI{{Gyw=lqVbx<f3Bv9#>T2;S(j{E45+F~sklrqq%mO>
zkd8yyFi_oY?kwMBJa>!qa2{`4mHFRjjGr0X!b#IKuL9Hm?+1_6{5T4DfLLt^qJROc
z*8UfD{T^-4UmFh3o-Drm@vl~{0S-_F?3!1v*8KcazQ(gfJm|xaZ1H~`Yy1rGfhYUb
Sc(Ew}0000<MNUMnLSTX<pybT}

literal 0
HcmV?d00001

diff --git a/src/org/openstreetmap/josm/actions/OverpassDownloadAction.java b/src/org/openstreetmap/josm/actions/OverpassDownloadAction.java
new file mode 100644
index 0000000..1a246e1
--- /dev/null
+++ b/src/org/openstreetmap/josm/actions/OverpassDownloadAction.java
@@ -0,0 +1,225 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.actions;
+
+import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Component;
+import java.awt.event.ActionEvent;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.concurrent.Future;
+
+import javax.swing.*;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.downloadtasks.DownloadOsmTask;
+import org.openstreetmap.josm.actions.downloadtasks.PostDownloadHandler;
+import org.openstreetmap.josm.data.Bounds;
+import org.openstreetmap.josm.data.DataSource;
+import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.data.preferences.CollectionProperty;
+import org.openstreetmap.josm.data.preferences.StringProperty;
+import org.openstreetmap.josm.gui.HelpAwareOptionPane;
+import org.openstreetmap.josm.gui.download.DownloadDialog;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+import org.openstreetmap.josm.gui.widgets.HistoryComboBox;
+import org.openstreetmap.josm.io.BoundingBoxDownloader;
+import org.openstreetmap.josm.io.OsmTransferException;
+import org.openstreetmap.josm.tools.GBC;
+import org.openstreetmap.josm.tools.Utils;
+
+public class OverpassDownloadAction extends JosmAction {
+
+    public OverpassDownloadAction() {
+        super(tr("Download from Overpass API ..."), "download-overpass", tr("Download map data from Overpass API server."),
+                null, true, "overpassdownload/download", true);
+        putValue("help", ht("/Action/OverpassDownload"));
+    }
+
+    @Override
+    public void actionPerformed(ActionEvent e) {
+        OverpassDownloadDialog dialog = OverpassDownloadDialog.getInstance();
+        dialog.restoreSettings();
+        dialog.setVisible(true);
+        if (!dialog.isCanceled()) {
+            dialog.rememberSettings();
+            Bounds area = dialog.getSelectedDownloadArea();
+            DownloadOsmTask task = new DownloadOsmTask();
+            Future<?> future = task.download(
+                    new OverpassDownloadReader(area, dialog.getOverpassQuery()),
+                    dialog.isNewLayerRequired(), area, null);
+            Main.worker.submit(new PostDownloadHandler(task, future));
+        }
+    }
+
+    static class OverpassDownloadDialog extends DownloadDialog {
+
+        protected HistoryComboBox overpassWizard;
+        protected JTextArea overpassQuery;
+        private static OverpassDownloadDialog instance;
+        static final CollectionProperty OVERPASS_WIZARD_HISTORY = new CollectionProperty("download.overpass.wizard", new ArrayList<String>());
+
+        private OverpassDownloadDialog(Component parent) {
+            super(parent);
+            cbDownloadOsmData.setEnabled(false);
+            cbDownloadOsmData.setSelected(false);
+            cbDownloadGpxData.setVisible(false);
+            cbDownloadNotes.setVisible(false);
+            cbStartup.setVisible(false);
+        }
+
+        static public OverpassDownloadDialog getInstance() {
+            if (instance == null) {
+                instance = new OverpassDownloadDialog(Main.parent);
+            }
+            return instance;
+        }
+
+        @Override
+        protected void buildMainPanelAboveDownloadSelections(JPanel pnl) {
+
+            pnl.add(new JLabel(), GBC.eol()); // needed for the invisible checkboxes cbDownloadGpxData, cbDownloadNotes
+
+            final String tooltip = tr("Builds an Overpass query using the Overpass Turbo query wizard");
+            overpassWizard = new HistoryComboBox();
+            overpassWizard.setToolTipText(tooltip);
+            final JButton buildQuery = new JButton(tr("Build query"));
+            buildQuery.addActionListener(new AbstractAction() {
+                @Override
+                public void actionPerformed(ActionEvent e) {
+                    final String overpassWizardText = overpassWizard.getText();
+                    try {
+                        overpassQuery.setText(OverpassTurboQueryWizard.getInstance().constructQuery(overpassWizardText));
+                    } catch (OverpassTurboQueryWizard.ParseException ex) {
+                        HelpAwareOptionPane.showOptionDialog(
+                                Main.parent,
+                                tr("<html>The Overpass wizard could not parse the following query:"
+                                        + Utils.joinAsHtmlUnorderedList(Collections.singleton(overpassWizardText))),
+                                tr("Parse error"),
+                                JOptionPane.ERROR_MESSAGE,
+                                null
+                        );
+                    }
+                }
+            });
+            buildQuery.setToolTipText(tooltip);
+            pnl.add(buildQuery, GBC.std().insets(5, 5, 5, 5));
+            pnl.add(overpassWizard, GBC.eol().fill(GBC.HORIZONTAL));
+
+            overpassQuery = new JTextArea("[timeout:15];", 8, 80);
+            JScrollPane scrollPane = new JScrollPane(overpassQuery);
+            pnl.add(new JLabel(tr("Overpass query: ")), GBC.std().insets(5, 5, 5, 5));
+            GBC gbc = GBC.eol().fill(GBC.HORIZONTAL);
+            gbc.ipady = 200;
+            pnl.add(scrollPane, gbc);
+        }
+
+        public String getOverpassQuery() {
+            return overpassQuery.getText();
+        }
+
+        @Override
+        public void restoreSettings() {
+            super.restoreSettings();
+            overpassWizard.setPossibleItems(OVERPASS_WIZARD_HISTORY.get());
+        }
+
+        @Override
+        public void rememberSettings() {
+            super.rememberSettings();
+            overpassWizard.addCurrentItemToHistory();
+            OVERPASS_WIZARD_HISTORY.put(overpassWizard.getHistory());
+        }
+
+    }
+
+    static class OverpassDownloadReader extends BoundingBoxDownloader {
+
+        final String overpassQuery;
+        static final StringProperty OVERPASS_URL = new StringProperty("download.overpass.url", "https://overpass-api.de/api/");
+
+        public OverpassDownloadReader(Bounds downloadArea, String overpassQuery) {
+            super(downloadArea);
+            this.overpassQuery = overpassQuery.trim();
+        }
+
+        @Override
+        protected String getBaseUrl() {
+            return OVERPASS_URL.get();
+        }
+
+        @Override
+        protected String getRequestForBbox(double lon1, double lat1, double lon2, double lat2) {
+            if (overpassQuery.isEmpty())
+                return super.getRequestForBbox(lon1, lat1, lon2, lat2);
+            else {
+                String realQuery = completeOverpassQuery(overpassQuery);
+                try {
+                    return "interpreter?data=" + URLEncoder.encode(realQuery, "UTF-8") + "&bbox=" + lon1 + "," + lat1 + "," + lon2 + "," + lat2;
+                } catch (UnsupportedEncodingException e) {
+                    throw new IllegalStateException();
+                }
+            }
+        }
+
+        private String completeOverpassQuery(String query) {
+            int firstColon = query.indexOf(";");
+            if (firstColon == -1) {
+                return "[bbox];" + query;
+            }
+            int bboxPos = query.indexOf("[bbox");
+            if (bboxPos > -1 && bboxPos < firstColon) {
+                return query;
+            }
+
+            int bracketCount = 0;
+            int pos = 0;
+            for (; pos < firstColon; ++pos) {
+                if (query.charAt(pos) == '[')
+                    ++bracketCount;
+                else if (query.charAt(pos) == '[')
+                    --bracketCount;
+                else if (bracketCount == 0) {
+                    if (!Character.isWhitespace(query.charAt(pos)))
+                        break;
+                }
+            }
+
+            if (pos < firstColon) {
+                // We start with a statement, not with declarations
+                return "[bbox];" + query;
+            }
+
+            // We start with declarations. Add just one more declaration in this case.
+            return "[bbox]" + query;
+        }
+
+        @Override
+        public DataSet parseOsm(ProgressMonitor progressMonitor) throws OsmTransferException {
+
+            DataSet ds = super.parseOsm(progressMonitor);
+
+            // add bounds if necessary (note that Overpass API does not return bounds in the response XML)
+            if (ds != null && ds.dataSources.isEmpty()) {
+                if (crosses180th) {
+                    Bounds bounds = new Bounds(lat1, lon1, lat2, 180.0);
+                    DataSource src = new DataSource(bounds, getBaseUrl());
+                    ds.dataSources.add(src);
+
+                    bounds = new Bounds(lat1, -180.0, lat2, lon2);
+                    src = new DataSource(bounds, getBaseUrl());
+                    ds.dataSources.add(src);
+                } else {
+                    Bounds bounds = new Bounds(lat1, lon1, lat2, lon2);
+                    DataSource src = new DataSource(bounds, getBaseUrl());
+                    ds.dataSources.add(src);
+                }
+            }
+
+            return ds;
+        }
+    }
+}
diff --git a/src/org/openstreetmap/josm/actions/OverpassTurboQueryWizard.java b/src/org/openstreetmap/josm/actions/OverpassTurboQueryWizard.java
new file mode 100644
index 0000000..22b440e
--- /dev/null
+++ b/src/org/openstreetmap/josm/actions/OverpassTurboQueryWizard.java
@@ -0,0 +1,91 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.actions;
+
+import org.openstreetmap.josm.io.CachedFile;
+
+import javax.script.Invocable;
+import javax.script.ScriptEngine;
+import javax.script.ScriptEngineManager;
+import javax.script.ScriptException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.util.regex.Pattern;
+
+/**
+ * Uses <a href="https://github.com/tyrasd/overpass-turbo/">Overpass Turbo</a> query wizard code
+ * to build an Overpass QL from a {@link org.openstreetmap.josm.actions.search.SearchAction} like query.
+ *
+ * Requires a JavaScript {@link ScriptEngine}.
+ */
+public class OverpassTurboQueryWizard {
+
+    private static OverpassTurboQueryWizard instance;
+    private final ScriptEngine engine = new ScriptEngineManager().getEngineByName("JavaScript");
+
+    /**
+     * An exception to indicate a failed parse.
+     */
+    public static class ParseException extends RuntimeException {
+    }
+
+    /**
+     * Replies the unique instance of this class.
+     *
+     * @return the unique instance of this class
+     */
+    public static synchronized OverpassTurboQueryWizard getInstance() {
+        if (instance == null) {
+            instance = new OverpassTurboQueryWizard();
+        }
+        return instance;
+    }
+
+    private OverpassTurboQueryWizard() {
+        try {
+            engine.eval("var console = {log: function(){}};");
+            final String baseUrl = "https://raw.githubusercontent.com/tyrasd/overpass-turbo/b0ef5ebbdd353c1bc7e45da527cab03498b4cbef/";
+            // lodash is MIT Licensed
+            initEngine(baseUrl + "/libs/lodash/lodash-2.4.1.js");
+            // overpass-turbo is MIT Licensed
+            initEngine(baseUrl + "/js/ffs.js");
+            initEngine(baseUrl + "/js/ffs/free.js");
+            initEngine(baseUrl + "/js/ffs/parser.js");
+            engine.eval("var construct_query = turbo.ffs().construct_query;");
+        } catch (ScriptException | IOException ex) {
+            throw new RuntimeException("Failed to initialize OverpassTurboQueryWizard", ex);
+        }
+    }
+
+    private void initEngine(String url) throws ScriptException, IOException {
+        try (Reader reader = new InputStreamReader(new CachedFile(url).getInputStream(), StandardCharsets.UTF_8)) {
+            engine.eval(reader);
+        }
+    }
+
+    /**
+     * Builds an Overpass QL from a {@link org.openstreetmap.josm.actions.search.SearchAction} like query.
+     * @param search the {@link org.openstreetmap.josm.actions.search.SearchAction} like query
+     * @return an Overpass QL query
+     * @throws ParseException when the parsing fails
+     */
+    public String constructQuery(String search) throws ParseException {
+        try {
+            final Object result = ((Invocable) engine).invokeFunction("construct_query", search);
+            if (result == Boolean.FALSE) {
+                throw new ParseException();
+            }
+            String query = (String) result;
+            query = Pattern.compile("^.*\\[out:json\\]", Pattern.DOTALL).matcher(query).replaceFirst("");
+            query = Pattern.compile("^out.*", Pattern.MULTILINE).matcher(query).replaceAll("out meta;");
+            query = query.replace("({{bbox}})", "");
+            return query;
+        } catch (NoSuchMethodException e) {
+            throw new IllegalStateException();
+        } catch (ScriptException e) {
+            throw new RuntimeException("Failed to execute OverpassTurboQueryWizard", e);
+        }
+    }
+
+}
diff --git a/src/org/openstreetmap/josm/gui/MainMenu.java b/src/org/openstreetmap/josm/gui/MainMenu.java
index 304f536..a9befbf 100644
--- a/src/org/openstreetmap/josm/gui/MainMenu.java
+++ b/src/org/openstreetmap/josm/gui/MainMenu.java
@@ -81,6 +81,7 @@
 import org.openstreetmap.josm.actions.OpenLocationAction;
 import org.openstreetmap.josm.actions.OrthogonalizeAction;
 import org.openstreetmap.josm.actions.OrthogonalizeAction.Undo;
+import org.openstreetmap.josm.actions.OverpassDownloadAction;
 import org.openstreetmap.josm.actions.PasteAction;
 import org.openstreetmap.josm.actions.PasteTagsAction;
 import org.openstreetmap.josm.actions.PreferenceToggleAction;
@@ -166,6 +167,8 @@
     public final GpxExportAction gpxExport = new GpxExportAction();
     /** File / Download from OSM... **/
     public final DownloadAction download = new DownloadAction();
+    /** File / Download from Overpass API... **/
+    public final OverpassDownloadAction overpassDownload = new OverpassDownloadAction();
     /** File / Download object... **/
     public final DownloadPrimitiveAction downloadPrimitive = new DownloadPrimitiveAction();
     /** File / Download notes in current view **/
@@ -639,6 +642,7 @@ public MainMenu() {
         add(fileMenu, gpxExport, true);
         fileMenu.addSeparator();
         add(fileMenu, download);
+        add(fileMenu, overpassDownload);
         add(fileMenu, downloadPrimitive);
         add(fileMenu, searchNotes);
         add(fileMenu, downloadNotesInView);
-- 
2.4.2

