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