diff --git a/modules/core/tree.js b/modules/core/tree.js index 674395500..02adad09a 100644 --- a/modules/core/tree.js +++ b/modules/core/tree.js @@ -4,23 +4,68 @@ import { coreDifference } from './difference'; export function coreTree(head) { - var rtree = new RBush(); - var bboxes = {}; + // tree for entities + var _rtree = new RBush(); + var _bboxes = {}; + + // maintain a separate tree for granular way segments + var _segmentsRTree = new RBush(); + var _segmentsBBoxes = {}; + var _segmentsByWayId = {}; + var tree = {}; function entityBBox(entity) { var bbox = entity.extent(head).bbox(); bbox.id = entity.id; - bboxes[entity.id] = bbox; + _bboxes[entity.id] = bbox; return bbox; } + function segmentBBox(segment) { + var bbox = segment.extent(head).bbox(); + bbox.segment = segment; + _segmentsBBoxes[segment.id] = bbox; + return bbox; + } + + + function removeEntity(entity) { + _rtree.remove(_bboxes[entity.id]); + delete _bboxes[entity.id]; + + if (_segmentsByWayId[entity.id]) { + _segmentsByWayId[entity.id].forEach(function(segment) { + _segmentsRTree.remove(_segmentsBBoxes[segment.id]); + delete _segmentsBBoxes[segment.id]; + }); + delete _segmentsByWayId[entity.id]; + } + } + + + function loadEntities(entities) { + _rtree.load(entities.map(entityBBox)); + + var segments = []; + entities.forEach(function(entity) { + if (entity.segments) { + var entitySegments = entity.segments(head); + // cache these to make them easy to remove later + _segmentsByWayId[entity.id] = entitySegments; + segments = segments.concat(entitySegments); + } + }); + if (segments.length) _segmentsRTree.load(segments.map(segmentBBox)); + } + + function updateParents(entity, insertions, memo) { head.parentWays(entity).forEach(function(way) { - if (bboxes[way.id]) { - rtree.remove(bboxes[way.id]); + if (_bboxes[way.id]) { + removeEntity(way); insertions[way.id] = way; } updateParents(way, insertions, memo); @@ -29,8 +74,8 @@ export function coreTree(head) { head.parentRelations(entity).forEach(function(relation) { if (memo[entity.id]) return; memo[entity.id] = true; - if (bboxes[relation.id]) { - rtree.remove(bboxes[relation.id]); + if (_bboxes[relation.id]) { + removeEntity(relation); insertions[relation.id] = relation; } updateParents(relation, insertions, memo); @@ -45,11 +90,11 @@ export function coreTree(head) { var entity = entities[i]; if (!entity.visible) continue; - if (head.entities.hasOwnProperty(entity.id) || bboxes[entity.id]) { + if (head.entities.hasOwnProperty(entity.id) || _bboxes[entity.id]) { if (!force) { continue; - } else if (bboxes[entity.id]) { - rtree.remove(bboxes[entity.id]); + } else if (_bboxes[entity.id]) { + removeEntity(entity); } } @@ -57,51 +102,61 @@ export function coreTree(head) { updateParents(entity, insertions, {}); } - rtree.load(Object.values(insertions).map(entityBBox)); + loadEntities(Object.values(insertions)); return tree; }; - tree.intersects = function(extent, graph) { - if (graph !== head) { - var diff = coreDifference(head, graph); - var changed = diff.didChange; + function updateToGraph(graph) { + if (graph === head) return; - head = graph; + var diff = coreDifference(head, graph); - if (changed.addition || changed.deletion || changed.geometry) { - var insertions = {}; + head = graph; - if (changed.deletion) { - diff.deleted().forEach(function(entity) { - rtree.remove(bboxes[entity.id]); - delete bboxes[entity.id]; - }); - } + var changed = diff.didChange; + if (!changed.addition && !changed.deletion && !changed.geometry) return; - if (changed.geometry) { - diff.modified().forEach(function(entity) { - rtree.remove(bboxes[entity.id]); - insertions[entity.id] = entity; - updateParents(entity, insertions, {}); - }); - } + var insertions = {}; - if (changed.addition) { - diff.created().forEach(function(entity) { - insertions[entity.id] = entity; - }); - } - - rtree.load(Object.values(insertions).map(entityBBox)); - } + if (changed.deletion) { + diff.deleted().forEach(function(entity) { + removeEntity(entity); + }); } - return rtree.search(extent.bbox()) + if (changed.geometry) { + diff.modified().forEach(function(entity) { + removeEntity(entity); + insertions[entity.id] = entity; + updateParents(entity, insertions, {}); + }); + } + + if (changed.addition) { + diff.created().forEach(function(entity) { + insertions[entity.id] = entity; + }); + } + + loadEntities(Object.values(insertions)); + } + + // returns an array of entities with bounding boxes overlapping `extent` for the given `graph` + tree.intersects = function(extent, graph) { + updateToGraph(graph); + return _rtree.search(extent.bbox()) .map(function(bbox) { return graph.entity(bbox.id); }); }; + // returns an array of segment objects with bounding boxes overlapping `extent` for the given `graph` + tree.waySegments = function(extent, graph) { + updateToGraph(graph); + return _segmentsRTree.search(extent.bbox()) + .map(function(bbox) { return bbox.segment; }); + }; + return tree; } diff --git a/modules/osm/way.js b/modules/osm/way.js index e330eb675..741621de3 100644 --- a/modules/osm/way.js +++ b/modules/osm/way.js @@ -263,6 +263,40 @@ Object.assign(osmWay.prototype, { }, + // returns an array of objects representing the segments between the nodes in this way + segments: function(graph) { + + function segmentExtent(graph) { + var n1 = graph.hasEntity(this.nodes[0]); + var n2 = graph.hasEntity(this.nodes[1]); + return n1 && n2 && geoExtent([ + [ + Math.min(n1.loc[0], n2.loc[0]), + Math.min(n1.loc[1], n2.loc[1]) + ], + [ + Math.max(n1.loc[0], n2.loc[0]), + Math.max(n1.loc[1], n2.loc[1]) + ] + ]); + } + + return graph.transient(this, 'segments', function() { + var segments = []; + for (var i = 0; i < this.nodes.length - 1; i++) { + segments.push({ + id: this.id + '-' + i, + wayId: this.id, + index: i, + nodes: [this.nodes[i], this.nodes[i + 1]], + extent: segmentExtent + }); + } + return segments; + }); + }, + + // If this way is not closed, append the beginning node to the end of the nodelist to close it. close: function() { if (this.isClosed() || !this.nodes.length) return this; diff --git a/modules/validations/crossing_ways.js b/modules/validations/crossing_ways.js index bd8863f4b..5b9acaec9 100644 --- a/modules/validations/crossing_ways.js +++ b/modules/validations/crossing_ways.js @@ -207,12 +207,12 @@ export function validationCrossingWays(context) { var checkedSingleCrossingWays = {}; // declare vars ahead of time to reduce garbage collection - var i, j, nodeIndex; + var i, j; var extent; - var n1, n2, nA, nB; + var n1, n2, nA, nB, nAId, nBId; var segment1, segment2; var oneOnly; - var intersected, way2, way2FeatureType, way2Nodes; + var segmentInfos, segment2Info, way2, way2FeatureType; var way1Nodes = graph.childNodes(way1); var comparedWays = {}; for (i = 0; i < way1Nodes.length - 1; i++) { @@ -229,20 +229,24 @@ export function validationCrossingWays(context) { ] ]); - intersected = tree.intersects(extent, graph); - for (j = 0; j < intersected.length; j++) { - way2 = intersected[j]; + // Optimize by only checking overlapping segments, not every segment + // of overlapping ways + segmentInfos = tree.waySegments(extent, graph); - if (way2.type !== 'way') continue; + for (j = 0; j < segmentInfos.length; j++) { + segment2Info = segmentInfos[j]; // don't check for self-intersection in this validation - if (way2.id === way1.id) continue; + if (segment2Info.wayId === way1.id) continue; // skip if this way was already checked and only one issue is needed - if (checkedSingleCrossingWays[way2.id]) continue; + if (checkedSingleCrossingWays[segment2Info.wayId]) continue; // mark this way as checked even if there are no crossings - comparedWays[way2.id] = true; + comparedWays[segment2Info.wayId] = true; + + way2 = graph.hasEntity(segment2Info.wayId); + if (!way2) continue; // only check crossing highway, waterway, building, and railway way2FeatureType = getFeatureTypeForCrossingCheck(way2, graph); @@ -253,39 +257,41 @@ export function validationCrossingWays(context) { // create only one issue for building crossings oneOnly = way1FeatureType === 'building' || way2FeatureType === 'building'; - segment1 = [n1.loc, n2.loc]; - way2Nodes = graph.childNodes(way2); - for (nodeIndex = 0; nodeIndex < way2Nodes.length - 1; nodeIndex++) { - nA = way2Nodes[nodeIndex]; - nB = way2Nodes[nodeIndex + 1]; - if (nA.id === n1.id || nA.id === n2.id || - nB.id === n1.id || nB.id === n2.id) { - // n1 or n2 is a connection node; skip - continue; - } - segment2 = [nA.loc, nB.loc]; - var point = geoLineIntersection(segment1, segment2); - if (point) { - edgeCrossInfos.push({ - wayInfos: [ - { - way: way1, - featureType: way1FeatureType, - edge: [n1.id, n2.id] - }, - { - way: way2, - featureType: way2FeatureType, - edge: [nA.id, nB.id] - } - ], - crossPoint: point - }); - if (oneOnly) { - checkedSingleCrossingWays[way2.id] = true; - break; - } + nAId = segment2Info.nodes[0]; + nBId = segment2Info.nodes[1]; + if (nAId === n1.id || nAId === n2.id || + nBId === n1.id || nBId === n2.id) { + // n1 or n2 is a connection node; skip + continue; + } + nA = graph.hasEntity(nAId); + if (!nA) continue; + nB = graph.hasEntity(nBId); + if (!nB) continue; + + segment1 = [n1.loc, n2.loc]; + segment2 = [nA.loc, nB.loc]; + var point = geoLineIntersection(segment1, segment2); + if (point) { + edgeCrossInfos.push({ + wayInfos: [ + { + way: way1, + featureType: way1FeatureType, + edge: [n1.id, n2.id] + }, + { + way: way2, + featureType: way2FeatureType, + edge: [nA.id, nB.id] + } + ], + crossPoint: point + }); + if (oneOnly) { + checkedSingleCrossingWays[way2.id] = true; + break; } } }