001// License: GPL. See LICENSE file for details.
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    public static final Collection<String> POWER_LINE_TAGS = Arrays.asList("line", "minor_line");
040    /** Values for {@code power} key interpreted as power towers */
041    public static final Collection<String> POWER_TOWER_TAGS = Arrays.asList("tower", "pole");
042    /** Values for {@code power} key interpreted as power stations */
043    public static final Collection<String> POWER_STATION_TAGS = Arrays.asList("station", "sub_station", "plant", "generator");
044    /** Values for {@code power} key interpreted as allowed power items */
045    public static final Collection<String> POWER_ALLOWED_TAGS = Arrays.asList("switch", "transformer", "busbar", "generator");
046
047    private final Map<Way, String> towerPoleTagMap = new HashMap<Way, String>();
048
049    private final List<PowerLineError> potentialErrors = new ArrayList<PowerLineError>();
050
051    private final List<OsmPrimitive> powerStations = new ArrayList<OsmPrimitive>();
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)) {
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                            potentialErrors.add(new PowerLineError(n, w));
071                            erroneous = true;
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 (erroneous && canFix) {
083                    towerPoleTagMap.put(w, fixValue);
084                }
085            } else if (w.isClosed() && isPowerStation(w)) {
086                powerStations.add(w);
087            }
088        }
089    }
090
091    @Override
092    public void visit(Relation r) {
093        if (r.isMultipolygon() && isPowerStation(r)) {
094            powerStations.add(r);
095        }
096    }
097
098    @Override
099    public void startTest(ProgressMonitor progressMonitor) {
100        super.startTest(progressMonitor);
101        towerPoleTagMap.clear();
102        powerStations.clear();
103        potentialErrors.clear();
104    }
105
106    @Override
107    public void endTest() {
108        for (PowerLineError e : potentialErrors) {
109            Node n = e.getNode();
110            if (n != null && !isInPowerStation(n)) {
111                errors.add(e);
112            }
113        }
114        potentialErrors.clear();
115        super.endTest();
116    }
117
118    protected final boolean isInPowerStation(Node n) {
119        for (OsmPrimitive station : powerStations) {
120            List<List<Node>> nodesLists = new ArrayList<List<Node>>();
121            if (station instanceof Way) {
122                nodesLists.add(((Way)station).getNodes());
123            } else if (station instanceof Relation) {
124                Multipolygon polygon = MultipolygonCache.getInstance().get(Main.map.mapView, (Relation) station);
125                if (polygon != null) {
126                    for (JoinedWay outer : Multipolygon.joinWays(polygon.getOuterWays())) {
127                        nodesLists.add(outer.getNodes());
128                    }
129                }
130            }
131            for (List<Node> nodes : nodesLists) {
132                if (Geometry.nodeInsidePolygon(n, nodes)) {
133                    return true;
134                }
135            }
136        }
137        return false;
138    }
139
140    @Override
141    public Command fixError(TestError testError) {
142        if (testError instanceof PowerLineError && isFixable(testError)) {
143            // primitives list can be empty if all primitives have been purged
144            Iterator<? extends OsmPrimitive> it = testError.getPrimitives().iterator();
145            if (it.hasNext()) {
146                return new ChangePropertyCommand(it.next(),
147                        "power", towerPoleTagMap.get(((PowerLineError)testError).line));
148            }
149        }
150        return null;
151    }
152
153    @Override
154    public boolean isFixable(TestError testError) {
155        return testError instanceof PowerLineError && towerPoleTagMap.containsKey(((PowerLineError)testError).line);
156    }
157
158    /**
159     * Determines if the specified way denotes a power line.
160     * @param w The way to be tested
161     * @return {@code true} if power key is set and equal to line/minor_line
162     */
163    protected static final boolean isPowerLine(Way w) {
164        return isPowerIn(w, POWER_LINE_TAGS);
165    }
166
167    /**
168     * Determines if the specified primitive denotes a power station.
169     * @param p The primitive to be tested
170     * @return {@code true} if power key is set and equal to station/sub_station/plant
171     */
172    protected static final boolean isPowerStation(OsmPrimitive p) {
173        return isPowerIn(p, POWER_STATION_TAGS);
174    }
175
176    /**
177     * Determines if the specified node denotes a power tower/pole.
178     * @param n The node to be tested
179     * @return {@code true} if power key is set and equal to tower/pole
180     */
181    protected static final boolean isPowerTower(Node n) {
182        return isPowerIn(n, POWER_TOWER_TAGS);
183    }
184
185    /**
186     * Determines if the specified node denotes a power infrastructure allowed on a power line.
187     * @param n The node to be tested
188     * @return True if power key is set and equal to switch/tranformer/busbar/generator
189     */
190    protected static final boolean isPowerAllowed(Node n) {
191        return isPowerIn(n, POWER_ALLOWED_TAGS);
192    }
193
194    /**
195     * Helper function to check if power tags is a certain value.
196     * @param p The primitive to be tested
197     * @param values List of possible values
198     * @return {@code true} if power key is set and equal to possible values
199     */
200    private static final boolean isPowerIn(OsmPrimitive p, Collection<String> values) {
201        String v = p.get("power");
202        return v != null && values != null && values.contains(v);
203    }
204
205    protected class PowerLineError extends TestError {
206        private final Way line;
207        public PowerLineError(Node n, Way line) {
208            super(PowerLines.this, Severity.WARNING,
209                    tr("Missing power tower/pole within power line"), POWER_LINES, n);
210            this.line = line;
211        }
212        public final Node getNode() {
213            // primitives list can be empty if all primitives have been purged
214            Iterator<? extends OsmPrimitive> it = getPrimitives().iterator();
215            return it.hasNext() ? (Node) it.next() : null;
216        }
217    }
218}