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