001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.corrector;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.util.ArrayList;
007import java.util.Collection;
008import java.util.HashMap;
009import java.util.List;
010import java.util.Locale;
011import java.util.Map;
012import java.util.function.Function;
013import java.util.regex.Matcher;
014import java.util.regex.Pattern;
015
016import org.openstreetmap.josm.command.Command;
017import org.openstreetmap.josm.data.correction.RoleCorrection;
018import org.openstreetmap.josm.data.correction.TagCorrection;
019import org.openstreetmap.josm.data.osm.AbstractPrimitive;
020import org.openstreetmap.josm.data.osm.Node;
021import org.openstreetmap.josm.data.osm.OsmPrimitive;
022import org.openstreetmap.josm.data.osm.OsmUtils;
023import org.openstreetmap.josm.data.osm.Relation;
024import org.openstreetmap.josm.data.osm.RelationMember;
025import org.openstreetmap.josm.data.osm.Tag;
026import org.openstreetmap.josm.data.osm.TagCollection;
027import org.openstreetmap.josm.data.osm.Tagged;
028import org.openstreetmap.josm.data.osm.Way;
029import org.openstreetmap.josm.tools.UserCancelException;
030
031/**
032 * A ReverseWayTagCorrector handles necessary corrections of tags
033 * when a way is reversed. E.g. oneway=yes needs to be changed
034 * to oneway=-1 and vice versa.
035 *
036 * The Corrector offers the automatic resolution in an dialog
037 * for the user to confirm.
038 */
039public class ReverseWayTagCorrector extends TagCorrector<Way> {
040
041    private static final String SEPARATOR = "[:_]";
042
043    private static Pattern getPatternFor(String s) {
044        return getPatternFor(s, false);
045    }
046
047    private static Pattern getPatternFor(String s, boolean exactMatch) {
048        if (exactMatch) {
049            return Pattern.compile("(^)(" + s + ")($)");
050        } else {
051            return Pattern.compile("(^|.*" + SEPARATOR + ")(" + s + ")(" + SEPARATOR + ".*|$)",
052                    Pattern.CASE_INSENSITIVE);
053        }
054    }
055
056    private static final Collection<Pattern> IGNORED_KEYS = new ArrayList<>();
057    static {
058        for (String s : AbstractPrimitive.getUninterestingKeys()) {
059            IGNORED_KEYS.add(getPatternFor(s));
060        }
061        for (String s : new String[]{"name", "ref", "tiger:county"}) {
062            IGNORED_KEYS.add(getPatternFor(s, false));
063        }
064        for (String s : new String[]{"tiger:county", "turn:lanes", "change:lanes", "placement"}) {
065            IGNORED_KEYS.add(getPatternFor(s, true));
066        }
067    }
068
069    private interface IStringSwitcher extends Function<String, String> {
070
071        static IStringSwitcher combined(IStringSwitcher... switchers) {
072            return key -> {
073                for (IStringSwitcher switcher : switchers) {
074                    final String newKey = switcher.apply(key);
075                    if (!key.equals(newKey)) {
076                        return newKey;
077                    }
078                }
079                return key;
080            };
081        }
082    }
083
084    private static class StringSwitcher implements IStringSwitcher {
085
086        private final String a;
087        private final String b;
088        private final Pattern pattern;
089
090        StringSwitcher(String a, String b) {
091            this.a = a;
092            this.b = b;
093            this.pattern = getPatternFor(a + '|' + b);
094        }
095
096        @Override
097        public String apply(String text) {
098            Matcher m = pattern.matcher(text);
099
100            if (m.lookingAt()) {
101                String leftRight = m.group(2).toLowerCase(Locale.ENGLISH);
102
103                StringBuilder result = new StringBuilder();
104                result.append(text.substring(0, m.start(2)))
105                      .append(leftRight.equals(a) ? b : a)
106                      .append(text.substring(m.end(2)));
107
108                return result.toString();
109            }
110            return text;
111        }
112    }
113
114    /**
115     * Reverses a given tag.
116     * @since 5787
117     */
118    public static final class TagSwitcher {
119
120        private TagSwitcher() {
121            // Hide implicit public constructor for utility class
122        }
123
124        /**
125         * Reverses a given tag.
126         * @param tag The tag to reverse
127         * @return The reversed tag (is equal to <code>tag</code> if no change is needed)
128         */
129        public static Tag apply(final Tag tag) {
130            return apply(tag.getKey(), tag.getValue());
131        }
132
133        /**
134         * Reverses a given tag (key=value).
135         * @param key The tag key
136         * @param value The tag value
137         * @return The reversed tag (is equal to <code>key=value</code> if no change is needed)
138         */
139        public static Tag apply(final String key, final String value) {
140            String newKey = key;
141            String newValue = value;
142
143            if (key.startsWith("oneway") || key.endsWith("oneway")) {
144                if (OsmUtils.isReversed(value)) {
145                    newValue = OsmUtils.TRUE_VALUE;
146                } else if (OsmUtils.isTrue(value)) {
147                    newValue = OsmUtils.REVERSE_VALUE;
148                }
149                newKey = COMBINED_SWITCHERS.apply(key);
150            } else if (key.startsWith("incline") || key.endsWith("incline")) {
151                newValue = UP_DOWN.apply(value);
152                if (newValue.equals(value)) {
153                    newValue = invertNumber(value);
154                }
155            } else if (key.startsWith("direction") || key.endsWith("direction")) {
156                newValue = COMBINED_SWITCHERS.apply(value);
157            } else if (key.endsWith(":forward") || key.endsWith(":backward")) {
158                // Change key but not left/right value (fix #8518)
159                newKey = FORWARD_BACKWARD.apply(key);
160            } else if (!ignoreKeyForCorrection(key)) {
161                newKey = COMBINED_SWITCHERS.apply(key);
162                newValue = COMBINED_SWITCHERS.apply(value);
163            }
164            return new Tag(newKey, newValue);
165        }
166    }
167
168    private static final StringSwitcher FORWARD_BACKWARD = new StringSwitcher("forward", "backward");
169    private static final StringSwitcher UP_DOWN = new StringSwitcher("up", "down");
170    private static final IStringSwitcher COMBINED_SWITCHERS = IStringSwitcher.combined(
171        new StringSwitcher("left", "right"),
172        new StringSwitcher("forwards", "backwards"),
173        new StringSwitcher("east", "west"),
174        new StringSwitcher("north", "south"),
175        FORWARD_BACKWARD, UP_DOWN
176    );
177
178    /**
179     * Tests whether way can be reversed without semantic change, i.e., whether tags have to be changed.
180     * Looks for keys like oneway, oneway:bicycle, cycleway:right:oneway, left/right.
181     * @param way way to test
182     * @return false if tags should be changed to keep semantic, true otherwise.
183     */
184    public static boolean isReversible(Way way) {
185        for (Tag tag : TagCollection.from(way)) {
186            if (!tag.equals(TagSwitcher.apply(tag))) {
187                return false;
188            }
189        }
190        return true;
191    }
192
193    /**
194     * Returns the subset of irreversible ways.
195     * @param ways all ways
196     * @return the subset of irreversible ways
197     * @see #isReversible(Way)
198     */
199    public static List<Way> irreversibleWays(List<Way> ways) {
200        List<Way> newWays = new ArrayList<>(ways);
201        for (Way way : ways) {
202            if (isReversible(way)) {
203                newWays.remove(way);
204            }
205        }
206        return newWays;
207    }
208
209    /**
210     * Inverts sign of a numeric value.
211     * @param value numeric value
212     * @return opposite numeric value
213     */
214    public static String invertNumber(String value) {
215        Pattern pattern = Pattern.compile("^([+-]?)(\\d.*)$", Pattern.CASE_INSENSITIVE);
216        Matcher matcher = pattern.matcher(value);
217        if (!matcher.matches()) return value;
218        String sign = matcher.group(1);
219        String rest = matcher.group(2);
220        sign = "-".equals(sign) ? "" : "-";
221        return sign + rest;
222    }
223
224    static List<TagCorrection> getTagCorrections(Tagged way) {
225        List<TagCorrection> tagCorrections = new ArrayList<>();
226        for (Map.Entry<String, String> entry : way.getKeys().entrySet()) {
227            final String key = entry.getKey();
228            final String value = entry.getValue();
229            Tag newTag = TagSwitcher.apply(key, value);
230            String newKey = newTag.getKey();
231            String newValue = newTag.getValue();
232
233            boolean needsCorrection = !key.equals(newKey);
234            if (way.get(newKey) != null && way.get(newKey).equals(newValue)) {
235                needsCorrection = false;
236            }
237            if (!value.equals(newValue)) {
238                needsCorrection = true;
239            }
240
241            if (needsCorrection) {
242                tagCorrections.add(new TagCorrection(key, value, newKey, newValue));
243            }
244        }
245        return tagCorrections;
246    }
247
248    static List<RoleCorrection> getRoleCorrections(Way oldway) {
249        List<RoleCorrection> roleCorrections = new ArrayList<>();
250
251        Collection<OsmPrimitive> referrers = oldway.getReferrers();
252        for (OsmPrimitive referrer: referrers) {
253            if (!(referrer instanceof Relation)) {
254                continue;
255            }
256            Relation relation = (Relation) referrer;
257            int position = 0;
258            for (RelationMember member : relation.getMembers()) {
259                if (!member.getMember().hasEqualSemanticAttributes(oldway)
260                        || !member.hasRole()) {
261                    position++;
262                    continue;
263                }
264
265                final String newRole = COMBINED_SWITCHERS.apply(member.getRole());
266                if (!member.getRole().equals(newRole)) {
267                    roleCorrections.add(new RoleCorrection(relation, position, member, newRole));
268                }
269
270                position++;
271            }
272        }
273        return roleCorrections;
274    }
275
276    static Map<OsmPrimitive, List<TagCorrection>> getTagCorrectionsMap(Way way) {
277        Map<OsmPrimitive, List<TagCorrection>> tagCorrectionsMap = new HashMap<>();
278        List<TagCorrection> tagCorrections = getTagCorrections(way);
279        if (!tagCorrections.isEmpty()) {
280            tagCorrectionsMap.put(way, tagCorrections);
281        }
282        for (Node node : way.getNodes()) {
283            final List<TagCorrection> corrections = getTagCorrections(node);
284            if (!corrections.isEmpty()) {
285                tagCorrectionsMap.put(node, corrections);
286            }
287        }
288        return tagCorrectionsMap;
289    }
290
291    @Override
292    public Collection<Command> execute(Way oldway, Way way) throws UserCancelException {
293        Map<OsmPrimitive, List<TagCorrection>> tagCorrectionsMap = getTagCorrectionsMap(way);
294
295        Map<OsmPrimitive, List<RoleCorrection>> roleCorrectionMap = new HashMap<>();
296        List<RoleCorrection> roleCorrections = getRoleCorrections(oldway);
297        if (!roleCorrections.isEmpty()) {
298            roleCorrectionMap.put(way, roleCorrections);
299        }
300
301        return applyCorrections(oldway.getDataSet(), tagCorrectionsMap, roleCorrectionMap,
302                tr("When reversing this way, the following changes are suggested in order to maintain data consistency."));
303    }
304
305    private static boolean ignoreKeyForCorrection(String key) {
306        for (Pattern ignoredKey : IGNORED_KEYS) {
307            if (ignoredKey.matcher(key).matches()) {
308                return true;
309            }
310        }
311        return false;
312    }
313}