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