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