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

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

fix #14374 - automatic tag conflict resolution of source for French cadastre and Canadian CanVec (patch from Tyndare, modified)

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