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