source: josm/trunk/src/org/openstreetmap/josm/gui/conflict/tags/TagConflictResolutionUtil.java@ 12620

Last change on this file since 12620 was 12620, checked in by Don-vip, 7 years ago

see #15182 - deprecate all Main logging methods and introduce suitable replacements in Logging for most of them

  • Property svn:eol-style set to native
File size: 19.6 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.conflict.tags;
3
4import java.util.ArrayList;
5import java.util.Arrays;
6import java.util.Collection;
7import java.util.Collections;
8import java.util.HashMap;
9import java.util.LinkedHashSet;
10import java.util.List;
11import java.util.Set;
12import java.util.TreeSet;
13import java.util.regex.Pattern;
14import java.util.regex.PatternSyntaxException;
15import java.util.stream.Collectors;
16
17import org.openstreetmap.josm.Main;
18import org.openstreetmap.josm.data.Preferences.pref;
19import org.openstreetmap.josm.data.osm.OsmPrimitive;
20import org.openstreetmap.josm.data.osm.Tag;
21import org.openstreetmap.josm.data.osm.TagCollection;
22import org.openstreetmap.josm.tools.Logging;
23import org.openstreetmap.josm.tools.Pair;
24
25/**
26 * Collection of utility methods for tag conflict resolution
27 *
28 */
29public final class TagConflictResolutionUtil {
30
31 /** The OSM key 'source' */
32 private static final String KEY_SOURCE = "source";
33
34 /** The group identifier for French Cadastre choices */
35 private static final String GRP_FR_CADASTRE = "FR:cadastre";
36
37 /** The group identifier for Canadian CANVEC choices */
38 private static final String GRP_CA_CANVEC = "CA:canvec";
39
40 /**
41 * Default preferences for the list of AutomaticCombine tag conflict resolvers.
42 */
43 private static final Collection<AutomaticCombine> defaultAutomaticTagConflictCombines = Arrays.asList(
44 new AutomaticCombine("tiger:tlid", "US TIGER tlid", false, ":", "Integer"),
45 new AutomaticCombine("tiger:(?!tlid$).*", "US TIGER not tlid", true, ":", "String")
46 );
47
48 /**
49 * Default preferences for the list of AutomaticChoice tag conflict resolvers.
50 */
51 private static final Collection<AutomaticChoice> defaultAutomaticTagConflictChoices = Arrays.asList(
52 /* "source" "FR:cadastre" - https://wiki.openstreetmap.org/wiki/FR:WikiProject_France/Cadastre
53 * List of choices for the "source" tag of data exported from the French cadastre,
54 * which ends by the exported year generating many conflicts.
55 * The generated score begins with the year number to select the most recent one.
56 */
57 new AutomaticChoice(KEY_SOURCE, GRP_FR_CADASTRE, "FR cadastre source, manual value", true,
58 "cadastre", "0"),
59 new AutomaticChoice(KEY_SOURCE, GRP_FR_CADASTRE, "FR cadastre source, initial format", true,
60 "extraction vectorielle v1 cadastre-dgi-fr source : Direction G[eé]n[eé]rale des Imp[oô]ts"
61 + " - Cadas\\. Mise [aà] jour : (2[0-9]{3})",
62 "$1 1"),
63 new AutomaticChoice(KEY_SOURCE, GRP_FR_CADASTRE, "FR cadastre source, last format", true,
64 "(?:cadastre-dgi-fr source : )?Direction G[eé]n[eé]rale des (?:Imp[oô]ts|Finances Publiques)"
65 + " - Cadas(?:tre)?(?:\\.| ;) [Mm]ise [aà] jour : (2[0-9]{3})",
66 "$1 2"),
67 /* "source" "CA:canvec" - https://wiki.openstreetmap.org/wiki/CanVec
68 * List of choices for the "source" tag of data exported from Natural Resources Canada (NRCan)
69 */
70 new AutomaticChoice(KEY_SOURCE, GRP_CA_CANVEC, "CA canvec source, initial value", true,
71 "CanVec_Import_2009", "00"),
72 new AutomaticChoice(KEY_SOURCE, GRP_CA_CANVEC, "CA canvec source, 4.0/6.0 value", true,
73 "CanVec ([1-9]).0 - NRCan", "0$1"),
74 new AutomaticChoice(KEY_SOURCE, GRP_CA_CANVEC, "CA canvec source, 7.0/8.0 value", true,
75 "NRCan-CanVec-([1-9]).0", "0$1"),
76 new AutomaticChoice(KEY_SOURCE, GRP_CA_CANVEC, "CA canvec source, 10.0/12.0 value", true,
77 "NRCan-CanVec-(1[012]).0", "$1")
78 );
79
80 private static volatile Collection<AutomaticTagConflictResolver> automaticTagConflictResolvers;
81
82 private TagConflictResolutionUtil() {
83 // no constructor, just static utility methods
84 }
85
86 /**
87 * Normalizes the tags in the tag collection <code>tc</code> before resolving tag conflicts.
88 *
89 * Removes irrelevant tags like "created_by".
90 *
91 * For tags which are not present on at least one of the merged nodes, the empty value ""
92 * is added to the list of values for this tag, but only if there are at least two
93 * primitives with tags, and at least one tagged primitive do not have this tag.
94 *
95 * @param tc the tag collection
96 * @param merged the collection of merged primitives
97 */
98 public static void normalizeTagCollectionBeforeEditing(TagCollection tc, Collection<? extends OsmPrimitive> merged) {
99 // remove irrelevant tags
100 //
101 for (String key : OsmPrimitive.getDiscardableKeys()) {
102 tc.removeByKey(key);
103 }
104
105 Collection<OsmPrimitive> taggedPrimitives = new ArrayList<>();
106 for (OsmPrimitive p: merged) {
107 if (p.isTagged()) {
108 taggedPrimitives.add(p);
109 }
110 }
111 if (taggedPrimitives.size() <= 1)
112 return;
113
114 for (String key: tc.getKeys()) {
115 // make sure the empty value is in the tag set if a tag is not present
116 // on all merged nodes
117 //
118 for (OsmPrimitive p: taggedPrimitives) {
119 if (p.get(key) == null) {
120 tc.add(new Tag(key, "")); // add a tag with key and empty value
121 }
122 }
123 }
124 }
125
126 /**
127 * Completes tags in the tag collection <code>tc</code> with the empty value
128 * for each tag. If the empty value is present the tag conflict resolution dialog
129 * will offer an option for removing the tag and not only options for selecting
130 * one of the current values of the tag.
131 *
132 * @param tc the tag collection
133 */
134 public static void completeTagCollectionForEditing(TagCollection tc) {
135 for (String key: tc.getKeys()) {
136 // make sure the empty value is in the tag set such that we can delete the tag
137 // in the conflict dialog if necessary
138 //
139 tc.add(new Tag(key, ""));
140 }
141 }
142
143 /**
144 * Automatically resolve some tag conflicts.
145 * The list of automatic resolution is taken from the preferences.
146 * @param tc the tag collection
147 * @since 11606
148 */
149 public static void applyAutomaticTagConflictResolution(TagCollection tc) {
150 applyAutomaticTagConflictResolution(tc, getAutomaticTagConflictResolvers());
151 }
152
153 /**
154 * Get the AutomaticTagConflictResolvers configured in the Preferences or the default ones.
155 * @return the configured AutomaticTagConflictResolvers.
156 * @since 11606
157 */
158 public static Collection<AutomaticTagConflictResolver> getAutomaticTagConflictResolvers() {
159 if (automaticTagConflictResolvers == null) {
160 Collection<AutomaticCombine> automaticTagConflictCombines =
161 Main.pref.getListOfStructs(
162 "automatic-tag-conflict-resolution.combine",
163 defaultAutomaticTagConflictCombines, AutomaticCombine.class);
164 Collection<AutomaticChoiceGroup> automaticTagConflictChoiceGroups =
165 AutomaticChoiceGroup.groupChoices(Main.pref.getListOfStructs(
166 "automatic-tag-conflict-resolution.choice",
167 defaultAutomaticTagConflictChoices, AutomaticChoice.class));
168 // Use a tmp variable to fully construct the collection before setting
169 // the volatile variable automaticTagConflictResolvers.
170 ArrayList<AutomaticTagConflictResolver> tmp = new ArrayList<>();
171 tmp.addAll(automaticTagConflictCombines);
172 tmp.addAll(automaticTagConflictChoiceGroups);
173 automaticTagConflictResolvers = tmp;
174 }
175 return Collections.unmodifiableCollection(automaticTagConflictResolvers);
176 }
177
178 /**
179 * An automatic tag conflict resolver interface.
180 * @since 11606
181 */
182 interface AutomaticTagConflictResolver {
183 /**
184 * Check if this resolution apply to the given Tag key.
185 * @param key The Tag key to match.
186 * @return true if this automatic resolution apply to the given Tag key.
187 */
188 boolean matchesKey(String key);
189
190 /**
191 * Try to resolve a conflict between a set of values for a Tag
192 * @param values the set of conflicting values for the Tag.
193 * @return the resolved value or null if resolution was not possible.
194 */
195 String resolve(Set<String> values);
196 }
197
198 /**
199 * Automatically resolve some given conflicts using the given resolvers.
200 * @param tc the tag collection.
201 * @param resolvers the list of automatic tag conflict resolvers to apply.
202 * @since 11606
203 */
204 public static void applyAutomaticTagConflictResolution(TagCollection tc,
205 Collection<AutomaticTagConflictResolver> resolvers) {
206 for (String key: tc.getKeysWithMultipleValues()) {
207 for (AutomaticTagConflictResolver resolver : resolvers) {
208 try {
209 if (resolver.matchesKey(key)) {
210 String result = resolver.resolve(tc.getValues(key));
211 if (result != null) {
212 tc.setUniqueForKey(key, result);
213 break;
214 }
215 }
216 } catch (PatternSyntaxException e) {
217 // Can happen if a particular resolver has an invalid regular expression pattern
218 // but it should not stop the other automatic tag conflict resolution.
219 Logging.error(e);
220 }
221 }
222 }
223 }
224
225 /**
226 * Preference for automatic tag-conflict resolver by combining the tag values using a separator.
227 * @since 11606
228 */
229 public static class AutomaticCombine implements AutomaticTagConflictResolver {
230
231 /** The Tag key to match */
232 @pref public String key;
233
234 /** A free description */
235 @pref public String description = "";
236
237 /** If regular expression must be used to match the Tag key or the value. */
238 @pref public boolean isRegex;
239
240 /** The separator to use to combine the values. */
241 @pref public String separator = ";";
242
243 /** If the combined values must be sorted.
244 * Possible values:
245 * <ul>
246 * <li> Integer - Sort using Integer natural order.</li>
247 * <li> String - Sort using String natural order.</li>
248 * <li> * - No ordering.</li>
249 * </ul>
250 */
251 @pref public String sort;
252
253 /** Default constructor. */
254 public AutomaticCombine() {
255 // needed for instantiation from Preferences
256 }
257
258 /** Instantiate an automatic tag-conflict resolver which combining the values using a separator.
259 * @param key The Tag key to match.
260 * @param description A free description.
261 * @param isRegex If regular expression must be used to match the Tag key or the value.
262 * @param separator The separator to use to combine the values.
263 * @param sort If the combined values must be sorted.
264 */
265 public AutomaticCombine(String key, String description, boolean isRegex, String separator, String sort) {
266 this.key = key;
267 this.description = description;
268 this.isRegex = isRegex;
269 this.separator = separator;
270 this.sort = sort;
271 }
272
273 @Override
274 public boolean matchesKey(String k) {
275 if (isRegex) {
276 return Pattern.matches(this.key, k);
277 } else {
278 return this.key.equals(k);
279 }
280 }
281
282 Set<String> instantiateSortedSet() {
283 if ("String".equals(sort)) {
284 return new TreeSet<>();
285 } else if ("Integer".equals(sort)) {
286 return new TreeSet<>((String v1, String v2) -> Long.valueOf(v1).compareTo(Long.valueOf(v2)));
287 } else {
288 return new LinkedHashSet<>();
289 }
290 }
291
292 @Override
293 public String resolve(Set<String> values) {
294 Set<String> results = instantiateSortedSet();
295 for (String value: values) {
296 for (String part: value.split(Pattern.quote(separator))) {
297 results.add(part);
298 }
299 }
300 return String.join(separator, results);
301 }
302
303 @Override
304 public String toString() {
305 return AutomaticCombine.class.getSimpleName()
306 + "(key='" + key + "', description='" + description + "', isRegex="
307 + isRegex + ", separator='" + separator + "', sort='" + sort + "')";
308 }
309 }
310
311 /**
312 * Preference for a particular choice from a group for automatic tag conflict resolution.
313 * {@code AutomaticChoice}s are grouped into {@link AutomaticChoiceGroup}.
314 * @since 11606
315 */
316 public static class AutomaticChoice {
317
318 /** The Tag key to match. */
319 @pref public String key;
320
321 /** The name of the {link AutomaticChoice group} this choice belongs to. */
322 @pref public String group;
323
324 /** A free description. */
325 @pref public String description = "";
326
327 /** If regular expression must be used to match the Tag key or the value. */
328 @pref public boolean isRegex;
329
330 /** The Tag value to match. */
331 @pref public String value;
332
333 /**
334 * The score to give to this choice in order to choose the best value
335 * Natural String ordering is used to identify the best score.
336 */
337 @pref public String score;
338
339 /** Default constructor. */
340 public AutomaticChoice() {
341 // needed for instantiation from Preferences
342 }
343
344 /**
345 * Instantiate a particular choice from a group for automatic tag conflict resolution.
346 * @param key The Tag key to match.
347 * @param group The name of the {link AutomaticChoice group} this choice belongs to.
348 * @param description A free description.
349 * @param isRegex If regular expression must be used to match the Tag key or the value.
350 * @param value The Tag value to match.
351 * @param score The score to give to this choice in order to choose the best value.
352 */
353 public AutomaticChoice(String key, String group, String description, boolean isRegex, String value, String score) {
354 this.key = key;
355 this.group = group;
356 this.description = description;
357 this.isRegex = isRegex;
358 this.value = value;
359 this.score = score;
360 }
361
362 /**
363 * Check if this choice match the given Tag value.
364 * @param v the Tag value to match.
365 * @return true if this choice correspond to the given tag value.
366 */
367 public boolean matchesValue(String v) {
368 if (isRegex) {
369 return Pattern.matches(this.value, v);
370 } else {
371 return this.value.equals(v);
372 }
373 }
374
375 /**
376 * Return the score associated to this choice for the given Tag value.
377 * For the result to be valid the given tag value must {@link #matchesValue(String) match} this choice.
378 * @param v the Tag value of which to get the score.
379 * @return the score associated to the given Tag value.
380 * @throws PatternSyntaxException if the regular expression syntax is invalid
381 */
382 public String computeScoreFromValue(String v) {
383 if (isRegex) {
384 return v.replaceAll("^" + this.value + "$", this.score);
385 } else {
386 return this.score;
387 }
388 }
389
390 @Override
391 public String toString() {
392 return AutomaticChoice.class.getSimpleName()
393 + "(key='" + key + "', group='" + group + "', description='" + description
394 + "', isRegex=" + isRegex + ", value='" + value + "', score='" + score + "')";
395 }
396 }
397
398 /**
399 * Preference for an automatic tag conflict resolver which choose from
400 * a group of possible {@link AutomaticChoice choice} values.
401 * @since 11606
402 */
403 public static class AutomaticChoiceGroup implements AutomaticTagConflictResolver {
404
405 /** The Tag key to match. */
406 @pref public String key;
407
408 /** The name of the group. */
409 final String group;
410
411 /** If regular expression must be used to match the Tag key. */
412 @pref public boolean isRegex;
413
414 /** The list of choice to choose from. */
415 final List<AutomaticChoice> choices;
416
417 /** Instantiate an automatic tag conflict resolver which choose from
418 * a given list of {@link AutomaticChoice choice} values.
419 *
420 * @param key The Tag key to match.
421 * @param group The name of the group.
422 * @param isRegex If regular expression must be used to match the Tag key.
423 * @param choices The list of choice to choose from.
424 */
425 public AutomaticChoiceGroup(String key, String group, boolean isRegex, List<AutomaticChoice> choices) {
426 this.key = key;
427 this.group = group;
428 this.isRegex = isRegex;
429 this.choices = choices;
430 }
431
432 /**
433 * Group a given list of {@link AutomaticChoice} by the Tag key and the choice group name.
434 * @param choices the list of {@link AutomaticChoice choices} to group.
435 * @return the resulting list of group.
436 */
437 public static Collection<AutomaticChoiceGroup> groupChoices(Collection<AutomaticChoice> choices) {
438 HashMap<Pair<String, String>, AutomaticChoiceGroup> results = new HashMap<>();
439 for (AutomaticChoice choice: choices) {
440 Pair<String, String> id = new Pair<>(choice.key, choice.group);
441 AutomaticChoiceGroup group = results.get(id);
442 if (group == null) {
443 boolean isRegex = choice.isRegex && !Pattern.quote(choice.key).equals(choice.key);
444 group = new AutomaticChoiceGroup(choice.key, choice.group, isRegex, new ArrayList<>());
445 results.put(id, group);
446 }
447 group.choices.add(choice);
448 }
449 return results.values();
450 }
451
452 @Override
453 public boolean matchesKey(String k) {
454 if (isRegex) {
455 return Pattern.matches(this.key, k);
456 } else {
457 return this.key.equals(k);
458 }
459 }
460
461 @Override
462 public String resolve(Set<String> values) {
463 String bestScore = "";
464 String bestValue = "";
465 for (String value : values) {
466 String score = null;
467 for (AutomaticChoice choice : choices) {
468 if (choice.matchesValue(value)) {
469 score = choice.computeScoreFromValue(value);
470 }
471 }
472 if (score == null) {
473 // This value is not matched in this group
474 // so we can not choose from this group for this key.
475 return null;
476 }
477 if (score.compareTo(bestScore) >= 0) {
478 bestScore = score;
479 bestValue = value;
480 }
481 }
482 return bestValue;
483 }
484
485 @Override
486 public String toString() {
487 Collection<String> stringChoices = choices.stream().map(AutomaticChoice::toString).collect(Collectors.toCollection(ArrayList::new));
488 return AutomaticChoiceGroup.class.getSimpleName() + "(key='" + key + "', group='" + group +
489 "', isRegex=" + isRegex + ", choices=(\n " + String.join(",\n ", stringChoices) + "))";
490 }
491 }
492}
Note: See TracBrowser for help on using the repository browser.