Merge pull request #4693 from openstreetmap/doubled_back_routes

Fixes for joining and splitting bugs
This commit is contained in:
Bryan Housel
2018-01-18 17:01:38 -05:00
committed by GitHub
10 changed files with 2032 additions and 984 deletions
+162 -19
View File
@@ -1,32 +1,175 @@
import { osmJoinWays } from '../osm';
import _clone from 'lodash-es/clone';
import _groupBy from 'lodash-es/groupBy';
import _omit from 'lodash-es/omit';
import { osmJoinWays, osmWay } from '../osm';
export function actionAddMember(relationId, member, memberIndex) {
return function(graph) {
export function actionAddMember(relationId, member, memberIndex, insertPair) {
return function action(graph) {
var relation = graph.entity(relationId);
if (isNaN(memberIndex) && member.type === 'way') {
var members = relation.indexedMembers();
members.push(member);
if ((isNaN(memberIndex) || insertPair) && member.type === 'way') {
// Try to perform sensible inserts based on how the ways join together
graph = addWayMember(relation, graph);
} else {
graph = graph.replace(relation.addMember(member, memberIndex));
}
var joined = osmJoinWays(members, graph);
for (var i = 0; i < joined.length; i++) {
var segment = joined[i];
for (var j = 0; j < segment.length && segment.length >= 2; j++) {
if (segment[j] !== member)
continue;
return graph;
};
if (j === 0) {
memberIndex = segment[j + 1].index;
} else if (j === segment.length - 1) {
memberIndex = segment[j - 1].index + 1;
// Add a way member into the relation "wherever it makes sense".
// In this situation we were not supplied a memberIndex.
function addWayMember(relation, graph) {
var groups, tempWay, item, i, j, k;
if (insertPair) {
// We're adding a member that must stay paired with an existing member.
// (This feature is used by `actionSplit`)
//
// This is tricky because the members may exist multiple times in the
// member list, and with different A-B/B-A ordering and different roles.
// (e.g. a bus route that loops out and back - #4589).
//
// Replace the existing member with a temporary way,
// so that `osmJoinWays` can treat the pair like a single way.
tempWay = osmWay({ id: 'wTemp', nodes: insertPair.nodes });
graph = graph.replace(tempWay);
var tempMember = { id: tempWay.id, type: 'way', role: member.role };
var tempRelation = relation.replaceMember({id: insertPair.originalID}, tempMember, true);
groups = _groupBy(tempRelation.members, function(m) { return m.type; });
groups.way = groups.way || [];
} else {
// Add the member anywhere, one time. Just push and let `osmJoinWays` decide where to put it.
groups = _groupBy(relation.members, function(m) { return m.type; });
groups.way = groups.way || [];
groups.way.push(member);
}
var members = withIndex(groups.way);
var joined = osmJoinWays(members, graph);
// `joined` might not contain all of the way members,
// But will contain only the completed (downloaded) members
for (i = 0; i < joined.length; i++) {
var segment = joined[i];
var nodes = segment.nodes.slice();
var startIndex = segment[0].index;
// j = array index in `members` where this segment starts
for (j = 0; j < members.length; j++) {
if (members[j].index === startIndex) {
break;
}
}
// k = each member in segment
for (k = 0; k < segment.length; k++) {
item = segment[k];
var way = graph.entity(item.id);
// If this is a paired item, generate members in correct order and role
if (tempWay && item.id === tempWay.id) {
if (nodes[0].id === insertPair.nodes[0]) {
item.pair = [
{ id: insertPair.originalID, type: 'way', role: item.role },
{ id: insertPair.insertedID, type: 'way', role: item.role }
];
} else {
memberIndex = Math.min(segment[j - 1].index + 1, segment[j + 1].index + 1);
item.pair = [
{ id: insertPair.insertedID, type: 'way', role: item.role },
{ id: insertPair.originalID, type: 'way', role: item.role }
];
}
}
// reorder `members` if necessary
if (k > 0) {
if (j+k >= members.length || item.index !== members[j+k].index) {
moveMember(members, item.index, j+k);
}
}
nodes.splice(0, way.nodes.length - 1);
}
}
return graph.replace(relation.addMember(member, memberIndex));
};
if (tempWay) {
graph = graph.remove(tempWay);
}
// Final pass: skip dead items, split pairs, remove index properties
var wayMembers = [];
for (i = 0; i < members.length; i++) {
item = members[i];
if (item.index === -1) continue;
if (item.pair) {
wayMembers.push(item.pair[0]);
wayMembers.push(item.pair[1]);
} else {
wayMembers.push(_omit(item, 'index'));
}
}
// Write members in the order: nodes, ways, relations
// This is reccomended for Public Transport routes:
// see https://wiki.openstreetmap.org/wiki/Public_transport#Service_routes
var newMembers = (groups.node || []).concat(wayMembers, (groups.relation || []));
return graph.replace(relation.update({members: newMembers}));
// `moveMember()` changes the `members` array in place by splicing
// the item with `.index = findIndex` to where it belongs,
// and marking the old position as "dead" with `.index = -1`
//
// j=5, k=0 jk
// segment 5 4 7 6
// members 0 1 2 3 4 5 6 7 8 9 keep 5 in j+k
//
// j=5, k=1 j k
// segment 5 4 7 6
// members 0 1 2 3 4 5 6 7 8 9 move 4 to j+k
// members 0 1 2 3 x 5 4 6 7 8 9 moved
//
// j=5, k=2 j k
// segment 5 4 7 6
// members 0 1 2 3 x 5 4 6 7 8 9 move 7 to j+k
// members 0 1 2 3 x 5 4 7 6 x 8 9 moved
//
// j=5, k=3 j k
// segment 5 4 7 6
// members 0 1 2 3 x 5 4 7 6 x 8 9 keep 6 in j+k
//
function moveMember(arr, findIndex, toIndex) {
for (var i = 0; i < arr.length; i++) {
if (arr[i].index === findIndex) {
break;
}
}
var item = _clone(arr[i]);
arr[i].index = -1; // mark as dead
item.index = toIndex;
arr.splice(toIndex, 0, item);
}
// This is the same as `Relation.indexedMembers`,
// Except we don't want to index all the members, only the ways
function withIndex(arr) {
var result = new Array(arr.length);
for (var i = 0; i < arr.length; i++) {
result[i] = arr[i];
result[i].index = i;
}
return result;
}
}
}
+17 -17
View File
@@ -1,13 +1,8 @@
import _extend from 'lodash-es/extend';
import _groupBy from 'lodash-es/groupBy';
import _map from 'lodash-es/map';
import { actionDeleteWay } from './delete_way';
import {
osmIsInterestingTag,
osmJoinWays
} from '../osm';
import { osmIsInterestingTag, osmJoinWays } from '../osm';
// Join ways at the end node they share.
@@ -27,25 +22,30 @@ export function actionJoin(ids) {
var action = function(graph) {
var ways = ids.map(graph.entity, graph),
survivor = ways[0];
var ways = ids.map(graph.entity, graph);
var survivorID = ways[0].id;
// Prefer to keep an existing way.
for (var i = 0; i < ways.length; i++) {
if (!ways[i].isNew()) {
survivor = ways[i];
survivorID = ways[i].id;
break;
}
}
var joined = osmJoinWays(ways, graph)[0];
var sequences = osmJoinWays(ways, graph);
var joined = sequences[0];
survivor = survivor.update({nodes: _map(joined.nodes, 'id')});
// We might need to reverse some of these ways before joining them. #4688
// `joined.actions` property will contain any actions we need to apply.
graph = sequences.actions.reduce(function(g, action) { return action(g); }, graph);
var survivor = graph.entity(survivorID);
survivor = survivor.update({ nodes: joined.nodes.map(function(n) { return n.id; }) });
graph = graph.replace(survivor);
joined.forEach(function(way) {
if (way.id === survivor.id)
return;
if (way.id === survivorID) return;
graph.parentRelations(way).forEach(function(parent) {
graph = graph.replace(parent.replaceMember(way, survivor));
@@ -70,10 +70,10 @@ export function actionJoin(ids) {
if (joined.length > 1)
return 'not_adjacent';
var nodeIds = _map(joined[0].nodes, 'id').slice(1, -1),
relation,
tags = {},
conflicting = false;
var nodeIds = joined[0].nodes.map(function(n) { return n.id; }).slice(1, -1);
var relation;
var tags = {};
var conflicting = false;
joined[0].forEach(function(way) {
var parents = graph.parentRelations(way);
+32 -24
View File
@@ -29,7 +29,7 @@ import { utilWrap } from '../util';
// https://github.com/systemed/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/SplitWayAction.as
//
export function actionSplit(nodeId, newWayIds) {
var wayIds;
var _wayIDs;
// if the way is closed, we need to search for a partner node
// to split the way at.
@@ -42,11 +42,11 @@ export function actionSplit(nodeId, newWayIds) {
// For example: bone-shaped areas get split across their waist
// line, circles across the diameter.
function splitArea(nodes, idxA, graph) {
var lengths = new Array(nodes.length),
length,
i,
best = 0,
idxB;
var lengths = new Array(nodes.length);
var length;
var i;
var best = 0;
var idxB;
function wrap(index) {
return utilWrap(index, nodes.length);
@@ -84,16 +84,17 @@ export function actionSplit(nodeId, newWayIds) {
function split(graph, wayA, newWayId) {
var wayB = osmWay({id: newWayId, tags: wayA.tags}),
nodesA,
nodesB,
isArea = wayA.isArea(),
isOuter = osmIsSimpleMultipolygonOuterMember(wayA, graph);
var wayB = osmWay({id: newWayId, tags: wayA.tags});
var origNodes = wayA.nodes.slice();
var nodesA;
var nodesB;
var isArea = wayA.isArea();
var isOuter = osmIsSimpleMultipolygonOuterMember(wayA, graph);
if (wayA.isClosed()) {
var nodes = wayA.nodes.slice(0, -1),
idxA = _indexOf(nodes, nodeId),
idxB = splitArea(nodes, idxA, graph);
var nodes = wayA.nodes.slice(0, -1);
var idxA = _indexOf(nodes, nodeId);
var idxB = splitArea(nodes, idxA, graph);
if (idxB < idxA) {
nodesA = nodes.slice(idxA).concat(nodes.slice(0, idxB + 1));
@@ -134,7 +135,13 @@ export function actionSplit(nodeId, newWayIds) {
role: relation.memberById(wayA.id).role
};
graph = actionAddMember(relation.id, member)(graph);
var insertPair = {
originalID: wayA.id,
insertedID: wayB.id,
nodes: origNodes
};
graph = actionAddMember(relation.id, member, undefined, insertPair)(graph);
}
});
@@ -144,7 +151,8 @@ export function actionSplit(nodeId, newWayIds) {
members: [
{id: wayA.id, role: 'outer', type: 'way'},
{id: wayB.id, role: 'outer', type: 'way'}
]});
]
});
graph = graph.replace(multipolygon);
graph = graph.replace(wayA.update({tags: {}}));
@@ -165,15 +173,15 @@ export function actionSplit(nodeId, newWayIds) {
action.ways = function(graph) {
var node = graph.entity(nodeId),
parents = graph.parentWays(node),
hasLines = _some(parents, function(parent) { return parent.geometry(graph) === 'line'; });
var node = graph.entity(nodeId);
var parents = graph.parentWays(node);
var hasLines = _some(parents, function(parent) { return parent.geometry(graph) === 'line'; });
return parents.filter(function(parent) {
if (wayIds && wayIds.indexOf(parent.id) === -1)
if (_wayIDs && _wayIDs.indexOf(parent.id) === -1)
return false;
if (!wayIds && hasLines && parent.geometry(graph) !== 'line')
if (!_wayIDs && hasLines && parent.geometry(graph) !== 'line')
return false;
if (parent.isClosed()) {
@@ -193,14 +201,14 @@ export function actionSplit(nodeId, newWayIds) {
action.disabled = function(graph) {
var candidates = action.ways(graph);
if (candidates.length === 0 || (wayIds && wayIds.length !== candidates.length))
if (candidates.length === 0 || (_wayIDs && _wayIDs.length !== candidates.length))
return 'not_eligible';
};
action.limitWays = function(_) {
if (!arguments.length) return wayIds;
wayIds = _;
if (!arguments.length) return _wayIDs;
_wayIDs = _;
return action;
};
+86 -50
View File
@@ -1,5 +1,6 @@
import { actionReverse } from '../actions/reverse';
import { osmIsInterestingTag } from './tags';
import { osmWay } from './way';
// For fixing up rendering of multipolygons with tags on the outer member.
@@ -62,87 +63,122 @@ export function osmSimpleMultipolygonOuterMember(entity, graph) {
}
// Join `array` into sequences of connecting ways.
//
// Join `toJoin` array into sequences of connecting ways.
// Segments which share identical start/end nodes will, as much as possible,
// be connected with each other.
//
// The return value is a nested array. Each constituent array contains elements
// of `array` which have been determined to connect. Each consitituent array
// also has a `nodes` property whose value is an ordered array of member nodes,
// with appropriate order reversal and start/end coordinate de-duplication.
// of `toJoin` which have been determined to connect.
//
// Members of `array` must have, at minimum, `type` and `id` properties.
// Thus either an array of `osmWay`s or a relation member array may be
// used.
// Each consitituent array also has a `nodes` property whose value is an
// ordered array of member nodes, with appropriate order reversal and
// start/end coordinate de-duplication.
//
// If an member has a `tags` property, its tags will be reversed via
// Members of `toJoin` must have, at minimum, `type` and `id` properties.
// Thus either an array of `osmWay`s or a relation member array may be used.
//
// If an member is an `osmWay`, its tags and childnodes may be reversed via
// `actionReverse` in the output.
//
// The returned sequences array also has an `actions` array property, containing
// any reversal actions that should be applied to the graph, should the calling
// code attempt to actually join the given ways.
//
// Incomplete members (those for which `graph.hasEntity(element.id)` returns
// false) and non-way members are ignored.
//
export function osmJoinWays(array, graph) {
var joined = [], member, current, nodes, first, last, i, how, what;
array = array.filter(function(member) {
return member.type === 'way' && graph.hasEntity(member.id);
});
export function osmJoinWays(toJoin, graph) {
function resolve(member) {
return graph.childNodes(graph.entity(member.id));
}
function reverse(member) {
return member.tags ? actionReverse(member.id, { reverseOneway: true })(graph).entity(member.id) : member;
function reverse(item) {
var action = actionReverse(item.id, { reverseOneway: true });
sequences.actions.push(action);
return (item instanceof osmWay) ? action(graph).entity(item.id) : item;
}
while (array.length) {
member = array.shift();
current = [member];
current.nodes = nodes = resolve(member).slice();
joined.push(current);
// make a copy containing only the items to join
toJoin = toJoin.filter(function(member) {
return member.type === 'way' && graph.hasEntity(member.id);
});
while (array.length && nodes[0] !== nodes[nodes.length - 1]) {
first = nodes[0];
last = nodes[nodes.length - 1];
for (i = 0; i < array.length; i++) {
member = array[i];
what = resolve(member);
var sequences = [];
sequences.actions = [];
if (last === what[0]) {
how = nodes.push;
what = what.slice(1);
while (toJoin.length) {
// start a new sequence
var item = toJoin.shift();
var currWays = [item];
var currNodes = resolve(item).slice();
var doneSequence = false;
// add to it
while (toJoin.length && !doneSequence) {
var start = currNodes[0];
var end = currNodes[currNodes.length - 1];
var fn = null;
var nodes = null;
var i;
// Find the next way/member to join.
for (i = 0; i < toJoin.length; i++) {
item = toJoin[i];
nodes = resolve(item);
// Strongly prefer to generate a forward path that preserves the order
// of the members array. For multipolygons and most relations, member
// order does not matter - but for routes, it does. If we started this
// sequence backwards (i.e. next member way attaches to the start node
// and not the end node), reverse the initial way before continuing.
if (currWays.length === 1 && nodes[0] !== end && nodes[nodes.length - 1] !== end &&
(nodes[nodes.length - 1] === start || nodes[0] === start)
) {
currWays[0] = reverse(currWays[0]);
currNodes.reverse();
start = currNodes[0];
end = currNodes[currNodes.length - 1];
}
if (nodes[0] === end) {
fn = currNodes.push; // join to end
nodes = nodes.slice(1);
break;
} else if (last === what[what.length - 1]) {
how = nodes.push;
what = what.slice(0, -1).reverse();
member = reverse(member);
} else if (nodes[nodes.length - 1] === end) {
fn = currNodes.push; // join to end
nodes = nodes.slice(0, -1).reverse();
item = reverse(item);
break;
} else if (first === what[what.length - 1]) {
how = nodes.unshift;
what = what.slice(0, -1);
} else if (nodes[nodes.length - 1] === start) {
fn = currNodes.unshift; // join to beginning
nodes = nodes.slice(0, -1);
break;
} else if (first === what[0]) {
how = nodes.unshift;
what = what.slice(1).reverse();
member = reverse(member);
} else if (nodes[0] === start) {
fn = currNodes.unshift; // join to beginning
nodes = nodes.slice(1).reverse();
item = reverse(item);
break;
} else {
what = how = null;
fn = nodes = null;
}
}
if (!what)
break; // No more joinable ways.
if (!nodes) { // couldn't find a joinable way/member
doneSequence = true;
break;
}
how.apply(current, [member]);
how.apply(nodes, what);
fn.apply(currWays, [item]);
fn.apply(currNodes, nodes);
array.splice(i, 1);
toJoin.splice(i, 1);
}
currWays.nodes = currNodes;
sequences.push(currWays);
}
return joined;
return sequences;
}
+4 -4
View File
@@ -161,9 +161,9 @@ _extend(osmRelation.prototype, {
// Wherever a member appears with id `needle.id`, replace it with a member
// with id `replacement.id`, type `replacement.type`, and the original role,
// unless a member already exists with that id and role. Return an updated
// relation.
replaceMember: function(needle, replacement) {
// By default, adding a duplicate member (by id and role) is prevented.
// Return an updated relation.
replaceMember: function(needle, replacement, keepDuplicates) {
if (!this.memberById(needle.id))
return this;
@@ -173,7 +173,7 @@ _extend(osmRelation.prototype, {
var member = this.members[i];
if (member.id !== needle.id) {
members.push(member);
} else if (!this.memberByIdAndRole(replacement.id, member.role)) {
} else if (keepDuplicates || !this.memberByIdAndRole(replacement.id, member.role)) {
members.push({id: replacement.id, type: replacement.type, role: member.role});
}
}