Files
iD/modules/actions/add_member.js
Thomas Petillon 3ff06f9045 Fix relation handling on way split
Depending on which way is the longest, the new way is inserted into the
relation before the existing one. This case must be explicitly handled
for the relation to remain correct.
2021-12-06 21:25:23 +01:00

208 lines
8.2 KiB
JavaScript

import { osmJoinWays } from '../osm/multipolygon';
import { osmWay } from '../osm/way';
import { utilArrayGroupBy, utilObjectOmit } from '../util';
export function actionAddMember(relationId, member, memberIndex, insertPair) {
return function action(graph) {
var relation = graph.entity(relationId);
// There are some special rules for Public Transport v2 routes.
var isPTv2 = /stop|platform/.test(member.role);
if ((isNaN(memberIndex) || insertPair) && member.type === 'way' && !isPTv2) {
// Try to perform sensible inserts based on how the ways join together
graph = addWayMember(relation, graph);
} else {
// see https://wiki.openstreetmap.org/wiki/Public_transport#Service_routes
// Stops and Platforms for PTv2 should be ordered first.
// hack: We do not currently have the ability to place them in the exactly correct order.
if (isPTv2 && isNaN(memberIndex)) {
memberIndex = 0;
}
graph = graph.replace(relation.addMember(member, memberIndex));
}
return graph;
};
// 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, insertPairIsReversed, item, i, j, k;
// remove PTv2 stops and platforms before doing anything.
var PTv2members = [];
var members = [];
for (i = 0; i < relation.members.length; i++) {
var m = relation.members[i];
if (/stop|platform/.test(m.role)) {
PTv2members.push(m);
} else {
members.push(m);
}
}
relation = relation.update({ members: members });
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 = utilArrayGroupBy(tempRelation.members, 'type');
groups.way = groups.way || [];
// Insert pair is reversed if the inserted way comes before the original one.
// (Except when they form a loop.)
var originalWay = graph.entity(insertPair.originalID);
var insertedWay = graph.entity(insertPair.insertedID);
insertPairIsReversed = originalWay.nodes.length > 0 && insertedWay.nodes.length > 0 &&
insertedWay.nodes[insertedWay.nodes.length - 1] === originalWay.nodes[0] &&
originalWay.nodes[originalWay.nodes.length - 1] !== insertedWay.nodes[0];
} else {
// Add the member anywhere, one time. Just push and let `osmJoinWays` decide where to put it.
groups = utilArrayGroupBy(relation.members, 'type');
groups.way = groups.way || [];
groups.way.push(member);
}
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) {
var reverse = nodes[0].id !== insertPair.nodes[0] ^ insertPairIsReversed;
if (reverse) {
item.pair = [
{ id: insertPair.insertedID, type: 'way', role: item.role },
{ id: insertPair.originalID, type: 'way', role: item.role }
];
} else {
item.pair = [
{ id: insertPair.originalID, type: 'way', role: item.role },
{ id: insertPair.insertedID, 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);
}
}
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(utilObjectOmit(item, ['index']));
}
}
// Put stops and platforms first, then nodes, ways, relations
// This is recommended for Public Transport v2 routes:
// see https://wiki.openstreetmap.org/wiki/Public_transport#Service_routes
var newMembers = PTv2members.concat( (groups.node || []), 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) {
var i;
for (i = 0; i < arr.length; i++) {
if (arr[i].index === findIndex) {
break;
}
}
var item = Object.assign({}, arr[i]); // shallow copy
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] = Object.assign({}, arr[i]); // shallow copy
result[i].index = i;
}
return result;
}
}
}