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}