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