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<Pattern>();
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 class TagSwitcher {
095
096        /**
097         * Reverses a given tag.
098         * @param tag The tag to reverse
099         * @return The reversed tag (is equal to <code>tag</code> if no change is needed)
100         */
101        public static final Tag apply(final Tag tag) {
102            return apply(tag.getKey(), tag.getValue());
103        }
104
105        /**
106         * Reverses a given tag (key=value).
107         * @param key The tag key
108         * @param value The tag value
109         * @return The reversed tag (is equal to <code>key=value</code> if no change is needed)
110         */
111        public static final Tag apply(final String key, final String value) {
112            String newKey = key;
113            String newValue = value;
114
115            if (key.startsWith("oneway") || key.endsWith("oneway")) {
116                if (OsmUtils.isReversed(value)) {
117                    newValue = OsmUtils.trueval;
118                } else if (OsmUtils.isTrue(value)) {
119                    newValue = OsmUtils.reverseval;
120                }
121            } else if (key.startsWith("incline") || key.endsWith("incline")
122                    || key.startsWith("direction") || key.endsWith("direction")) {
123                newValue = UP_DOWN.apply(value);
124                if (newValue.equals(value)) {
125                    newValue = invertNumber(value);
126                }
127            } else if (key.endsWith(":forward") || key.endsWith(":backward")) {
128                // Change key but not left/right value (fix #8518)
129                newKey = FORWARD_BACKWARD.apply(key);
130
131            } else if (!ignoreKeyForCorrection(key)) {
132                for (StringSwitcher prefixSuffixSwitcher : stringSwitchers) {
133                    newKey = prefixSuffixSwitcher.apply(key);
134                    if (!key.equals(newKey)) {
135                        break;
136                    }
137                    newValue = prefixSuffixSwitcher.apply(value);
138                    if (!value.equals(newValue)) {
139                        break;
140                    }
141                }
142            }
143            return new Tag(newKey, newValue);
144        }
145    }
146
147    private static final StringSwitcher FORWARD_BACKWARD = new StringSwitcher("forward", "backward");
148    private static final StringSwitcher UP_DOWN = new StringSwitcher("up", "down");
149
150    private static final StringSwitcher[] stringSwitchers = new StringSwitcher[] {
151        new StringSwitcher("left", "right"),
152        new StringSwitcher("forwards", "backwards"),
153        new StringSwitcher("east", "west"),
154        new StringSwitcher("north", "south"),
155        FORWARD_BACKWARD, UP_DOWN
156    };
157
158    /**
159     * Tests whether way can be reversed without semantic change, i.e., whether tags have to be changed.
160     * Looks for keys like oneway, oneway:bicycle, cycleway:right:oneway, left/right.
161     * @param way
162     * @return false if tags should be changed to keep semantic, true otherwise.
163     */
164    public static boolean isReversible(Way way) {
165        for (Tag tag : TagCollection.from(way)) {
166            if (!tag.equals(TagSwitcher.apply(tag))) {
167                return false;
168            }
169        }
170        return true;
171    }
172
173    public static List<Way> irreversibleWays(List<Way> ways) {
174        List<Way> newWays = new ArrayList<Way>(ways);
175        for (Way way : ways) {
176            if (isReversible(way)) {
177                newWays.remove(way);
178            }
179        }
180        return newWays;
181    }
182
183    public static String invertNumber(String value) {
184        Pattern pattern = Pattern.compile("^([+-]?)(\\d.*)$", Pattern.CASE_INSENSITIVE);
185        Matcher matcher = pattern.matcher(value);
186        if (!matcher.matches()) return value;
187        String sign = matcher.group(1);
188        String rest = matcher.group(2);
189        sign = sign.equals("-") ? "" : "-";
190        return sign + rest;
191    }
192
193    @Override
194    public Collection<Command> execute(Way oldway, Way way) throws UserCancelException {
195        Map<OsmPrimitive, List<TagCorrection>> tagCorrectionsMap =
196            new HashMap<OsmPrimitive, List<TagCorrection>>();
197
198        List<TagCorrection> tagCorrections = new ArrayList<TagCorrection>();
199        for (String key : way.keySet()) {
200            String value = way.get(key);
201            Tag newTag = TagSwitcher.apply(key, value);
202            String newKey = newTag.getKey();
203            String newValue = newTag.getValue();
204
205            boolean needsCorrection = !key.equals(newKey);
206            if (way.get(newKey) != null && way.get(newKey).equals(newValue)) {
207                needsCorrection = false;
208            }
209            if (!value.equals(newValue)) {
210                needsCorrection = true;
211            }
212
213            if (needsCorrection) {
214                tagCorrections.add(new TagCorrection(key, value, newKey, newValue));
215            }
216        }
217        if (!tagCorrections.isEmpty()) {
218            tagCorrectionsMap.put(way, tagCorrections);
219        }
220
221        Map<OsmPrimitive, List<RoleCorrection>> roleCorrectionMap =
222            new HashMap<OsmPrimitive, List<RoleCorrection>>();
223        List<RoleCorrection> roleCorrections = new ArrayList<RoleCorrection>();
224
225        Collection<OsmPrimitive> referrers = oldway.getReferrers();
226        for (OsmPrimitive referrer: referrers) {
227            if (! (referrer instanceof Relation)) {
228                continue;
229            }
230            Relation relation = (Relation)referrer;
231            int position = 0;
232            for (RelationMember member : relation.getMembers()) {
233                if (!member.getMember().hasEqualSemanticAttributes(oldway)
234                        || !member.hasRole()) {
235                    position++;
236                    continue;
237                }
238
239                boolean found = false;
240                String newRole = null;
241                for (StringSwitcher prefixSuffixSwitcher : stringSwitchers) {
242                    newRole = prefixSuffixSwitcher.apply(member.getRole());
243                    if (!newRole.equals(member.getRole())) {
244                        found = true;
245                        break;
246                    }
247                }
248
249                if (found) {
250                    roleCorrections.add(new RoleCorrection(relation, position, member, newRole));
251                }
252
253                position++;
254            }
255        }
256        if (!roleCorrections.isEmpty()) {
257            roleCorrectionMap.put(way, roleCorrections);
258        }
259
260        return applyCorrections(tagCorrectionsMap, roleCorrectionMap,
261                tr("When reversing this way, the following changes are suggested in order to maintain data consistency."));
262    }
263
264    private static boolean ignoreKeyForCorrection(String key) {
265        for (Pattern ignoredKey : ignoredKeys) {
266            if (ignoredKey.matcher(key).matches()) {
267                return true;
268            }
269        }
270        return false;
271    }
272}