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