mirror of
https://github.com/FoggedLens/iD.git
synced 2026-05-24 09:04:02 +02:00
Significantly improve the performance of the crossing_ways validation in areas with long ways (close #7656)
This commit is contained in:
+95
-40
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user