Index: /trunk/src/org/openstreetmap/josm/data/osm/Way.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/osm/Way.java	(revision 18552)
+++ /trunk/src/org/openstreetmap/josm/data/osm/Way.java	(revision 18553)
@@ -11,4 +11,5 @@
 import java.util.Set;
 import java.util.stream.Collectors;
+import java.util.stream.DoubleStream;
 import java.util.stream.IntStream;
 
@@ -609,5 +610,21 @@
     @Override
     public boolean hasIncompleteNodes() {
-        return Arrays.stream(nodes).anyMatch(Node::isIncomplete);
+        /*
+         * Ideally, we would store this as a flag, but a node may become
+         * incomplete under some circumstances without being able to notify the
+         * way to recalculate the flag.
+         *
+         * When profiling #20716 on Mesa County, CO (overpass download), the
+         * Arrays.stream method was fairly expensive. When switching to the for
+         * loop, the CPU samples for hasIncompleteNodes went from ~150k samples
+         * to ~8.5k samples (94% improvement) and the memory allocations for
+         * hasIncompleteNodes went from ~15.6 GB to 0.
+         */
+        for (Node node : nodes) {
+            if (node.isIncomplete()) {
+                return true;
+            }
+        }
+        return false;
     }
 
@@ -649,4 +666,14 @@
 
     /**
+     * Replies the segment lengths of the way as computed by {@link ILatLon#greatCircleDistance}.
+     *
+     * @return The segment lengths of a way in metres, following way direction
+     * @since 18553
+     */
+    public double[] getSegmentLengths() {
+        return this.segmentLengths().toArray();
+    }
+
+    /**
      * Replies the length of the longest segment of the way, in metres, as computed by {@link ILatLon#greatCircleDistance}.
      * @return The length of the segment, in metres
@@ -654,16 +681,22 @@
      */
     public double getLongestSegmentLength() {
-        double length = 0;
+        return this.segmentLengths().max().orElse(0);
+    }
+
+    /**
+     * Get the segment lengths as a stream
+     * @return The stream of segment lengths (ordered)
+     */
+    private DoubleStream segmentLengths() {
+        DoubleStream.Builder builder = DoubleStream.builder();
         Node lastN = null;
-        for (Node n:nodes) {
-            if (lastN != null && lastN.isLatLonKnown() && n.isLatLonKnown()) {
-                double l = n.greatCircleDistance(lastN);
-                if (l > length) {
-                    length = l;
-                }
+        for (Node n : nodes) {
+            if (lastN != null && n.isLatLonKnown() && lastN.isLatLonKnown()) {
+                double distance = n.greatCircleDistance(lastN);
+                builder.accept(distance);
             }
             lastN = n;
         }
-        return length;
+        return builder.build();
     }
 
Index: /trunk/src/org/openstreetmap/josm/data/validation/tests/CrossingWays.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/validation/tests/CrossingWays.java	(revision 18552)
+++ /trunk/src/org/openstreetmap/josm/data/validation/tests/CrossingWays.java	(revision 18553)
@@ -16,4 +16,5 @@
 
 import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.coor.ILatLon;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
 import org.openstreetmap.josm.data.osm.OsmUtils;
@@ -354,11 +355,9 @@
         for (int i = 0; i < nodesSize - 1; i++) {
             final WaySegment es1 = new WaySegment(w, i);
-            final EastNorth en1 = es1.getFirstNode().getEastNorth();
-            final EastNorth en2 = es1.getSecondNode().getEastNorth();
-            if (en1 == null || en2 == null) {
+            if (!es1.getFirstNode().isLatLonKnown() || !es1.getSecondNode().isLatLonKnown()) {
                 Logging.warn("Crossing ways test skipped " + es1);
                 continue;
             }
-            for (List<WaySegment> segments : getSegments(cellSegments, en1, en2)) {
+            for (List<WaySegment> segments : getSegments(cellSegments, es1.getFirstNode(), es1.getSecondNode())) {
                 for (WaySegment es2 : segments) {
                     List<Way> prims;
@@ -416,4 +415,19 @@
 
     /**
+     * Returns all the cells this segment crosses.  Each cell contains the list
+     * of segments already processed
+     * @param cellSegments map with already collected way segments
+     * @param n1 The first EastNorth
+     * @param n2 The second EastNorth
+     * @return A list with all the cells the segment crosses
+     * @since 18553
+     */
+    public static List<List<WaySegment>> getSegments(Map<Point2D, List<WaySegment>> cellSegments, ILatLon n1, ILatLon n2) {
+        return ValUtil.getSegmentCells(n1, n2, OsmValidator.getGridDetail()).stream()
+                .map(cell -> cellSegments.computeIfAbsent(cell, k -> new ArrayList<>()))
+                .collect(Collectors.toList());
+    }
+
+    /**
      * Find ways which are crossing without sharing a node.
      * @param w way that is to be checked
Index: /trunk/src/org/openstreetmap/josm/data/validation/tests/PowerLines.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/validation/tests/PowerLines.java	(revision 18552)
+++ /trunk/src/org/openstreetmap/josm/data/validation/tests/PowerLines.java	(revision 18553)
@@ -4,11 +4,17 @@
 import static org.openstreetmap.josm.tools.I18n.tr;
 
+import java.awt.geom.Point2D;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.LinkedHashSet;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
+import org.openstreetmap.josm.data.coor.ILatLon;
 import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
@@ -16,61 +22,77 @@
 import org.openstreetmap.josm.data.osm.RelationMember;
 import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.osm.WaySegment;
 import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon;
 import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.JoinedWay;
 import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
+import org.openstreetmap.josm.data.preferences.sources.ValidatorPrefHelper;
 import org.openstreetmap.josm.data.validation.Severity;
 import org.openstreetmap.josm.data.validation.Test;
 import org.openstreetmap.josm.data.validation.TestError;
 import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+import org.openstreetmap.josm.spi.preferences.Config;
 import org.openstreetmap.josm.tools.Geometry;
+import org.openstreetmap.josm.tools.Logging;
+import org.openstreetmap.josm.tools.Utils;
 
 /**
- * Checks for nodes in power lines/minor_lines that do not have a power=tower/pole tag.<br>
- * See #7812 for discussions about this test.
+ * Checks for
+ * <ul>
+ * <li>nodes in power lines/minor_lines that do not have a power=tower/pole/portal tag
+ * <li>nodes where the reference numbering not consistent
+ * <li>ways where are unusually long segments without line support feature
+ * <li>ways where the line type is possibly misused
+ * </ul>
+ * See #7812 and #20716 for discussions about this test.
  */
 public class PowerLines extends Test {
-
-    /** Test identifier */
-    protected static final int POWER_LINES = 2501;
+    // Common strings
+    private static final String MINOR_LINE = "minor_line";
+    private static final String BUILDING = "building";
+    private static final String POWER = "power";
+
+    // Test identifiers
+    protected static final int POWER_SUPPORT = 2501;
     protected static final int POWER_CONNECTION = 2502;
+    protected static final int POWER_SEGMENT_LENGTH = 2503;
+    protected static final int POWER_LOCAL_REF_CONTINUITY = 2504;
+    protected static final int POWER_WAY_REF_CONTINUITY = 2505;
+    protected static final int POWER_LINE_TYPE = 2506;
+
+    protected static final String PREFIX = ValidatorPrefHelper.PREFIX + "." + PowerLines.class.getSimpleName();
 
     /** Values for {@code power} key interpreted as power lines */
-    static final Collection<String> POWER_LINE_TAGS = Arrays.asList("line", "minor_line");
+    static final Collection<String> POWER_LINE_TAGS = Arrays.asList("line", MINOR_LINE);
     /** Values for {@code power} key interpreted as power towers */
-    static final Collection<String> POWER_TOWER_TAGS = Arrays.asList("tower", "pole");
+    static final Collection<String> POWER_TOWER_TAGS = Arrays.asList("catenary_mast", "pole", "portal", "tower");
     /** Values for {@code power} key interpreted as power stations */
-    static final Collection<String> POWER_STATION_TAGS = Arrays.asList("station", "sub_station", "substation", "plant", "generator");
+    static final Collection<String> POWER_STATION_TAGS = Arrays.asList("generator", "plant", "substation");
     /** Values for {@code building} key interpreted as power stations */
-    static final Collection<String> BUILDING_STATION_TAGS = Arrays.asList("transformer_tower");
+    static final Collection<String> BUILDING_STATION_TAGS = Collections.singletonList("transformer_tower");
     /** Values for {@code power} key interpreted as allowed power items */
-    static final Collection<String> POWER_ALLOWED_TAGS = Arrays.asList("switch", "transformer", "busbar", "generator", "switchgear",
-            "portal", "terminal", "insulator", "connection");
-
-    private final Set<Node> badConnections = new LinkedHashSet<>();
-    private final Set<Node> missingTowerOrPole = new LinkedHashSet<>();
-
+    static final Collection<String> POWER_INFRASTRUCTURE_TAGS = Arrays.asList("compensator", "connection", "converter",
+            "generator", "insulator", "switch", "switchgear", "terminal", "transformer");
+
+    private double hillyCompensation;
+    private double hillyThreshold;
+    private final Set<Node> badConnections = new HashSet<>();
+    private final Set<Node> missingTags = new HashSet<>();
+    private final Set<Way> wrongLineType = new HashSet<>();
+    private final Set<WaySegment> missingNodes = new HashSet<>();
+    private final Set<OsmPrimitive> refDiscontinuities = new HashSet<>();
+
+    private final List<Set<Node>> segmentRefDiscontinuities = new ArrayList<>();
     private final List<OsmPrimitive> powerStations = new ArrayList<>();
 
+    private final Collection<Way> foundPowerLines = new HashSet<>();
+    /** All waterway segments, grouped by cells */
+    private final Map<Point2D, List<WaySegment>> cellSegmentsWater = new HashMap<>(32);
+
     /**
      * Constructs a new {@code PowerLines} test.
      */
     public PowerLines() {
-        super(tr("Power lines"), tr("Checks for nodes in power lines that do not have a power=tower/pole/connection tag."));
-    }
-
-    @Override
-    public void visit(Way w) {
-        if (w.isUsable()) {
-            if (isPowerLine(w) && !w.hasTag("location", "underground")) {
-                for (Node n : w.getNodes()) {
-                    if (!isPowerTower(n) && !isPowerAllowed(n) && IN_DOWNLOADED_AREA.test(n)
-                        && (!w.isFirstLastNode(n) || !isPowerStation(n))) {
-                        missingTowerOrPole.add(n);
-                    }
-                }
-            } else if (w.isClosed() && isPowerStation(w)) {
-                powerStations.add(w);
-            }
-        }
+        super(tr("Power lines"), tr("Checks if power line missing a support node and " +
+                "for nodes in power lines that do not have a power=tower/pole tag"));
     }
 
@@ -80,9 +102,8 @@
         boolean connectedToUnrelated = false;
         for (Way parent : n.getParentWays()) {
-            if (parent.hasTag("power", "line", "minor_line", "cable"))
+            if (parent.hasTag(POWER, "line", MINOR_LINE, "cable"))
                 nodeInLineOrCable = true;
-            else if (!isRelatedToPower(parent)) {
+            else if (!isRelatedToPower(parent))
                 connectedToUnrelated = true;
-            }
         }
         if (nodeInLineOrCable && connectedToUnrelated)
@@ -90,9 +111,469 @@
     }
 
+    @Override
+    public void visit(Way w) {
+        if (!isPrimitiveUsable(w)) return;
+
+        if (isPowerLine(w) && !w.hasKey("line") && !w.hasTag("location", "underground") && w.isUsable()) {
+            foundPowerLines.add(w);
+        } else if (w.isClosed() && isPowerStation(w)) {
+            powerStations.add(w);
+        } else if (concernsWaterArea(w)) {
+            this.addWaterWaySegments(w);
+        }
+    }
+
+    /**
+     * Add segments to the appropriate cells
+     * @param w The way to add segments from
+     */
+    private void addWaterWaySegments(Way w) {
+        for (int i = 0; i < w.getNodesCount() - 1; i++) {
+            final WaySegment es1 = new WaySegment(w, i);
+            CrossingWays.getSegments(this.cellSegmentsWater, es1.getFirstNode(), es1.getSecondNode()).forEach(list -> list.add(es1));
+        }
+    }
+
+    @Override
+    public void visit(Relation r) {
+        if (r.isMultipolygon() && isPowerStation(r)) {
+            powerStations.add(r);
+        } else if (concernsWaterArea(r)) {
+            r.getMemberPrimitives(Way.class).forEach(this::addWaterWaySegments);
+        }
+    }
+
+    @Override
+    public void startTest(ProgressMonitor progressMonitor) {
+        super.startTest(progressMonitor);
+        hillyCompensation = Config.getPref().getDouble(PREFIX + ".hilly_compensation", 0.2);
+        hillyThreshold = Config.getPref().getDouble(PREFIX + ".hilly_threshold", 4.0);
+    }
+
+    @Override
+    public void endTest() {
+        // Do the actual checks
+        for (Way w : this.foundPowerLines) {
+            powerlineChecks(w);
+        }
+        // Then return the errors
+        for (Node n : missingTags) {
+            if (!isInPowerStation(n)) {
+                errors.add(TestError.builder(this, Severity.WARNING, POWER_SUPPORT)
+                        // the "missing tag" grouping can become broken if the MapCSS message get reworded
+                        .message(tr("missing tag"), tr("node without power=*"))
+                        .primitives(n)
+                        .build());
+            }
+        }
+
+        for (Node n : badConnections) {
+            errors.add(TestError.builder(this, Severity.WARNING, POWER_CONNECTION)
+                    .message(tr("Node connects a power line or cable with an object "
+                            + "which is not related to the power infrastructure"))
+                    .primitives(n)
+                    .build());
+        }
+
+        for (WaySegment s : missingNodes) {
+            errors.add(TestError.builder(this, Severity.WARNING, POWER_SEGMENT_LENGTH)
+                    .message(tr("Possibly missing line support node within power line"))
+                    .primitives(s.getFirstNode(), s.getSecondNode())
+                    .highlightWaySegments(new HashSet<>(Collections.singleton(s)))
+                    .build());
+        }
+
+        for (OsmPrimitive p : refDiscontinuities) {
+            if (p instanceof Way)
+                errors.add(TestError.builder(this, Severity.WARNING, POWER_WAY_REF_CONTINUITY)
+                        .message(tr("Mixed reference numbering"))
+                        .primitives(p)
+                        .build());
+        }
+
+        final String discontinuityMsg = tr("Reference numbering don''t match majority of way''s nodes");
+
+        for (OsmPrimitive p : refDiscontinuities) {
+            if (p instanceof Node)
+                errors.add(TestError.builder(this, Severity.WARNING, POWER_LOCAL_REF_CONTINUITY)
+                        .message(discontinuityMsg)
+                        .primitives(p)
+                        .build());
+        }
+
+        for (Set<Node> nodes : segmentRefDiscontinuities) {
+            errors.add(TestError.builder(this, Severity.WARNING, POWER_LOCAL_REF_CONTINUITY)
+                    .message(discontinuityMsg)
+                    .primitives(nodes)
+                    .build());
+        }
+
+        for (Way w : wrongLineType) {
+            errors.add(TestError.builder(this, Severity.WARNING, POWER_LINE_TYPE)
+                    .message(tr("Possibly wrong power line type used"))
+                    .primitives(w)
+                    .build());
+        }
+
+        super.endTest();
+    }
+
+    /**
+     * The base powerline checks
+     * @param w The powerline to check
+     */
+    private void powerlineChecks(Way w) {
+        final int segmentCount = w.getNodesCount() - 1;
+        final double mean = w.getLength() / segmentCount;
+        final double stdDev = Utils.getStandardDeviation(w.getSegmentLengths(), mean);
+        final boolean isContinuesAsMinorLine = isContinuesAsMinorLine(w);
+        boolean isCrossingWater = false;
+        int poleCount = 0;
+        int towerCount = 0;
+        Node prevNode = w.firstNode();
+
+        double baseThreshold = w.hasTag(POWER, "line") ? 1.6 : 1.8;
+        if (mean / stdDev < hillyThreshold) {
+            //compensate for possibly hilly areas where towers can't be put anywhere
+            baseThreshold += hillyCompensation;
+        }
+
+        for (Node n : w.getNodes()) {
+
+            /// handle power station line connections (e.g. power=line + line=*)
+            if (isConnectedToStationLine(n, w) || n.hasTag(POWER, "connection")) {
+                prevNode = n;
+                continue;   // skip, it would be false positive
+            }
+
+            /// handle missing power line support tags (e.g. tower)
+            if (!isPowerTower(n) && !isPowerInfrastructure(n) && IN_DOWNLOADED_AREA.test(n)
+                    && (!w.isFirstLastNode(n) || !isPowerStation(n)))
+                missingTags.add(n);
+
+            /// handle missing nodes
+            double segmentLen = n.greatCircleDistance(prevNode);
+            final Set<Way> crossingWaterWays = new HashSet<>(8);
+            final Set<ILatLon> crossingPositions = new HashSet<>(8);
+            findCrossings(this.cellSegmentsWater, w, crossingWaterWays, crossingPositions);
+
+            if (!crossingWaterWays.isEmpty()) {
+                double compensation = calculateIntersectingLen(prevNode, crossingPositions);
+                segmentLen -= compensation;
+            }
+
+            if (segmentCount > 4
+                    && segmentLen > mean * baseThreshold
+                    && !isPowerInfrastructure(n)
+                    && IN_DOWNLOADED_AREA.test(n))
+                missingNodes.add(WaySegment.forNodePair(w, prevNode, n));
+
+            /// handle wrong line types
+            if (!crossingWaterWays.isEmpty())
+                isCrossingWater = true;
+
+            if (n.hasTag(POWER, "pole"))
+                poleCount++;
+            else if (n.hasTag(POWER, "tower", "portal"))
+                towerCount++;
+
+            prevNode = n;
+        }
+
+        /// handle ref=* numbering discontinuities
+        if (detectDiscontinuity(w, refDiscontinuities, segmentRefDiscontinuities))
+            refDiscontinuities.add(w);
+
+        /// handle wrong line types
+        if (((poleCount > towerCount && w.hasTag(POWER, "line"))
+                || (poleCount < towerCount && w.hasTag(POWER, MINOR_LINE)
+                && !isCrossingWater
+                && !isContinuesAsMinorLine))
+                && IN_DOWNLOADED_AREA.test(w))
+            wrongLineType.add(w);
+
+    }
+
+    /**
+     * The summarized length (in metres) of a way where a power line hangs over a water area.
+     * @param ref Reference point
+     * @param crossingNodes Crossing nodes, unordered
+     * @return The summarized length (in metres) of a way where a power line hangs over a water area
+     */
+    private static double calculateIntersectingLen(Node ref, Set<ILatLon> crossingNodes) {
+        double min = Double.POSITIVE_INFINITY;
+        double max = Double.NEGATIVE_INFINITY;
+
+        for (ILatLon coor : crossingNodes) {
+
+            if (ref != null && coor != null) {
+                double dist = ref.greatCircleDistance(coor);
+
+                if (dist < min)
+                    min = dist;
+                if (dist > max)
+                    max = dist;
+            }
+        }
+        return max - min;
+    }
+
+    /**
+     * Searches for way intersections, which intersect the {@code pair} attribute.
+     * @param ways collection of ways to search for crossings
+     * @param parent parent powerline way to find crossings for
+     * @param crossingWays found crossing ways
+     * @param crossingPositions collection of the crossing positions
+     * @implNote Inspired by {@code utilsplugin2/selection/NodeWayUtils.java#addWaysIntersectingWay()}
+     */
+    private static void findCrossings(Map<Point2D, List<WaySegment>> ways, Way parent, Set<Way> crossingWays,
+                                      Set<ILatLon> crossingPositions) {
+        int nodesSize = parent.getNodesCount();
+        for (int i = 0; i < nodesSize - 1; i++) {
+            final WaySegment es1 = new WaySegment(parent, i);
+            if (!es1.getFirstNode().isLatLonKnown() || !es1.getSecondNode().isLatLonKnown()) {
+                Logging.warn("PowerLines crossing ways test section skipped " + es1);
+                continue;
+            }
+            for (List<WaySegment> segments : CrossingWays.getSegments(ways, es1.getFirstNode(), es1.getSecondNode())) {
+                for (WaySegment segment : segments) {
+                    if (es1.intersects(segment)) {
+                        final ILatLon ll = Geometry.getSegmentSegmentIntersection(es1.getFirstNode(), es1.getSecondNode(),
+                                segment.getFirstNode(), segment.getSecondNode());
+                        if (ll != null) {
+                            crossingWays.add(es1.getWay());
+                            crossingPositions.add(ll);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /** Power line support features ref=* numbering direction. */
+    private enum NumberingDirection {
+        /** No direction */
+        NONE,
+        /** Numbering follows way direction */
+        SAME,
+        /** Numbering goes opposite way direction */
+        OPPOSITE
+    }
+
+    /** Helper class for reference numbering test. Used for storing continuous reference segment info. */
+    private static class SegmentInfo {
+        /** Node index, follows way direction */
+        private final int startIndex;
+        /** ref=* value at {@link SegmentInfo#startIndex} */
+        private final int startRef;
+        /** Segment length */
+        private final int length;
+        /** Segment direction */
+        private final NumberingDirection direction;
+
+        SegmentInfo(int startIndex, int length, int ref, NumberingDirection direction) {
+            this.startIndex = startIndex;
+            this.length = length;
+            this.direction = direction;
+
+            if (direction == NumberingDirection.SAME)
+                this.startRef = ref - length;
+            else
+                this.startRef = ref + length;
+
+            if (length == 0 && direction != NumberingDirection.NONE) {
+                throw new IllegalArgumentException("When the segment length is zero, the direction should be NONE");
+            }
+        }
+
+        @Override
+        public String toString() {
+            return String.format("SegmentInfo{startIndex=%d, startRef=%d, length=%d, direction=%s}",
+                    startIndex, startRef, length, direction);
+        }
+    }
+
+    /**
+     * Detects ref=* numbering discontinuities in the given way.
+     * @param way checked way
+     * @param nRefDiscontinuities single node ref=* discontinuities
+     * @param sRefDiscontinuities continuous node ref=* discontinuities
+     * @return {@code true} if warning needs to be issued for the whole way
+     */
+    static boolean detectDiscontinuity(Way way, Set<OsmPrimitive> nRefDiscontinuities, List<Set<Node>> sRefDiscontinuities) {
+        final RefChecker checker = new RefChecker(way);
+        final List<SegmentInfo> segments = checker.getSegments();
+        final SegmentInfo referenceSegment = checker.getLongestSegment();
+
+        if (referenceSegment == null)
+            return !segments.isEmpty();
+
+        // collect disconnected ref segments which are not align up to the reference
+        for (SegmentInfo segment : segments) {
+            if (!isSegmentAlign(referenceSegment, segment)) {
+                if (referenceSegment.length == 0)
+                    return true;
+
+                if (segment.length == 0) {
+                    nRefDiscontinuities.add(way.getNode(segment.startIndex));
+                } else {
+                    Set<Node> nodeGroup = new HashSet<>();
+
+                    for (int i = segment.startIndex; i <= segment.startIndex + segment.length; i++) {
+                        nodeGroup.add(way.getNode(i));
+                    }
+                    sRefDiscontinuities.add(nodeGroup);
+                }
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Checks if parameter segments align. The {@code reference} is expected to be at least as long as the {@code candidate}.
+     * @param reference Reference segment to check against
+     * @param candidate Candidate segment
+     * @return {@code true} if the two segments ref=* numbering align
+     */
+    private static boolean isSegmentAlign(SegmentInfo reference, SegmentInfo candidate) {
+        if (reference.direction == NumberingDirection.NONE
+                || reference.direction == candidate.direction
+                || candidate.direction == NumberingDirection.NONE)
+            return Math.abs(candidate.startIndex - reference.startIndex) == Math.abs(candidate.startRef - reference.startRef);
+        return false;
+    }
+
+    /**
+     * Detects continuous reference numbering sequences. Ignores the first and last node because
+     * ways can be connected, and the connection nodes can have different numbering.
+     * <p>
+     * If the numbering switches in the middle of the way, this can also be seen as error,
+     * because line relations would require split ways.
+     */
+    static class RefChecker {
+        private final List<SegmentInfo> segments = new ArrayList<>();
+        private NumberingDirection direction = NumberingDirection.NONE;
+        private Integer startIndex;
+        private Integer previousRef;
+
+        RefChecker(final Way way) {
+            run(way);
+        }
+
+        private void run(Way way) {
+            final int wayLength = way.getNodesCount();
+
+            // first and last node skipped
+            for (int i = 1; i < wayLength - 1; i++) {
+                Node n = way.getNode(i);
+                if (!isPowerTower(n)) {
+                    continue;
+                }
+                maintain(parseRef(n.get("ref")), i);
+            }
+
+            // needed for creation of the last segment
+            maintain(null, wayLength - 1);
+        }
+
+        /**
+         * Maintains class variables and constructs a new segment when necessary.
+         * @param ref   recognised ref=* number
+         * @param index node index in a {@link Way}
+         */
+        private void maintain(Integer ref, int index) {
+            if (previousRef == null && ref != null) {
+                // ref change: null -> number
+                startIndex = index;
+            } else if (previousRef != null && ref == null) {
+                // ref change: number -> null
+                segments.add(new SegmentInfo(startIndex, index - 1 - startIndex, previousRef, direction));
+                direction = NumberingDirection.NONE;    // to fix directionality
+            } else if (previousRef != null) {
+                // ref change: number -> number
+                if (Math.abs(ref - previousRef) != 1) {
+                    segments.add(new SegmentInfo(startIndex, index - 1 - startIndex, previousRef, direction));
+                    startIndex = index;
+                    previousRef = ref;                  // to fix directionality
+                }
+                direction = detectDirection(ref, previousRef);
+            }
+            previousRef = ref;
+        }
+
+        /**
+         * Parses integer tag values. Later can be relatively easily extended or rewritten to handle
+         * complex references like 25/A, 25/B etc.
+         * @param value the value to be parsed
+         * @return parsed int or {@code null} in case of {@link NumberFormatException}
+         */
+        private static Integer parseRef(String value) {
+            try {
+                return Integer.parseInt(value);
+            } catch (NumberFormatException ignore) {
+                Logging.trace("The " + RefChecker.class + " couldn't parse ref=" + value + ", consider rewriting the parser");
+                return null;
+            }
+        }
+
+        /**
+         * Detects numbering direction. The parameters should follow way direction.
+         * @param ref         last known reference value
+         * @param previousRef reference value before {@code ref}
+         * @return recognised direction
+         */
+        private static NumberingDirection detectDirection(int ref, int previousRef) {
+            if (ref > previousRef)
+                return NumberingDirection.SAME;
+            else if (ref < previousRef)
+                return NumberingDirection.OPPOSITE;
+            return NumberingDirection.NONE;
+        }
+
+        /**
+         * Calculates the longest segment.
+         * @return the longest segment, or the lowest index if there are more than one with same length and direction,
+         * or {@code null} if there are more than one with same length and different direction
+         */
+        SegmentInfo getLongestSegment() {
+            final Set<NumberingDirection> directions = EnumSet.noneOf(NumberingDirection.class);
+            int longestLength = -1;
+            int counter = 0;
+            SegmentInfo longest = null;
+
+            for (SegmentInfo segment : segments) {
+                if (segment.length > longestLength) {
+                    longestLength = segment.length;
+                    longest = segment;
+                    counter = 0;
+                    directions.clear();
+                    directions.add(segment.direction);
+                } else if (segment.length == longestLength) {
+                    counter++;
+                    directions.add(segment.direction);
+                }
+            }
+
+            // there are multiple segments with the same longest length and their directions don't match
+            if (counter > 0 && directions.size() > 1)
+                return null;
+
+            return longest;
+        }
+
+        /**
+         * @return the detected segments
+         */
+        List<SegmentInfo> getSegments() {
+            return segments;
+        }
+    }
+
     private static boolean isRelatedToPower(Way way) {
-        if (way.hasTag("power") || way.hasTag("building"))
+        if (way.hasTag(POWER) || way.hasTag(BUILDING))
             return true;
         for (OsmPrimitive ref : way.getReferrers()) {
-            if (ref instanceof Relation && ref.isMultipolygon() && (ref.hasTag("power") || ref.hasTag("building"))) {
+            if (ref instanceof Relation && ref.isMultipolygon() && (ref.hasTag(POWER) || ref.hasTag(BUILDING))) {
                 for (RelationMember rm : ((Relation) ref).getMembers()) {
                     if (way == rm.getMember())
@@ -104,38 +585,51 @@
     }
 
-    @Override
-    public void visit(Relation r) {
-        if (r.isMultipolygon() && isPowerStation(r)) {
-            powerStations.add(r);
-        }
-    }
-
-    @Override
-    public void startTest(ProgressMonitor progressMonitor) {
-        super.startTest(progressMonitor);
-        clearCollections();
-    }
-
-    @Override
-    public void endTest() {
-        for (Node n : missingTowerOrPole) {
-            if (!isInPowerStation(n)) {
-                errors.add(TestError.builder(this, Severity.WARNING, POWER_LINES)
-                        .message(tr("Missing power tower/pole/connection within power line"))
-                        .primitives(n)
-                        .build());
-            }
-        }
-
-        for (Node n : badConnections) {
-            errors.add(TestError.builder(this, Severity.WARNING, POWER_CONNECTION)
-                    .message(tr("Node connects a power line or cable with an object "
-                            + "which is not related to the power infrastructure."))
-                    .primitives(n).build());
-        }
-        clearCollections();
-        super.endTest();
-    }
-
+    /**
+     * Determines if the current node connected to a line which usually used inside power stations.
+     * @param n node to check
+     * @param w parent way of {@code n}
+     * @return {@code true} if {@code n} connected to power=line + line=*
+     */
+    private static boolean isConnectedToStationLine(Node n, Way w) {
+        for (OsmPrimitive p : n.getReferrers()) {
+            if (p instanceof Way && !p.equals(w) && isPowerLine((Way) p) && p.hasKey("line"))
+                return true;
+        }
+        return false;
+    }
+
+    /**
+     * Checks if the way continues as a power=minor_line.
+     * @param way Way to be checked
+     * @return {@code true} if the way continues as a power=minor_line
+     */
+    private static boolean isContinuesAsMinorLine(Way way) {
+        return way.firstNode().referrers(Way.class).filter(referrer -> !way.equals(referrer)).anyMatch(PowerLines::isMinorLine) ||
+                way.lastNode().referrers(Way.class).filter(referrer -> !way.equals(referrer)).anyMatch(PowerLines::isMinorLine);
+    }
+
+    /**
+     * Checks if the given primitive denotes a power=minor_line.
+     * @param p primitive to be checked
+     * @return {@code true} if the given primitive denotes a power=minor_line
+     */
+    private static boolean isMinorLine(OsmPrimitive p) {
+        return p.hasTag(POWER, MINOR_LINE);
+    }
+
+    /**
+     * Check if primitive has a tag that marks it as a water area or boundary of a water area.
+     * @param p the primitive
+     * @return {@code true} if primitive has a tag that marks it as a water area or boundary of a water area
+     */
+    private static boolean concernsWaterArea(OsmPrimitive p) {
+        return p.hasTag("water", "river", "lake") || p.hasKey("waterway") || p.hasTag("natural", "coastline");
+    }
+
+    /**
+     * Checks if the given node is inside a power station.
+     * @param n Node to be checked
+     * @return true if the given node is inside a power station
+     */
     protected final boolean isInPowerStation(Node n) {
         for (OsmPrimitive station : powerStations) {
@@ -165,5 +659,5 @@
      * @return {@code true} if power key is set and equal to line/minor_line
      */
-    protected static final boolean isPowerLine(Way w) {
+    protected static boolean isPowerLine(Way w) {
         return isPowerIn(w, POWER_LINE_TAGS);
     }
@@ -172,16 +666,16 @@
      * Determines if the specified primitive denotes a power station.
      * @param p The primitive to be tested
-     * @return {@code true} if power key is set and equal to station/sub_station/plant
-     */
-    protected static final boolean isPowerStation(OsmPrimitive p) {
+     * @return {@code true} if power key is set and equal to generator/substation/plant
+     */
+    protected static boolean isPowerStation(OsmPrimitive p) {
         return isPowerIn(p, POWER_STATION_TAGS) || isBuildingIn(p, BUILDING_STATION_TAGS);
     }
 
     /**
-     * Determines if the specified node denotes a power tower/pole.
+     * Determines if the specified node denotes a power support feature.
      * @param n The node to be tested
-     * @return {@code true} if power key is set and equal to tower/pole
-     */
-    protected static final boolean isPowerTower(Node n) {
+     * @return {@code true} if power key is set and equal to pole/tower/portal/catenary_mast
+     */
+    protected static boolean isPowerTower(Node n) {
         return isPowerIn(n, POWER_TOWER_TAGS);
     }
@@ -190,8 +684,9 @@
      * Determines if the specified node denotes a power infrastructure allowed on a power line.
      * @param n The node to be tested
-     * @return True if power key is set and equal to switch/tranformer/busbar/generator
-     */
-    protected static final boolean isPowerAllowed(Node n) {
-        return isPowerIn(n, POWER_ALLOWED_TAGS);
+     * @return {@code true} if power key is set and equal to compensator/converter/generator/insulator
+     * /switch/switchgear/terminal/transformer
+     */
+    protected static boolean isPowerInfrastructure(Node n) {
+        return isPowerIn(n, POWER_INFRASTRUCTURE_TAGS);
     }
 
@@ -203,5 +698,5 @@
      */
     private static boolean isPowerIn(OsmPrimitive p, Collection<String> values) {
-        return p.hasTag("power", values);
+        return p.hasTag(POWER, values);
     }
 
@@ -213,11 +708,19 @@
      */
     private static boolean isBuildingIn(OsmPrimitive p, Collection<String> values) {
-        return p.hasTag("building", values);
-    }
-
-    private void clearCollections() {
+        return p.hasTag(BUILDING, values);
+    }
+
+    @Override
+    public void clear() {
+        super.clear();
+        badConnections.clear();
+        cellSegmentsWater.clear();
+        foundPowerLines.clear();
+        missingNodes.clear();
+        missingTags.clear();
         powerStations.clear();
-        badConnections.clear();
-        missingTowerOrPole.clear();
+        refDiscontinuities.clear();
+        segmentRefDiscontinuities.clear();
+        wrongLineType.clear();
     }
 }
Index: /trunk/src/org/openstreetmap/josm/data/validation/util/ValUtil.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/validation/util/ValUtil.java	(revision 18552)
+++ /trunk/src/org/openstreetmap/josm/data/validation/util/ValUtil.java	(revision 18553)
@@ -11,6 +11,8 @@
 
 import org.openstreetmap.josm.data.coor.EastNorth;
+import org.openstreetmap.josm.data.coor.ILatLon;
 import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.projection.ProjectionRegistry;
 import org.openstreetmap.josm.data.validation.OsmValidator;
 import org.openstreetmap.josm.tools.CheckParameterUtil;
@@ -45,10 +47,12 @@
         double griddetail = OsmValidator.getGridDetail();
 
+        final EastNorth en1 = n1.getEastNorth();
+        final EastNorth en2 = n2.getEastNorth();
         // First, round coordinates
         // CHECKSTYLE.OFF: SingleSpaceSeparator
-        long x0 = Math.round(n1.getEastNorth().east()  * griddetail);
-        long y0 = Math.round(n1.getEastNorth().north() * griddetail);
-        long x1 = Math.round(n2.getEastNorth().east()  * griddetail);
-        long y1 = Math.round(n2.getEastNorth().north() * griddetail);
+        long x0 = Math.round(en1.east()  * griddetail);
+        long y0 = Math.round(en1.north() * griddetail);
+        long x1 = Math.round(en2.east()  * griddetail);
+        long y1 = Math.round(en2.north() * griddetail);
         // CHECKSTYLE.ON: SingleSpaceSeparator
 
@@ -67,8 +71,8 @@
         // Then floor coordinates, in case the way is in the border of the cell.
         // CHECKSTYLE.OFF: SingleSpaceSeparator
-        x0 = (long) Math.floor(n1.getEastNorth().east()  * griddetail);
-        y0 = (long) Math.floor(n1.getEastNorth().north() * griddetail);
-        x1 = (long) Math.floor(n2.getEastNorth().east()  * griddetail);
-        y1 = (long) Math.floor(n2.getEastNorth().north() * griddetail);
+        x0 = (long) Math.floor(en1.east()  * griddetail);
+        y0 = (long) Math.floor(en1.north() * griddetail);
+        x1 = (long) Math.floor(en2.east()  * griddetail);
+        y1 = (long) Math.floor(en2.north() * griddetail);
         // CHECKSTYLE.ON: SingleSpaceSeparator
 
@@ -100,7 +104,23 @@
      */
     public static List<Point2D> getSegmentCells(Node n1, Node n2, double gridDetail) {
+        return getSegmentCells((ILatLon) n1, n2, gridDetail);
+    }
+
+    /**
+     * Returns the coordinates of all cells in a grid that a line between 2 nodes intersects with.
+     *
+     * @param n1 The first latlon.
+     * @param n2 The second latlon.
+     * @param gridDetail The detail of the grid. Bigger values give smaller
+     * cells, but a bigger number of them.
+     * @return A list with the coordinates of all cells
+     * @throws IllegalArgumentException if n1 or n2 is {@code null} or without coordinates
+     * @since 18553
+     */
+    public static List<Point2D> getSegmentCells(ILatLon n1, ILatLon n2, double gridDetail) {
         CheckParameterUtil.ensureParameterNotNull(n1, "n1");
         CheckParameterUtil.ensureParameterNotNull(n1, "n2");
-        return getSegmentCells(n1.getEastNorth(), n2.getEastNorth(), gridDetail);
+        return getSegmentCells(n1.getEastNorth(ProjectionRegistry.getProjection()), n2.getEastNorth(ProjectionRegistry.getProjection()),
+                gridDetail);
     }
 
Index: /trunk/src/org/openstreetmap/josm/gui/dialogs/InspectPrimitiveDataText.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/dialogs/InspectPrimitiveDataText.java	(revision 18552)
+++ /trunk/src/org/openstreetmap/josm/gui/dialogs/InspectPrimitiveDataText.java	(revision 18553)
@@ -33,4 +33,5 @@
 import org.openstreetmap.josm.tools.Geometry;
 import org.openstreetmap.josm.tools.Pair;
+import org.openstreetmap.josm.tools.Utils;
 
 /**
@@ -175,7 +176,15 @@
                     ProjectionRegistry.getProjection().eastNorth2latlon(Geometry.getCentroid(((IWay<?>) o).getNodes()))));
             if (o instanceof Way) {
-                double dist = ((Way) o).getLength();
-                String distText = SystemOfMeasurement.getSystemOfMeasurement().getDistText(dist);
-                add(tr("Length: {0}", distText));
+                double length = ((Way) o).getLength();
+                String lenText = SystemOfMeasurement.getSystemOfMeasurement().getDistText(length);
+                add(tr("Length: {0}", lenText));
+
+                double avgNodeDistance = length / (((Way) o).getNodesCount() - 1);
+                String nodeDistText = SystemOfMeasurement.getSystemOfMeasurement().getDistText(avgNodeDistance);
+                add(tr("Average segment length: {0}", nodeDistText));
+
+                double stdDev = Utils.getStandardDeviation(((Way) o).getSegmentLengths(), avgNodeDistance);
+                String stdDevText = SystemOfMeasurement.getSystemOfMeasurement().getDistText(stdDev);
+                add(tr("Standard deviation: {0}", stdDevText));
             }
             if (o instanceof Way && ((Way) o).concernsArea() && ((Way) o).isClosed()) {
Index: /trunk/src/org/openstreetmap/josm/tools/Geometry.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/tools/Geometry.java	(revision 18552)
+++ /trunk/src/org/openstreetmap/josm/tools/Geometry.java	(revision 18553)
@@ -288,11 +288,14 @@
      * @param p4 the coordinates of the end point of the second specified line segment
      * @return EastNorth null if no intersection was found, the EastNorth coordinates of the intersection otherwise
+     * @see #getSegmentSegmentIntersection(ILatLon, ILatLon, ILatLon, ILatLon)
      */
     public static EastNorth getSegmentSegmentIntersection(EastNorth p1, EastNorth p2, EastNorth p3, EastNorth p4) {
-
-        CheckParameterUtil.ensureThat(p1.isValid(), () -> p1 + " invalid");
-        CheckParameterUtil.ensureThat(p2.isValid(), () -> p2 + " invalid");
-        CheckParameterUtil.ensureThat(p3.isValid(), () -> p3 + " invalid");
-        CheckParameterUtil.ensureThat(p4.isValid(), () -> p4 + " invalid");
+        // see the ILatLon version for an explanation why the checks are in the if statement
+        if (!(p1.isValid() && p2.isValid() && p3.isValid() && p4.isValid())) {
+            CheckParameterUtil.ensureThat(p1.isValid(), () -> p1 + " invalid");
+            CheckParameterUtil.ensureThat(p2.isValid(), () -> p2 + " invalid");
+            CheckParameterUtil.ensureThat(p3.isValid(), () -> p3 + " invalid");
+            CheckParameterUtil.ensureThat(p4.isValid(), () -> p4 + " invalid");
+        }
 
         double x1 = p1.getX();
@@ -304,4 +307,61 @@
         double x4 = p4.getX();
         double y4 = p4.getY();
+        double[] en = getSegmentSegmentIntersection(x1, y1, x2, y2, x3, y3, x4, y4);
+        if (en != null && en.length == 2) {
+            return new EastNorth(en[0], en[1]);
+        }
+        return null;
+    }
+
+    /**
+     * Finds the intersection of two line segments.
+     * @param p1 the coordinates of the start point of the first specified line segment
+     * @param p2 the coordinates of the end point of the first specified line segment
+     * @param p3 the coordinates of the start point of the second specified line segment
+     * @param p4 the coordinates of the end point of the second specified line segment
+     * @return LatLon null if no intersection was found, the LatLon coordinates of the intersection otherwise
+     * @see #getSegmentSegmentIntersection(EastNorth, EastNorth, EastNorth, EastNorth)
+     * @since 18553
+     */
+    public static ILatLon getSegmentSegmentIntersection(ILatLon p1, ILatLon p2, ILatLon p3, ILatLon p4) {
+        // Avoid lambda creation if at all possible -- this pretty much removes all memory allocations
+        // from this method (11.4 GB to 0) when testing #20716 with Mesa County, CO (overpass download).
+        // There was also a 2/3 decrease in CPU samples for the method.
+        if (!(p1.isLatLonKnown() && p2.isLatLonKnown() && p3.isLatLonKnown() && p4.isLatLonKnown())) {
+            CheckParameterUtil.ensureThat(p1.isLatLonKnown(), () -> p1 + " invalid");
+            CheckParameterUtil.ensureThat(p2.isLatLonKnown(), () -> p2 + " invalid");
+            CheckParameterUtil.ensureThat(p3.isLatLonKnown(), () -> p3 + " invalid");
+            CheckParameterUtil.ensureThat(p4.isLatLonKnown(), () -> p4 + " invalid");
+        }
+
+        double x1 = p1.lon();
+        double y1 = p1.lat();
+        double x2 = p2.lon();
+        double y2 = p2.lat();
+        double x3 = p3.lon();
+        double y3 = p3.lat();
+        double x4 = p4.lon();
+        double y4 = p4.lat();
+        double[] en = getSegmentSegmentIntersection(x1, y1, x2, y2, x3, y3, x4, y4);
+        if (en != null && en.length == 2) {
+            return new LatLon(en[1], en[0]);
+        }
+        return null;
+    }
+
+    /**
+     * Get the segment segment intersection of two line segments
+     * @param x1 The x coordinate of the first point (first segment)
+     * @param y1 The y coordinate of the first point (first segment)
+     * @param x2 The x coordinate of the second point (first segment)
+     * @param y2 The y coordinate of the second point (first segment)
+     * @param x3 The x coordinate of the third point (second segment)
+     * @param y3 The y coordinate of the third point (second segment)
+     * @param x4 The x coordinate of the fourth point (second segment)
+     * @param y4 The y coordinate of the fourth point (second segment)
+     * @return {@code null} if no intersection was found, otherwise [x, y]
+     */
+    private static double[] getSegmentSegmentIntersection(double x1, double y1, double x2, double y2, double x3, double y3,
+            double x4, double y4) {
 
         //TODO: do this locally.
@@ -334,5 +394,5 @@
                 if (u < 0) u = 0;
                 if (u > 1) u = 1.0;
-                return new EastNorth(x1+a1*u, y1+a2*u);
+                return new double[] {x1+a1*u, y1+a2*u};
             } else {
                 return null;
Index: /trunk/src/org/openstreetmap/josm/tools/Utils.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/tools/Utils.java	(revision 18552)
+++ /trunk/src/org/openstreetmap/josm/tools/Utils.java	(revision 18553)
@@ -487,5 +487,5 @@
      */
     public static String md5Hex(String data) {
-        MessageDigest md = null;
+        MessageDigest md;
         try {
             md = MessageDigest.getInstance("MD5");
@@ -1302,4 +1302,42 @@
             return distance > 0 && distance <= 2;
         }
+    }
+
+    /**
+     * Calculates the <a href="https://en.wikipedia.org/wiki/Standard_deviation">standard deviation</a> of population.
+     * @param values an array of values
+     * @return standard deviation of the given array, or -1.0 if the array has less than two values
+     * @see #getStandardDeviation(double[], double)
+     * @since 18553
+     */
+    public static double getStandardDeviation(double[] values) {
+        return getStandardDeviation(values, Double.NaN);
+    }
+
+    /**
+     * Calculates the <a href="https://en.wikipedia.org/wiki/Standard_deviation">standard deviation</a> of population with the given
+     * mean value.
+     * @param values an array of values
+     * @param mean precalculated average value of the array
+     * @return standard deviation of the given array, or -1.0 if the array has less than two values
+     * @see #getStandardDeviation(double[])
+     * @since 18553
+     */
+    public static double getStandardDeviation(double[] values, double mean) {
+        if (values.length < 2) {
+            return -1.0;
+        }
+
+        double standardDeviation = 0;
+
+        if (Double.isNaN(mean)) {
+            mean = Arrays.stream(values).average().orElse(0);
+        }
+
+        for (double length : values) {
+            standardDeviation += Math.pow(length - mean, 2);
+        }
+
+        return Math.sqrt(standardDeviation / values.length);
     }
 
@@ -1705,5 +1743,5 @@
     public static Date getJavaExpirationDate() {
         try {
-            Object value = null;
+            Object value;
             Class<?> c = Class.forName("com.sun.deploy.config.BuiltInProperties");
             try {
Index: /trunk/test/unit/org/openstreetmap/josm/data/osm/WayTest.java
===================================================================
--- /trunk/test/unit/org/openstreetmap/josm/data/osm/WayTest.java	(revision 18552)
+++ /trunk/test/unit/org/openstreetmap/josm/data/osm/WayTest.java	(revision 18553)
@@ -8,4 +8,5 @@
 
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashSet;
 
@@ -49,7 +50,7 @@
         Way way = new Way(1);
         assertFalse(way.getBBox().isValid());
-        way.setNodes(Arrays.asList(n1));
+        way.setNodes(Collections.singletonList(n1));
         assertFalse(way.getBBox().isValid());
-        way.setNodes(Arrays.asList(n2));
+        way.setNodes(Collections.singletonList(n2));
         assertTrue(way.getBBox().isValid());
         way.setNodes(Arrays.asList(n1, n2));
@@ -116,5 +117,5 @@
         assertEquals(Arrays.asList(n1, n2, n1), way.getNodes());
         way.setNodes(Arrays.asList(n1, n2, n3, n4, n1));
-        way.removeNodes(new HashSet<>(Arrays.asList(n1)));
+        way.removeNodes(new HashSet<>(Collections.singletonList(n1)));
         assertEquals(Arrays.asList(n2, n3, n4, n2), way.getNodes());
     }
@@ -135,3 +136,27 @@
         assertThrows(IllegalArgumentException.class, () -> new Way().load(new NodeData()));
     }
+
+    @Test
+    void getLongestSegmentLength() {
+        DataSet ds = new DataSet();
+        Node n1 = new Node(1);
+        Node n2 = new Node(2);
+        Node n3 = new Node(3);
+        Node n4 = new Node(4);
+        n1.setCoor(new LatLon(0.01, 0.01));
+        n2.setCoor(new LatLon(0.02, 0.02));
+        n3.setCoor(new LatLon(0.03, 0.03));
+        n4.setCoor(new LatLon(0.05, 0.05));
+        ds.addPrimitive(n1);
+        ds.addPrimitive(n2);
+        ds.addPrimitive(n3);
+        ds.addPrimitive(n4);
+        Way way = new Way(1);
+        ds.addPrimitive(way);
+
+        assertEquals(0.0, way.getLongestSegmentLength());
+        way.setNodes(Arrays.asList(n1, n2, n2, n3, n4));
+
+        assertEquals(3148.5902810874577, way.getLongestSegmentLength());
+    }
 }
Index: /trunk/test/unit/org/openstreetmap/josm/data/validation/tests/PowerLinesTest.java
===================================================================
--- /trunk/test/unit/org/openstreetmap/josm/data/validation/tests/PowerLinesTest.java	(revision 18553)
+++ /trunk/test/unit/org/openstreetmap/josm/data/validation/tests/PowerLinesTest.java	(revision 18553)
@@ -0,0 +1,121 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.validation.tests;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.TagMap;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.testutils.JOSMTestRules;
+import org.openstreetmap.josm.testutils.annotations.BasicPreferences;
+
+/**
+ * Test class for {@link PowerLines}
+ * @since 18553
+ */
+@BasicPreferences
+class PowerLinesTest {
+    private PowerLines powerLines;
+    private DataSet ds;
+
+    @RegisterExtension
+    static JOSMTestRules josmTestRules = new JOSMTestRules().projection();
+
+    @BeforeEach
+    public void setUp() throws Exception {
+        ds = new DataSet();
+
+        powerLines = new PowerLines();
+        powerLines.initialize();
+        powerLines.startTest(null);
+    }
+
+    @Test
+    void testNoBreakInLine() {
+        Way powerline = new Way();
+        powerline.setKeys(new TagMap("power", "line"));
+        ds.addPrimitive(powerline);
+
+        for (int i = 0; i < 10; i++) {
+            Node node = new Node(new LatLon(0, 0.001 * i));
+            node.setKeys(new TagMap("power", "tower"));
+            ds.addPrimitive(node);
+            powerline.addNode(node);
+        }
+        powerLines.visit(powerline);
+        powerLines.endTest();
+        assertTrue(powerLines.getErrors().isEmpty());
+    }
+
+    @Test
+    void testBreakInLine() {
+        Way powerline = new Way();
+        powerline.setKeys(new TagMap("power", "line"));
+        ds.addPrimitive(powerline);
+
+        for (int i = 0; i < 10; i++) {
+            if (i != 4 && i != 5) {
+                Node node = new Node(new LatLon(0, 0.001 * i));
+                node.setKeys(new TagMap("power", "tower"));
+                ds.addPrimitive(node);
+                powerline.addNode(node);
+            }
+        }
+        powerLines.visit(powerline);
+        powerLines.endTest();
+        assertFalse(powerLines.getErrors().isEmpty());
+    }
+
+    @Test
+    void testConnectionAndRefInLine() {
+        Way powerline = new Way();
+        powerline.setKeys(new TagMap("power", "line"));
+        ds.addPrimitive(powerline);
+
+        int connectionCount = 0;
+
+        for (int i = 0; i < 10; i++) {
+            Node node = new Node(new LatLon(0, 0.001 * i));
+            node.setKeys(new TagMap("power", "tower", "ref", Integer.toString(i)));
+            if (i == 4 || i == 5) {
+                node.setKeys(new TagMap("power", "connection"));
+                connectionCount++;
+            }
+            if (i > 5) {
+                node.setKeys(new TagMap("power", "tower", "ref", Integer.toString(i - connectionCount)));
+            }
+            ds.addPrimitive(node);
+            powerline.addNode(node);
+        }
+        powerLines.visit(powerline);
+        powerLines.endTest();
+        assertTrue(powerLines.getErrors().isEmpty());
+    }
+
+    @Test
+    void testRefDiscontinuityInLine() {
+        Way powerline = new Way();
+        powerline.setKeys(new TagMap("power", "minor_line"));
+        ds.addPrimitive(powerline);
+
+        for (int i = 0; i < 10; i++) {
+            Node node = new Node(new LatLon(0, 0.001 * i));
+            node.setKeys(new TagMap("power", "tower", "ref", Integer.toString(i)));
+            if (i < 4) {
+                // add discontinuity
+                node.setKeys(new TagMap("power", "tower", "ref", Integer.toString(i + 1)));
+            }
+            ds.addPrimitive(node);
+            powerline.addNode(node);
+        }
+        powerLines.visit(powerline);
+        powerLines.endTest();
+        assertFalse(powerLines.getErrors().isEmpty());
+    }
+}
Index: /trunk/test/unit/org/openstreetmap/josm/tools/UtilsTest.java
===================================================================
--- /trunk/test/unit/org/openstreetmap/josm/tools/UtilsTest.java	(revision 18552)
+++ /trunk/test/unit/org/openstreetmap/josm/tools/UtilsTest.java	(revision 18553)
@@ -128,5 +128,5 @@
     @Test
     void testPositionListString() {
-        assertEquals("1", Utils.getPositionListString(Arrays.asList(1)));
+        assertEquals("1", Utils.getPositionListString(Collections.singletonList(1)));
         assertEquals("1-2", Utils.getPositionListString(Arrays.asList(1, 2)));
         assertEquals("1-3", Utils.getPositionListString(Arrays.asList(1, 2, 3)));
@@ -251,5 +251,5 @@
     @Test
     void testJoinAsHtmlUnorderedList() {
-        List<? extends Object> items = Arrays.asList("1", Integer.valueOf(2));
+        List<?> items = Arrays.asList("1", 2);
         assertEquals("<ul><li>1</li><li>2</li></ul>", Utils.joinAsHtmlUnorderedList(items));
         assertEquals("<ul></ul>", Utils.joinAsHtmlUnorderedList(Collections.emptyList()));
@@ -532,3 +532,16 @@
         assertEquals("Hello World", output);
     }
+
+    /**
+     * Test of {@link Utils#getStandardDeviation(double[])} and {@link Utils#getStandardDeviation(double[], double)}
+     */
+    @Test
+    void testGetStandardDeviation() {
+        assertEquals(0.0, Utils.getStandardDeviation(new double[]{1, 1, 1, 1}));
+        assertEquals(0.0, Utils.getStandardDeviation(new double[]{1, 1, 1, 1}, 1.0));
+        assertEquals(0.5, Utils.getStandardDeviation(new double[]{1, 1, 2, 2}));
+
+        assertEquals(-1.0, Utils.getStandardDeviation(new double[]{}));
+        assertEquals(-1.0, Utils.getStandardDeviation(new double[]{0}));
+    }
 }
