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.List;
010
011import org.openstreetmap.josm.Main;
012import org.openstreetmap.josm.command.ChangePropertyCommand;
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.Way;
017import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon;
018import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.JoinedWay;
019import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
020import org.openstreetmap.josm.data.validation.Severity;
021import org.openstreetmap.josm.data.validation.Test;
022import org.openstreetmap.josm.data.validation.TestError;
023import org.openstreetmap.josm.gui.progress.ProgressMonitor;
024import org.openstreetmap.josm.tools.Geometry;
025
026/**
027 * Checks for nodes in power lines/minor_lines that do not have a power=tower/pole tag.<br>
028 * See #7812 for discussions about this test.
029 */
030public class PowerLines extends Test {
031
032    protected static final int POWER_LINES = 2501;
033
034    /** Values for {@code power} key interpreted as power lines */
035    static final Collection<String> POWER_LINE_TAGS = Arrays.asList("line", "minor_line");
036    /** Values for {@code power} key interpreted as power towers */
037    static final Collection<String> POWER_TOWER_TAGS = Arrays.asList("tower", "pole");
038    /** Values for {@code power} key interpreted as power stations */
039    static final Collection<String> POWER_STATION_TAGS = Arrays.asList("station", "sub_station", "substation", "plant", "generator");
040    /** Values for {@code building} key interpreted as power stations */
041    static final Collection<String> BUILDING_STATION_TAGS = Arrays.asList("transformer_tower");
042    /** Values for {@code power} key interpreted as allowed power items */
043    static final Collection<String> POWER_ALLOWED_TAGS = Arrays.asList("switch", "transformer", "busbar", "generator", "switchgear",
044            "portal", "terminal", "insulator");
045
046    private final List<TestError> potentialErrors = new ArrayList<>();
047
048    private final List<OsmPrimitive> powerStations = new ArrayList<>();
049
050    /**
051     * Constructs a new {@code PowerLines} test.
052     */
053    public PowerLines() {
054        super(tr("Power lines"), tr("Checks for nodes in power lines that do not have a power=tower/pole tag."));
055    }
056
057    @Override
058    public void visit(Way w) {
059        if (w.isUsable()) {
060            if (isPowerLine(w) && !w.hasTag("location", "underground")) {
061                String fixValue = null;
062                TestError.Builder error = null;
063                Node errorNode = null;
064                boolean canFix = false;
065                for (Node n : w.getNodes()) {
066                    if (!isPowerTower(n)) {
067                        if (!isPowerAllowed(n) && IN_DOWNLOADED_AREA.test(n) && (!w.isFirstLastNode(n) || !isPowerStation(n))) {
068                            error = TestError.builder(this, Severity.WARNING, POWER_LINES)
069                                    .message(tr("Missing power tower/pole within power line"))
070                                    .primitives(n);
071                            errorNode = n;
072                        }
073                    } else if (fixValue == null) {
074                        // First tower/pole tag found, remember it
075                        fixValue = n.get("power");
076                        canFix = true;
077                    } else if (!fixValue.equals(n.get("power"))) {
078                        // The power line contains both "tower" and "pole" -> cannot fix this error
079                        canFix = false;
080                    }
081                }
082                if (error != null && canFix) {
083                    final ChangePropertyCommand fix = new ChangePropertyCommand(errorNode, "power", fixValue);
084                    potentialErrors.add(error.fix(() -> fix).build());
085                } else if (error != null) {
086                    potentialErrors.add(error.build());
087                }
088            } else if (w.isClosed() && isPowerStation(w)) {
089                powerStations.add(w);
090            }
091        }
092    }
093
094    @Override
095    public void visit(Relation r) {
096        if (r.isMultipolygon() && isPowerStation(r)) {
097            powerStations.add(r);
098        }
099    }
100
101    @Override
102    public void startTest(ProgressMonitor progressMonitor) {
103        super.startTest(progressMonitor);
104        powerStations.clear();
105        potentialErrors.clear();
106    }
107
108    @Override
109    public void endTest() {
110        for (TestError e : potentialErrors) {
111            e.getPrimitives().stream()
112                    .map(Node.class::cast)
113                    .filter(n -> !isInPowerStation(n))
114                    .findAny()
115                    .ifPresent(ignore -> errors.add(e));
116        }
117        potentialErrors.clear();
118        super.endTest();
119    }
120
121    protected final boolean isInPowerStation(Node n) {
122        for (OsmPrimitive station : powerStations) {
123            List<List<Node>> nodesLists = new ArrayList<>();
124            if (station instanceof Way) {
125                nodesLists.add(((Way) station).getNodes());
126            } else if (station instanceof Relation) {
127                Multipolygon polygon = MultipolygonCache.getInstance().get(Main.map.mapView, (Relation) station);
128                if (polygon != null) {
129                    for (JoinedWay outer : Multipolygon.joinWays(polygon.getOuterWays())) {
130                        nodesLists.add(outer.getNodes());
131                    }
132                }
133            }
134            for (List<Node> nodes : nodesLists) {
135                if (Geometry.nodeInsidePolygon(n, nodes)) {
136                    return true;
137                }
138            }
139        }
140        return false;
141    }
142
143    /**
144     * Determines if the specified way denotes a power line.
145     * @param w The way to be tested
146     * @return {@code true} if power key is set and equal to line/minor_line
147     */
148    protected static final boolean isPowerLine(Way w) {
149        return isPowerIn(w, POWER_LINE_TAGS);
150    }
151
152    /**
153     * Determines if the specified primitive denotes a power station.
154     * @param p The primitive to be tested
155     * @return {@code true} if power key is set and equal to station/sub_station/plant
156     */
157    protected static final boolean isPowerStation(OsmPrimitive p) {
158        return isPowerIn(p, POWER_STATION_TAGS) || isBuildingIn(p, BUILDING_STATION_TAGS);
159    }
160
161    /**
162     * Determines if the specified node denotes a power tower/pole.
163     * @param n The node to be tested
164     * @return {@code true} if power key is set and equal to tower/pole
165     */
166    protected static final boolean isPowerTower(Node n) {
167        return isPowerIn(n, POWER_TOWER_TAGS);
168    }
169
170    /**
171     * Determines if the specified node denotes a power infrastructure allowed on a power line.
172     * @param n The node to be tested
173     * @return True if power key is set and equal to switch/tranformer/busbar/generator
174     */
175    protected static final boolean isPowerAllowed(Node n) {
176        return isPowerIn(n, POWER_ALLOWED_TAGS);
177    }
178
179    /**
180     * Helper function to check if power tag is a certain value.
181     * @param p The primitive to be tested
182     * @param values List of possible values
183     * @return {@code true} if power key is set and equal to possible values
184     */
185    private static boolean isPowerIn(OsmPrimitive p, Collection<String> values) {
186        String v = p.get("power");
187        return v != null && values != null && values.contains(v);
188    }
189
190    /**
191     * Helper function to check if building tag is a certain value.
192     * @param p The primitive to be tested
193     * @param values List of possible values
194     * @return {@code true} if power key is set and equal to possible values
195     */
196    private static boolean isBuildingIn(OsmPrimitive p, Collection<String> values) {
197        String v = p.get("building");
198        return v != null && values != null && values.contains(v);
199    }
200}