cd josm && svn diff core plugins
Index: core/src/org/openstreetmap/josm/gui/conflict/tags/CombinePrimitiveResolverDialog.java
===================================================================
--- core/src/org/openstreetmap/josm/gui/conflict/tags/CombinePrimitiveResolverDialog.java	(révision 11568)
+++ core/src/org/openstreetmap/josm/gui/conflict/tags/CombinePrimitiveResolverDialog.java	(copie de travail)
@@ -475,7 +475,7 @@
         CheckParameterUtil.ensureParameterNotNull(targetPrimitives, "targetPrimitives");
 
         final TagCollection completeWayTags = new TagCollection(tagsOfPrimitives);
-        TagConflictResolutionUtil.combineTigerTags(completeWayTags);
+        TagConflictResolutionUtil.applyAutomaticTagConflictResolution(completeWayTags);
         TagConflictResolutionUtil.normalizeTagCollectionBeforeEditing(completeWayTags, primitives);
         final TagCollection tagsToEdit = new TagCollection(completeWayTags);
         TagConflictResolutionUtil.completeTagCollectionForEditing(tagsToEdit);
Index: core/src/org/openstreetmap/josm/gui/conflict/tags/TagConflictResolutionUtil.java
===================================================================
--- core/src/org/openstreetmap/josm/gui/conflict/tags/TagConflictResolutionUtil.java	(révision 11568)
+++ core/src/org/openstreetmap/josm/gui/conflict/tags/TagConflictResolutionUtil.java	(copie de travail)
@@ -2,12 +2,22 @@
 package org.openstreetmap.josm.gui.conflict.tags;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
 
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.data.Preferences.pref;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
 import org.openstreetmap.josm.data.osm.Tag;
 import org.openstreetmap.josm.data.osm.TagCollection;
