Ticket #20716: josm_20716_power_v6.patch
File josm_20716_power_v6.patch, 23.3 KB (added by , 3 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 650 650 return length; 651 651 } 652 652 653 /** 654 * Calculates the segment lengths of the way as computed by {@link LatLon#greatCircleDistance}. 655 * @return The segment lengths of a way in metres, from the first to last node 656 * @since xxx 657 */ 658 public double[] getSegmentLengths() { 659 double[] segmentLengths = new double[nodes.length - 1]; 660 Node prevNode = this.firstNode(); 661 662 for (int i = 1; i < nodes.length; i++) { 663 Node n = nodes[i]; 664 665 double distance = n.getCoor().greatCircleDistance(prevNode.getCoor()); 666 segmentLengths[i - 1] = distance; 667 prevNode = n; 668 } 669 670 return segmentLengths; 671 } 672 653 673 /** 654 674 * Replies the length of the longest segment of the way, in metres, as computed by {@link LatLon#greatCircleDistance}. 655 675 * @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 * <ul> 36 * <li>nodes in power lines/minor_lines that do not have a power=tower/pole/portal tag 37 * <li>nodes where the reference numbering not consistent 38 * <li>ways where are unusually long segments without line support feature 39 * <li>ways where the line type is possibly misused 40 * </ul> 41 * See #7812 and #20716 for discussions about this test. 30 42 */ 31 43 public class PowerLines extends Test { 32 44 33 /** Test identifier */34 protected static final int POWER_ LINES= 2501;45 /** Test identifiers */ 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 * Power line support features ref=* numbering direction. 84 */ 85 private enum NumberingDirection { 86 /** No direction */ 87 NONE, 88 /** Numbering follows way direction */ 89 SAME, 90 /** Numbering goes opposite way direction */ 91 OPPOSITE, 92 /** Not distinguishable numbering direction */ 93 MIXED 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); 116 clearCollections(); 201 setShowElements(true); 202 203 // collect all waterways 204 getLayerManager() 205 .getActiveDataSet() 206 .getWays() 207 .parallelStream() 208 .filter(way -> 209 way.hasTag("water", "river", "lake") || 210 way.hasKey("waterway") || 211 way.referrers(Relation.class) 212 .collect(Collectors.toSet()) 213 // no parallel stream here, just makes the iteration slower 214 .stream() 215 .anyMatch(relation -> relation.hasTag("water", "river", "lake") || 216 relation.hasKey("waterway") 217 )) 218 .forEach(datasetWaterways::add); 117 219 } 118 220 119 221 @Override 120 222 public void endTest() { 121 for (Node n : missingT owerOrPole) {223 for (Node n : missingTags) { 122 224 if (!isInPowerStation(n)) { 123 errors.add(TestError.builder(this, Severity.WARNING, POWER_ LINES)124 .message(tr("Missing power tower/pole within power line"))225 errors.add(TestError.builder(this, Severity.WARNING, POWER_SUPPORT) 226 .message(tr("Missing power line support tag from node")) 125 227 .primitives(n) 126 228 .build()); 127 229 } … … 130 232 for (Node n : badConnections) { 131 233 errors.add(TestError.builder(this, Severity.WARNING, POWER_CONNECTION) 132 234 .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()); 235 + "which is not related to the power infrastructure")) 236 .primitives(n) 237 .build()); 238 } 239 240 for (WaySegment segment : missingNodes) { 241 errors.add(TestError.builder(this, Severity.WARNING, POWER_SEGMENT_LENGTH) 242 .message(tr("Missing line support node within power line")) 243 .primitives(segment.getFirstNode(), segment.getSecondNode()) 244 .highlightWaySegments(new HashSet<>(Collections.singleton(segment))) 245 .build()); 135 246 } 136 clearCollections(); 247 248 for (Node n : refDiscontinuities) { 249 errors.add(TestError.builder(this, Severity.WARNING, POWER_REF) 250 .message(tr("Mixed reference numbering")) 251 .primitives(n) 252 .build()); 253 } 254 255 for (Way w : wrongLineType) { 256 errors.add(TestError.builder(this, Severity.WARNING, POWER_LINE_TYPE) 257 .message(tr("Possibly wrong power line type used")) 258 .primitives(w) 259 .build()); 260 } 261 137 262 super.endTest(); 138 263 } 139 264 265 /** 266 * Checks reference numbering discontinuities on the given way. 267 */ 268 private void checkDiscontinuities(Way w) { 269 final List<Integer> discontinuityAtIndexes = new ArrayList<>(); 270 final NumberingDirection direction = detectDirection(w, discontinuityAtIndexes); 271 272 if (direction == NumberingDirection.MIXED) { 273 for (int disC : discontinuityAtIndexes) { 274 refDiscontinuities.add(w.getNode(disC)); 275 } 276 } 277 } 278 279 /** 280 * Calculates the standard deviation from the given segment lengths. 281 * 282 * @return standard deviation 283 */ 284 private double getStdDev(double[] segmentLengths, double mean) { 285 double standardDeviation = 0; 286 int size = segmentLengths.length; 287 288 for (double length : segmentLengths) { 289 standardDeviation += Math.pow(length - mean, 2); 290 } 291 292 return Math.sqrt(standardDeviation / size); 293 } 294 295 /** 296 * The summarized length (in metres) of a way where a power line hangs over a water area. 297 * @param ref Reference point 298 * @param crossingNodes Crossing nodes, unordered 299 * @return The summarized length (in metres) of a way where a power line hangs over a water area 300 */ 301 private double calculateIntersectingLen(Node ref, Set<Node> crossingNodes) { 302 double min = Double.POSITIVE_INFINITY; 303 double max = Double.NEGATIVE_INFINITY; 304 305 for (Node n : crossingNodes) { 306 double dist = ref.getCoor().greatCircleDistance(n.getCoor()); 307 308 if (dist < min) 309 min = dist; 310 if (dist > max) 311 max = dist; 312 } 313 return max - min; 314 } 315 316 /** 317 * Searches for way intersections, which intersect the <code>pair<code/> attribute. 318 * @param ways collection of ways to search 319 * @param parent parent way for <code>pair</code> param 320 * @param pair {@link Node} pair among which search for another way 321 * @param crossingWays found crossing ways 322 * @param crossingPositions collection of the crossing positions 323 * @implNote Inspired by <code>utilsplugin2/selection/NodeWayUtils.java#addWaysIntersectingWay()<code/> 324 */ 325 private static void findCrossings(Collection<Way> ways, Way parent, Pair<Node, Node> pair, Set<Way> crossingWays, 326 Set<Node> crossingPositions) { 327 ways.parallelStream() 328 .filter(way -> !way.isDisabled() 329 && !crossingWays.contains(way) 330 && way.getBBox().intersects(parent.getBBox())) 331 .forEach((way) -> way.getNodePairs(false) 332 .forEach(pair2 -> { 333 EastNorth eastNorth = Geometry.getSegmentSegmentIntersection( 334 pair.a.getEastNorth(), pair.b.getEastNorth(), 335 pair2.a.getEastNorth(), pair2.b.getEastNorth()); 336 if (eastNorth != null) { 337 crossingWays.add(way); 338 crossingPositions.add(new Node(eastNorth)); 339 } 340 }) 341 ); 342 } 343 344 /** 345 * Detects ref=* numbering direction. Ignores the first and last node, because ways can be connected and the "main" 346 * power line could have a different numbering. 347 * @param way Way to check 348 * @param discontinuityAtIndex List of node indexes where the discontinuity found 349 * @return way numbering direction 350 */ 351 private static NumberingDirection detectDirection(Way way, List<Integer> discontinuityAtIndex) { 352 final Set<NumberingDirection> directions = new HashSet<>(4); 353 int prevRef = -1; 354 int ref; 355 int waySize = way.getNodes().size(); 356 357 for (int i = 1; i < waySize - 1; i++) { 358 final Node n = way.getNode(i); 359 360 if (n.hasTag("ref")) { 361 try { 362 ref = Integer.parseInt(n.get("ref")); 363 if (i == 1) { 364 prevRef = ref; 365 continue; 366 } 367 368 if (ref > prevRef) directions.add(NumberingDirection.SAME); 369 else if (ref < prevRef) directions.add(NumberingDirection.OPPOSITE); 370 else { 371 directions.add(NumberingDirection.MIXED); 372 discontinuityAtIndex.add(i); 373 } 374 375 prevRef = ref; 376 } catch (NumberFormatException ignore) { 377 prevRef = getPrevRef(prevRef, directions); 378 } 379 } else if (prevRef != -1) { 380 prevRef = getPrevRef(prevRef, directions); 381 } 382 } 383 384 if (directions.isEmpty()) 385 return NumberingDirection.NONE; 386 else if (directions.size() > 1) 387 return NumberingDirection.MIXED; 388 else 389 return directions.stream().findAny().get(); 390 } 391 392 /** 393 * @return Guessed ref number of the next power=pole/tower 394 */ 395 private static int getPrevRef(int prevRef, Set<NumberingDirection> directions) { 396 if (directions.size() == 1 && directions.stream().findFirst().get() == NumberingDirection.SAME) 397 prevRef += 1; 398 else if (directions.size() == 1 && directions.stream().findFirst().get() == NumberingDirection.OPPOSITE) 399 prevRef -= 1; 400 return prevRef; 401 } 402 403 private static boolean isRelatedToPower(Way way) { 404 if (way.hasTag("power") || way.hasTag("building")) 405 return true; 406 for (OsmPrimitive ref : way.getReferrers()) { 407 if (ref instanceof Relation && ref.isMultipolygon() && (ref.hasTag("power") || ref.hasTag("building"))) { 408 for (RelationMember rm : ((Relation) ref).getMembers()) { 409 if (way == rm.getMember()) 410 return true; 411 } 412 } 413 } 414 return false; 415 } 416 417 /** 418 * Determines if the current node connected to a line which used usually used inside power stations. 419 * @param n node to check 420 * @param w parent way of {@code n} 421 * @return {@code true} if {@code n} connected to power=line + line=* 422 */ 423 private static boolean isConnectedToStationLine(Node n, Way w) { 424 for (OsmPrimitive p : n.getReferrers()) { 425 if (p instanceof Way && !p.equals(w) && isPowerLine((Way) p) && p.hasKey("line")) 426 return true; 427 } 428 return false; 429 } 430 431 /** 432 * Checks if the given node is inside a power station. 433 * @param n Node to checked 434 */ 140 435 protected final boolean isInPowerStation(Node n) { 141 436 for (OsmPrimitive station : powerStations) { 142 437 List<List<Node>> nodesLists = new ArrayList<>(); … … 171 466 /** 172 467 * Determines if the specified primitive denotes a power station. 173 468 * @param p The primitive to be tested 174 * @return {@code true} if power key is set and equal to station/sub_station/plant469 * @return {@code true} if power key is set and equal to generator/substation/plant 175 470 */ 176 471 protected static final boolean isPowerStation(OsmPrimitive p) { 177 472 return isPowerIn(p, POWER_STATION_TAGS) || isBuildingIn(p, BUILDING_STATION_TAGS); 178 473 } 179 474 180 475 /** 181 * Determines if the specified node denotes a power tower/pole.476 * Determines if the specified node denotes a power support feature. 182 477 * @param n The node to be tested 183 * @return {@code true} if power key is set and equal to tower/pole478 * @return {@code true} if power key is set and equal to pole/tower/portal/catenary_mast 184 479 */ 185 480 protected static final boolean isPowerTower(Node n) { 186 481 return isPowerIn(n, POWER_TOWER_TAGS); … … 189 484 /** 190 485 * Determines if the specified node denotes a power infrastructure allowed on a power line. 191 486 * @param n The node to be tested 192 * @return True if power key is set and equal to switch/tranformer/busbar/generator 487 * @return True if power key is set and equal to compensator/converter/generator/insulator 488 * /switch/switchgear/terminal/transformer 193 489 */ 194 protected static final boolean isPower Allowed(Node n) {195 return isPowerIn(n, POWER_ ALLOWED_TAGS);490 protected static final boolean isPowerInfrastructure(Node n) { 491 return isPowerIn(n, POWER_INFRASTRUCTURE_TAGS); 196 492 } 197 493 198 494 /** … … 215 511 return p.hasTag("building", values); 216 512 } 217 513 218 private void clearCollections() { 514 @Override 515 public void clear() { 516 super.clear(); 219 517 powerStations.clear(); 220 518 badConnections.clear(); 221 missingTowerOrPole.clear(); 519 missingTags.clear(); 520 missingNodes.clear(); 521 wrongLineType.clear(); 522 refDiscontinuities.clear(); 523 datasetWaterways.clear(); 222 524 } 223 525 }