001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.validation.tests;
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.LinkedHashSet;
010import java.util.List;
011import java.util.Set;
012
013import org.openstreetmap.josm.data.osm.Node;
014import org.openstreetmap.josm.data.osm.OsmPrimitive;
015import org.openstreetmap.josm.data.osm.Relation;
016import org.openstreetmap.josm.data.osm.RelationMember;
017import org.openstreetmap.josm.data.osm.Way;
018import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon;
019import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.JoinedWay;
020import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
021import org.openstreetmap.josm.data.validation.Severity;
022import org.openstreetmap.josm.data.validation.Test;
023import org.openstreetmap.josm.data.validation.TestError;
024import org.openstreetmap.josm.gui.progress.ProgressMonitor;
025import org.openstreetmap.josm.tools.Geometry;
026
027/**
028 * Checks for nodes in power lines/minor_lines that do not have a power=tower/pole tag.<br>
029 * See #7812 for discussions about this test.
030 */
031public class PowerLines extends Test {
032
033    /** Test identifier */
034    protected static final int POWER_LINES = 2501;
035    protected static final int POWER_CONNECTION = 2502;
036
037    /** Values for {@code power} key interpreted as power lines */
038    static final Collection<String> POWER_LINE_TAGS = Arrays.asList("line", "minor_line");
039    /** Values for {@code power} key interpreted as power towers */
040    static final Collection<String> POWER_TOWER_TAGS = Arrays.asList("tower", "pole");
041    /** Values for {@code power} key interpreted as power stations */
042    static final Collection<String> POWER_STATION_TAGS = Arrays.asList("station", "sub_station", "substation", "plant", "generator");
043    /** Values for {@code building} key interpreted as power stations */
044    static final Collection<String> BUILDING_STATION_TAGS = Arrays.asList("transformer_tower");
045    /** Values for {@code power} key interpreted as allowed power items */
046    static final Collection<String> POWER_ALLOWED_TAGS = Arrays.asList("switch", "transformer", "busbar", "generator", "switchgear",
047            "portal", "terminal", "insulator", "connection");
048
049    private final Set<Node> badConnections = new LinkedHashSet<>();
050    private final Set<Node> missingTowerOrPole = new LinkedHashSet<>();
051
052    private final List<OsmPrimitive> powerStations = new ArrayList<>();
053
054    /**
055     * Constructs a new {@code PowerLines} test.
056     */
057    public PowerLines() {
058        super(tr("Power lines"), tr("Checks for nodes in power lines that do not have a power=tower/pole/connection tag."));
059    }
060
061    @Override
062    public void visit(Way w) {
063        if (w.isUsable()) {
064            if (isPowerLine(w) && !w.hasTag("location", "underground")) {
065                for (Node n : w.getNodes()) {
066                    if (!isPowerTower(n) && !isPowerAllowed(n) && IN_DOWNLOADED_AREA.test(n)
067                        && (!w.isFirstLastNode(n) || !isPowerStation(n))) {
068                        missingTowerOrPole.add(n);
069                    }
070                }
071            } else if (w.isClosed() && isPowerStation(w)) {
072                powerStations.add(w);
073            }
074        }
075    }
076
077    @Override
078    public void visit(Node n) {
079        boolean nodeInLineOrCable = false;
080        boolean connectedToUnrelated = false;
081        for (Way parent : n.getParentWays()) {
082            if (parent.hasTag("power", "line", "minor_line", "cable"))
083                nodeInLineOrCable = true;
084            else if (!isRelatedToPower(parent)) {
085                connectedToUnrelated = true;
086            }
087        }
088        if (nodeInLineOrCable && connectedToUnrelated)
089            badConnections.add(n);
090    }
091
092    private static boolean isRelatedToPower(Way way) {
093        if (way.hasTag("power") || way.hasTag("building"))
094            return true;
095        for (OsmPrimitive ref : way.getReferrers()) {
096            if (ref instanceof Relation && ref.isMultipolygon() && (ref.hasTag("power") || ref.hasTag("building"))) {
097                for (RelationMember rm : ((Relation) ref).getMembers()) {
098                    if (way == rm.getMember())
099                        return true;
100                }
101            }
102        }
103        return false;
104    }
105
106    @Override
107    public void visit(Relation r) {
108        if (r.isMultipolygon() && isPowerStation(r)) {
109            powerStations.add(r);
110        }
111    }
112
113    @Override
114    public void startTest(ProgressMonitor progressMonitor) {
115        super.startTest(progressMonitor);
116        clearCollections();
117    }
118
119    @Override
120    public void endTest() {
121        for (Node n : missingTowerOrPole) {
122            if (!isInPowerStation(n)) {
123                errors.add(TestError.builder(this, Severity.WARNING, POWER_LINES)
124                        .message(tr("Missing power tower/pole/connection within power line"))
125                        .primitives(n)
126                        .build());
127            }
128        }
129
130        for (Node n : badConnections) {
131            errors.add(TestError.builder(this, Severity.WARNING, POWER_CONNECTION)
132                    .message(tr("Node connects a power line or cable with an object "
133                            + "which is not related to the power infrastructure."))
134                    .primitives(n).build());
135        }
136        clearCollections();
137        super.endTest();
138    }
139
140    protected final boolean isInPowerStation(Node n) {
141        for (OsmPrimitive station : powerStations) {
142            List<List<Node>> nodesLists = new ArrayList<>();
143            if (station instanceof Way) {
144                nodesLists.add(((Way) station).getNodes());
145            } else if (station instanceof Relation) {
146                Multipolygon polygon = MultipolygonCache.getInstance().get((Relation) station);
147                if (polygon != null) {
148                    for (JoinedWay outer : Multipolygon.joinWays(polygon.getOuterWays())) {
149                        nodesLists.add(outer.getNodes());
150                    }
151                }
152            }
153            for (List<Node> nodes : nodesLists) {
154                if (Geometry.nodeInsidePolygon(n, nodes)) {
155                    return true;
156                }
157            }
158        }
159        return false;
160    }
161
162    /**
163     * Determines if the specified way denotes a power line.
164     * @param w The way to be tested
165     * @return {@code true} if power key is set and equal to line/minor_line
166     */
167    protected static final boolean isPowerLine(Way w) {
168        return isPowerIn(w, POWER_LINE_TAGS);
169    }
170
171    /**
172     * Determines if the specified primitive denotes a power station.
173     * @param p The primitive to be tested
174     * @return {@code true} if power key is set and equal to station/sub_station/plant
175     */
176    protected static final boolean isPowerStation(OsmPrimitive p) {
177        return isPowerIn(p, POWER_STATION_TAGS) || isBuildingIn(p, BUILDING_STATION_TAGS);
178    }
179
180    /**
181     * Determines if the specified node denotes a power tower/pole.
182     * @param n The node to be tested
183     * @return {@code true} if power key is set and equal to tower/pole
184     */
185    protected static final boolean isPowerTower(Node n) {
186        return isPowerIn(n, POWER_TOWER_TAGS);
187    }
188
189    /**
190     * Determines if the specified node denotes a power infrastructure allowed on a power line.
191     * @param n The node to be tested
192     * @return True if power key is set and equal to switch/tranformer/busbar/generator
193     */
194    protected static final boolean isPowerAllowed(Node n) {
195        return isPowerIn(n, POWER_ALLOWED_TAGS);
196    }
197
198    /**
199     * Helper function to check if power tag is a certain value.
200     * @param p The primitive to be tested
201     * @param values List of possible values
202     * @return {@code true} if power key is set and equal to possible values
203     */
204    private static boolean isPowerIn(OsmPrimitive p, Collection<String> values) {
205        return p.hasTag("power", values);
206    }
207
208    /**
209     * Helper function to check if building tag is a certain value.
210     * @param p The primitive to be tested
211     * @param values List of possible values
212     * @return {@code true} if power key is set and equal to possible values
213     */
214    private static boolean isBuildingIn(OsmPrimitive p, Collection<String> values) {
215        return p.hasTag("building", values);
216    }
217
218    private void clearCollections() {
219        powerStations.clear();
220        badConnections.clear();
221        missingTowerOrPole.clear();
222    }
223}