Index: 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/src/org/openstreetmap/josm/data/osm/Way.java	(revision 18531)
+++ b/src/org/openstreetmap/josm/data/osm/Way.java	(date 1659458768470)
@@ -10,6 +10,7 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.stream.Collectors;
+import java.util.stream.DoubleStream;
 import java.util.stream.IntStream;
 
 import org.openstreetmap.josm.data.coor.ILatLon;
@@ -608,7 +609,23 @@
 
     @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;
     }
 
     /**
@@ -647,24 +664,40 @@
         return length;
     }
 
+    /**
+     * 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 xxx
+     */
+    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
      * @since 8320
      */
     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: 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/src/org/openstreetmap/josm/data/validation/tests/PowerLines.java	(revision 18531)
+++ b/src/org/openstreetmap/josm/data/validation/tests/PowerLines.java	(date 1659454229751)
@@ -1,77 +1,102 @@
 // License: GPL. For details, see LICENSE file.
 package org.openstreetmap.josm.data.validation.tests;
 
+import static org.openstreetmap.josm.gui.MainApplication.getLayerManager;
 import static org.openstreetmap.josm.tools.I18n.tr;
 
 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.HashSet;
 import java.util.List;
 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;
 import org.openstreetmap.josm.data.osm.Relation;
 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.Pair;
+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;
+    // 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();
+    private double hillyCompensation;
+    private double hillyThreshold;
 
     /** Values for {@code power} key interpreted as power lines */
     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");
+    static final Collection<String> POWER_INFRASTRUCTURE_TAGS = Arrays.asList("compensator", "connection", "converter",
+            "generator", "insulator", "switch", "switchgear", "terminal", "transformer");
 
-    private final Set<Node> badConnections = new LinkedHashSet<>();
-    private final Set<Node> missingTowerOrPole = new LinkedHashSet<>();
+    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> datasetWaterways = new HashSet<>(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."));
+        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"));
     }
 
-    @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);
-            }
-        }
+    /** Power line support features ref=* numbering direction. */
+    private enum NumberingDirection {
+        /** No direction */
+        NONE,
+        /** Numbering follows way direction */
+        SAME,
+        /** Numbering goes opposite way direction */
+        OPPOSITE
     }
 
     @Override
@@ -81,26 +106,91 @@
         for (Way parent : n.getParentWays()) {
             if (parent.hasTag("power", "line", "minor_line", "cable"))
                 nodeInLineOrCable = true;
-            else if (!isRelatedToPower(parent)) {
+            else if (!isRelatedToPower(parent))
                 connectedToUnrelated = true;
-            }
         }
         if (nodeInLineOrCable && connectedToUnrelated)
             badConnections.add(n);
     }
 
