001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.validation.tests;
003
004import static org.openstreetmap.josm.data.validation.tests.CrossingWays.HIGHWAY;
005import static org.openstreetmap.josm.tools.I18n.tr;
006
007import java.util.HashSet;
008import java.util.Map;
009import java.util.Set;
010
011import org.openstreetmap.josm.command.Command;
012import org.openstreetmap.josm.data.osm.DataSet;
013import org.openstreetmap.josm.data.osm.OsmDataManager;
014import org.openstreetmap.josm.data.osm.OsmPrimitive;
015import org.openstreetmap.josm.data.osm.OsmUtils;
016import org.openstreetmap.josm.data.osm.Relation;
017import org.openstreetmap.josm.data.osm.RelationMember;
018import org.openstreetmap.josm.data.osm.Way;
019import org.openstreetmap.josm.data.validation.Severity;
020import org.openstreetmap.josm.data.validation.Test;
021import org.openstreetmap.josm.data.validation.TestError;
022import org.openstreetmap.josm.gui.progress.ProgressMonitor;
023
024/**
025 * Checks for untagged ways
026 *
027 * @author frsantos
028 */
029public class UntaggedWay extends Test {
030
031    // CHECKSTYLE.OFF: SingleSpaceSeparator
032    /** Empty way error */
033    protected static final int EMPTY_WAY        = 301;
034    /** Untagged way error */
035    protected static final int UNTAGGED_WAY     = 302;
036    /** Unnamed way error */
037    protected static final int UNNAMED_WAY      = 303;
038    /** One node way error */
039    protected static final int ONE_NODE_WAY     = 304;
040    /** Unnamed junction error */
041    protected static final int UNNAMED_JUNCTION = 305;
042    /** Untagged, but commented way error */
043    protected static final int COMMENTED_WAY    = 306;
044    // CHECKSTYLE.ON: SingleSpaceSeparator
045
046    private Set<Way> waysUsedInRelations;
047
048    /** Ways that must have a name */
049    static final Set<String> NAMED_WAYS = new HashSet<>();
050    static {
051        NAMED_WAYS.add("motorway");
052        NAMED_WAYS.add("trunk");
053        NAMED_WAYS.add("primary");
054        NAMED_WAYS.add("secondary");
055        NAMED_WAYS.add("tertiary");
056        NAMED_WAYS.add("residential");
057        NAMED_WAYS.add("pedestrian");
058    }
059
060    /** Whitelist of roles allowed to reference an untagged way */
061    static final Set<String> WHITELIST = new HashSet<>();
062    static {
063        WHITELIST.add("outer");
064        WHITELIST.add("inner");
065        WHITELIST.add("perimeter");
066        WHITELIST.add("edge");
067        WHITELIST.add("outline");
068    }
069
070    /**
071     * Constructor
072     */
073    public UntaggedWay() {
074        super(tr("Untagged, empty and one node ways"),
075              tr("This test checks for untagged, empty and one node ways."));
076    }
077
078    @Override
079    public void visit(Way w) {
080        if (!w.isUsable())
081            return;
082
083        Map<String, String> tags = w.getKeys();
084        if (!tags.isEmpty()) {
085            String highway = tags.get(HIGHWAY);
086            if (highway != null && NAMED_WAYS.contains(highway) && !tags.containsKey("name") && !tags.containsKey("ref")
087                    && !"yes".equals(tags.get("noname"))) {
088                boolean isJunction = false;
089                boolean hasName = false;
090                for (String key : tags.keySet()) {
091                    hasName = key.startsWith("name:") || key.endsWith("_name") || key.endsWith("_ref");
092                    if (hasName) {
093                        break;
094                    }
095                    if ("junction".equals(key)) {
096                        isJunction = true;
097                        break;
098                    }
099                }
100
101                if (!hasName && !isJunction) {
102                    errors.add(TestError.builder(this, Severity.WARNING, UNNAMED_WAY)
103                            .message(tr("Unnamed ways"))
104                            .primitives(w)
105                            .build());
106                } else if (isJunction) {
107                    errors.add(TestError.builder(this, Severity.OTHER, UNNAMED_JUNCTION)
108                            .message(tr("Unnamed junction"))
109                            .primitives(w)
110                            .build());
111                }
112            }
113        }
114
115        // #20393 - ways tagged with just area=yes are catched by MapCSS tests
116        if (!w.isTagged() && !w.hasTag("area", OsmUtils.TRUE_VALUE) && !waysUsedInRelations.contains(w)) {
117            if (w.hasKeys()) {
118                errors.add(TestError.builder(this, Severity.WARNING, COMMENTED_WAY)
119                        .message(tr("Untagged ways (commented)"))
120                        .primitives(w)
121                        .build());
122            } else {
123                errors.add(TestError.builder(this, Severity.WARNING, UNTAGGED_WAY)
124                        .message(tr("Untagged ways"))
125                        .primitives(w)
126                        .build());
127            }
128        }
129
130        if (w.isEmpty()) {
131            errors.add(TestError.builder(this, Severity.ERROR, EMPTY_WAY)
132                    .message(tr("Empty ways"))
133                    .primitives(w)
134                    .build());
135        } else if (w.getNodesCount() == 1) {
136            errors.add(TestError.builder(this, Severity.ERROR, ONE_NODE_WAY)
137                    .message(tr("One node ways"))
138                    .primitives(w)
139                    .build());
140        }
141    }
142
143    @Override
144    public void startTest(ProgressMonitor monitor) {
145        super.startTest(monitor);
146        DataSet ds = OsmDataManager.getInstance().getActiveDataSet();
147        if (ds == null)
148            return;
149        waysUsedInRelations = new HashSet<>();
150        for (Relation r : ds.getRelations()) {
151            if (r.isUsable()) {
152                for (RelationMember m : r.getMembers()) {
153                    if (r.isMultipolygon() || WHITELIST.contains(m.getRole())) {
154                        OsmPrimitive member = m.getMember();
155                        if (member instanceof Way && member.isUsable() && !member.isTagged()) {
156                            waysUsedInRelations.add((Way) member);
157                        }
158                    }
159                }
160            }
161        }
162    }
163
164    @Override
165    public void endTest() {
166        waysUsedInRelations = null;
167        super.endTest();
168    }
169
170    @Override
171    public boolean isFixable(TestError testError) {
172        if (testError.getTester() instanceof UntaggedWay)
173            return testError.getCode() == EMPTY_WAY
174                || testError.getCode() == ONE_NODE_WAY;
175
176        return false;
177    }
178
179    @Override
180    public Command fixError(TestError testError) {
181        return deletePrimitivesIfNeeded(testError.getPrimitives());
182    }
183
184    @Override
185    public boolean isPrimitiveUsable(OsmPrimitive p) {
186        return p instanceof Way;
187    }
188}