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