001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.conflict.tags; 003 004import java.util.ArrayList; 005import java.util.Arrays; 006import java.util.Collection; 007import java.util.Collections; 008import java.util.HashMap; 009import java.util.LinkedHashSet; 010import java.util.List; 011import java.util.Set; 012import java.util.TreeSet; 013import java.util.regex.Pattern; 014import java.util.regex.PatternSyntaxException; 015import java.util.stream.Collectors; 016 017import org.openstreetmap.josm.data.StructUtils; 018import org.openstreetmap.josm.data.StructUtils.StructEntry; 019import org.openstreetmap.josm.data.osm.OsmPrimitive; 020import org.openstreetmap.josm.data.osm.Tag; 021import org.openstreetmap.josm.data.osm.TagCollection; 022import org.openstreetmap.josm.spi.preferences.Config; 023import org.openstreetmap.josm.tools.JosmRuntimeException; 024import org.openstreetmap.josm.tools.Logging; 025import org.openstreetmap.josm.tools.Pair; 026 027/** 028 * Collection of utility methods for tag conflict resolution 029 * 030 */ 031public final class TagConflictResolutionUtil { 032 033 /** The OSM key 'source' */ 034 private static final String KEY_SOURCE = "source"; 035 036 /** The group identifier for French Cadastre choices */ 037 private static final String GRP_FR_CADASTRE = "FR:cadastre"; 038 039 /** The group identifier for Canadian CANVEC choices */ 040 private static final String GRP_CA_CANVEC = "CA:canvec"; 041 042 /** 043 * Default preferences for the list of AutomaticCombine tag conflict resolvers. 044 */ 045 private static final Collection<AutomaticCombine> defaultAutomaticTagConflictCombines = Arrays.asList( 046 new AutomaticCombine("tiger:tlid", "US TIGER tlid", false, ":", "Integer"), 047 new AutomaticCombine("tiger:(?!tlid$).*", "US TIGER not tlid", true, ":", "String") 048 ); 049 050 /** 051 * Default preferences for the list of AutomaticChoice tag conflict resolvers. 052 */ 053 private static final Collection<AutomaticChoice> defaultAutomaticTagConflictChoices = Arrays.asList( 054 /* "source" "FR:cadastre" - https://wiki.openstreetmap.org/wiki/FR:WikiProject_France/Cadastre 055 * List of choices for the "source" tag of data exported from the French cadastre, 056 * which ends by the exported year generating many conflicts. 057 * The generated score begins with the year number to select the most recent one. 058 */ 059 new AutomaticChoice(KEY_SOURCE, GRP_FR_CADASTRE, "FR cadastre source, manual value", true, 060 "cadastre", "0"), 061 new AutomaticChoice(KEY_SOURCE, GRP_FR_CADASTRE, "FR cadastre source, initial format", true, 062 "extraction vectorielle v1 cadastre-dgi-fr source : Direction G[eé]n[eé]rale des Imp[oô]ts" 063 + " - Cadas\\. Mise [aà] jour : (2[0-9]{3})", 064 "$1 1"), 065 new AutomaticChoice(KEY_SOURCE, GRP_FR_CADASTRE, "FR cadastre source, last format", true, 066 "(?:cadastre-dgi-fr source : )?Direction G[eé]n[eé]rale des (?:Imp[oô]ts|Finances Publiques)" 067 + " - Cadas(?:tre)?(?:\\.| ;) [Mm]ise [aà] jour : (2[0-9]{3})", 068 "$1 2"), 069 /* "source" "CA:canvec" - https://wiki.openstreetmap.org/wiki/CanVec 070 * List of choices for the "source" tag of data exported from Natural Resources Canada (NRCan) 071 */ 072 new AutomaticChoice(KEY_SOURCE, GRP_CA_CANVEC, "CA canvec source, initial value", true, 073 "CanVec_Import_2009", "00"), 074 new AutomaticChoice(KEY_SOURCE, GRP_CA_CANVEC, "CA canvec source, 4.0/6.0 value", true, 075 "CanVec ([1-9]).0 - NRCan", "0$1"), 076 new AutomaticChoice(KEY_SOURCE, GRP_CA_CANVEC, "CA canvec source, 7.0/8.0 value", true, 077 "NRCan-CanVec-([1-9]).0", "0$1"), 078 new AutomaticChoice(KEY_SOURCE, GRP_CA_CANVEC, "CA canvec source, 10.0/12.0 value", true, 079 "NRCan-CanVec-(1[012]).0", "$1") 080 ); 081 082 private static volatile Collection<AutomaticTagConflictResolver> automaticTagConflictResolvers; 083 084 private TagConflictResolutionUtil() { 085 // no constructor, just static utility methods 086 } 087 088 /** 089 * Normalizes the tags in the tag collection <code>tc</code> before resolving tag conflicts. 090 * 091 * Removes irrelevant tags like "created_by". 092 * 093 * For tags which are not present on at least one of the merged nodes, the empty value "" 094 * is added to the list of values for this tag, but only if there are at least two 095 * primitives with tags, and at least one tagged primitive do not have this tag. 096 * 097 * @param tc the tag collection 098 * @param merged the collection of merged primitives 099 */ 100 public static void normalizeTagCollectionBeforeEditing(TagCollection tc, Collection<? extends OsmPrimitive> merged) { 101 // remove irrelevant tags 102 // 103 for (String key : OsmPrimitive.getDiscardableKeys()) { 104 tc.removeByKey(key); 105 } 106 107 Collection<OsmPrimitive> taggedPrimitives = new ArrayList<>(); 108 for (OsmPrimitive p: merged) { 109 if (p.isTagged()) { 110 taggedPrimitives.add(p); 111 } 112 } 113 if (taggedPrimitives.size() <= 1) 114 return; 115 116 for (String key: tc.getKeys()) { 117 // make sure the empty value is in the tag set if a tag is not present 118 // on all merged nodes 119 // 120 for (OsmPrimitive p: taggedPrimitives) { 121 if (p.get(key) == null) { 122 tc.add(new Tag(key, "")); // add a tag with key and empty value 123 } 124 } 125 } 126 } 127 128 /** 129 * Completes tags in the tag collection <code>tc</code> with the empty value 130 * for each tag. If the empty value is present the tag conflict resolution dialog 131 * will offer an option for removing the tag and not only options for selecting 132 * one of the current values of the tag. 133 * 134 * @param tc the tag collection 135 */ 136 public static void completeTagCollectionForEditing(TagCollection tc) { 137 for (String key: tc.getKeys()) { 138 // make sure the empty value is in the tag set such that we can delete the tag 139 // in the conflict dialog if necessary 140 tc.add(new Tag(key, "")); 141 } 142 } 143 144 /** 145 * Automatically resolve some tag conflicts. 146 * The list of automatic resolution is taken from the preferences. 147 * @param tc the tag collection 148 * @since 11606 149 */ 150 public static void applyAutomaticTagConflictResolution(TagCollection tc) { 151 try { 152 applyAutomaticTagConflictResolution(tc, getAutomaticTagConflictResolvers()); 153 } catch (JosmRuntimeException e) { 154 Logging.log(Logging.LEVEL_ERROR, "Unable to automatically resolve tag conflicts", e); 155 } 156 } 157 158 /** 159 * Get the AutomaticTagConflictResolvers configured in the Preferences or the default ones. 160 * @return the configured AutomaticTagConflictResolvers. 161 * @since 11606 162 */ 163 public static Collection<AutomaticTagConflictResolver> getAutomaticTagConflictResolvers() { 164 if (automaticTagConflictResolvers == null) { 165 Collection<AutomaticCombine> automaticTagConflictCombines = StructUtils.getListOfStructs( 166 Config.getPref(), 167 "automatic-tag-conflict-resolution.combine", 168 defaultAutomaticTagConflictCombines, AutomaticCombine.class); 169 Collection<AutomaticChoiceGroup> automaticTagConflictChoiceGroups = 170 AutomaticChoiceGroup.groupChoices(StructUtils.getListOfStructs( 171 Config.getPref(), 172 "automatic-tag-conflict-resolution.choice", 173 defaultAutomaticTagConflictChoices, AutomaticChoice.class)); 174 // Use a tmp variable to fully construct the collection before setting 175 // the volatile variable automaticTagConflictResolvers. 176 ArrayList<AutomaticTagConflictResolver> tmp = new ArrayList<>(); 177 tmp.addAll(automaticTagConflictCombines); 178 tmp.addAll(automaticTagConflictChoiceGroups); 179 automaticTagConflictResolvers = tmp; 180 } 181 return Collections.unmodifiableCollection(automaticTagConflictResolvers); 182 } 183 184 /** 185 * An automatic tag conflict resolver interface. 186 * @since 11606 187 */ 188 interface AutomaticTagConflictResolver { 189 /** 190 * Check if this resolution apply to the given Tag key. 191 * @param key The Tag key to match. 192 * @return true if this automatic resolution apply to the given Tag key. 193 */ 194 boolean matchesKey(String key); 195 196 /** 197 * Try to resolve a conflict between a set of values for a Tag 198 * @param values the set of conflicting values for the Tag. 199 * @return the resolved value or null if resolution was not possible. 200 */ 201 String resolve(Set<String> values); 202 } 203 204 /** 205 * Automatically resolve some given conflicts using the given resolvers. 206 * @param tc the tag collection. 207 * @param resolvers the list of automatic tag conflict resolvers to apply. 208 * @since 11606 209 */ 210 public static void applyAutomaticTagConflictResolution(TagCollection tc, 211 Collection<AutomaticTagConflictResolver> resolvers) { 212 for (String key: tc.getKeysWithMultipleValues()) { 213 for (AutomaticTagConflictResolver resolver : resolvers) { 214 try { 215 if (resolver.matchesKey(key)) { 216 String result = resolver.resolve(tc.getValues(key)); 217 if (result != null) { 218 tc.setUniqueForKey(key, result); 219 break; 220 } 221 } 222 } catch (PatternSyntaxException e) { 223 // Can happen if a particular resolver has an invalid regular expression pattern 224 // but it should not stop the other automatic tag conflict resolution. 225 Logging.error(e); 226 } 227 } 228 } 229 } 230 231 /** 232 * Preference for automatic tag-conflict resolver by combining the tag values using a separator. 233 * @since 11606 234 */ 235 public static class AutomaticCombine implements AutomaticTagConflictResolver { 236 237 /** The Tag key to match */ 238 @StructEntry public String key; 239 240 /** A free description */ 241 @StructEntry public String description = ""; 242 243 /** If regular expression must be used to match the Tag key or the value. */ 244 @StructEntry public boolean isRegex; 245 246 /** The separator to use to combine the values. */ 247 @StructEntry public String separator = ";"; 248 249 /** If the combined values must be sorted. 250 * Possible values: 251 * <ul> 252 * <li> Integer - Sort using Integer natural order.</li> 253 * <li> String - Sort using String natural order.</li> 254 * <li> * - No ordering.</li> 255 * </ul> 256 */ 257 @StructEntry public String sort; 258 259 /** Default constructor. */ 260 public AutomaticCombine() { 261 // needed for instantiation from Preferences 262 } 263 264 /** Instantiate an automatic tag-conflict resolver which combining the values using a separator. 265 * @param key The Tag key to match. 266 * @param description A free description. 267 * @param isRegex If regular expression must be used to match the Tag key or the value. 268 * @param separator The separator to use to combine the values. 269 * @param sort If the combined values must be sorted. 270 */ 271 public AutomaticCombine(String key, String description, boolean isRegex, String separator, String sort) { 272 this.key = key; 273 this.description = description; 274 this.isRegex = isRegex; 275 this.separator = separator; 276 this.sort = sort; 277 } 278 279 @Override 280 public boolean matchesKey(String k) { 281 if (isRegex) { 282 return Pattern.matches(this.key, k); 283 } else { 284 return this.key.equals(k); 285 } 286 } 287 288 Set<String> instantiateSortedSet() { 289 if ("String".equals(sort)) { 290 return new TreeSet<>(); 291 } else if ("Integer".equals(sort)) { 292 return new TreeSet<>((String v1, String v2) -> Long.valueOf(v1).compareTo(Long.valueOf(v2))); 293 } else { 294 return new LinkedHashSet<>(); 295 } 296 } 297 298 @Override 299 public String resolve(Set<String> values) { 300 Set<String> results = instantiateSortedSet(); 301 for (String value: values) { 302 for (String part: value.split(Pattern.quote(separator))) { 303 results.add(part); 304 } 305 } 306 return String.join(separator, results); 307 } 308 309 @Override 310 public String toString() { 311 return AutomaticCombine.class.getSimpleName() 312 + "(key='" + key + "', description='" + description + "', isRegex=" 313 + isRegex + ", separator='" + separator + "', sort='" + sort + "')"; 314 } 315 } 316 317 /** 318 * Preference for a particular choice from a group for automatic tag conflict resolution. 319 * {@code AutomaticChoice}s are grouped into {@link AutomaticChoiceGroup}. 320 * @since 11606 321 */ 322 public static class AutomaticChoice { 323 324 /** The Tag key to match. */ 325 @StructEntry public String key; 326 327 /** The name of the {link AutomaticChoice group} this choice belongs to. */ 328 @StructEntry public String group; 329 330 /** A free description. */ 331 @StructEntry public String description = ""; 332 333 /** If regular expression must be used to match the Tag key or the value. */ 334 @StructEntry public boolean isRegex; 335 336 /** The Tag value to match. */ 337 @StructEntry public String value; 338 339 /** 340 * The score to give to this choice in order to choose the best value 341 * Natural String ordering is used to identify the best score. 342 */ 343 @StructEntry public String score; 344 345 /** Default constructor. */ 346 public AutomaticChoice() { 347 // needed for instantiation from Preferences 348 } 349 350 /** 351 * Instantiate a particular choice from a group for automatic tag conflict resolution. 352 * @param key The Tag key to match. 353 * @param group The name of the {link AutomaticChoice group} this choice belongs to. 354 * @param description A free description. 355 * @param isRegex If regular expression must be used to match the Tag key or the value. 356 * @param value The Tag value to match. 357 * @param score The score to give to this choice in order to choose the best value. 358 */ 359 public AutomaticChoice(String key, String group, String description, boolean isRegex, String value, String score) { 360 this.key = key; 361 this.group = group; 362 this.description = description; 363 this.isRegex = isRegex; 364 this.value = value; 365 this.score = score; 366 } 367 368 /** 369 * Check if this choice match the given Tag value. 370 * @param v the Tag value to match. 371 * @return true if this choice correspond to the given tag value. 372 */ 373 public boolean matchesValue(String v) { 374 if (isRegex) { 375 return Pattern.matches(this.value, v); 376 } else { 377 return this.value.equals(v); 378 } 379 } 380 381 /** 382 * Return the score associated to this choice for the given Tag value. 383 * For the result to be valid the given tag value must {@link #matchesValue(String) match} this choice. 384 * @param v the Tag value of which to get the score. 385 * @return the score associated to the given Tag value. 386 * @throws PatternSyntaxException if the regular expression syntax is invalid 387 */ 388 public String computeScoreFromValue(String v) { 389 if (isRegex) { 390 return v.replaceAll("^" + this.value + "$", this.score); 391 } else { 392 return this.score; 393 } 394 } 395 396 @Override 397 public String toString() { 398 return AutomaticChoice.class.getSimpleName() 399 + "(key='" + key + "', group='" + group + "', description='" + description 400 + "', isRegex=" + isRegex + ", value='" + value + "', score='" + score + "')"; 401 } 402 } 403 404 /** 405 * Preference for an automatic tag conflict resolver which choose from 406 * a group of possible {@link AutomaticChoice choice} values. 407 * @since 11606 408 */ 409 public static class AutomaticChoiceGroup implements AutomaticTagConflictResolver { 410 411 /** The Tag key to match. */ 412 @StructEntry public String key; 413 414 /** The name of the group. */ 415 final String group; 416 417 /** If regular expression must be used to match the Tag key. */ 418 @StructEntry public boolean isRegex; 419 420 /** The list of choice to choose from. */ 421 final List<AutomaticChoice> choices; 422 423 /** Instantiate an automatic tag conflict resolver which choose from 424 * a given list of {@link AutomaticChoice choice} values. 425 * 426 * @param key The Tag key to match. 427 * @param group The name of the group. 428 * @param isRegex If regular expression must be used to match the Tag key. 429 * @param choices The list of choice to choose from. 430 */ 431 public AutomaticChoiceGroup(String key, String group, boolean isRegex, List<AutomaticChoice> choices) { 432 this.key = key; 433 this.group = group; 434 this.isRegex = isRegex; 435 this.choices = choices; 436 } 437 438 /** 439 * Group a given list of {@link AutomaticChoice} by the Tag key and the choice group name. 440 * @param choices the list of {@link AutomaticChoice choices} to group. 441 * @return the resulting list of group. 442 */ 443 public static Collection<AutomaticChoiceGroup> groupChoices(Collection<AutomaticChoice> choices) { 444 HashMap<Pair<String, String>, AutomaticChoiceGroup> results = new HashMap<>(); 445 for (AutomaticChoice choice: choices) { 446 Pair<String, String> id = new Pair<>(choice.key, choice.group); 447 AutomaticChoiceGroup group = results.get(id); 448 if (group == null) { 449 boolean isRegex = choice.isRegex && !Pattern.quote(choice.key).equals(choice.key); 450 group = new AutomaticChoiceGroup(choice.key, choice.group, isRegex, new ArrayList<>()); 451 results.put(id, group); 452 } 453 group.choices.add(choice); 454 } 455 return results.values(); 456 } 457 458 @Override 459 public boolean matchesKey(String k) { 460 if (isRegex) { 461 return Pattern.matches(this.key, k); 462 } else { 463 return this.key.equals(k); 464 } 465 } 466 467 @Override 468 public String resolve(Set<String> values) { 469 String bestScore = ""; 470 String bestValue = ""; 471 for (String value : values) { 472 String score = null; 473 for (AutomaticChoice choice : choices) { 474 if (choice.matchesValue(value)) { 475 score = choice.computeScoreFromValue(value); 476 } 477 } 478 if (score == null) { 479 // This value is not matched in this group 480 // so we can not choose from this group for this key. 481 return null; 482 } 483 if (score.compareTo(bestScore) >= 0) { 484 bestScore = score; 485 bestValue = value; 486 } 487 } 488 return bestValue; 489 } 490 491 @Override 492 public String toString() { 493 Collection<String> stringChoices = choices.stream().map(AutomaticChoice::toString).collect(Collectors.toCollection(ArrayList::new)); 494 return AutomaticChoiceGroup.class.getSimpleName() + "(key='" + key + "', group='" + group + 495 "', isRegex=" + isRegex + ", choices=(\n " + String.join(",\n ", stringChoices) + "))"; 496 } 497 } 498}