-import org.openstreetmap.josm.data.osm.TigerUtils;
+import org.openstreetmap.josm.tools.Pair;
 
 /**
  * Collection of utility methods for tag conflict resolution
@@ -59,18 +69,6 @@
         }
     }
 
-    /**
-     * Combines tags from TIGER data
-     *
-     * @param tc the tag collection
-     */
-    public static void combineTigerTags(TagCollection tc) {
-        for (String key: tc.getKeys()) {
-            if (TigerUtils.isTigerTag(key)) {
-                tc.setUniqueForKey(key, TigerUtils.combineTags(tc.getValues(key)));
-            }
-        }
-    }
 
     /**
      * Completes tags in the tag collection <code>tc</code> with the empty value
@@ -88,4 +86,389 @@
             tc.add(new Tag(key, ""));
         }
     }
+
+
+    /**
+     * Automatically resolve some tag conflicts.
+     * The list of automatic resolution is taken from the preferences.
+     * @param tc the tag collection
+     */
+    public static void applyAutomaticTagConflictResolution(TagCollection tc) {
+        applyAutomaticTagConflictResolution(tc, getAutomaticTagConflictResolvers());
+    }
+
+
+    /**
+     * Default preferences for the list of AutomaticCombine tag conflict resolvers.
+     */
+    public static final Collection<AutomaticCombine> defaultAutomaticTagConflictCombines = Arrays.asList(
+            new AutomaticCombine("tiger:tlid", "US TIGER tlid", false, ":", "Integer"),
+            new AutomaticCombine("tiger:(?!tlid$).*", "US TIGER not tlid", true, ":", "String")
+    );
+
+
+    /**
+     * Default preferences for the list of AutomaticChoice tag conflict resolvers.
+     */
+    public static final Collection<AutomaticChoice> defaultAutomaticTagConflictChoices = Arrays.asList(
+            /* "source" "FR:cadastre"
+             * List of choices for the "source" tag of data exported from the French cadastre,
+             * which ends by the exported year generating many conflicts.
+             * The generated score begins with the year number to select the most recent one.
+             */
+            new AutomaticChoice("source", "FR:cadastre", "FR cadastre source, manual value", true,
+                    "cadastre", "0"),
+            new AutomaticChoice("source", "FR:cadastre", "FR cadastre source, initial format", true,
+                    "extraction vectorielle v1 cadastre-dgi-fr source : Direction G[eé]n[eé]rale des Imp[ôo]ts"
+                    + " - Cadas\\. Mise [àa] jour : ([0-9]{4})",
+                    "$1 1"),
+            new AutomaticChoice("source", "FR:cadastre", "FR cadastre source, old format", true,
+                    "cadastre-dgi-fr source : Direction G[eé]n[eé]rale des Imp[ôo]ts - Cadastre\\. Mise [àa] jour : ([0-9]{4})",
+                    "$1 2"),
+            new AutomaticChoice("source", "FR:cadastre", "FR cadastre source, last format", true,
+                    "cadastre-dgi-fr source : Direction G[eé]n[ée]rale des Finances Publiques - Cadastre\\. Mise [aà] jour : ([0-9]{4})",
+                    "$1 3")
+    );
+
+
+    /**
+     * Get the AutomaticTagConflictResolvers configured in the Preferences or the default ones.
+     * @return the configured AutomaticTagConflictResolvers.
+     */
+    public static Collection<AutomaticTagConflictResolver> getAutomaticTagConflictResolvers() {
+        if (automaticTagConflictResolvers == null) {
+            Collection<AutomaticCombine> automaticTagConflictCombines =
+                    Main.pref.getListOfStructs(
+                            "automatic-tag-conflict-resolution.combine",
+                            defaultAutomaticTagConflictCombines, AutomaticCombine.class);
+            Collection<AutomaticChoiceGroup> automaticTagConflictChoiceGroups =
+                    AutomaticChoiceGroup.groupChoices(Main.pref.getListOfStructs(
+                            "automatic-tag-conflict-resolution.choice",
+                            defaultAutomaticTagConflictChoices, AutomaticChoice.class));
+            // Use a tmp variable to fully construct the collection before setting
+            // the volatile variable automaticTagConflictResolvers.
+            ArrayList<AutomaticTagConflictResolver> tmp = new ArrayList<>();
+            tmp.addAll(automaticTagConflictCombines);
+            tmp.addAll(automaticTagConflictChoiceGroups);
+            automaticTagConflictResolvers = tmp;
+        }
+        return automaticTagConflictResolvers;
+    }
+
+    private static volatile Collection<AutomaticTagConflictResolver> automaticTagConflictResolvers;
+
+
+    /**
+     * An automatic tag conflict resolver interface.
+     */
+    interface AutomaticTagConflictResolver {
+        /**
+         * Check if this resolution apply to the given Tag key.
+         * @param key The Tag key to match.
+         * @return true if this automatic resolution apply to the given Tag key.
+         */
+        boolean matchesKey(String key);
+
+        /**
+         * Try to resolve a conflict between a set of values for a Tag
+         * @param values the set of conflicting values for the Tag.
+         * @return the resolved value or null if resolution was not possible.
+         */
+        String resolve(Set<String> values);
+    }
+
+
+    /**
+     * Automatically resolve some given conflicts using the given resolvers.
+     * @param tc the tag collection.
+     * @param resolvers the list of automatic tag conflict resolvers to apply.
+     */
+    public static void applyAutomaticTagConflictResolution(TagCollection tc,
+            Collection<AutomaticTagConflictResolver> resolvers) {
+        for (String key: tc.getKeysWithMultipleValues()) {
+            for (AutomaticTagConflictResolver resolver: resolvers) {
+                try {
+                    if (resolver.matchesKey(key)) {
+                        String result = resolver.resolve(tc.getValues(key));
+                        if (result != null) {
+                            tc.setUniqueForKey(key, result);
+                            break;
+                        }
+                    }
+                } catch (RuntimeException e) {
+                    // Can happen for instance if a particular resolver has an invalid regular
+                    // expression pattern (java.util.regex.PatternSyntaxException)
+                    // but it should not stop the other automatic tag conflict resolution.
+                    Main.error(e);
+                }
+            }
+        }
+    }
+
+
+    /**
+     * Preference for automatic tag-conflict resolver by combining the tag values
+     * using a separator.
+     */
+    public static class AutomaticCombine implements AutomaticTagConflictResolver {
+
+        /** The Tag key to match */
+        @pref public String key;
+
+        /** A free description */
+        @pref public String description = "";
+
+        /** If regular expression must be used to match the Tag key or the value. */
+        @pref public boolean isRegex = false;
+
+        /** The separator to use to combine the values. */
+        @pref public String separator = ";";
+
+        /** If the combined values must be sorted.
+         * Possible values:
+         * <ul>
+         * <li> Integer - Sort using Integer natural order.</li>
+         * <li> String - Sort using String natural order.</li>
+         * <li> * - No ordering.</li>
+         * </ul>
+         */
+        @pref public String sort;
+
+        /** Default constructor needed for instantiation from Preferences */
+        public AutomaticCombine() {}
+
+        /** Instantiate an automatic tag-conflict resolver which combining the values using a separator.
+         * @param key The Tag key to match.
+         * @param description A free description.
+         * @param isRegex If regular expression must be used to match the Tag key or the value.
+         * @param separator The separator to use to combine the values.
+         * @param sort If the combined values must be sorted.
+         */
+        public AutomaticCombine(String key, String description, boolean isRegex, String separator, String sort) {
+            this.key = key;
+            this.description = description;
+            this.isRegex = isRegex;
+            this.separator = separator;
+            this.sort = sort;
+        }
+
+        @Override
+        public boolean matchesKey(String k) {
+            if (isRegex) {
+                return Pattern.matches(this.key, k);
+            } else {
+                return this.key.equals(k);
+            }
+        }
+
+        Set<String> instantiateSortedSet() {
+            if ("String".equals(sort)) {
+                return new TreeSet<>();
+            } else if ("Integer".equals(sort)) {
+                return new TreeSet<>((String v1, String v2) -> Long.valueOf(v1).compareTo(Long.valueOf(v2)));
+            } else {
+                return new LinkedHashSet<>();
+            }
+        }
+
+        @Override
+        public String resolve(Set<String> values) {
+            Set<String> results = instantiateSortedSet();
+            for (String value: values) {
+                for (String part: value.split(Pattern.quote(separator))) {
+                    results.add(part);
+                }
+            }
+            return String.join(separator, results);
+        }
+
+        @Override
+        public String toString() {
+            return AutomaticCombine.class.getSimpleName()
+                    + "(key='" + key + "', description='" + description + "', isRegex="
+                    + isRegex + ", separator='" + separator + "', sort='" + sort + "')";
+        }
+    }
+
+    /**
+     * Preference for a particular choice from a group for automatic tag conflict resolution.
+     * AutomaticChoice are grouped into {@link AutomaticChoiceGroup}.
+     */
+    public static class AutomaticChoice {
+
+        /** The Tag key to match. */
+        @pref public String key;
+
+        /** The name of the {link AutomaticChoice group} this choice belongs to. */
+        @pref public String group;
+
+        /** A free description. */
+        @pref public String description = "";
+
+        /** If regular expression must be used to match the Tag key or the value. */
+        @pref public boolean isRegex = false;
+
+        /** The Tag value to match. */
+        @pref public String value;
+
+        /**
+         * The score to give to this choice in order to choose the best value
+         * Natural String ordering is used to identify the best score.
+         */
+        @pref public String score;
+
+        /** Default constructor needed for instantiation from Preferences */
+        public AutomaticChoice() {}
+
+        /**
+         * Instantiate a particular choice from a group for automatic tag conflict resolution.
+         * @param key The Tag key to match.
+         * @param group The name of the {link AutomaticChoice group} this choice belongs to.
+         * @param description A free description.
+         * @param isRegex If regular expression must be used to match the Tag key or the value.
+         * @param value The Tag value to match.
+         * @param score The score to give to this choice in order to choose the best value.
+         */
+        public AutomaticChoice(String key, String group, String description, boolean isRegex, String value, String score) {
+            this.key = key;
+            this.group = group;
+            this.description = description;
+            this.isRegex = isRegex;
+            this.value = value;
+            this.score = score;
+        }
+
+        /**
+         * Check if this choice match the given Tag value.
+         * @param v the Tag value to match.
+         * @return true if this choice correspond to the given tag value.
+         */
+        public boolean matchesValue(String v) {
+            if (isRegex) {
+                return Pattern.matches(this.value, v);
+            } else {
+                return this.value.equals(v);
+            }
+        }
+
+        /**
+         * Return the score associated to this choice for the given Tag value.
+         * For the result to be valid the given tag value must {@link #matchesValue(String) match} this choice.
+         * @param v the Tag value of which to get the score.
+         * @return the score associated to the given Tag value.
+         */
+        public String computeScoreFromValue(String v) {
+            if (isRegex) {
+                return v.replaceAll("^" + this.value + "$", this.score);
+            } else {
+                return this.score;
+            }
+        }
+
+        @Override
+        public String toString() {
+            return AutomaticChoice.class.getSimpleName()
+                    + "(key='" + key + "', group='" + group + "', description='" + description
+                    + "', isRegex=" + isRegex + ", value='" + value + "', score='" + score + "')";
+        }
+    }
+
+
+    /**
+     * Preference for an automatic tag conflict resolver which choose from
+     * a group of possible {@link AutomaticChoice choice} values.
+     */
+    public static class AutomaticChoiceGroup implements AutomaticTagConflictResolver {
+
+        /** The Tag key to match. */
+        @pref public String key;
+
+        /** The name of the group. */
+        public String group;
+
+        /** If regular expression must be used to match the Tag key. */
+        @pref public boolean isRegex = false;
+
+        /** The list of choice to choose from. */
+        public List<AutomaticChoice> choices;
+
+        /** Instantiate an automatic tag conflict resolver which choose from
+         * a given list of {@link AutomaticChoice choice} values.
+         *
+         * @param key The Tag key to match.
+         * @param group The name of the group.
+         * @param isRegex If regular expression must be used to match the Tag key.
+         * @param choices The list of choice to choose from.
+         */
+        public AutomaticChoiceGroup(String key, String group, boolean isRegex, List<AutomaticChoice> choices) {
+            this.key = key;
+            this.group = group;
+            this.isRegex = isRegex;
+            this.choices = choices;
+        }
+
+        /**
+         * Group a given list of {@link AutomaticChoice} by the Tag key and the choice group name.
+         * @param choices the list of {@link AutomaticChoice choices} to group.
+         * @return the resulting list of group.
+         */
+        public static Collection<AutomaticChoiceGroup> groupChoices(Collection<AutomaticChoice> choices) {
+            HashMap<Pair<String, String>, AutomaticChoiceGroup> results = new HashMap<>();
+            for (AutomaticChoice choice: choices) {
+                Pair<String, String> id = new Pair<>(choice.key, choice.group);
+                AutomaticChoiceGroup group = results.get(id);
+                if (group == null) {
+                    boolean isRegex = choice.isRegex && !Pattern.quote(choice.key).equals(choice.key);
+                    group = new AutomaticChoiceGroup(choice.key, choice.group, isRegex, new ArrayList<>());
+                    results.put(id, group);
+                }
+                group.choices.add(choice);
+            }
+            return results.values();
+        }
+
+        @Override
+        public boolean matchesKey(String k) {
+            if (isRegex) {
+                return Pattern.matches(this.key, k);
+            } else {
+                return this.key.equals(k);
+            }
+        }
+
+        @Override
+        public String resolve(Set<String> values) {
+            String bestScore = "";
+            String bestValue = "";
+            for (String value: values) {
+                String score = null;
+                for (AutomaticChoice choice: choices) {
+                    if (choice.matchesValue(value)) {
+                        score = choice.computeScoreFromValue(value);
+                    }
+                }
+                if (score == null) {
+                    // This value is not matched in this group
+                    // so we can not choose from this group for this key.
+                    return null;
+                }
+                if (score.compareTo(bestScore) >= 0) {
+                    bestScore = score;
+                    bestValue = value;
+                }
+            }
+            return bestValue;
+        }
+
+        @Override
+        public String toString() {
+            Collection<String> stringChoices = choices.stream().map(AutomaticChoice::toString).collect(Collectors.toCollection(ArrayList::new));
+            return AutomaticChoiceGroup.class.getSimpleName()
+                    + "(key='" + key + "', group='" + group +
+                    "', isRegex=" + isRegex + ", choices=(\n  "
+                    + String.join(",\n  ", stringChoices) + "))";
+        }
+
+    }
+
 }
