Ticket #20716: 20716.1.patch

File 20716.1.patch, 51.8 KB (added by taylor.smock, 4 years ago)

Decrease memory usage by ~83%, CPU samples by ~78%, deduplicate some code in Way for segment lengths,

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

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/osm/Way.java b/src/org/openstreetmap/josm/data/osm/Way.java
    a b  
    1010import java.util.Map;
    1111import java.util.Set;
    1212import java.util.stream.Collectors;
     13import java.util.stream.DoubleStream;
    1314import java.util.stream.IntStream;
    1415
    1516import org.openstreetmap.josm.data.coor.ILatLon;
     
    608609
    609610    @Override
    610611    public boolean hasIncompleteNodes() {
    611         return Arrays.stream(nodes).anyMatch(Node::isIncomplete);
     612        /*
     613         * Ideally, we would store this as a flag, but a node may become
     614         * incomplete under some circumstances without being able to notify the
     615         * way to recalculate the flag.
     616         *
     617         * When profiling #20716 on Mesa County, CO (overpass download), the
     618         * Arrays.stream method was fairly expensive. When switching to the for
     619         * loop, the CPU samples for hasIncompleteNodes went from ~150k samples
     620         * to ~8.5k samples (94% improvement) and the memory allocations for
     621         * hasIncompleteNodes went from ~15.6 GB to 0.
     622         */
     623        for (Node node : nodes) {
     624            if (node.isIncomplete()) {
     625                return true;
     626            }
     627        }
     628        return false;
    612629    }
    613630
    614631    /**
     
    647664        return length;
    648665    }
    649666
     667    /**
     668     * Replies the segment lengths of the way as computed by {@link ILatLon#greatCircleDistance}.
     669     *
     670     * @return The segment lengths of a way in metres, following way direction
     671     * @since xxx
     672     */
     673    public double[] getSegmentLengths() {
     674        return this.segmentLengths().toArray();
     675    }
     676
    650677    /**
    651678     * Replies the length of the longest segment of the way, in metres, as computed by {@link ILatLon#greatCircleDistance}.
    652679     * @return The length of the segment, in metres
    653680     * @since 8320
    654681     */
    655682    public double getLongestSegmentLength() {
    656         double length = 0;
     683        return this.segmentLengths().max().orElse(0);
     684    }
     685
     686    /**
     687     * Get the segment lengths as a stream
     688     * @return The stream of segment lengths (ordered)
     689     */
     690    private DoubleStream segmentLengths() {
     691        DoubleStream.Builder builder = DoubleStream.builder();
    657692        Node lastN = null;
    658         for (Node n:nodes) {
    659             if (lastN != null && lastN.isLatLonKnown() && n.isLatLonKnown()) {
    660                 double l = n.greatCircleDistance(lastN);
    661                 if (l > length) {
    662                     length = l;
    663                 }
     693        for (Node n : nodes) {
     694            if (lastN != null && n.isLatLonKnown() && lastN.isLatLonKnown()) {
     695                double distance = n.greatCircleDistance(lastN);
     696                builder.accept(distance);
    664697            }
    665698            lastN = n;
    666699        }
    667         return length;
     700        return builder.build();
    668701    }
    669702
    670703    /**
  • src/org/openstreetmap/josm/data/validation/tests/PowerLines.java

    IDEA additional info:
    Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
    <+>UTF-8
    diff --git a/src/org/openstreetmap/josm/data/validation/tests/PowerLines.java b/src/org/openstreetmap/josm/data/validation/tests/PowerLines.java
    a b  
    11// License: GPL. For details, see LICENSE file.
    22package org.openstreetmap.josm.data.validation.tests;
    33
     4import static org.openstreetmap.josm.gui.MainApplication.getLayerManager;
    45import static org.openstreetmap.josm.tools.I18n.tr;
    56
    67import java.util.ArrayList;
    78import java.util.Arrays;
    89import java.util.Collection;
    9 import java.util.LinkedHashSet;
     10import java.util.Collections;
     11import java.util.EnumSet;
     12import java.util.HashSet;
    1013import java.util.List;
    1114import java.util.Set;
    1215
     16import org.openstreetmap.josm.data.coor.ILatLon;
    1317import org.openstreetmap.josm.data.osm.Node;
    1418import org.openstreetmap.josm.data.osm.OsmPrimitive;
    1519import org.openstreetmap.josm.data.osm.Relation;
    1620import org.openstreetmap.josm.data.osm.RelationMember;
    1721import org.openstreetmap.josm.data.osm.Way;
     22import org.openstreetmap.josm.data.osm.WaySegment;
    1823import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon;
    1924import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.JoinedWay;
    2025import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
     26import org.openstreetmap.josm.data.preferences.sources.ValidatorPrefHelper;
    2127import org.openstreetmap.josm.data.validation.Severity;
    2228import org.openstreetmap.josm.data.validation.Test;
    2329import org.openstreetmap.josm.data.validation.TestError;
    2430import org.openstreetmap.josm.gui.progress.ProgressMonitor;
     31import org.openstreetmap.josm.spi.preferences.Config;
    2532import org.openstreetmap.josm.tools.Geometry;
     33import org.openstreetmap.josm.tools.Logging;
     34import org.openstreetmap.josm.tools.Pair;
     35import org.openstreetmap.josm.tools.Utils;
    2636
    2737/**
    28  * Checks for nodes in power lines/minor_lines that do not have a power=tower/pole tag.<br>
    29  * See #7812 for discussions about this test.
     38 * Checks for
     39 * <ul>
     40 * <li>nodes in power lines/minor_lines that do not have a power=tower/pole/portal tag
     41 * <li>nodes where the reference numbering not consistent
     42 * <li>ways where are unusually long segments without line support feature
     43 * <li>ways where the line type is possibly misused
     44 * </ul>
     45 * See #7812 and #20716 for discussions about this test.
    3046 */
    3147public class PowerLines extends Test {
    3248
    33     /** Test identifier */
    34     protected static final int POWER_LINES = 2501;
     49    // Test identifiers
     50    protected static final int POWER_SUPPORT = 2501;
    3551    protected static final int POWER_CONNECTION = 2502;
     52    protected static final int POWER_SEGMENT_LENGTH = 2503;
     53    protected static final int POWER_LOCAL_REF_CONTINUITY = 2504;
     54    protected static final int POWER_WAY_REF_CONTINUITY = 2505;
     55    protected static final int POWER_LINE_TYPE = 2506;
     56
     57    protected static final String PREFIX = ValidatorPrefHelper.PREFIX + "." + PowerLines.class.getSimpleName();
     58    private double hillyCompensation;
     59    private double hillyThreshold;
    3660
    3761    /** Values for {@code power} key interpreted as power lines */
    3862    static final Collection<String> POWER_LINE_TAGS = Arrays.asList("line", "minor_line");
    3963    /** Values for {@code power} key interpreted as power towers */
    40     static final Collection<String> POWER_TOWER_TAGS = Arrays.asList("tower", "pole");
     64    static final Collection<String> POWER_TOWER_TAGS = Arrays.asList("catenary_mast", "pole", "portal", "tower");
    4165    /** Values for {@code power} key interpreted as power stations */
    42     static final Collection<String> POWER_STATION_TAGS = Arrays.asList("station", "sub_station", "substation", "plant", "generator");
     66    static final Collection<String> POWER_STATION_TAGS = Arrays.asList("generator", "plant", "substation");
    4367    /** Values for {@code building} key interpreted as power stations */
    44     static final Collection<String> BUILDING_STATION_TAGS = Arrays.asList("transformer_tower");
     68    static final Collection<String> BUILDING_STATION_TAGS = Collections.singletonList("transformer_tower");
    4569    /** Values for {@code power} key interpreted as allowed power items */
    46     static final Collection<String> POWER_ALLOWED_TAGS = Arrays.asList("switch", "transformer", "busbar", "generator", "switchgear",
    47             "portal", "terminal", "insulator", "connection");
     70    static final Collection<String> POWER_INFRASTRUCTURE_TAGS = Arrays.asList("compensator", "connection", "converter",
     71            "generator", "insulator", "switch", "switchgear", "terminal", "transformer");
    4872
    49     private final Set<Node> badConnections = new LinkedHashSet<>();
    50     private final Set<Node> missingTowerOrPole = new LinkedHashSet<>();
     73    private final Set<Node> badConnections = new HashSet<>();
     74    private final Set<Node> missingTags = new HashSet<>();
     75    private final Set<Way> wrongLineType = new HashSet<>();
     76    private final Set<WaySegment> missingNodes = new HashSet<>();
     77    private final Set<OsmPrimitive> refDiscontinuities = new HashSet<>();
    5178
     79    private final List<Set<Node>> segmentRefDiscontinuities = new ArrayList<>();
    5280    private final List<OsmPrimitive> powerStations = new ArrayList<>();
    5381
     82    private final Collection<Way> datasetWaterways = new HashSet<>(32);
     83
    5484    /**
    5585     * Constructs a new {@code PowerLines} test.
    5686     */
    5787    public PowerLines() {
    58         super(tr("Power lines"), tr("Checks for nodes in power lines that do not have a power=tower/pole/connection tag."));
     88        super(tr("Power lines"), tr("Checks if power line missing a support node and " +
     89                "for nodes in power lines that do not have a power=tower/pole tag"));
    5990    }
    6091
    61     @Override
    62     public void visit(Way w) {
    63         if (w.isUsable()) {
    64             if (isPowerLine(w) && !w.hasTag("location", "underground")) {
    65                 for (Node n : w.getNodes()) {
    66                     if (!isPowerTower(n) && !isPowerAllowed(n) && IN_DOWNLOADED_AREA.test(n)
    67                         && (!w.isFirstLastNode(n) || !isPowerStation(n))) {
    68                         missingTowerOrPole.add(n);
    69                     }
    70                 }
    71             } else if (w.isClosed() && isPowerStation(w)) {
    72                 powerStations.add(w);
    73             }
    74         }
     92    /** Power line support features ref=* numbering direction. */
     93    private enum NumberingDirection {
     94        /** No direction */
     95        NONE,
     96        /** Numbering follows way direction */
     97        SAME,
     98        /** Numbering goes opposite way direction */
     99        OPPOSITE
    75100    }
    76101
    77102    @Override
     
    81106        for (Way parent : n.getParentWays()) {
    82107            if (parent.hasTag("power", "line", "minor_line", "cable"))
    83108                nodeInLineOrCable = true;
    84             else if (!isRelatedToPower(parent)) {
     109            else if (!isRelatedToPower(parent))
    85110                connectedToUnrelated = true;
    86             }
    87111        }
    88112        if (nodeInLineOrCable && connectedToUnrelated)
    89113            badConnections.add(n);
    90114    }
    91115
    92     private static boolean isRelatedToPower(Way way) {
    93         if (way.hasTag("power") || way.hasTag("building"))
    94             return true;
    95         for (OsmPrimitive ref : way.getReferrers()) {
    96             if (ref instanceof Relation && ref.isMultipolygon() && (ref.hasTag("power") || ref.hasTag("building"))) {
    97                 for (RelationMember rm : ((Relation) ref).getMembers()) {
    98                     if (way == rm.getMember())
    99                         return true;
     116    @Override
     117    public void visit(Way w) {
     118        if (!isPrimitiveUsable(w)) return;
     119
     120        if (isPowerLine(w) && !w.hasKey("line") && !w.hasTag("location", "underground")) {
     121            final int segmentCount = w.getNodesCount() - 1;
     122            final double mean = w.getLength() / segmentCount;
     123            final double stdDev = Utils.getStandardDeviation(w.getSegmentLengths(), mean);
     124            final boolean isContinuesAsMinorLine = isContinuesAsMinorLine(w);
     125            boolean isCrossingWater = false;
     126            int poleCount = 0;
     127            int towerCount = 0;
     128            Node prevNode = w.firstNode();
     129
     130            double baseThreshold = w.hasTag("power", "line") ? 1.6 : 1.8;
     131            if (mean / stdDev < hillyThreshold) {
     132                //compensate for possibly hilly areas where towers can't be put anywhere
     133                baseThreshold += hillyCompensation;
     134            }
     135
     136            for (Node n : w.getNodes()) {
     137
     138                /// handle power station line connections (e.g. power=line + line=*)
     139                if (isConnectedToStationLine(n, w) || n.hasTag("power", "connection")) {
     140                    prevNode = n;
     141                    continue;   // skip, it would be false positive
    100142                }
     143
     144                /// handle missing power line support tags (e.g. tower)
     145                if (!isPowerTower(n) && !isPowerInfrastructure(n) && IN_DOWNLOADED_AREA.test(n)
     146                        && (!w.isFirstLastNode(n) || !isPowerStation(n)))
     147                    missingTags.add(n);
     148
     149                /// handle missing nodes
     150                double segmentLen = n.greatCircleDistance(prevNode);
     151                final Pair<Node, Node> pair = Pair.create(prevNode, n);
     152                final Set<Way> crossingWaterWays = new HashSet<>(8);
     153                final Set<ILatLon> crossingPositions = new HashSet<>(8);
     154                findCrossings(datasetWaterways, w, pair, crossingWaterWays, crossingPositions);
     155
     156                if (!crossingWaterWays.isEmpty()) {
     157                    double compensation = calculateIntersectingLen(prevNode, crossingPositions);
     158                    segmentLen -= compensation;
     159                }
     160
     161                if (segmentCount > 4
     162                        && segmentLen > mean * baseThreshold
     163                        && !isPowerInfrastructure(n)
     164                        && IN_DOWNLOADED_AREA.test(n))
     165                    missingNodes.add(WaySegment.forNodePair(w, prevNode, n));
     166
     167                /// handle wrong line types
     168                if (!crossingWaterWays.isEmpty())
     169                    isCrossingWater = true;
     170
     171                if (n.hasTag("power", "pole"))
     172                    poleCount++;
     173                else if (n.hasTag("power", "tower", "portal"))
     174                    towerCount++;
     175
     176                prevNode = n;
    101177            }
     178
     179            /// handle ref=* numbering discontinuities
     180            if (detectDiscontinuity(w, refDiscontinuities, segmentRefDiscontinuities))
     181                refDiscontinuities.add(w);
     182
     183            /// handle wrong line types
     184            if (((poleCount > towerCount && w.hasTag("power", "line"))
     185                    || (poleCount < towerCount && w.hasTag("power", "minor_line")
     186                    && !isCrossingWater
     187                    && !isContinuesAsMinorLine))
     188                    && IN_DOWNLOADED_AREA.test(w))
     189                wrongLineType.add(w);
     190
     191        } else if (w.isClosed() && isPowerStation(w)) {
     192            powerStations.add(w);
    102193        }
    103         return false;
    104194    }
    105195
    106196    @Override
     
    113203    @Override
    114204    public void startTest(ProgressMonitor progressMonitor) {
    115205        super.startTest(progressMonitor);
    116         clearCollections();
     206        // the test run can take a bit of time, show detailed progress
     207        setShowElements(true);
     208
     209        hillyCompensation = Config.getPref().getDouble(PREFIX + ".hilly_compensation", 0.2);
     210        hillyThreshold = Config.getPref().getDouble(PREFIX + ".hilly_threshold", 4.0);
     211
     212        // collect all waterways
     213        getLayerManager()
     214                .getActiveDataSet()
     215                .getWays()
     216                .stream()
     217                .filter(way -> way.isUsable() && (concernsWaterArea(way)
     218                        || way.referrers(Relation.class).anyMatch(PowerLines::concernsWaterArea)))
     219                .forEach(datasetWaterways::add);
    117220    }
    118221
    119222    @Override
    120223    public void endTest() {
    121         for (Node n : missingTowerOrPole) {
     224        for (Node n : missingTags) {
    122225            if (!isInPowerStation(n)) {
    123                 errors.add(TestError.builder(this, Severity.WARNING, POWER_LINES)
    124                         .message(tr("Missing power tower/pole/connection within power line"))
     226                errors.add(TestError.builder(this, Severity.WARNING, POWER_SUPPORT)
     227                        // the "missing tag" grouping can become broken if the MapCSS message get reworded
     228                        .message(tr("missing tag"), tr("node without power=*"))
    125229                        .primitives(n)
    126230                        .build());
    127231            }
     
    130234        for (Node n : badConnections) {
    131235            errors.add(TestError.builder(this, Severity.WARNING, POWER_CONNECTION)
    132236                    .message(tr("Node connects a power line or cable with an object "
    133                             + "which is not related to the power infrastructure."))
    134                     .primitives(n).build());
     237                            + "which is not related to the power infrastructure"))
     238                    .primitives(n)
     239                    .build());
     240        }
     241
     242        for (WaySegment s : missingNodes) {
     243            errors.add(TestError.builder(this, Severity.WARNING, POWER_SEGMENT_LENGTH)
     244                    .message(tr("Possibly missing line support node within power line"))
     245                    .primitives(s.getFirstNode(), s.getSecondNode())
     246                    .highlightWaySegments(new HashSet<>(Collections.singleton(s)))
     247                    .build());
    135248        }
    136         clearCollections();
     249
     250        for (OsmPrimitive p : refDiscontinuities) {
     251            if (p instanceof Way)
     252                errors.add(TestError.builder(this, Severity.WARNING, POWER_WAY_REF_CONTINUITY)
     253                        .message(tr("Mixed reference numbering"))
     254                        .primitives(p)
     255                        .build());
     256        }
     257
     258        final String discontinuityMsg = tr("Reference numbering don''t match majority of way''s nodes");
     259
     260        for (OsmPrimitive p : refDiscontinuities) {
     261            if (p instanceof Node)
     262                errors.add(TestError.builder(this, Severity.WARNING, POWER_LOCAL_REF_CONTINUITY)
     263                        .message(discontinuityMsg)
     264                        .primitives(p)
     265                        .build());
     266        }
     267
     268        for (Set<Node> nodes : segmentRefDiscontinuities) {
     269            errors.add(TestError.builder(this, Severity.WARNING, POWER_LOCAL_REF_CONTINUITY)
     270                    .message(discontinuityMsg)
     271                    .primitives(nodes)
     272                    .build());
     273        }
     274
     275        for (Way w : wrongLineType) {
     276            errors.add(TestError.builder(this, Severity.WARNING, POWER_LINE_TYPE)
     277                    .message(tr("Possibly wrong power line type used"))
     278                    .primitives(w)
     279                    .build());
     280        }
     281
    137282        super.endTest();
    138283    }
    139284
     285    /**
     286     * The summarized length (in metres) of a way where a power line hangs over a water area.
     287     * @param ref Reference point
     288     * @param crossingNodes Crossing nodes, unordered
     289     * @return The summarized length (in metres) of a way where a power line hangs over a water area
     290     */
     291    private static double calculateIntersectingLen(Node ref, Set<ILatLon> crossingNodes) {
     292        double min = Double.POSITIVE_INFINITY;
     293        double max = Double.NEGATIVE_INFINITY;
     294
     295        for (ILatLon coor : crossingNodes) {
     296
     297            if (ref != null && coor != null) {
     298                double dist = ref.greatCircleDistance(coor);
     299
     300                if (dist < min)
     301                    min = dist;
     302                if (dist > max)
     303                    max = dist;
     304            }
     305        }
     306        return max - min;
     307    }
     308
     309    /**
     310     * Searches for way intersections, which intersect the {@code pair} attribute.
     311     * @param ways collection of ways to search
     312     * @param parent parent way for {@code pair} param
     313     * @param pair node pair among which search for another way
     314     * @param crossingWays found crossing ways
     315     * @param crossingPositions collection of the crossing positions
     316     * @implNote Inspired by {@code utilsplugin2/selection/NodeWayUtils.java#addWaysIntersectingWay()}
     317     */
     318    private static void findCrossings(Collection<Way> ways, Way parent, Pair<Node, Node> pair, Set<Way> crossingWays,
     319                                      Set<ILatLon> crossingPositions) {
     320        for (Way way : ways) {
     321            if (way.isUsable()
     322                    && !crossingWays.contains(way)
     323                    && way.getBBox().intersects(parent.getBBox())) {
     324                for (Pair<Node, Node> pair2 : way.getNodePairs(false)) {
     325                    ILatLon ll = Geometry.getSegmentSegmentIntersection(pair.a, pair.b, pair2.a, pair2.b);
     326                    if (ll != null) {
     327                        crossingPositions.add(ll);
     328                        crossingWays.add(way);
     329                    }
     330                }
     331            }
     332        }
     333    }
     334
     335    /** Helper class for reference numbering test. Used for storing continuous reference segment info. */
     336    private static class SegmentInfo {
     337        /** Node index, follows way direction */
     338        private final int startIndex;
     339        /** ref=* value at {@link SegmentInfo#startIndex} */
     340        private final int startRef;
     341        /** Segment length */
     342        private final int length;
     343        /** Segment direction */
     344        private final NumberingDirection direction;
     345
     346        SegmentInfo(int startIndex, int length, int ref, NumberingDirection direction) {
     347            this.startIndex = startIndex;
     348            this.length = length;
     349            this.direction = direction;
     350
     351            if (direction == NumberingDirection.SAME)
     352                this.startRef = ref - length;
     353            else
     354                this.startRef = ref + length;
     355
     356            if (length == 0 && direction != NumberingDirection.NONE) {
     357                throw new IllegalArgumentException("When the segment length is zero, the direction should be NONE");
     358            }
     359        }
     360
     361        @Override
     362        public String toString() {
     363            return String.format("SegmentInfo{startIndex=%d, startRef=%d, length=%d, direction=%s}",
     364                    startIndex, startRef, length, direction);
     365        }
     366    }
     367
     368    /**
     369     * Detects ref=* numbering discontinuities in the given way.
     370     * @param way checked way
     371     * @param nRefDiscontinuities single node ref=* discontinuities
     372     * @param sRefDiscontinuities continuous node ref=* discontinuities
     373     * @return {@code true} if warning needs to be issued for the whole way
     374     */
     375    static boolean detectDiscontinuity(Way way, Set<OsmPrimitive> nRefDiscontinuities, List<Set<Node>> sRefDiscontinuities) {
     376        final RefChecker checker = new RefChecker(way);
     377        final List<SegmentInfo> segments = checker.getSegments();
     378        final SegmentInfo referenceSegment = checker.getLongestSegment();
     379
     380        if (referenceSegment == null)
     381            return !segments.isEmpty();
     382
     383        // collect disconnected ref segments which are not align up to the reference
     384        for (SegmentInfo segment : segments) {
     385            if (!isSegmentAlign(referenceSegment, segment)) {
     386                if (referenceSegment.length == 0)
     387                    return true;
     388
     389                if (segment.length == 0) {
     390                    nRefDiscontinuities.add(way.getNode(segment.startIndex));
     391                } else {
     392                    Set<Node> nodeGroup = new HashSet<>();
     393
     394                    for (int i = segment.startIndex; i <= segment.startIndex + segment.length; i++) {
     395                        nodeGroup.add(way.getNode(i));
     396                    }
     397                    sRefDiscontinuities.add(nodeGroup);
     398                }
     399            }
     400        }
     401
     402        return false;
     403    }
     404
     405    /**
     406     * Checks if parameter segments align. The {@code reference} is expected to be at least as long as the {@code candidate}.
     407     * @param reference Reference segment to check against
     408     * @param candidate Candidate segment
     409     * @return {@code true} if the two segments ref=* numbering align
     410     */
     411    private static boolean isSegmentAlign(SegmentInfo reference, SegmentInfo candidate) {
     412        if (reference.direction == NumberingDirection.NONE
     413                || reference.direction == candidate.direction
     414                || candidate.direction == NumberingDirection.NONE)
     415            return Math.abs(candidate.startIndex - reference.startIndex) == Math.abs(candidate.startRef - reference.startRef);
     416        return false;
     417    }
     418
     419    /**
     420     * Detects continuous reference numbering sequences. Ignores the first and last node because
     421     * ways can be connected, and the connection nodes can have different numbering.
     422     * <p>
     423     * If the numbering switches in the middle of the way, this can also be seen as error,
     424     * because line relations would require split ways.
     425     */
     426    static class RefChecker {
     427        private final List<SegmentInfo> segments = new ArrayList<>();
     428        private NumberingDirection direction = NumberingDirection.NONE;
     429        private Integer startIndex;
     430        private Integer previousRef;
     431
     432        RefChecker(final Way way) {
     433            run(way);
     434        }
     435
     436        private void run(Way way) {
     437            final int wayLength = way.getNodesCount();
     438
     439            // first and last node skipped
     440            for (int i = 1; i < wayLength - 1; i++) {
     441                Node n = way.getNode(i);
     442                if (!isPowerTower(n)) {
     443                    continue;
     444                }
     445                maintain(parseRef(n.get("ref")), i);
     446            }
     447
     448            // needed for creation of the last segment
     449            maintain(null, wayLength - 1);
     450        }
     451
     452        /**
     453         * Maintains class variables and constructs a new segment when necessary.
     454         * @param ref   recognised ref=* number
     455         * @param index node index in a {@link Way}
     456         */
     457        private void maintain(Integer ref, int index) {
     458            if (previousRef == null && ref != null) {
     459                // ref change: null -> number
     460                startIndex = index;
     461            } else if (previousRef != null && ref == null) {
     462                // ref change: number -> null
     463                segments.add(new SegmentInfo(startIndex, index - 1 - startIndex, previousRef, direction));
     464                direction = NumberingDirection.NONE;    // to fix directionality
     465            } else if (previousRef != null) {
     466                // ref change: number -> number
     467                if (Math.abs(ref - previousRef) != 1) {
     468                    segments.add(new SegmentInfo(startIndex, index - 1 - startIndex, previousRef, direction));
     469                    startIndex = index;
     470                    previousRef = ref;                  // to fix directionality
     471                }
     472                direction = detectDirection(ref, previousRef);
     473            }
     474            previousRef = ref;
     475        }
     476
     477        /**
     478         * Parses integer tag values. Later can be relatively easily extended or rewritten to handle
     479         * complex references like 25/A, 25/B etc.
     480         * @param value the value to be parsed
     481         * @return parsed int or {@code null} in case of {@link NumberFormatException}
     482         */
     483        private static Integer parseRef(String value) {
     484            try {
     485                return Integer.parseInt(value);
     486            } catch (NumberFormatException ignore) {
     487                Logging.trace("The " + RefChecker.class + " couldn't parse ref=" + value + ", consider rewriting the parser");
     488                return null;
     489            }
     490        }
     491
     492        /**
     493         * Detects numbering direction. The parameters should follow way direction.
     494         * @param ref         last known reference value
     495         * @param previousRef reference value before {@code ref}
     496         * @return recognised direction
     497         */
     498        private static NumberingDirection detectDirection(int ref, int previousRef) {
     499            if (ref > previousRef)
     500                return NumberingDirection.SAME;
     501            else if (ref < previousRef)
     502                return NumberingDirection.OPPOSITE;
     503            return NumberingDirection.NONE;
     504        }
     505
     506        /**
     507         * Calculates the longest segment.
     508         * @return the longest segment, or the lowest index if there are more than one with same length and direction,
     509         * or {@code null} if there are more than one with same length and different direction
     510         */
     511        SegmentInfo getLongestSegment() {
     512            final Set<NumberingDirection> directions = EnumSet.noneOf(NumberingDirection.class);
     513            int longestLength = -1;
     514            int counter = 0;
     515            SegmentInfo longest = null;
     516
     517            for (SegmentInfo segment : segments) {
     518                if (segment.length > longestLength) {
     519                    longestLength = segment.length;
     520                    longest = segment;
     521                    counter = 0;
     522                    directions.clear();
     523                    directions.add(segment.direction);
     524                } else if (segment.length == longestLength) {
     525                    counter++;
     526                    directions.add(segment.direction);
     527                }
     528            }
     529
     530            // there are multiple segments with the same longest length and their directions don't match
     531            if (counter > 0 && directions.size() > 1)
     532                return null;
     533
     534            return longest;
     535        }
     536
     537        /**
     538         * @return the detected segments
     539         */
     540        List<SegmentInfo> getSegments() {
     541            return segments;
     542        }
     543    }
     544
     545    private static boolean isRelatedToPower(Way way) {
     546        if (way.hasTag("power") || way.hasTag("building"))
     547            return true;
     548        for (OsmPrimitive ref : way.getReferrers()) {
     549            if (ref instanceof Relation && ref.isMultipolygon() && (ref.hasTag("power") || ref.hasTag("building"))) {
     550                for (RelationMember rm : ((Relation) ref).getMembers()) {
     551                    if (way == rm.getMember())
     552                        return true;
     553                }
     554            }
     555        }
     556        return false;
     557    }
     558
     559    /**
     560     * Determines if the current node connected to a line which usually used inside power stations.
     561     * @param n node to check
     562     * @param w parent way of {@code n}
     563     * @return {@code true} if {@code n} connected to power=line + line=*
     564     */
     565    private static boolean isConnectedToStationLine(Node n, Way w) {
     566        for (OsmPrimitive p : n.getReferrers()) {
     567            if (p instanceof Way && !p.equals(w) && isPowerLine((Way) p) && p.hasKey("line"))
     568                return true;
     569        }
     570        return false;
     571    }
     572
     573    /**
     574     * Checks if the way continues as a power=minor_line.
     575     * @param way Way to be checked
     576     * @return {@code true} if the way continues as a power=minor_line
     577     */
     578    private static boolean isContinuesAsMinorLine(Way way) {
     579        return way.firstNode().referrers(Way.class).filter(referrer -> !way.equals(referrer)).anyMatch(PowerLines::isMinorLine) ||
     580                way.lastNode().referrers(Way.class).filter(referrer -> !way.equals(referrer)).anyMatch(PowerLines::isMinorLine);
     581    }
     582
     583    /**
     584     * Checks if the given primitive denotes a power=minor_line.
     585     * @param p primitive to be checked
     586     * @return {@code true} if the given primitive denotes a power=minor_line
     587     */
     588    private static boolean isMinorLine(OsmPrimitive p) {
     589        return p.hasTag("power", "minor_line");
     590    }
     591
     592    /**
     593     * Check if primitive has a tag that marks it as a water area or boundary of a water area.
     594     * @param p the primitive
     595     * @return {@code true} if primitive has a tag that marks it as a water area or boundary of a water area
     596     */
     597    private static boolean concernsWaterArea(OsmPrimitive p) {
     598        return p.hasTag("water", "river", "lake") || p.hasKey("waterway") || p.hasTag("natural", "coastline");
     599    }
     600
     601    /**
     602     * Checks if the given node is inside a power station.
     603     * @param n Node to be checked
     604     * @return true if the given node is inside a power station
     605     */
    140606    protected final boolean isInPowerStation(Node n) {
    141607        for (OsmPrimitive station : powerStations) {
    142608            List<List<Node>> nodesLists = new ArrayList<>();
     
    164630     * @param w The way to be tested
    165631     * @return {@code true} if power key is set and equal to line/minor_line
    166632     */
    167     protected static final boolean isPowerLine(Way w) {
     633    protected static boolean isPowerLine(Way w) {
    168634        return isPowerIn(w, POWER_LINE_TAGS);
    169635    }
    170636
    171637    /**
    172638     * Determines if the specified primitive denotes a power station.
    173639     * @param p The primitive to be tested
    174      * @return {@code true} if power key is set and equal to station/sub_station/plant
     640     * @return {@code true} if power key is set and equal to generator/substation/plant
    175641     */
    176     protected static final boolean isPowerStation(OsmPrimitive p) {
     642    protected static boolean isPowerStation(OsmPrimitive p) {
    177643        return isPowerIn(p, POWER_STATION_TAGS) || isBuildingIn(p, BUILDING_STATION_TAGS);
    178644    }
    179645
    180646    /**
    181      * Determines if the specified node denotes a power tower/pole.
     647     * Determines if the specified node denotes a power support feature.
    182648     * @param n The node to be tested
    183      * @return {@code true} if power key is set and equal to tower/pole
     649     * @return {@code true} if power key is set and equal to pole/tower/portal/catenary_mast
    184650     */
    185     protected static final boolean isPowerTower(Node n) {
     651    protected static boolean isPowerTower(Node n) {
    186652        return isPowerIn(n, POWER_TOWER_TAGS);
    187653    }
    188654
    189655    /**
    190656     * Determines if the specified node denotes a power infrastructure allowed on a power line.
    191657     * @param n The node to be tested
    192      * @return True if power key is set and equal to switch/tranformer/busbar/generator
     658     * @return {@code true} if power key is set and equal to compensator/converter/generator/insulator
     659     * /switch/switchgear/terminal/transformer
    193660     */
    194     protected static final boolean isPowerAllowed(Node n) {
    195         return isPowerIn(n, POWER_ALLOWED_TAGS);
     661    protected static boolean isPowerInfrastructure(Node n) {
     662        return isPowerIn(n, POWER_INFRASTRUCTURE_TAGS);
    196663    }
    197664
    198665    /**
     
    215682        return p.hasTag("building", values);
    216683    }
    217684
    218     private void clearCollections() {
     685    @Override
     686    public void clear() {
     687        super.clear();
    219688        powerStations.clear();
    220689        badConnections.clear();
    221         missingTowerOrPole.clear();
     690        missingTags.clear();
     691        missingNodes.clear();
     692        wrongLineType.clear();
     693        refDiscontinuities.clear();
     694        segmentRefDiscontinuities.clear();
     695        datasetWaterways.clear();
    222696    }
    223697}
  • 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 b  
    3232import org.openstreetmap.josm.data.projection.proj.TransverseMercator.Hemisphere;
    3333import org.openstreetmap.josm.tools.Geometry;
    3434import org.openstreetmap.josm.tools.Pair;
     35import org.openstreetmap.josm.tools.Utils;
    3536
    3637/**
    3738 * Textual representation of primitive contents, used in {@code InspectPrimitiveDialog}.
     
    174175            add(tr("Centroid: "), toStringCSV(false,
    175176                    ProjectionRegistry.getProjection().eastNorth2latlon(Geometry.getCentroid(((IWay<?>) o).getNodes()))));
    176177            if (o instanceof Way) {
    177                 double dist = ((Way) o).getLength();
    178                 String distText = SystemOfMeasurement.getSystemOfMeasurement().getDistText(dist);
    179                 add(tr("Length: {0}", distText));
     178                double length = ((Way) o).getLength();
     179                String lenText = SystemOfMeasurement.getSystemOfMeasurement().getDistText(length);
     180                add(tr("Length: {0}", lenText));
     181
     182                double avgNodeDistance = length / (((Way) o).getNodesCount() - 1);
     183                String nodeDistText = SystemOfMeasurement.getSystemOfMeasurement().getDistText(avgNodeDistance);
     184                add(tr("Average segment length: {0}", nodeDistText));
     185
     186                double stdDev = Utils.getStandardDeviation(((Way) o).getSegmentLengths(), avgNodeDistance);
     187                String stdDevText = SystemOfMeasurement.getSystemOfMeasurement().getDistText(stdDev);
     188                add(tr("Standard deviation: {0}", stdDevText));
    180189            }
    181190            if (o instanceof Way && ((Way) o).concernsArea() && ((Way) o).isClosed()) {
    182191                double area = Geometry.closedWayArea((Way) o);
  • 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 b  
    287287     * @param p3 the coordinates of the start point of the second specified line segment
    288288     * @param p4 the coordinates of the end point of the second specified line segment
    289289     * @return EastNorth null if no intersection was found, the EastNorth coordinates of the intersection otherwise
     290     * @see #getSegmentSegmentIntersection(ILatLon, ILatLon, ILatLon, ILatLon)
    290291     */
    291292    public static EastNorth getSegmentSegmentIntersection(EastNorth p1, EastNorth p2, EastNorth p3, EastNorth p4) {
    292 
    293         CheckParameterUtil.ensureThat(p1.isValid(), () -> p1 + " invalid");
    294         CheckParameterUtil.ensureThat(p2.isValid(), () -> p2 + " invalid");
    295         CheckParameterUtil.ensureThat(p3.isValid(), () -> p3 + " invalid");
    296         CheckParameterUtil.ensureThat(p4.isValid(), () -> p4 + " invalid");
     293        // see the ILatLon version for an explanation why the checks are in the if statement
     294        if (!(p1.isValid() && p2.isValid() && p3.isValid() && p4.isValid())) {
     295            CheckParameterUtil.ensureThat(p1.isValid(), () -> p1 + " invalid");
     296            CheckParameterUtil.ensureThat(p2.isValid(), () -> p2 + " invalid");
     297            CheckParameterUtil.ensureThat(p3.isValid(), () -> p3 + " invalid");
     298            CheckParameterUtil.ensureThat(p4.isValid(), () -> p4 + " invalid");
     299        }
    297300
    298301        double x1 = p1.getX();
    299302        double y1 = p1.getY();
     
    303306        double y3 = p3.getY();
    304307        double x4 = p4.getX();
    305308        double y4 = p4.getY();
     309        double[] en = getSegmentSegmentIntersection(x1, y1, x2, y2, x3, y3, x4, y4);
     310        if (en != null && en.length == 2) {
     311            return new EastNorth(en[0], en[1]);
     312        }
     313        return null;
     314    }
     315
     316    /**
     317     * Finds the intersection of two line segments.
     318     * @param p1 the coordinates of the start point of the first specified line segment
     319     * @param p2 the coordinates of the end point of the first specified line segment
     320     * @param p3 the coordinates of the start point of the second specified line segment
     321     * @param p4 the coordinates of the end point of the second specified line segment
     322     * @return LatLon null if no intersection was found, the LatLon coordinates of the intersection otherwise
     323     * @see #getSegmentSegmentIntersection(EastNorth, EastNorth, EastNorth, EastNorth)
     324     * @since xxx
     325     */
     326    public static ILatLon getSegmentSegmentIntersection(ILatLon p1, ILatLon p2, ILatLon p3, ILatLon p4) {
     327        // Avoid lambda creation if at all possible -- this pretty much removes all memory allocations
     328        // from this method (11.4 GB to 0) when testing #20716 with Mesa County, CO (overpass download).
     329        // There was also a 2/3 decrease in CPU samples for the method.
     330        if (!(p1.isLatLonKnown() && p2.isLatLonKnown() && p3.isLatLonKnown() && p4.isLatLonKnown())) {
     331            CheckParameterUtil.ensureThat(p1.isLatLonKnown(), () -> p1 + " invalid");
     332            CheckParameterUtil.ensureThat(p2.isLatLonKnown(), () -> p2 + " invalid");
     333            CheckParameterUtil.ensureThat(p3.isLatLonKnown(), () -> p3 + " invalid");
     334            CheckParameterUtil.ensureThat(p4.isLatLonKnown(), () -> p4 + " invalid");
     335        }
     336
     337        double x1 = p1.lon();
     338        double y1 = p1.lat();
     339        double x2 = p2.lon();
     340        double y2 = p2.lat();
     341        double x3 = p3.lon();
     342        double y3 = p3.lat();
     343        double x4 = p4.lon();
     344        double y4 = p4.lat();
     345        double[] en = getSegmentSegmentIntersection(x1, y1, x2, y2, x3, y3, x4, y4);
     346        if (en != null && en.length == 2) {
     347            return new LatLon(en[1], en[0]);
     348        }
     349        return null;
     350    }
     351
     352    /**
     353     * Get the segment segment intersection of two line segments
     354     * @param x1 The x coordinate of the first point (first segment)
     355     * @param y1 The y coordinate of the first point (first segment)
     356     * @param x2 The x coordinate of the second point (first segment)
     357     * @param y2 The y coordinate of the second point (first segment)
     358     * @param x3 The x coordinate of the third point (second segment)
     359     * @param y3 The y coordinate of the third point (second segment)
     360     * @param x4 The x coordinate of the fourth point (second segment)
     361     * @param y4 The y coordinate of the fourth point (second segment)
     362     * @return {@code null} if no intersection was found, otherwise [x, y]
     363     */
     364    private static double[] getSegmentSegmentIntersection(double x1, double y1, double x2, double y2, double x3, double y3,
     365            double x4, double y4) {
    306366
    307367        //TODO: do this locally.
    308368        //TODO: remove this check after careful testing
     
    333393            if (u > -1e-8 && u < 1+1e-8 && v > -1e-8 && v < 1+1e-8) {
    334394                if (u < 0) u = 0;
    335395                if (u > 1) u = 1.0;
    336                 return new EastNorth(x1+a1*u, y1+a2*u);
     396                return new double[] {x1+a1*u, y1+a2*u};
    337397            } else {
    338398                return null;
    339399            }
  • 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 b  
    226226        if (array != null) {
    227227            return Arrays.copyOf(array, array.length);
    228228        }
    229         return array;
     229        return null;
    230230    }
    231231
    232232    /**
     
    239239        if (array != null) {
    240240            return Arrays.copyOf(array, array.length);
    241241        }
    242         return array;
     242        return null;
    243243    }
    244244
    245245    /**
     
    252252        if (array != null) {
    253253            return Arrays.copyOf(array, array.length);
    254254        }
    255         return array;
     255        return null;
    256256    }
    257257
    258258    /**
     
    265265        if (array != null) {
    266266            return Arrays.copyOf(array, array.length);
    267267        }
    268         return array;
     268        return null;
    269269    }
    270270
    271271    /**
     
    13031303        }
    13041304    }
    13051305
     1306    /**
     1307     * Calculates the <a href="https://en.wikipedia.org/wiki/Standard_deviation">standard deviation</a> of population.
     1308     * @param values an array of values
     1309     * @return standard deviation of the given array, or -1.0 if the array has less than two values
     1310     * @see #getStandardDeviation(double[], double)
     1311     * @since xxx
     1312     */
     1313    public static double getStandardDeviation(double[] values) {
     1314        return getStandardDeviation(values, Double.NaN);
     1315    }
     1316
     1317    /**
     1318     * Calculates the <a href="https://en.wikipedia.org/wiki/Standard_deviation">standard deviation</a> of population with the given
     1319     * mean value.
     1320     * @param values an array of values
     1321     * @param mean precalculated average value of the array
     1322     * @return standard deviation of the given array, or -1.0 if the array has less than two values
     1323     * @see #getStandardDeviation(double[])
     1324     * @since xxx
     1325     */
     1326    public static double getStandardDeviation(double[] values, double mean) {
     1327        if (values.length < 2) {
     1328            return -1.0;
     1329        }
     1330
     1331        double standardDeviation = 0;
     1332
     1333        if (Double.isNaN(mean)) {
     1334            mean = Arrays.stream(values).average().orElse(0);
     1335        }
     1336
     1337        for (double length : values) {
     1338            standardDeviation += Math.pow(length - mean, 2);
     1339        }
     1340
     1341        return Math.sqrt(standardDeviation / values.length);
     1342    }
     1343
    13061344    /**
    13071345     * A ForkJoinWorkerThread that will always inherit caller permissions,
    13081346     * unlike JDK's InnocuousForkJoinWorkerThread, used if a security manager exists.
  • 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 b  
    134134    void testLoadIAE() {
    135135        assertThrows(IllegalArgumentException.class, () -> new Way().load(new NodeData()));
    136136    }
     137
     138    @Test
     139    void getLongestSegmentLength() {
     140        DataSet ds = new DataSet();
     141        Node n1 = new Node(1);
     142        Node n2 = new Node(2);
     143        Node n3 = new Node(3);
     144        Node n4 = new Node(4);
     145        n1.setCoor(new LatLon(0.01, 0.01));
     146        n2.setCoor(new LatLon(0.02, 0.02));
     147        n3.setCoor(new LatLon(0.03, 0.03));
     148        n4.setCoor(new LatLon(0.05, 0.05));
     149        ds.addPrimitive(n1);
     150        ds.addPrimitive(n2);
     151        ds.addPrimitive(n3);
     152        ds.addPrimitive(n4);
     153        Way way = new Way(1);
     154        ds.addPrimitive(way);
     155
     156        assertEquals(0.0, way.getLongestSegmentLength());
     157        way.setNodes(Arrays.asList(n1, n2, n2, n3, n4));
     158
     159        assertEquals(3148.5902810874577, way.getLongestSegmentLength());
     160    }
    137161}
  • new file 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
    - +  
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.validation.tests;
     3
     4import static org.junit.jupiter.api.Assertions.assertFalse;
     5import static org.junit.jupiter.api.Assertions.assertTrue;
     6
     7import org.junit.jupiter.api.BeforeEach;
     8import org.junit.jupiter.api.Test;
     9import org.openstreetmap.josm.JOSMFixture;
     10import org.openstreetmap.josm.data.coor.LatLon;
     11import org.openstreetmap.josm.data.osm.DataSet;
     12import org.openstreetmap.josm.data.osm.Node;
     13import org.openstreetmap.josm.data.osm.TagMap;
     14import org.openstreetmap.josm.data.osm.Way;
     15import org.openstreetmap.josm.gui.MainApplication;
     16import org.openstreetmap.josm.gui.layer.OsmDataLayer;
     17
     18
     19public class PowerLinesTest {
     20    private PowerLines powerLines;
     21    private DataSet ds;
     22
     23    @BeforeEach
     24    public void setUp() throws Exception {
     25        JOSMFixture.createUnitTestFixture().init();
     26        ds = new DataSet();
     27        MainApplication.getLayerManager().addLayer(new OsmDataLayer(ds, null, null));
     28
     29        powerLines = new PowerLines();
     30        powerLines.initialize();
     31        powerLines.startTest(null);
     32    }
     33
     34    @Test
     35    void testNoBreakInLine() {
     36        Way powerline = new Way();
     37        powerline.setKeys(new TagMap("power", "line"));
     38        ds.addPrimitive(powerline);
     39
     40        for (int i = 0; i < 10; i++) {
     41            Node node = new Node(new LatLon(0, 0.001 * i));
     42            node.setKeys(new TagMap("power", "tower"));
     43            ds.addPrimitive(node);
     44            powerline.addNode(node);
     45        }
     46        powerLines.visit(powerline);
     47        powerLines.endTest();
     48        assertTrue(powerLines.getErrors().isEmpty());
     49    }
     50
     51    @Test
     52    void testBreakInLine() {
     53        Way powerline = new Way();
     54        powerline.setKeys(new TagMap("power", "line"));
     55        ds.addPrimitive(powerline);
     56
     57        for (int i = 0; i < 10; i++) {
     58            if (i != 4 && i != 5) {
     59                Node node = new Node(new LatLon(0, 0.001 * i));
     60                node.setKeys(new TagMap("power", "tower"));
     61                ds.addPrimitive(node);
     62                powerline.addNode(node);
     63            }
     64        }
     65        powerLines.visit(powerline);
     66        powerLines.endTest();
     67        assertFalse(powerLines.getErrors().isEmpty());
     68    }
     69
     70    @Test
     71    void testConnectionAndRefInLine() {
     72        Way powerline = new Way();
     73        powerline.setKeys(new TagMap("power", "line"));
     74        ds.addPrimitive(powerline);
     75
     76        int connectionCount = 0;
     77
     78        for (int i = 0; i < 10; i++) {
     79            Node node = new Node(new LatLon(0, 0.001 * i));
     80            node.setKeys(new TagMap("power", "tower", "ref", Integer.toString(i)));
     81            if (i == 4 || i == 5) {
     82                node.setKeys(new TagMap("power", "connection"));
     83                connectionCount++;
     84            }
     85            if (i > 5) {
     86                node.setKeys(new TagMap("power", "tower", "ref", Integer.toString(i - connectionCount)));
     87            }
     88            ds.addPrimitive(node);
     89            powerline.addNode(node);
     90        }
     91        powerLines.visit(powerline);
     92        powerLines.endTest();
     93        assertTrue(powerLines.getErrors().isEmpty());
     94    }
     95
     96    @Test
     97    void testRefDiscontinuityInLine() {
     98        Way powerline = new Way();
     99        powerline.setKeys(new TagMap("power", "minor_line"));
     100        ds.addPrimitive(powerline);
     101
     102        for (int i = 0; i < 10; i++) {
     103            Node node = new Node(new LatLon(0, 0.001 * i));
     104            node.setKeys(new TagMap("power", "tower", "ref", Integer.toString(i)));
     105            if (i < 4) {
     106                // add discontinuity
     107                node.setKeys(new TagMap("power", "tower", "ref", Integer.toString(i + 1)));
     108            }
     109            ds.addPrimitive(node);
     110            powerline.addNode(node);
     111        }
     112        powerLines.visit(powerline);
     113        powerLines.endTest();
     114        assertFalse(powerLines.getErrors().isEmpty());
     115    }
     116}
  • 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 b  
    250250     */
    251251    @Test
    252252    void testJoinAsHtmlUnorderedList() {
    253         List<? extends Object> items = Arrays.asList("1", Integer.valueOf(2));
     253        List<?> items = Arrays.asList("1", 2);
    254254        assertEquals("<ul><li>1</li><li>2</li></ul>", Utils.joinAsHtmlUnorderedList(items));
    255255        assertEquals("<ul></ul>", Utils.joinAsHtmlUnorderedList(Collections.emptyList()));
    256256    }
     
    531531        final String output = Utils.execOutput(Arrays.asList("echo", "Hello", "World"));
    532532        assertEquals("Hello World", output);
    533533    }
     534
     535    /**
     536     * Test of {@link Utils#getStandardDeviation(double[])} and {@link Utils#getStandardDeviation(double[], double)}
     537     */
     538    @Test
     539    void testGetStandardDeviation() {
     540        assertEquals(0.0, Utils.getStandardDeviation(new double[]{1, 1, 1, 1}));
     541        assertEquals(0.0, Utils.getStandardDeviation(new double[]{1, 1, 1, 1}, 1.0));
     542        assertEquals(0.5, Utils.getStandardDeviation(new double[]{1, 1, 2, 2}));
     543
     544        assertEquals(-1.0, Utils.getStandardDeviation(new double[]{}));
     545        assertEquals(-1.0, Utils.getStandardDeviation(new double[]{0}));
     546    }
    534547}