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

Last change on this file was 18979, checked in by taylor.smock, 2 months ago

Dependency updates

ivy.xml

  • OpeningHoursParser: 0.28.0 -> 0.28.1
  • junit: 5.10.1 -> 5.10.2
  • equalsverifier: 3.15.5 -> 3.15.6
  • tag2link: 2023.11.21 -> 2024.2.8

tools/ivy.xml

  • proguard: 7.4.1 -> 7.4.2
  • error_prone: 2.24.0 -> 2.24.1
File size: 10.7 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.Arrays;
12import java.util.Collections;
13import java.util.EnumMap;
14import java.util.List;
15import java.util.Map;
16import java.util.Objects;
17import java.util.Optional;
18import java.util.Set;
19import java.util.function.Supplier;
20import java.util.function.UnaryOperator;
21import java.util.regex.Matcher;
22import java.util.regex.Pattern;
23import java.util.stream.Collectors;
24
25import jakarta.json.Json;
26import jakarta.json.JsonArray;
27import jakarta.json.JsonReader;
28import jakarta.json.JsonValue;
29
30import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
31import org.openstreetmap.josm.data.osm.OsmUtils;
32import org.openstreetmap.josm.data.preferences.CachingProperty;
33import org.openstreetmap.josm.data.preferences.ListProperty;
34import org.openstreetmap.josm.io.CachedFile;
35
36/**
37 * Extracts web links from OSM tags.
38 *
39 * The following rules are used:
40 * <ul>
41 * <li>internal rules for basic tags</li>
42 * <li>rules from Wikidata based on OSM tag or key (P1282); formatter URL (P1630); third-party formatter URL (P3303)</li>
43 * <li>rules from OSM Sophox based on permanent key ID (P16); formatter URL (P8)</li>
44 * </ul>
45 *
46 * @since 15673
47 */
48public final class Tag2Link {
49
50 // Related implementations:
51 // - https://github.com/openstreetmap/openstreetmap-website/blob/master/app/helpers/browse_tags_helper.rb
52
53 /**
54 * Maps OSM keys to formatter URLs from Wikidata and OSM Sophox where {@code "$1"} has to be replaced by a value.
55 */
56 static final MultiMap<String, String> wikidataRules = new MultiMap<>();
57
58 static final Map<String, UnaryOperator<String>> valueFormatter = Collections.singletonMap(
59 "ref:bag", v -> String.format("%16s", v).replace(' ', '0')
60 );
61
62 static final String languagePattern = LanguageInfo.getLanguageCodes(null).stream()
63 .map(Pattern::quote)
64 .collect(Collectors.joining("|"));
65
66 static final ListProperty PREF_SOURCE = new ListProperty("tag2link.source",
67 Collections.singletonList("resource://META-INF/resources/webjars/tag2link/2024.2.8/index.json"));
68
69 static final CachingProperty<List<String>> PREF_SEARCH_ENGINES = new ListProperty("tag2link.search",
70 Arrays.asList("https://duckduckgo.com/?q=$1", "https://www.google.com/search?q=$1")).cached();
71 private static final Pattern PATTERN_DOLLAR_ONE = Pattern.compile("$1", Pattern.LITERAL);
72
73 private Tag2Link() {
74 // private constructor for utility class
75 }
76
77 /**
78 * Represents an operation that accepts a link.
79 */
80 @FunctionalInterface
81 public interface LinkConsumer {
82 /**
83 * Performs the operation on the given arguments.
84 * @param name the name/label of the link
85 * @param url the URL of the link
86 * @param icon the icon to use
87 */
88 void acceptLink(String name, String url, ImageResource icon);
89 }
90
91 /**
92 * Initializes the tag2link rules
93 */
94 public static void initialize() {
95 try {
96 wikidataRules.clear();
97 for (String source : PREF_SOURCE.get()) {
98 initializeFromResources(new CachedFile(source));
99 }
100 } catch (Exception e) {
101 Logging.error("Failed to initialize tag2link rules");
102 Logging.error(e);
103 }
104 }
105
106 /**
107 * Initializes the tag2link rules from the resources.
108 *
109 * @param resource the source
110 * @throws IOException in case of I/O error
111 */
112 private static void initializeFromResources(CachedFile resource) throws IOException {
113 final JsonArray rules;
114 try (InputStream inputStream = resource.getInputStream();
115 JsonReader jsonReader = Json.createReader(inputStream)) {
116 rules = jsonReader.readArray();
117 }
118
119 for (JsonValue rule : rules) {
120 final String key = rule.asJsonObject().getString("key");
121 final String url = rule.asJsonObject().getString("url");
122 if (key.startsWith("Key:")) {
123 wikidataRules.put(key.substring("Key:".length()), url);
124 }
125 }
126 // We handle those keys ourselves
127 wikidataRules.keySet().removeIf(key -> key.matches("^(.+[:_])?website([:_].+)?$")
128 || key.matches("^(.+[:_])?url([:_].+)?$")
129 || key.matches("wikimedia_commons|image")
130 || key.matches("wikipedia(:(?<lang>\\p{Lower}{2,}))?")
131 || key.matches("(.*:)?wikidata"));
132
133 final int size = wikidataRules.size();
134 Logging.info(trn(
135 "Obtained {0} Tag2Link rule from {1}",
136 "Obtained {0} Tag2Link rules from {1}",
137 size, size, resource));
138 }
139
140 /**
141 * Generates the links for the tag given by {@code key} and {@code value}, and sends 0, 1 or more links to the {@code linkConsumer}.
142 * @param key the tag key
143 * @param value the tag value
144 * @param linkConsumer the receiver of the generated links
145 */
146 public static void getLinksForTag(String key, String value, LinkConsumer linkConsumer) {
147
148 if (Utils.isEmpty(value)) {
149 return;
150 }
151
152 final Map<OsmPrimitiveType, Optional<ImageResource>> memoize = new EnumMap<>(OsmPrimitiveType.class);
153 final Supplier<ImageResource> imageResource = () -> memoize
154 .computeIfAbsent(OsmPrimitiveType.NODE, type -> OsmPrimitiveImageProvider.getResource(key, value, type))
155 .orElse(null);
156
157 // Search
158 if (key.matches("^(.+[:_])?name([:_]" + languagePattern + ")?$")) {
159 final ImageResource search = new ImageProvider("dialogs/search").getResource();
160 PREF_SEARCH_ENGINES.get().forEach(url ->
161 linkConsumer.acceptLink(tr("Search on {0}", getHost(url, url)), url.replace("$1", Utils.encodeUrl(value)), search));
162 }
163
164 // Common
165 final List<String> validURLs = value.startsWith("http:") || value.startsWith("https:") || value.startsWith("www.")
166 ? OsmUtils.splitMultipleValues(value)
167 .map(v -> v.startsWith("http:") || v.startsWith("https:")
168 ? v
169 : v.startsWith("www.")
170 ? "http://" + v
171 : null)
172 .filter(Objects::nonNull)
173 .collect(Collectors.toList())
174 : Collections.emptyList();
175 if (key.matches("^(.+[:_])?website([:_].+)?$") && !validURLs.isEmpty()) {
176 validURLs.forEach(validURL -> linkConsumer.acceptLink(getLinkName(validURL, key), validURL, imageResource.get()));
177 }
178 if (key.matches("^(.+[:_])?source([:_].+)?$") && !validURLs.isEmpty()) {
179 validURLs.forEach(validURL -> linkConsumer.acceptLink(getLinkName(validURL, key), validURL, imageResource.get()));
180 }
181 if (key.matches("^(.+[:_])?url([:_].+)?$") && !validURLs.isEmpty()) {
182 validURLs.forEach(validURL -> linkConsumer.acceptLink(getLinkName(validURL, key), validURL, imageResource.get()));
183 }
184 if (key.matches("image") && !validURLs.isEmpty()) {
185 validURLs.forEach(validURL -> linkConsumer.acceptLink(tr("View image"), validURL, imageResource.get()));
186 }
187
188 // Wikimedia
189 final Matcher keyMatcher = Pattern.compile("wikipedia(:(?<lang>\\p{Lower}{2,}))?").matcher(key);
190 final Matcher valueMatcher = Pattern.compile("((?<lang>\\p{Lower}{2,}):)?(?<article>.*)").matcher(value);
191 if (keyMatcher.matches() && valueMatcher.matches()) {
192 final String lang = Utils.firstNotEmptyString("en", keyMatcher.group("lang"), valueMatcher.group("lang"));
193 final String url = "https://" + lang + ".wikipedia.org/wiki/" + valueMatcher.group("article").replace(' ', '_');
194 linkConsumer.acceptLink(tr("View Wikipedia article"), url, imageResource.get());
195 }
196 if (key.matches("(.*:)?wikidata")) {
197 OsmUtils.splitMultipleValues(value).forEach(q -> linkConsumer.acceptLink(
198 tr("View Wikidata item"), "https://www.wikidata.org/wiki/" + q, imageResource.get()));
199 }
200 if (key.matches("(.*:)?species")) {
201 final String url = "https://species.wikimedia.org/wiki/" + value;
202 linkConsumer.acceptLink(getLinkName(url, key), url, imageResource.get());
203 }
204 if (key.matches("wikimedia_commons|image") && value.matches("(?i:File):.*")) {
205 OsmUtils.splitMultipleValues(value).forEach(i -> linkConsumer.acceptLink(
206 tr("View image on Wikimedia Commons"), getWikimediaCommonsUrl(i), imageResource.get()));
207 }
208 if (key.matches("wikimedia_commons|image") && value.matches("(?i:Category):.*")) {
209 OsmUtils.splitMultipleValues(value).forEach(i -> linkConsumer.acceptLink(
210 tr("View category on Wikimedia Commons"), getWikimediaCommonsUrl(i), imageResource.get()));
211 }
212
213 final Set<String> formatterUrls = wikidataRules.getValues(key);
214 if (!formatterUrls.isEmpty()) {
215 final String formattedValue = valueFormatter.getOrDefault(key, x -> x).apply(value);
216
217 final String urlKey = formatterUrls.stream().map(urlFormatter -> PATTERN_DOLLAR_ONE.matcher(urlFormatter)
218 .replaceAll(Matcher.quoteReplacement("(.*)"))).map(PatternUtils::compile)
219 .map(pattern -> pattern.matcher(value)).filter(Matcher::matches)
220 .map(matcher -> matcher.group(1)).findFirst().orElse(formattedValue);
221
222 formatterUrls.forEach(urlFormatter -> {
223 // Check if the current value matches the formatter pattern -- some keys can take a full url or a key for
224 // the formatter. Example: https://wiki.openstreetmap.org/wiki/Key:contact:facebook
225 final String url = PATTERN_DOLLAR_ONE.matcher(urlFormatter).replaceAll(urlKey);
226 linkConsumer.acceptLink(getLinkName(url, key), url, imageResource.get());
227 });
228 }
229 }
230
231 private static String getWikimediaCommonsUrl(String i) {
232 i = i.replace(' ', '_');
233 i = Utils.encodeUrl(i);
234 return "https://commons.wikimedia.org/wiki/" + i;
235 }
236
237 private static String getLinkName(String url, String fallback) {
238 return tr("Open {0}", getHost(url, fallback));
239 }
240
241 private static String getHost(String url, String fallback) {
242 try {
243 return new URL(url).getHost().replaceFirst("^www\\.", "");
244 } catch (MalformedURLException e) {
245 return fallback;
246 }
247 }
248
249}
Note: See TracBrowser for help on using the repository browser.