source: josm/trunk/src/org/openstreetmap/josm/tools/Tag2Link.java@ 16774

Last change on this file since 16774 was 16774, checked in by simon04, 4 years ago

fix #19293 - Tag2link/wikipedia: replace space with underscore

File size: 7.2 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.tools;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5import static org.openstreetmap.josm.tools.I18n.trn;
6
7import java.io.IOException;
8import java.io.InputStream;
9import java.net.MalformedURLException;
10import java.net.URL;
11import java.util.Collections;
12import java.util.Map;
13import java.util.function.UnaryOperator;
14import java.util.regex.Matcher;
15import java.util.regex.Pattern;
16import java.util.stream.Collectors;
17import java.util.stream.Stream;
18
19import javax.json.Json;
20import javax.json.JsonArray;
21import javax.json.JsonReader;
22import javax.json.JsonValue;
23
24import org.openstreetmap.josm.data.osm.OsmUtils;
25
26/**
27 * Extracts web links from OSM tags.
28 *
29 * The following rules are used:
30 * <ul>
31 * <li>internal rules for basic tags</li>
32 * <li>rules from Wikidata based on OSM tag or key (P1282); formatter URL (P1630); third-party formatter URL (P3303)</li>
33 * <li>rules from OSM Sophox based on permanent key ID (P16); formatter URL (P8)</li>
34 * </ul>
35 *
36 * @since 15673
37 */
38public final class Tag2Link {
39
40 // Related implementations:
41 // - https://github.com/openstreetmap/openstreetmap-website/blob/master/app/helpers/browse_tags_helper.rb
42
43 /**
44 * Maps OSM keys to formatter URLs from Wikidata and OSM Sophox where {@code "$1"} has to be replaced by a value.
45 */
46 static final MultiMap<String, String> wikidataRules = new MultiMap<>();
47
48 static final Map<String, UnaryOperator<String>> valueFormatter = Collections.singletonMap(
49 "ref:bag", v -> String.format("%16s", v).replace(' ', '0')
50 );
51
52 static final String languagePattern = LanguageInfo.getLanguageCodes(null).stream()
53 .map(Pattern::quote)
54 .collect(Collectors.joining("|"));
55
56 private Tag2Link() {
57 // private constructor for utility class
58 }
59
60 /**
61 * Represents an operation that accepts a link.
62 */
63 @FunctionalInterface
64 public interface LinkConsumer {
65 /**
66 * Performs the operation on the given arguments.
67 * @param name the name/label of the link
68 * @param url the URL of the link
69 */
70 void acceptLink(String name, String url);
71 }
72
73 /**
74 * Initializes the tag2link rules
75 */
76 public static void initialize() {
77 try {
78 wikidataRules.clear();
79 initializeFromResources();
80 } catch (Exception e) {
81 Logging.error("Failed to initialize tag2link rules");
82 Logging.error(e);
83 }
84 }
85
86 /**
87 * Initializes the tag2link rules from the resources.
88 *
89 * @throws IOException in case of I/O error
90 */
91 private static void initializeFromResources() throws IOException {
92 final String resource = "META-INF/resources/webjars/tag2link/2020.5.16/index.json";
93 final JsonArray rules;
94 try (InputStream inputStream = Tag2Link.class.getClassLoader().getResourceAsStream(resource);
95 JsonReader jsonReader = Json.createReader(inputStream)) {
96 rules = jsonReader.readArray();
97 }
98
99 for (JsonValue rule : rules) {
100 final String key = rule.asJsonObject().getString("key");
101 final String url = rule.asJsonObject().getString("url");
102 if (key.startsWith("Key:")) {
103 wikidataRules.put(key.substring("Key:".length()), url);
104 }
105 }
106 // We handle those keys ourselves
107 Stream.of("image", "url", "website", "wikidata", "wikimedia_commons")
108 .forEach(wikidataRules::remove);
109
110 final int size = wikidataRules.size();
111 Logging.info(trn(
112 "Obtained {0} Tag2Link rule from {1}",
113 "Obtained {0} Tag2Link rules from {1}",
114 size, size, resource));
115 }
116
117 /**
118 * Generates the links for the tag given by {@code key} and {@code value}, and sends 0, 1 or more links to the {@code linkConsumer}.
119 * @param key the tag key
120 * @param value the tag value
121 * @param linkConsumer the receiver of the generated links
122 */
123 public static void getLinksForTag(String key, String value, LinkConsumer linkConsumer) {
124
125 if (value == null || value.isEmpty()) {
126 return;
127 }
128
129 // Search
130 if (key.matches("^(.+[:_])?name([:_]" + languagePattern + ")?$")) {
131 linkConsumer.acceptLink(tr("Search on DuckDuckGo"), "https://duckduckgo.com/?q=" + value);
132 }
133
134 // Common
135 final String validURL = value.startsWith("http:") || value.startsWith("https:")
136 ? value
137 : value.startsWith("www.")
138 ? "http://" + value
139 : null;
140 if (key.matches("^(.+[:_])?website([:_].+)?$") && validURL != null) {
141 linkConsumer.acceptLink(getLinkName(validURL, key), validURL);
142 }
143 if (key.matches("^(.+[:_])?source([:_].+)?$") && validURL != null) {
144 linkConsumer.acceptLink(getLinkName(validURL, key), validURL);
145 }
146 if (key.matches("^(.+[:_])?url([:_].+)?$") && validURL != null) {
147 linkConsumer.acceptLink(getLinkName(validURL, key), validURL);
148 }
149 if (key.matches("image") && validURL != null) {
150 linkConsumer.acceptLink(tr("View image"), validURL);
151 }
152
153 // Wikimedia
154 final Matcher keyMatcher = Pattern.compile("wikipedia(:(?<lang>\\p{Lower}{2,}))?").matcher(key);
155 final Matcher valueMatcher = Pattern.compile("((?<lang>\\p{Lower}{2,}):)?(?<article>.*)").matcher(value);
156 if (keyMatcher.matches() && valueMatcher.matches()) {
157 final String lang = Utils.firstNotEmptyString("en", keyMatcher.group("lang"), valueMatcher.group("lang"));
158 final String url = "https://" + lang + ".wikipedia.org/wiki/" + valueMatcher.group("article").replace(' ', '_');
159 linkConsumer.acceptLink(tr("View Wikipedia article"), url);
160 }
161 if (key.matches("(.*:)?wikidata")) {
162 OsmUtils.splitMultipleValues(value)
163 .forEach(q -> linkConsumer.acceptLink(tr("View Wikidata item"), "https://www.wikidata.org/wiki/" + q));
164 }
165 if (key.matches("(.*:)?species")) {
166 final String url = "https://species.wikimedia.org/wiki/" + value;
167 linkConsumer.acceptLink(getLinkName(url, key), url);
168 }
169 if (key.matches("wikimedia_commons|image") && value.matches("(?i:File):.*")) {
170 linkConsumer.acceptLink(tr("View image on Wikimedia Commons"), "https://commons.wikimedia.org/wiki/" + value);
171 }
172 if (key.matches("wikimedia_commons|image") && value.matches("(?i:Category):.*")) {
173 linkConsumer.acceptLink(tr("View category on Wikimedia Commons"), "https://commons.wikimedia.org/wiki/" + value);
174 }
175
176 wikidataRules.getValues(key).forEach(urlFormatter -> {
177 final String formattedValue = valueFormatter.getOrDefault(key, x -> x).apply(value);
178 final String url = urlFormatter.replace("$1", formattedValue);
179 linkConsumer.acceptLink(getLinkName(url, key), url);
180 });
181 }
182
183 private static String getLinkName(String url, String fallback) {
184 try {
185 return tr("Open {0}", new URL(url).getHost());
186 } catch (MalformedURLException e) {
187 return tr("Open {0}", fallback);
188 }
189 }
190
191}
Note: See TracBrowser for help on using the repository browser.