Ticket #20716: josm_20716_power_v7_reupload.patch

File josm_20716_power_v7_reupload.patch, 23.7 KB (added by gaben, 4 years ago)

added missing Way.java part

  • src/org/openstreetmap/josm/data/osm/Way.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/osm/Way.java b/src/org/openstreetmap/josm/data/osm/Way.java
    a b  
    650650        return length;
    651651    }
    652652
     653    /**
     654     * Calculates the segment lengths of the way as computed by {@link LatLon#greatCircleDistance}.
     655     * @return The segment lengths of a way in metres, from the first to last node
     656     * @since xxx
     657     */
     658    public double[] getSegmentLengths() {
     659        double[] segmentLengths = new double[nodes.length - 1];
     660        Node prevNode = this.firstNode();
     661
     662        for (int i = 1; i < nodes.length; i++) {
     663            Node n = nodes[i];
     664
     665            double distance = n.getCoor().greatCircleDistance(prevNode.getCoor());
     666            segmentLengths[i - 1] = distance;
     667            prevNode = n;
     668        }
     669
     670        return segmentLengths;
     671    }
     672
    653673    /**
    654674     * Replies the length of the longest segment of the way, in metres, as computed by {@link LatLon#greatCircleDistance}.
    655675     * @return The length of the segment, in metres
  • src/org/openstreetmap/josm/data/validation/tests/PowerLines.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/validation/tests/PowerLines.java b/src/org/openstreetmap/josm/data/validation/tests/PowerLines.java
    a b  
    11// License: GPL. For details, see LICENSE file.
    22package org.openstreetmap.josm.data.validation.tests;
    33
     4import static org.openstreetmap.josm.gui.MainApplication.getLayerManager;
    45import static org.openstreetmap.josm.tools.I18n.tr;
    56
    67import java.util.ArrayList;
    78import java.util.Arrays;
    89import java.util.Collection;
    9 import java.util.LinkedHashSet;
     10import java.util.Collections;
     11import java.util.HashSet;
    1012import java.util.List;
    1113import java.util.Set;
     14import java.util.stream.Collectors;
    1215
     16import org.openstreetmap.josm.data.coor.EastNorth;
    1317import org.openstreetmap.josm.data.osm.Node;
    1418import org.openstreetmap.josm.data.osm.OsmPrimitive;
    1519import org.openstreetmap.josm.data.osm.Relation;
    1620import org.openstreetmap.josm.data.osm.RelationMember;
    1721import org.openstreetmap.josm.data.osm.Way;
     22import org.openstreetmap.josm.data.osm.WaySegment;
    1823import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon;
    1924import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.JoinedWay;
    2025import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
     
    2328import org.openstreetmap.josm.data.validation.TestError;
    2429import org.openstreetmap.josm.gui.progress.ProgressMonitor;
    2530import org.openstreetmap.josm.tools.Geometry;
     31import org.openstreetmap.josm.tools.Pair;
    2632
    2733/**
    28  * Checks for nodes in power lines/minor_lines that do not have a power=tower/pole tag.<br>
    29  * See #7812 for discussions about this test.
     34 * Checks for
     35 * <ul>
     36 * <li>nodes in power lines/minor_lines that do not have a power=tower/pole/portal tag
     37 * <li>nodes where the reference numbering not consistent
     38 * <li>ways where are unusually long segments without line support feature
     39 * <li>ways where the line type is possibly misused
     40 * </ul>
     41 * See #7812 and #20716 for discussions about this test.
    3042 */
    3143public class PowerLines extends Test {
    3244
    33     /** Test identifier */
    34     protected static final int POWER_LINES = 2501;
     45    /** Test identifiers */
     46    protected static final int POWER_SUPPORT = 2501;
    3547    protected static final int POWER_CONNECTION = 2502;
     48    protected static final int POWER_SEGMENT_LENGTH = 2503;
     49    protected static final int POWER_REF = 2504;
     50    protected static final int POWER_LINE_TYPE = 2505;
    3651
    3752    /** Values for {@code power} key interpreted as power lines */
    3853    static final Collection<String> POWER_LINE_TAGS = Arrays.asList("line", "minor_line");
    3954    /** Values for {@code power} key interpreted as power towers */
    40     static final Collection<String> POWER_TOWER_TAGS = Arrays.asList("tower", "pole");
     55    static final Collection<String> POWER_TOWER_TAGS = Arrays.asList("catenary_mast", "pole", "portal", "tower");
    4156    /** Values for {@code power} key interpreted as power stations */
    42     static final Collection<String> POWER_STATION_TAGS = Arrays.asList("station", "sub_station", "substation", "plant", "generator");
     57    static final Collection<String> POWER_STATION_TAGS = Arrays.asList("generator", "plant", "substation");
    4358    /** Values for {@code building} key interpreted as power stations */
    4459    static final Collection<String> BUILDING_STATION_TAGS = Arrays.asList("transformer_tower");
    4560    /** Values for {@code power} key interpreted as allowed power items */
    46     static final Collection<String> POWER_ALLOWED_TAGS = Arrays.asList("switch", "transformer", "busbar", "generator", "switchgear",
    47             "portal", "terminal", "insulator");
     61    static final Collection<String> POWER_INFRASTRUCTURE_TAGS = Arrays.asList("compensator", "converter",
     62            "generator", "insulator", "switch", "switchgear", "terminal", "transformer");
    4863
    49     private final Set<Node> badConnections = new LinkedHashSet<>();
    50     private final Set<Node> missingTowerOrPole = new LinkedHashSet<>();
     64    private final Set<Node> badConnections = new HashSet<>();
     65    private final Set<Node> missingTags = new HashSet<>();
     66    private final Set<Node> refDiscontinuities = new HashSet<>();
     67    private final Set<Way> wrongLineType = new HashSet<>();
     68    private final Set<WaySegment> missingNodes = new HashSet<>();
    5169
    5270    private final List<OsmPrimitive> powerStations = new ArrayList<>();
    5371
     72    private final Collection<Way> datasetWaterways = new HashSet<>(64);
     73
    5474    /**
    5575     * Constructs a new {@code PowerLines} test.
    5676     */
    5777    public PowerLines() {
    58         super(tr("Power lines"), tr("Checks for nodes in power lines that do not have a power=tower/pole tag."));
    59     }
    60 
    61     @Override
    62     public void visit(Way w) {
    63         if (w.isUsable()) {
    64             if (isPowerLine(w) && !w.hasTag("location", "underground")) {
    65                 for (Node n : w.getNodes()) {
    66                     if (!isPowerTower(n) && !isPowerAllowed(n) && IN_DOWNLOADED_AREA.test(n)
    67                         && (!w.isFirstLastNode(n) || !isPowerStation(n))) {
    68                         missingTowerOrPole.add(n);
    69                     }
    70                 }
    71             } else if (w.isClosed() && isPowerStation(w)) {
    72                 powerStations.add(w);
    73             }
    74         }
     78        super(tr("Power lines"), tr("Checks if power line missing a support node and " +
     79                "for nodes in power lines that do not have a power=tower/pole tag"));
     80    }
     81
     82    /**
     83     * Power line support features ref=* numbering direction.
     84     */
     85    private enum NumberingDirection {
     86        /** No direction */
     87        NONE,
     88        /** Numbering follows way direction */
     89        SAME,
     90        /** Numbering goes opposite way direction */
     91        OPPOSITE,
     92        /** Not distinguishable numbering direction */
     93        MIXED
    7594    }
    7695
    7796    @Override
     
    89108            badConnections.add(n);
    90109    }
    91110
    92     private static boolean isRelatedToPower(Way way) {
    93         if (way.hasTag("power") || way.hasTag("building"))
    94             return true;
    95         for (OsmPrimitive ref : way.getReferrers()) {
    96             if (ref instanceof Relation && ref.isMultipolygon() && (ref.hasTag("power") || ref.hasTag("building"))) {
    97                 for (RelationMember rm : ((Relation) ref).getMembers()) {
    98                     if (way == rm.getMember())
    99                         return true;
     111    @Override
     112    public void visit(Way w) {
     113        if (!isPrimitiveUsable(w)) return;
     114
     115        if (isPowerLine(w) && !w.hasKey("line") && !w.hasTag("location", "underground")) {
     116            final int segmentCount = w.getNodesCount() - 1;
     117            final double mean = w.getLength() / segmentCount;
     118            final double stdDev = calculateStdDev(w.getSegmentLengths(), mean);
     119            int poleCount = 0;
     120            int towerCount = 0;
     121            Node prevNode = w.firstNode();
     122
     123            double threshold = w.hasTag("power", "line") ? 1.6 : 1.8;
     124            if (mean / stdDev < 4) {
     125                //compensate for possibly hilly areas where towers can't be put anywhere
     126                threshold += 0.2;
     127            }
     128
     129            for (int i = 1; i < w.getRealNodesCount(); i++) {
     130                final Node n = w.getNode(i);
     131
     132                /// handle power station line connections (eg. power=line + line=*)
     133                if (isConnectedToStationLine(n, w)) {
     134                    prevNode = n;
     135                    continue;   // skip, it would be false positive
    100136                }
     137
     138                /// handle missing power line support tags (eg. tower)
     139                if (!isPowerTower(n) && !isPowerInfrastructure(n) && IN_DOWNLOADED_AREA.test(n)
     140                        && (!w.isFirstLastNode(n) || !isPowerStation(n)))
     141                    missingTags.add(n);
     142
     143                /// handle missing nodes
     144                double segmentLen = n.getCoor().greatCircleDistance(prevNode.getCoor());
     145
     146                // check power=line waterway crossing
     147                final Pair<Node, Node> pair = Pair.create(prevNode, n);
     148                final Set<Way> crossingWaterWays = new HashSet<>(8);
     149                final Set<Node> crossingPositions = new HashSet<>(8);
     150                findCrossings(datasetWaterways, w, pair, crossingWaterWays, crossingPositions);
     151
     152                if (!crossingWaterWays.isEmpty()) {
     153                    double compensation = calculateIntersectingLen(prevNode, crossingPositions);
     154                    segmentLen -= compensation;
     155                }
     156
     157                if (segmentCount > 4 && segmentLen > mean * threshold && !isPowerInfrastructure(n))
     158                    missingNodes.add(WaySegment.forNodePair(w, prevNode, n));
     159
     160                /// handle wrong line types
     161                if (n.hasTag("power", "pole"))
     162                    poleCount++;
     163                else if (n.hasTag("power", "tower"))
     164                    towerCount++;
     165
     166                prevNode = n;
    101167            }
     168
     169            /// handle ref=* numbering discontinuities
     170            checkDiscontinuities(w);
     171
     172            /// handle wrong line types
     173            if ((poleCount > towerCount && w.hasTag("power", "line")) ||
     174                    (poleCount < towerCount && w.hasTag("power", "minor_line")))
     175                wrongLineType.add(w);
     176
     177        } else if (w.isClosed() && isPowerStation(w)) {
     178            powerStations.add(w);
    102179        }
    103         return false;
    104180    }
    105181
    106182    @Override
     
    113189    @Override
    114190    public void startTest(ProgressMonitor progressMonitor) {
    115191        super.startTest(progressMonitor);
    116         clearCollections();
     192        setShowElements(true);
     193
     194        // collect all waterways
     195        getLayerManager()
     196                .getActiveDataSet()
     197                .getWays()
     198                .parallelStream()
     199                .filter(way ->
     200                        way.hasTag("water", "river", "lake") ||
     201                                way.hasKey("waterway") ||
     202                                way.referrers(Relation.class)
     203                                        .collect(Collectors.toSet())
     204                                        // no parallel stream here, just makes the iteration slower
     205                                        .stream()
     206                                        .anyMatch(relation -> relation.hasTag("water", "river", "lake") ||
     207                                                relation.hasKey("waterway")
     208                                        ))
     209                .forEach(datasetWaterways::add);
    117210    }
    118211
    119212    @Override
    120213    public void endTest() {
    121         for (Node n : missingTowerOrPole) {
     214        for (Node n : missingTags) {
    122215            if (!isInPowerStation(n)) {
    123                 errors.add(TestError.builder(this, Severity.WARNING, POWER_LINES)
    124                         .message(tr("Missing power tower/pole within power line"))
     216                errors.add(TestError.builder(this, Severity.WARNING, POWER_SUPPORT)
     217                        // the "missing tag" grouping can become broken if the MapCSS message get reworded
     218                        .message(tr("missing tag"), tr("node without power=*"))
    125219                        .primitives(n)
    126220                        .build());
    127221            }
     
    130224        for (Node n : badConnections) {
    131225            errors.add(TestError.builder(this, Severity.WARNING, POWER_CONNECTION)
    132226                    .message(tr("Node connects a power line or cable with an object "
    133                             + "which is not related to the power infrastructure."))
    134                     .primitives(n).build());
     227                            + "which is not related to the power infrastructure"))
     228                    .primitives(n)
     229                    .build());
     230        }
     231
     232        for (WaySegment segment : missingNodes) {
     233            errors.add(TestError.builder(this, Severity.WARNING, POWER_SEGMENT_LENGTH)
     234                    .message(tr("Missing line support node within power line"))
     235                    .primitives(segment.getFirstNode(), segment.getSecondNode())
     236                    .highlightWaySegments(new HashSet<>(Collections.singleton(segment)))
     237                    .build());
    135238        }
    136         clearCollections();
     239
     240        for (Node n : refDiscontinuities) {
     241            errors.add(TestError.builder(this, Severity.WARNING, POWER_REF)
     242                    .message(tr("Mixed reference numbering"))
     243                    .primitives(n)
     244                    .build());
     245        }
     246
     247        for (Way w : wrongLineType) {
     248            errors.add(TestError.builder(this, Severity.WARNING, POWER_LINE_TYPE)
     249                    .message(tr("Possibly wrong power line type used"))
     250                    .primitives(w)
     251                    .build());
     252        }
     253
    137254        super.endTest();
    138255    }
    139256
     257    /**
     258     * Checks reference numbering discontinuities on the given way.
     259     * @param w A way to be checked for ref=* discontinuities
     260     */
     261    private void checkDiscontinuities(Way w) {
     262        final List<Integer> discontinuityAtIndexes = new ArrayList<>();
     263        final NumberingDirection direction = detectDirection(w, discontinuityAtIndexes);
     264
     265        if (direction == NumberingDirection.MIXED) {
     266            for (int disC : discontinuityAtIndexes) {
     267                refDiscontinuities.add(w.getNode(disC));
     268            }
     269        }
     270    }
     271
     272    /**
     273     * Calculates the standard deviation from the given way segment lengths.
     274     * @param segmentLengths Segment lengths of a way
     275     * @param mean Precalculated average segment length of a way
     276     * @return standard deviation of the given way segment lengths
     277     */
     278    private static double calculateStdDev(double[] segmentLengths, double mean) {
     279        double standardDeviation = 0;
     280        int size = segmentLengths.length;
     281
     282        for (double length : segmentLengths) {
     283            standardDeviation += Math.pow(length - mean, 2);
     284        }
     285
     286        return Math.sqrt(standardDeviation / size);
     287    }
     288
     289    /**
     290     * The summarized length (in metres) of a way where a power line hangs over a water area.
     291     * @param ref Reference point
     292     * @param crossingNodes Crossing nodes, unordered
     293     * @return The summarized length (in metres) of a way where a power line hangs over a water area
     294     */
     295    private static double calculateIntersectingLen(Node ref, Set<Node> crossingNodes) {
     296        double min = Double.POSITIVE_INFINITY;
     297        double max = Double.NEGATIVE_INFINITY;
     298
     299        for (Node n : crossingNodes) {
     300            double dist = ref.getCoor().greatCircleDistance(n.getCoor());
     301
     302            if (dist < min)
     303                min = dist;
     304            if (dist > max)
     305                max = dist;
     306        }
     307        return max - min;
     308    }
     309
     310    /**
     311     * Searches for way intersections, which intersect the {@code pair} attribute.
     312     * @param ways collection of ways to search
     313     * @param parent parent way for {@code pair} param
     314     * @param pair {@link Node} pair among which search for another way
     315     * @param crossingWays found crossing ways
     316     * @param crossingPositions collection of the crossing positions
     317     * @implNote Inspired by {@code utilsplugin2/selection/NodeWayUtils.java#addWaysIntersectingWay()}
     318     */
     319    private static void findCrossings(Collection<Way> ways, Way parent, Pair<Node, Node> pair, Set<Way> crossingWays,
     320                                      Set<Node> crossingPositions) {
     321        ways.parallelStream()
     322                .filter(way -> !way.isDisabled()
     323                        && !crossingWays.contains(way)
     324                        && way.getBBox().intersects(parent.getBBox()))
     325                .forEach(way -> way.getNodePairs(false)
     326                        .forEach(pair2 -> {
     327                            EastNorth eastNorth = Geometry.getSegmentSegmentIntersection(
     328                                    pair.a.getEastNorth(), pair.b.getEastNorth(),
     329                                    pair2.a.getEastNorth(), pair2.b.getEastNorth());
     330                            if (eastNorth != null) {
     331                                crossingWays.add(way);
     332                                crossingPositions.add(new Node(eastNorth));
     333                            }
     334                        })
     335                );
     336    }
     337
     338    /**
     339     * Detects ref=* numbering direction. Ignores the first and last node, because ways can be connected and the
     340     * connection node could have a different numbering.
     341     * @param way Way to check
     342     * @param discontinuityAtIndex List of node indexes where the discontinuity found
     343     * @return way numbering direction
     344     */
     345    private static NumberingDirection detectDirection(Way way, List<Integer> discontinuityAtIndex) {
     346        final Set<NumberingDirection> directions = new HashSet<>(4);
     347        final int waySize = way.getNodesCount();
     348        int prevRef = -1;
     349        int ref;
     350
     351        // skip first and last node
     352        for (int i = 1; i < waySize - 1; i++) {
     353            final Node n = way.getNode(i);
     354
     355            if (n.hasTag("ref")) {
     356                try {
     357                    ref = Integer.parseInt(n.get("ref"));
     358
     359                    if (i == 1) {
     360                        prevRef = ref;
     361                        continue;
     362                    }
     363
     364                    if (ref > prevRef) {
     365                        directions.add(NumberingDirection.SAME);
     366                    } else if (ref < prevRef) {
     367                        directions.add(NumberingDirection.OPPOSITE);
     368                    } else {
     369                        directions.add(NumberingDirection.MIXED);
     370                        discontinuityAtIndex.add(i);
     371                    }
     372
     373                    prevRef = ref;
     374                } catch (NumberFormatException ignore) {
     375                    prevRef = getGuessedRef(prevRef, directions);
     376                }
     377            } else if (prevRef != -1) {
     378                prevRef = getGuessedRef(prevRef, directions);
     379            }
     380        }
     381
     382        if (directions.isEmpty())
     383            return NumberingDirection.NONE;
     384        else if (directions.size() > 1)
     385            return NumberingDirection.MIXED;
     386        else
     387            return directions.stream().findAny().get();
     388    }
     389
     390    /**
     391     * Guesses the ref=* number based on previous value and the recognized direction.
     392     * @param prevRef Previous node ref number
     393     * @param directions Recognised way numbering direction
     394     * @return Guessed ref number of the next power=pole/tower
     395     */
     396    private static int getGuessedRef(int prevRef, Set<NumberingDirection> directions) {
     397        if (directions.size() == 1) {
     398            NumberingDirection direction = directions.stream().findFirst().get();
     399            if (direction == NumberingDirection.SAME)
     400                prevRef += 1;
     401            else if (direction == NumberingDirection.OPPOSITE)
     402                prevRef -= 1;
     403        }
     404        return prevRef;
     405    }
     406
     407    private static boolean isRelatedToPower(Way way) {
     408        if (way.hasTag("power") || way.hasTag("building"))
     409            return true;
     410        for (OsmPrimitive ref : way.getReferrers()) {
     411            if (ref instanceof Relation && ref.isMultipolygon() && (ref.hasTag("power") || ref.hasTag("building"))) {
     412                for (RelationMember rm : ((Relation) ref).getMembers()) {
     413                    if (way == rm.getMember())
     414                        return true;
     415                }
     416            }
     417        }
     418        return false;
     419    }
     420
     421    /**
     422     * Determines if the current node connected to a line which used usually used inside power stations.
     423     * @param n node to check
     424     * @param w parent way of {@code n}
     425     * @return {@code true} if {@code n} connected to power=line + line=*
     426     */
     427    private static boolean isConnectedToStationLine(Node n, Way w) {
     428        for (OsmPrimitive p : n.getReferrers()) {
     429            if (p instanceof Way && !p.equals(w) && isPowerLine((Way) p) && p.hasKey("line"))
     430                return true;
     431        }
     432        return false;
     433    }
     434
     435    /**
     436     * Checks if the given node is inside a power station.
     437     * @param n Node to checked
     438     */
    140439    protected final boolean isInPowerStation(Node n) {
    141440        for (OsmPrimitive station : powerStations) {
    142441            List<List<Node>> nodesLists = new ArrayList<>();
     
    171470    /**
    172471     * Determines if the specified primitive denotes a power station.
    173472     * @param p The primitive to be tested
    174      * @return {@code true} if power key is set and equal to station/sub_station/plant
     473     * @return {@code true} if power key is set and equal to generator/substation/plant
    175474     */
    176475    protected static final boolean isPowerStation(OsmPrimitive p) {
    177476        return isPowerIn(p, POWER_STATION_TAGS) || isBuildingIn(p, BUILDING_STATION_TAGS);
    178477    }
    179478
    180479    /**
    181      * Determines if the specified node denotes a power tower/pole.
     480     * Determines if the specified node denotes a power support feature.
    182481     * @param n The node to be tested
    183      * @return {@code true} if power key is set and equal to tower/pole
     482     * @return {@code true} if power key is set and equal to pole/tower/portal/catenary_mast
    184483     */
    185484    protected static final boolean isPowerTower(Node n) {
    186485        return isPowerIn(n, POWER_TOWER_TAGS);
     
    189488    /**
    190489     * Determines if the specified node denotes a power infrastructure allowed on a power line.
    191490     * @param n The node to be tested
    192      * @return True if power key is set and equal to switch/tranformer/busbar/generator
     491     * @return True if power key is set and equal to compensator/converter/generator/insulator
     492     * /switch/switchgear/terminal/transformer
    193493     */
    194     protected static final boolean isPowerAllowed(Node n) {
    195         return isPowerIn(n, POWER_ALLOWED_TAGS);
     494    protected static final boolean isPowerInfrastructure(Node n) {
     495        return isPowerIn(n, POWER_INFRASTRUCTURE_TAGS);
    196496    }
    197497
    198498    /**
     
    215515        return p.hasTag("building", values);
    216516    }
    217517
    218     private void clearCollections() {
     518    @Override
     519    public void clear() {
     520        super.clear();
    219521        powerStations.clear();
    220522        badConnections.clear();
    221         missingTowerOrPole.clear();
     523        missingTags.clear();
     524        missingNodes.clear();
     525        wrongLineType.clear();
     526        refDiscontinuities.clear();
     527        datasetWaterways.clear();
    222528    }
    223529}