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}