Index: plugins/merge-overlap/src/mergeoverlap/MergeOverlapAction.java
===================================================================
--- plugins/merge-overlap/src/mergeoverlap/MergeOverlapAction.java	(révision 33143)
+++ plugins/merge-overlap/src/mergeoverlap/MergeOverlapAction.java	(copie de travail)
@@ -1,6 +1,6 @@
 package mergeoverlap;
 
-import static org.openstreetmap.josm.gui.conflict.tags.TagConflictResolutionUtil.combineTigerTags;
+import static org.openstreetmap.josm.gui.conflict.tags.TagConflictResolutionUtil.applyAutomaticTagConflictResolution;
 import static org.openstreetmap.josm.gui.conflict.tags.TagConflictResolutionUtil.completeTagCollectionForEditing;
 import static org.openstreetmap.josm.gui.conflict.tags.TagConflictResolutionUtil.normalizeTagCollectionBeforeEditing;
 import static org.openstreetmap.josm.tools.I18n.tr;
@@ -543,7 +543,7 @@
         modifiedTargetWay.setNodes(path);
 
         TagCollection completeWayTags = new TagCollection(wayTags);
-        combineTigerTags(completeWayTags);
+        applyAutomaticTagConflictResolution(completeWayTags);
         normalizeTagCollectionBeforeEditing(completeWayTags, ways);
         TagCollection tagsToEdit = new TagCollection(completeWayTags);
         completeTagCollectionForEditing(tagsToEdit);
