001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.dialogs.properties;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.event.ActionEvent;
007import java.awt.event.KeyEvent;
008import java.io.IOException;
009import java.net.URI;
010import java.net.URISyntaxException;
011import java.util.ArrayList;
012import java.util.Arrays;
013import java.util.List;
014import java.util.Map;
015import java.util.Objects;
016import java.util.function.Function;
017
018import javax.swing.AbstractAction;
019import javax.swing.JTable;
020import javax.swing.KeyStroke;
021
022import org.openstreetmap.josm.data.osm.Relation;
023import org.openstreetmap.josm.gui.MainApplication;
024import org.openstreetmap.josm.spi.preferences.Config;
025import org.openstreetmap.josm.tools.HttpClient;
026import org.openstreetmap.josm.tools.ImageProvider;
027import org.openstreetmap.josm.tools.LanguageInfo;
028import org.openstreetmap.josm.tools.Logging;
029import org.openstreetmap.josm.tools.OpenBrowser;
030import org.openstreetmap.josm.tools.Utils;
031
032/**
033 * Launch browser with wiki help for selected object.
034 * @since 13521
035 */
036public class HelpAction extends AbstractAction {
037    private final JTable tagTable;
038    private final Function<Integer, String> tagKeySupplier;
039    private final Function<Integer, Map<String, Integer>> tagValuesSupplier;
040
041    private final JTable membershipTable;
042    private final Function<Integer, Relation> memberValueSupplier;
043
044    /**
045     * Constructs a new {@code HelpAction}.
046     * @param tagTable The tag table. Cannot be null
047     * @param tagKeySupplier Finds the key from given row of tag table. Cannot be null
048     * @param tagValuesSupplier Finds the values from given row of tag table (map of values and number of occurrences). Cannot be null
049     * @param membershipTable The membership table. Can be null
050     * @param memberValueSupplier Finds the parent relation from given row of membership table. Can be null
051     */
052    public HelpAction(JTable tagTable, Function<Integer, String> tagKeySupplier, Function<Integer, Map<String, Integer>> tagValuesSupplier,
053            JTable membershipTable, Function<Integer, Relation> memberValueSupplier) {
054        this.tagTable = Objects.requireNonNull(tagTable);
055        this.tagKeySupplier = Objects.requireNonNull(tagKeySupplier);
056        this.tagValuesSupplier = Objects.requireNonNull(tagValuesSupplier);
057        this.membershipTable = membershipTable;
058        this.memberValueSupplier = memberValueSupplier;
059        putValue(NAME, tr("Go to OSM wiki for tag help"));
060        putValue(SHORT_DESCRIPTION, tr("Launch browser with wiki help for selected object"));
061        new ImageProvider("dialogs", "search").getResource().attachImageIcon(this, true);
062        putValue(ACCELERATOR_KEY, getKeyStroke());
063    }
064
065    /**
066     * Returns the keystroke launching this action (F1).
067     * @return the keystroke launching this action
068     */
069    public KeyStroke getKeyStroke() {
070        return KeyStroke.getKeyStroke(KeyEvent.VK_F1, 0);
071    }
072
073    @Override
074    public void actionPerformed(ActionEvent e) {
075        try {
076            String base = Config.getPref().get("url.openstreetmap-wiki", "https://wiki.openstreetmap.org/wiki/");
077            String lang = LanguageInfo.getWikiLanguagePrefix();
078            final List<URI> uris = new ArrayList<>();
079            if (tagTable.getSelectedRowCount() == 1) {
080                int row = tagTable.getSelectedRow();
081                String key = Utils.encodeUrl(tagKeySupplier.apply(row));
082                Map<String, Integer> m = tagValuesSupplier.apply(row);
083                if (!m.isEmpty()) {
084                    String val = Utils.encodeUrl(m.entrySet().iterator().next().getKey());
085                    uris.addAll(getTagURIs(base, lang, key, val));
086                }
087            } else if (membershipTable != null && membershipTable.getSelectedRowCount() == 1) {
088                int row = membershipTable.getSelectedRow();
089                uris.addAll(getRelationURIs(base, lang, memberValueSupplier.apply(row)));
090            } else {
091                // give the generic help page, if more than one element is selected
092                uris.addAll(getGenericURIs(base, lang));
093            }
094
095            MainApplication.worker.execute(() -> displayHelp(uris));
096        } catch (URISyntaxException e1) {
097            Logging.error(e1);
098        }
099    }
100
101    /**
102     * Returns a list of URIs for the given key/value.
103     * @param base OSM wiki base URL
104     * @param lang Language prefix
105     * @param key Key
106     * @param val Value
107     * @return a list of URIs for the given key/value by order of relevance
108     * @throws URISyntaxException in case of internal error
109     * @since 13522
110     */
111    public static List<URI> getTagURIs(String base, String lang, String key, String val) throws URISyntaxException {
112        return Arrays.asList(
113            new URI(String.format("%s%sTag:%s=%s", base, lang, key, val)),
114            new URI(String.format("%sTag:%s=%s", base, key, val)),
115            new URI(String.format("%s%sKey:%s", base, lang, key)),
116            new URI(String.format("%sKey:%s", base, key)),
117            new URI(String.format("%s%sMap_Features", base, lang)),
118            new URI(String.format("%sMap_Features", base))
119        );
120    }
121
122    /**
123     * Returns a list of URIs for the given relation.
124     * @param base OSM wiki base URL
125     * @param lang Language prefix
126     * @param rel Relation
127     * @return a list of URIs for the given relation by order of relevance
128     * @throws URISyntaxException in case of internal error
129     * @since 13522
130     */
131    public static List<URI> getRelationURIs(String base, String lang, Relation rel) throws URISyntaxException {
132        List<URI> uris = new ArrayList<>();
133        String type = rel.get("type");
134        if (type != null) {
135            type = Utils.encodeUrl(type);
136        }
137
138        if (type != null && !type.isEmpty()) {
139            uris.add(new URI(String.format("%s%sRelation:%s", base, lang, type)));
140            uris.add(new URI(String.format("%sRelation:%s", base, type)));
141        }
142
143        uris.add(new URI(String.format("%s%sRelations", base, lang)));
144        uris.add(new URI(String.format("%sRelations", base)));
145        return uris;
146    }
147
148    /**
149     * Returns a list of generic URIs (Map Features).
150     * @param base OSM wiki base URL
151     * @param lang Language prefix
152     * @return a list of generic URIs (Map Features)
153     * @throws URISyntaxException in case of internal error
154     * @since 13522
155     */
156    public static List<URI> getGenericURIs(String base, String lang) throws URISyntaxException {
157        return Arrays.asList(
158            new URI(String.format("%s%sMap_Features", base, lang)),
159            new URI(String.format("%sMap_Features", base))
160        );
161    }
162
163    /**
164     * Display help by searching the forst valid URI in the given list.
165     * @param uris list of URIs to test
166     * @since 13522
167     */
168    public static void displayHelp(final List<URI> uris) {
169        try {
170            // find a page that actually exists in the wiki
171            HttpClient.Response conn;
172            for (URI u : uris) {
173                conn = HttpClient.create(u.toURL(), "HEAD").connect();
174
175                if (conn.getResponseCode() != 200) {
176                    conn.disconnect();
177                } else {
178                    long osize = conn.getContentLength();
179                    if (osize > -1) {
180                        conn.disconnect();
181
182                        final URI newURI = new URI(u.toString()
183                                .replace("=", "%3D") /* do not URLencode whole string! */
184                                .replaceFirst("/wiki/", "/w/index.php?redirect=no&title=")
185                        );
186                        conn = HttpClient.create(newURI.toURL(), "HEAD").connect();
187                    }
188
189                    /* redirect pages have different content length, but retrieving a "nonredirect"
190                     *  page using index.php and the direct-link method gives slightly different
191                     *  content lengths, so we have to be fuzzy.. (this is UGLY, recode if u know better)
192                     */
193                    if (osize > -1 && conn.getContentLength() != -1 && Math.abs(conn.getContentLength() - osize) > 200) {
194                        Logging.info("{0} is a mediawiki redirect", u);
195                        conn.disconnect();
196                    } else {
197                        conn.disconnect();
198
199                        OpenBrowser.displayUrl(u.toString());
200                        break;
201                    }
202                }
203            }
204        } catch (URISyntaxException | IOException e1) {
205            Logging.error(e1);
206        }
207    }
208}