-    private static boolean isRelatedToPower(Way way) {
-        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"))) {
-                for (RelationMember rm : ((Relation) ref).getMembers()) {
-                    if (way == rm.getMember())
-                        return true;
+    @Override
+    public void visit(Way w) {
+        if (!isPrimitiveUsable(w)) return;
+
+        if (isPowerLine(w) && !w.hasKey("line") && !w.hasTag("location", "underground")) {
+            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 Pair<Node, Node> pair = Pair.create(prevNode, n);
+                final Set<Way> crossingWaterWays = new HashSet<>(8);
+                final Set<ILatLon> crossingPositions = new HashSet<>(8);
+                findCrossings(datasetWaterways, w, pair, 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);
+
+        } else if (w.isClosed() && isPowerStation(w)) {
+            powerStations.add(w);
         }
-        return false;
     }
 
     @Override
@@ -113,15 +203,29 @@
     @Override
     public void startTest(ProgressMonitor progressMonitor) {
         super.startTest(progressMonitor);
-        clearCollections();
+        // the test run can take a bit of time, show detailed progress
+        setShowElements(true);
+
+        hillyCompensation = Config.getPref().getDouble(PREFIX + ".hilly_compensation", 0.2);
+        hillyThreshold = Config.getPref().getDouble(PREFIX + ".hilly_threshold", 4.0);
+
+        // collect all waterways
+        getLayerManager()
+                .getActiveDataSet()
+                .getWays()
+                .stream()
+                .filter(way -> way.isUsable() && (concernsWaterArea(way)
+                        || way.referrers(Relation.class).anyMatch(PowerLines::concernsWaterArea)))
+                .forEach(datasetWaterways::add);
     }
 
     @Override
     public void endTest() {
-        for (Node n : missingTowerOrPole) {
+        for (Node n : missingTags) {
             if (!isInPowerStation(n)) {
-                errors.add(TestError.builder(this, Severity.WARNING, POWER_LINES)
-                        .message(tr("Missing power tower/pole/connection within power line"))
+                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());
             }
@@ -130,13 +234,375 @@
         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());
+                            + "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());
         }
-        clearCollections();
+
+        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 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
+     * @param parent parent way for {@code pair} param
+     * @param pair node pair among which search for another way
+     * @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(Collection<Way> ways, Way parent, Pair<Node, Node> pair, Set<Way> crossingWays,
+                                      Set<ILatLon> crossingPositions) {
+        for (Way way : ways) {
+            if (way.isUsable()
+                    && !crossingWays.contains(way)
+                    && way.getBBox().intersects(parent.getBBox())) {
+                for (Pair<Node, Node> pair2 : way.getNodePairs(false)) {
+                    ILatLon ll = Geometry.getSegmentSegmentIntersection(pair.a, pair.b, pair2.a, pair2.b);
+                    if (ll != null) {
+                        crossingPositions.add(ll);
+                        crossingWays.add(way);
+                    }
+                }
+            }
+        }
+    }
+
+    /** 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"))
+            return true;
+        for (OsmPrimitive ref : way.getReferrers()) {
+            if (ref instanceof Relation && ref.isMultipolygon() && (ref.hasTag("power") || ref.hasTag("building"))) {
+                for (RelationMember rm : ((Relation) ref).getMembers()) {
+                    if (way == rm.getMember())
+                        return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 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) {
             List<List<Node>> nodesLists = new ArrayList<>();
@@ -164,35 +630,36 @@
      * @param w The way to be tested
      * @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);
     }
 
     /**
      * 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
+     * @return {@code true} if power key is set and equal to generator/substation/plant
      */
-    protected static final boolean isPowerStation(OsmPrimitive p) {
+    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
+     * @return {@code true} if power key is set and equal to pole/tower/portal/catenary_mast
      */
-    protected static final boolean isPowerTower(Node n) {
+    protected static boolean isPowerTower(Node n) {
         return isPowerIn(n, POWER_TOWER_TAGS);
     }
 
     /**
      * 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
+     * @return {@code true} if power key is set and equal to compensator/converter/generator/insulator
+     * /switch/switchgear/terminal/transformer
      */
-    protected static final boolean isPowerAllowed(Node n) {
-        return isPowerIn(n, POWER_ALLOWED_TAGS);
+    protected static boolean isPowerInfrastructure(Node n) {
+        return isPowerIn(n, POWER_INFRASTRUCTURE_TAGS);
     }
 
     /**
@@ -215,9 +682,16 @@
         return p.hasTag("building", values);
     }
 
-    private void clearCollections() {
+    @Override
+    public void clear() {
+        super.clear();
         powerStations.clear();
         badConnections.clear();
-        missingTowerOrPole.clear();
+        missingTags.clear();
+        missingNodes.clear();
+        wrongLineType.clear();
+        refDiscontinuities.clear();
+        segmentRefDiscontinuities.clear();
+        datasetWaterways.clear();
     }
 }
Index: src/org/openstreetmap/josm/gui/dialogs/InspectPrimitiveDataText.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/org/openstreetmap/josm/gui/dialogs/InspectPrimitiveDataText.java b/src/org/openstreetmap/josm/gui/dialogs/InspectPrimitiveDataText.java
--- a/src/org/openstreetmap/josm/gui/dialogs/InspectPrimitiveDataText.java	(revision 18531)
+++ b/src/org/openstreetmap/josm/gui/dialogs/InspectPrimitiveDataText.java	(date 1659452763597)
@@ -32,6 +32,7 @@
 import org.openstreetmap.josm.data.projection.proj.TransverseMercator.Hemisphere;
 import org.openstreetmap.josm.tools.Geometry;
 import org.openstreetmap.josm.tools.Pair;
+import org.openstreetmap.josm.tools.Utils;
 
 /**
  * Textual representation of primitive contents, used in {@code InspectPrimitiveDialog}.
@@ -174,9 +175,17 @@
             add(tr("Centroid: "), toStringCSV(false,
                     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()) {
                 double area = Geometry.closedWayArea((Way) o);
Index: src/org/openstreetmap/josm/tools/Geometry.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/org/openstreetmap/josm/tools/Geometry.java b/src/org/openstreetmap/josm/tools/Geometry.java
--- a/src/org/openstreetmap/josm/tools/Geometry.java	(revision 18531)
+++ b/src/org/openstreetmap/josm/tools/Geometry.java	(date 1659460727435)
@@ -287,13 +287,16 @@
      * @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 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();
         double y1 = p1.getY();
@@ -303,6 +306,63 @@
         double y3 = p3.getY();
         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 xxx
+     */
+    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.
         //TODO: remove this check after careful testing
@@ -333,7 +393,7 @@
             if (u > -1e-8 && u < 1+1e-8 && v > -1e-8 && v < 1+1e-8) {
                 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: src/org/openstreetmap/josm/tools/Utils.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/src/org/openstreetmap/josm/tools/Utils.java b/src/org/openstreetmap/josm/tools/Utils.java
--- a/src/org/openstreetmap/josm/tools/Utils.java	(revision 18531)
+++ b/src/org/openstreetmap/josm/tools/Utils.java	(date 1659452763688)
@@ -226,7 +226,7 @@
         if (array != null) {
             return Arrays.copyOf(array, array.length);
         }
-        return array;
+        return null;
     }
 
     /**
@@ -239,7 +239,7 @@
         if (array != null) {
             return Arrays.copyOf(array, array.length);
         }
-        return array;
+        return null;
     }
 
     /**
@@ -252,7 +252,7 @@
         if (array != null) {
             return Arrays.copyOf(array, array.length);
         }
-        return array;
+        return null;
     }
 
     /**
@@ -265,7 +265,7 @@
         if (array != null) {
             return Arrays.copyOf(array, array.length);
         }
-        return array;
+        return null;
     }
 
     /**
@@ -1303,6 +1303,44 @@
         }
     }
 
+    /**
+     * 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 xxx
+     */
+    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 xxx
+     */
+    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);
+    }
+
     /**
      * A ForkJoinWorkerThread that will always inherit caller permissions,
      * unlike JDK's InnocuousForkJoinWorkerThread, used if a security manager exists.
Index: test/unit/org/openstreetmap/josm/data/osm/WayTest.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/test/unit/org/openstreetmap/josm/data/osm/WayTest.java b/test/unit/org/openstreetmap/josm/data/osm/WayTest.java
--- a/test/unit/org/openstreetmap/josm/data/osm/WayTest.java	(revision 18531)
+++ b/test/unit/org/openstreetmap/josm/data/osm/WayTest.java	(date 1659452763768)
@@ -134,4 +134,28 @@
     void testLoadIAE() {
         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: test/unit/org/openstreetmap/josm/data/validation/tests/PowerLinesTest.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/test/unit/org/openstreetmap/josm/data/validation/tests/PowerLinesTest.java b/test/unit/org/openstreetmap/josm/data/validation/tests/PowerLinesTest.java
new file mode 100644
--- /dev/null	(date 1659451627154)
+++ b/test/unit/org/openstreetmap/josm/data/validation/tests/PowerLinesTest.java	(date 1659451627154)
@@ -0,0 +1,116 @@
+// 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.openstreetmap.josm.JOSMFixture;
+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.gui.MainApplication;
+import org.openstreetmap.josm.gui.layer.OsmDataLayer;
+
+
+public class PowerLinesTest {
+    private PowerLines powerLines;
+    private DataSet ds;
+
+    @BeforeEach
+    public void setUp() throws Exception {
+        JOSMFixture.createUnitTestFixture().init();
+        ds = new DataSet();
+        MainApplication.getLayerManager().addLayer(new OsmDataLayer(ds, null, null));
+
+        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: test/unit/org/openstreetmap/josm/tools/UtilsTest.java
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/test/unit/org/openstreetmap/josm/tools/UtilsTest.java b/test/unit/org/openstreetmap/josm/tools/UtilsTest.java
--- a/test/unit/org/openstreetmap/josm/tools/UtilsTest.java	(revision 18531)
+++ b/test/unit/org/openstreetmap/josm/tools/UtilsTest.java	(date 1659452763771)
@@ -250,7 +250,7 @@
      */
     @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()));
     }
@@ -531,4 +531,17 @@
         final String output = Utils.execOutput(Arrays.asList("echo", "Hello", "World"));
         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}));
+    }
 }