Index: plugins/opendata/src/org/openstreetmap/josm/plugins/opendata/core/datasets/WayCombiner.java
===================================================================
--- plugins/opendata/src/org/openstreetmap/josm/plugins/opendata/core/datasets/WayCombiner.java	(révision 33143)
+++ plugins/opendata/src/org/openstreetmap/josm/plugins/opendata/core/datasets/WayCombiner.java	(copie de travail)
@@ -1,7 +1,7 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.plugins.opendata.core.datasets;
 
-import static org.openstreetmap.josm.gui.conflict.tags.TagConflictResolutionUtil.combineTigerTags;
+import static org.openstreetmap.josm.gui.conflict.tags.TagConflictResolutionUtil.applyAutomaticTagConflictResolution;
 import static org.openstreetmap.josm.gui.conflict.tags.TagConflictResolutionUtil.completeTagCollectionForEditing;
 import static org.openstreetmap.josm.gui.conflict.tags.TagConflictResolutionUtil.normalizeTagCollectionBeforeEditing;
 
@@ -125,7 +125,7 @@
         modifiedTargetWay.setNodes(path);
 
         TagCollection completeWayTags = new TagCollection(wayTags);
-        combineTigerTags(completeWayTags);
+        applyAutomaticTagConflictResolution(completeWayTags);
         normalizeTagCollectionBeforeEditing(completeWayTags, ways);
         TagCollection tagsToEdit = new TagCollection(completeWayTags);
         completeTagCollectionForEditing(tagsToEdit);
