001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.actions.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        FORWARD_BACKWARD, UP_DOWN
174    );
175
176    /**
177     * Tests whether way can be reversed without semantic change, i.e., whether tags have to be changed.
178     * Looks for keys like oneway, oneway:bicycle, cycleway:right:oneway, left/right.
179     * @param way way to test
180     * @return false if tags should be changed to keep semantic, true otherwise.
181     */
182    public static boolean isReversible(Way way) {
183        for (Tag tag : TagCollection.from(way)) {
184            if (!tag.equals(TagSwitcher.apply(tag))) {
185                return false;
186            }
187        }
188        return true;
189    }
190
191    /**
192     * Returns the subset of irreversible ways.
193     * @param ways all ways
194     * @return the subset of irreversible ways
195     * @see #isReversible(Way)
196     */
197    public static List<Way> irreversibleWays(List<Way> ways) {
198        List<Way> newWays = new ArrayList<>(ways);
199        for (Way way : ways) {
200            if (isReversible(way)) {
201                newWays.remove(way);
202            }
203        }
204        return newWays;
205    }
206
207    /**
208     * Inverts sign of a numeric value.
209     * @param value numeric value
210     * @return opposite numeric value
211     */
212    public static String invertNumber(String value) {
213        Pattern pattern = Pattern.compile("^([+-]?)(\\d.*)$", Pattern.CASE_INSENSITIVE);
214        Matcher matcher = pattern.matcher(value);
215        if (!matcher.matches()) return value;
216        String sign = matcher.group(1);
217        String rest = matcher.group(2);
218        sign = "-".equals(sign) ? "" : "-";
219        return sign + rest;
220    }
221
222    static List<TagCorrection> getTagCorrections(Tagged way) {
223        List<TagCorrection> tagCorrections = new ArrayList<>();
224        for (Map.Entry<String, String> entry : way.getKeys().entrySet()) {
225            final String key = entry.getKey();
226            final String value = entry.getValue();
227            Tag newTag = TagSwitcher.apply(key, value);
228            String newKey = newTag.getKey();
229            String newValue = newTag.getValue();
230
231            boolean needsCorrection = !key.equals(newKey);
232            if (way.get(newKey) != null && way.get(newKey).equals(newValue)) {
233                needsCorrection = false;
234            }
235            if (!value.equals(newValue)) {
236                needsCorrection = true;
237            }
238
239            if (needsCorrection) {
240                tagCorrections.add(new TagCorrection(key, value, newKey, newValue));
241            }
242        }
243        return tagCorrections;
244    }
245
246    static List<RoleCorrection> getRoleCorrections(Way oldway) {
247        List<RoleCorrection> roleCorrections = new ArrayList<>();
248
249        Collection<OsmPrimitive> referrers = oldway.getReferrers();
250        for (OsmPrimitive referrer: referrers) {
251            if (!(referrer instanceof Relation)) {
252                continue;
253            }
254            Relation relation = (Relation) referrer;
255            int position = 0;
256            for (RelationMember member : relation.getMembers()) {
257                if (!member.getMember().hasEqualSemanticAttributes(oldway)
258                        || !member.hasRole()) {
259                    position++;
260                    continue;
261                }
262
263                final String newRole = COMBINED_SWITCHERS.apply(member.getRole());
264                if (!member.getRole().equals(newRole)) {
265                    roleCorrections.add(new RoleCorrection(relation, position, member, newRole));
266                }
267
268                position++;
269            }
270        }
271        return roleCorrections;
272    }
273
274    static Map<OsmPrimitive, List<TagCorrection>> getTagCorrectionsMap(Way way) {
275        Map<OsmPrimitive, List<TagCorrection>> tagCorrectionsMap = new HashMap<>();
276        List<TagCorrection> tagCorrections = getTagCorrections(way);
277        if (!tagCorrections.isEmpty()) {
278            tagCorrectionsMap.put(way, tagCorrections);
279        }
280        for (Node node : way.getNodes()) {
281            final List<TagCorrection> corrections = getTagCorrections(node);
282            if (!corrections.isEmpty()) {
283                tagCorrectionsMap.put(node, corrections);
284            }
285        }
286        return tagCorrectionsMap;
287    }
288
289    @Override
290    public Collection<Command> execute(Way oldway, Way way) throws UserCancelException {
291        Map<OsmPrimitive, List<TagCorrection>> tagCorrectionsMap = getTagCorrectionsMap(way);
292
293        Map<OsmPrimitive, List<RoleCorrection>> roleCorrectionMap = new HashMap<>();
294        List<RoleCorrection> roleCorrections = getRoleCorrections(oldway);
295        if (!roleCorrections.isEmpty()) {
296            roleCorrectionMap.put(way, roleCorrections);
297        }
298
299        return applyCorrections(oldway.getDataSet(), tagCorrectionsMap, roleCorrectionMap,
300                tr("When reversing this way, the following changes are suggested in order to maintain data consistency."));
301    }
302
303    private static boolean ignoreKeyForCorrection(String key) {
304        for (Pattern ignoredKey : IGNORED_KEYS) {
305            if (ignoredKey.matcher(key).matches()) {
306                return true;
307            }
308        }
309        return false;
310    }
311}