Significantly improve the performance of the crossing_ways validation in areas with long ways (close #7656)

This commit is contained in:
Quincy Morgan
2020-06-22 16:49:32 -04:00
parent 2b4a5d9e66
commit d179f791a9
3 changed files with 177 additions and 82 deletions
+95 -40
View File
@@ -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;
}
+34
View File
@@ -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;
+48 -42
View File
@@ -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;
}
}
}