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