001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.tools;
003
004import java.io.IOException;
005import java.io.InputStreamReader;
006import java.io.Reader;
007import java.nio.charset.StandardCharsets;
008import java.util.regex.Pattern;
009
010import javax.script.Invocable;
011import javax.script.ScriptEngine;
012import javax.script.ScriptEngineManager;
013import javax.script.ScriptException;
014
015/**
016 * Uses <a href="https://github.com/tyrasd/overpass-turbo/">Overpass Turbo</a> query wizard code
017 * to build an Overpass QL from a {@link org.openstreetmap.josm.actions.search.SearchAction} like query.
018 *
019 * Requires a JavaScript {@link ScriptEngine}.
020 * @since 8744
021 */
022public final class OverpassTurboQueryWizard {
023
024    private static OverpassTurboQueryWizard instance;
025    private final ScriptEngine engine = new ScriptEngineManager().getEngineByName("JavaScript");
026
027    /**
028     * An exception to indicate a failed parse.
029     */
030    public static class ParseException extends RuntimeException {
031    }
032
033    /**
034     * Replies the unique instance of this class.
035     *
036     * @return the unique instance of this class
037     */
038    public static synchronized OverpassTurboQueryWizard getInstance() {
039        if (instance == null) {
040            instance = new OverpassTurboQueryWizard();
041        }
042        return instance;
043    }
044
045    private OverpassTurboQueryWizard() {
046        // overpass-turbo is MIT Licensed
047
048        try (final Reader reader = new InputStreamReader(
049                getClass().getResourceAsStream("/data/overpass-turbo-ffs.js"), StandardCharsets.UTF_8)) {
050            engine.eval("var console = {log: function(){}};");
051            engine.eval(reader);
052            engine.eval("var construct_query = turbo.ffs().construct_query;");
053        } catch (ScriptException | IOException ex) {
054            throw new RuntimeException("Failed to initialize OverpassTurboQueryWizard", ex);
055        }
056    }
057
058    /**
059     * Builds an Overpass QL from a {@link org.openstreetmap.josm.actions.search.SearchAction} like query.
060     * @param search the {@link org.openstreetmap.josm.actions.search.SearchAction} like query
061     * @return an Overpass QL query
062     * @throws ParseException when the parsing fails
063     */
064    public String constructQuery(String search) throws ParseException {
065        try {
066            final Object result = ((Invocable) engine).invokeFunction("construct_query", search);
067            if (result == Boolean.FALSE) {
068                throw new ParseException();
069            }
070            String query = (String) result;
071            query = Pattern.compile("^.*\\[out:json\\]", Pattern.DOTALL).matcher(query).replaceFirst("");
072            query = Pattern.compile("^out.*", Pattern.MULTILINE).matcher(query).replaceAll("out meta;");
073            query = query.replace("({{bbox}})", "");
074            return query;
075        } catch (NoSuchMethodException e) {
076            throw new IllegalStateException();
077        } catch (ScriptException e) {
078            throw new RuntimeException("Failed to execute OverpassTurboQueryWizard", e);
079        }
080    }
081
082}