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}