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