Ticket #20716: josm_20716_power_v5.patch
File josm_20716_power_v5.patch, 22.7 KB (added by , 4 years ago) |
---|
-
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 654 654 return length; 655 655 } 656 656 657 /** 658 * Calculates the segment lengths of the way as computed by {@link LatLon#greatCircleDistance}. 659 * @return The segment lengths of a way in metres, from the first to last node 660 * @since xxx 661 */ 662 public double[] getSegmentLengths() { 663 double[] segmentLengths = new double[nodes.length - 1]; 664 Node prevNode = this.firstNode(); 665 666 for (int i = 1; i < nodes.length; i++) { 667 Node n = nodes[i]; 668 669 double distance = n.getCoor().greatCircleDistance(prevNode.getCoor()); 670 segmentLengths[i - 1] = distance; 671 prevNode = n; 672 } 673 674 return segmentLengths; 675 } 676 657 677 /** 658 678 * Replies the length of the longest segment of the way, in metres, as computed by {@link LatLon#greatCircleDistance}. 659 679 * @return The length of the segment, in metres -
src/org/openstreetmap/josm/data/validation/tests/PowerLines.java
IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 diff --git a/src/org/openstreetmap/josm/data/validation/tests/PowerLines.java b/src/org/openstreetmap/josm/data/validation/tests/PowerLines.java
a b 1 1 // License: GPL. For details, see LICENSE file. 2 2 package org.openstreetmap.josm.data.validation.tests; 3 3 4 import static org.openstreetmap.josm.gui.MainApplication.getLayerManager; 4 5 import static org.openstreetmap.josm.tools.I18n.tr; 5 6 6 7 import java.util.ArrayList; 7 8 import java.util.Arrays; 8 9 import java.util.Collection; 9 import java.util.LinkedHashSet; 10 import java.util.Collections; 11 import java.util.HashSet; 10 12 import java.util.List; 11 13 import java.util.Set; 14 import java.util.stream.Collectors; 12 15 16 import org.openstreetmap.josm.data.coor.EastNorth; 13 17 import org.openstreetmap.josm.data.osm.Node; 14 18 import org.openstreetmap.josm.data.osm.OsmPrimitive; 15 19 import org.openstreetmap.josm.data.osm.Relation; 16 20 import org.openstreetmap.josm.data.osm.RelationMember; 17 21 import org.openstreetmap.josm.data.osm.Way; 22 import org.openstreetmap.josm.data.osm.WaySegment; 18 23 import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon; 19 24 import org.openstreetmap.josm.data.osm.visitor.paint.relations.Multipolygon.JoinedWay; 20 25 import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache; … … 23 28 import org.openstreetmap.josm.data.validation.TestError; 24 29 import org.openstreetmap.josm.gui.progress.ProgressMonitor; 25 30 import org.openstreetmap.josm.tools.Geometry; 31 import org.openstreetmap.josm.tools.Pair; 26 32 27 33 /** 28 * Checks for nodes in power lines/minor_lines that do not have a power=tower/pole tag.<br> 29 * See #7812 for discussions about this test. 34 * Checks for 35 * <p><ul> 36 * <li>nodes in power lines/minor_lines that do not have a power=tower/pole/portal tag 37 * <li>nodes where the reference numbering not 38 * <li>ways where are unusually long segments without line support feature 39 * <li>ways where the line type is possibly misused 40 * </ul> 41 * See #7812 and #20716 for discussions about this test. 30 42 */ 31 43 public class PowerLines extends Test { 32 44 33 45 /** Test identifier */ 34 protected static final int POWER_ LINES= 2501;46 protected static final int POWER_SUPPORT = 2501; 35 47 protected static final int POWER_CONNECTION = 2502; 48 protected static final int POWER_SEGMENT_LENGTH = 2503; 49 protected static final int POWER_REF = 2504; 50 protected static final int POWER_LINE_TYPE = 2505; 36 51 37 52 /** Values for {@code power} key interpreted as power lines */ 38 53 static final Collection<String> POWER_LINE_TAGS = Arrays.asList("line", "minor_line"); 39 54 /** Values for {@code power} key interpreted as power towers */ 40 static final Collection<String> POWER_TOWER_TAGS = Arrays.asList(" tower", "pole");55 static final Collection<String> POWER_TOWER_TAGS = Arrays.asList("catenary_mast", "pole", "portal", "tower"); 41 56 /** Values for {@code power} key interpreted as power stations */ 42 static final Collection<String> POWER_STATION_TAGS = Arrays.asList(" station", "sub_station", "substation", "plant", "generator");57 static final Collection<String> POWER_STATION_TAGS = Arrays.asList("generator", "plant", "substation"); 43 58 /** Values for {@code building} key interpreted as power stations */ 44 59 static final Collection<String> BUILDING_STATION_TAGS = Arrays.asList("transformer_tower"); 45 60 /** Values for {@code power} key interpreted as allowed power items */ 46 static final Collection<String> POWER_ ALLOWED_TAGS = Arrays.asList("switch", "transformer", "busbar", "generator", "switchgear",47 " portal", "terminal", "insulator");61 static final Collection<String> POWER_INFRASTRUCTURE_TAGS = Arrays.asList("compensator", "converter", 62 "generator", "insulator", "switch", "switchgear", "terminal", "transformer"); 48 63 49 private final Set<Node> badConnections = new LinkedHashSet<>(); 50 private final Set<Node> missingTowerOrPole = new LinkedHashSet<>(); 64 private final Set<Node> badConnections = new HashSet<>(); 65 private final Set<Node> missingTags = new HashSet<>(); 66 private final Set<Node> refDiscontinuities = new HashSet<>(); 67 private final Set<Way> wrongLineType = new HashSet<>(); 68 private final Set<WaySegment> missingNodes = new HashSet<>(); 51 69 52 70 private final List<OsmPrimitive> powerStations = new ArrayList<>(); 53 71 72 private final Collection<Way> dataSetWaterWays = new HashSet<>(64); 73 54 74 /** 55 75 * Constructs a new {@code PowerLines} test. 56 76 */ 57 77 public PowerLines() { 58 super(tr("Power lines"), tr("Checks for nodes in power lines that do not have a power=tower/pole tag.")); 59 } 60 61 @Override 62 public void visit(Way w) { 63 if (w.isUsable()) { 64 if (isPowerLine(w) && !w.hasTag("location", "underground")) { 65 for (Node n : w.getNodes()) { 66 if (!isPowerTower(n) && !isPowerAllowed(n) && IN_DOWNLOADED_AREA.test(n) 67 && (!w.isFirstLastNode(n) || !isPowerStation(n))) { 68 missingTowerOrPole.add(n); 69 } 70 } 71 } else if (w.isClosed() && isPowerStation(w)) { 72 powerStations.add(w); 73 } 74 } 78 super(tr("Power lines"), tr("Checks if power line missing a support node and " + 79 "for nodes in power lines that do not have a power=tower/pole tag")); 80 } 81 82 /** 83 * Support features ref=* numbering direction. 84 */ 85 private enum NumberingDirection { 86 /** No direction */ 87 NONE, 88 /** Numbering follows way direction */ 89 SAME, 90 /** Numbering goes opposite way direction */ 91 OPPOSITE, 92 /** Not distinguishable numbering direction */ 93 MIXED 75 94 } 76 95 77 96 @Override … … 89 108 badConnections.add(n); 90 109 } 91 110 92 private static boolean isRelatedToPower(Way way) { 93 if (way.hasTag("power") || way.hasTag("building")) 94 return true; 95 for (OsmPrimitive ref : way.getReferrers()) { 96 if (ref instanceof Relation && ref.isMultipolygon() && (ref.hasTag("power") || ref.hasTag("building"))) { 97 for (RelationMember rm : ((Relation) ref).getMembers()) { 98 if (way == rm.getMember()) 99 return true; 111 @Override 112 public void visit(Way w) { 113 if (!isPrimitiveUsable(w)) return; 114 115 if (isPowerLine(w) && !w.hasKey("line") && !w.hasTag("location", "underground")) { 116 final int segmentCount = w.getNodesCount() - 1; 117 final double mean = w.getLength() / segmentCount; 118 final double stdDev = getStdDev(w.getSegmentLengths(), mean); 119 int poleCount = 0; 120 int towerCount = 0; 121 Node prevNode = w.firstNode(); 122 123 double threshold = w.hasTag("power", "line") ? 1.6 : 1.8; 124 if (mean / stdDev < 4) { 125 //compensate for possibly hilly areas where towers can't be put anywhere 126 threshold += 0.2; 127 } 128 129 for (int i = 1; i < w.getRealNodesCount(); i++) { 130 final Node n = w.getNode(i); 131 132 /// handle power station line connections (eg. power=line + line=*) 133 { 134 if (isConnectedToStationLine(n, w)) { 135 prevNode = n; 136 continue; // skip, it would be false positive 137 } 100 138 } 139 140 /// handle missing power line support tags (eg. tower) 141 { 142 if (!isPowerTower(n) && !isPowerInfrastructure(n) && IN_DOWNLOADED_AREA.test(n) 143 && (!w.isFirstLastNode(n) || !isPowerStation(n))) 144 missingTags.add(n); 145 } 146 147 /// handle missing nodes 148 { 149 double segmentLen = n.getCoor().greatCircleDistance(prevNode.getCoor()); 150 151 // check power=line waterway crossing 152 final Pair<Node, Node> pair = Pair.create(prevNode, n); 153 final Set<Way> crossingWaterWays = new HashSet<>(8); 154 final Set<Node> crossingPositions = new HashSet<>(8); 155 findCrossings(dataSetWaterWays, w, pair, crossingWaterWays, crossingPositions); 156 157 if (!crossingWaterWays.isEmpty()) { 158 double compensation = calculateIntersectingLen(prevNode, crossingPositions); 159 segmentLen -= compensation; 160 } 161 162 if (segmentCount > 4 && segmentLen > mean * threshold && !isPowerInfrastructure(n)) 163 missingNodes.add(WaySegment.forNodePair(w, prevNode, n)); 164 } 165 166 /// handle wrong line types 167 { 168 if (n.hasTag("power", "pole")) 169 poleCount++; 170 else if (n.hasTag("power", "tower")) 171 towerCount++; 172 } 173 174 prevNode = n; 101 175 } 176 177 /// handle ref=* numbering discontinuities 178 checkDiscontinuities(w); 179 180 /// handle wrong line types 181 if ((poleCount > towerCount && w.hasTag("power", "line")) || 182 (poleCount < towerCount && w.hasTag("power", "minor_line"))) 183 wrongLineType.add(w); 184 185 186 } else if (w.isClosed() && isPowerStation(w)) { 187 powerStations.add(w); 102 188 } 103 return false;104 189 } 105 190 106 191 @Override … … 113 198 @Override 114 199 public void startTest(ProgressMonitor progressMonitor) { 115 200 super.startTest(progressMonitor); 201 setShowElements(true); 202 116 203 clearCollections(); 204 205 getLayerManager() 206 .getActiveDataSet() 207 .getWays() 208 .parallelStream() 209 .filter(way -> 210 way.hasTag("water", "river", "lake") || 211 way.hasKey("waterway") || 212 way.referrers(Relation.class) 213 .collect(Collectors.toSet()) 214 // no parallel stream here, just makes the iteration slower 215 .stream() 216 .anyMatch(relation -> relation.hasTag("water", "river", "lake") || 217 relation.hasKey("waterway") 218 )) 219 .forEach(dataSetWaterWays::add); 117 220 } 118 221 119 222 @Override 120 223 public void endTest() { 121 for (Node n : missingT owerOrPole) {224 for (Node n : missingTags) { 122 225 if (!isInPowerStation(n)) { 123 errors.add(TestError.builder(this, Severity.WARNING, POWER_ LINES)124 .message(tr("Missing power tower/pole within power line"))226 errors.add(TestError.builder(this, Severity.WARNING, POWER_SUPPORT) 227 .message(tr("Missing power line support tag from node")) 125 228 .primitives(n) 126 229 .build()); 127 230 } … … 130 233 for (Node n : badConnections) { 131 234 errors.add(TestError.builder(this, Severity.WARNING, POWER_CONNECTION) 132 235 .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()); 236 + "which is not related to the power infrastructure")) 237 .primitives(n) 238 .build()); 239 } 240 241 for (WaySegment segment : missingNodes) { 242 errors.add(TestError.builder(this, Severity.WARNING, POWER_SEGMENT_LENGTH) 243 .message(tr("Missing line support node within power line")) 244 .primitives(segment.getFirstNode(), segment.getSecondNode()) 245 .highlightWaySegments(new HashSet<>(Collections.singleton(segment))) 246 .build()); 135 247 } 248 249 for (Node n : refDiscontinuities) { 250 errors.add(TestError.builder(this, Severity.WARNING, POWER_REF) 251 .message(tr("Mixed reference numbering")) 252 .primitives(n) 253 .build()); 254 } 255 256 for (Way w : wrongLineType) { 257 errors.add(TestError.builder(this, Severity.WARNING, POWER_LINE_TYPE) 258 .message(tr("Possibly wrong power line type used")) 259 .primitives(w) 260 .build()); 261 } 262 136 263 clearCollections(); 137 264 super.endTest(); 138 265 } 139 266 267 private void checkDiscontinuities(Way w) { 268 final List<Integer> discontinuityAtIndexes = new ArrayList<>(); 269 final NumberingDirection direction = detectDirection(w, discontinuityAtIndexes); 270 271 if (direction == NumberingDirection.MIXED) { 272 for (int disC : discontinuityAtIndexes) { 273 refDiscontinuities.add(w.getNode(disC)); 274 } 275 } 276 } 277 278 /** 279 * @return Standard deviation 280 */ 281 private double getStdDev(double[] segmentLengths, double mean) { 282 double standardDeviation = 0; 283 int size = segmentLengths.length; 284 285 for (double length : segmentLengths) { 286 standardDeviation += Math.pow(length - mean, 2); 287 } 288 289 return Math.sqrt(standardDeviation / size); 290 } 291 292 /** 293 * @param ref Reference point 294 * @param crossingNodes Crossing nodes, unordered 295 * @return The summarized length (in metres) of a way where a power line hangs over a water area 296 */ 297 private double calculateIntersectingLen(Node ref, Set<Node> crossingNodes) { 298 double min = Double.POSITIVE_INFINITY; 299 double max = Double.NEGATIVE_INFINITY; 300 301 for (Node n : crossingNodes) { 302 double dist = ref.getCoor().greatCircleDistance(n.getCoor()); 303 304 if (dist < min) 305 min = dist; 306 if (dist > max) 307 max = dist; 308 } 309 return max - min; 310 } 311 312 /** 313 * Searches for way intersections, which intersect the <code>pair<code/> attribute. 314 * @param ways collection of ways to search 315 * @param parent parent way for <code>pair</code> param 316 * @param pair {@link Node} pair among which search for another way 317 * @param crossingWays found crossing ways 318 * @param crossingPositions collection of the crossing positions 319 * @implNote Inspired by <code>utilsplugin2/selection/NodeWayUtils.java -> addWaysIntersectingWay()<code/> 320 */ 321 private static void findCrossings(Collection<Way> ways, Way parent, Pair<Node, Node> pair, Set<Way> crossingWays, 322 Set<Node> crossingPositions) { 323 ways.parallelStream() 324 .filter(way -> !way.isDisabled() 325 && !crossingWays.contains(way) 326 && way.getBBox().intersects(parent.getBBox())) 327 .forEach((way) -> way.getNodePairs(false) 328 .forEach(pair2 -> { 329 EastNorth eastNorth = Geometry.getSegmentSegmentIntersection( 330 pair.a.getEastNorth(), pair.b.getEastNorth(), 331 pair2.a.getEastNorth(), pair2.b.getEastNorth()); 332 if (eastNorth != null) { 333 crossingWays.add(way); 334 crossingPositions.add(new Node(eastNorth)); 335 } 336 }) 337 ); 338 } 339 340 /** 341 * Detects ref=* numbering direction. Ignores the first and last node, because ways can be connected and the "main" 342 * power line could have a different numbering. 343 * @param way Way to check 344 * @param discontinuityAtIndex List of node indexes where the discontinuity found 345 * @return direction 346 */ 347 private static NumberingDirection detectDirection(Way way, List<Integer> discontinuityAtIndex) { 348 final Set<NumberingDirection> directions = new HashSet<>(4); 349 int prevRef = -1; 350 int ref; 351 int waySize = way.getNodes().size(); 352 353 for (int i = 1; i < waySize - 1; i++) { 354 final Node n = way.getNode(i); 355 356 if (n.hasTag("ref")) { 357 try { 358 ref = Integer.parseInt(n.get("ref")); 359 if (i == 1) { 360 prevRef = ref; 361 continue; 362 } 363 364 if (ref > prevRef) directions.add(NumberingDirection.SAME); 365 else if (ref < prevRef) directions.add(NumberingDirection.OPPOSITE); 366 else { 367 directions.add(NumberingDirection.MIXED); 368 discontinuityAtIndex.add(i); 369 } 370 371 prevRef = ref; 372 } catch (NumberFormatException ignore) { 373 prevRef = getPrevRef(prevRef, directions); 374 } 375 } else if (prevRef != -1) { 376 prevRef = getPrevRef(prevRef, directions); 377 } 378 } 379 380 if (directions.isEmpty()) 381 return NumberingDirection.NONE; 382 else if (directions.size() > 1) 383 return NumberingDirection.MIXED; 384 else 385 return directions.stream().findAny().get(); 386 } 387 388 /** 389 * @return Guessed ref number of the next power=pole/tower 390 */ 391 private static int getPrevRef(int prevRef, Set<NumberingDirection> directions) { 392 if (directions.size() == 1 && directions.stream().findFirst().get() == NumberingDirection.SAME) 393 prevRef += 1; 394 else if (directions.size() == 1 && directions.stream().findFirst().get() == NumberingDirection.OPPOSITE) 395 prevRef -= 1; 396 return prevRef; 397 } 398 399 private static boolean isRelatedToPower(Way way) { 400 if (way.hasTag("power") || way.hasTag("building")) 401 return true; 402 for (OsmPrimitive ref : way.getReferrers()) { 403 if (ref instanceof Relation && ref.isMultipolygon() && (ref.hasTag("power") || ref.hasTag("building"))) { 404 for (RelationMember rm : ((Relation) ref).getMembers()) { 405 if (way == rm.getMember()) 406 return true; 407 } 408 } 409 } 410 return false; 411 } 412 413 /** 414 * Determines if the current node connected to a line which used usually used inside power stations. 415 * @param n node to check 416 * @param w parent way of {@code n} 417 * @return {@code true} if {@code n} connected to power=line + line=* 418 */ 419 private static boolean isConnectedToStationLine(Node n, Way w) { 420 for (OsmPrimitive p : n.getReferrers()) { 421 if (p instanceof Way && !p.equals(w) && isPowerLine((Way) p) && p.hasKey("line")) 422 return true; 423 } 424 return false; 425 } 426 140 427 protected final boolean isInPowerStation(Node n) { 141 428 for (OsmPrimitive station : powerStations) { 142 429 List<List<Node>> nodesLists = new ArrayList<>(); … … 171 458 /** 172 459 * Determines if the specified primitive denotes a power station. 173 460 * @param p The primitive to be tested 174 * @return {@code true} if power key is set and equal to station/sub_station/plant461 * @return {@code true} if power key is set and equal to generator/substation/plant 175 462 */ 176 463 protected static final boolean isPowerStation(OsmPrimitive p) { 177 464 return isPowerIn(p, POWER_STATION_TAGS) || isBuildingIn(p, BUILDING_STATION_TAGS); 178 465 } 179 466 180 467 /** 181 * Determines if the specified node denotes a power tower/pole.468 * Determines if the specified node denotes a power support feature. 182 469 * @param n The node to be tested 183 * @return {@code true} if power key is set and equal to tower/pole470 * @return {@code true} if power key is set and equal to pole/tower/portal/catenary_mast 184 471 */ 185 472 protected static final boolean isPowerTower(Node n) { 186 473 return isPowerIn(n, POWER_TOWER_TAGS); … … 189 476 /** 190 477 * Determines if the specified node denotes a power infrastructure allowed on a power line. 191 478 * @param n The node to be tested 192 * @return True if power key is set and equal to switch/tranformer/busbar/generator 479 * @return True if power key is set and equal to compensator/converter/generator/insulator 480 * /switch/switchgear/terminal/transformer 193 481 */ 194 protected static final boolean isPower Allowed(Node n) {195 return isPowerIn(n, POWER_ ALLOWED_TAGS);482 protected static final boolean isPowerInfrastructure(Node n) { 483 return isPowerIn(n, POWER_INFRASTRUCTURE_TAGS); 196 484 } 197 485 198 486 /** … … 218 506 private void clearCollections() { 219 507 powerStations.clear(); 220 508 badConnections.clear(); 221 missingTowerOrPole.clear(); 509 missingTags.clear(); 510 missingNodes.clear(); 511 wrongLineType.clear(); 512 refDiscontinuities.clear(); 513 dataSetWaterWays.clear(); 222 514 } 223 515 }