modules/osm/relation.js (261 lines of code) (raw):
import { geoArea as d3_geoArea } from 'd3-geo';
import { osmEntity } from './entity';
import { osmJoinWays } from './multipolygon';
import { geomPolygonContainsPolygon, geomPolygonIntersectsPolygon } from '@id-sdk/geom';
import { Extent } from '@id-sdk/extent';
export function osmRelation() {
if (!(this instanceof osmRelation)) {
return (new osmRelation()).initialize(arguments);
} else if (arguments.length) {
this.initialize(arguments);
}
}
osmEntity.relation = osmRelation;
osmRelation.prototype = Object.create(osmEntity.prototype);
osmRelation.creationOrder = function(a, b) {
var aId = parseInt(osmEntity.id.toOSM(a.id), 10);
var bId = parseInt(osmEntity.id.toOSM(b.id), 10);
if (aId < 0 || bId < 0) return aId - bId;
return bId - aId;
};
Object.assign(osmRelation.prototype, {
type: 'relation',
members: [],
copy: function(resolver, copies) {
if (copies[this.id]) return copies[this.id];
var copy = osmEntity.prototype.copy.call(this, resolver, copies);
var members = this.members.map(function(member) {
return Object.assign({}, member, { id: resolver.entity(member.id).copy(resolver, copies).id });
});
copy = copy.update({members: members});
copies[this.id] = copy;
return copy;
},
extent: function(resolver, memo) {
return resolver.transient(this, 'extent', function() {
if (memo && memo[this.id]) return new Extent();
memo = memo || {};
memo[this.id] = true;
var extent = new Extent();
for (var i = 0; i < this.members.length; i++) {
var member = resolver.hasEntity(this.members[i].id);
if (member) {
extent = extent.extend(member.extent(resolver, memo));
}
}
return extent;
});
},
geometry: function(graph) {
return graph.transient(this, 'geometry', function() {
return this.isMultipolygon() ? 'area' : 'relation';
});
},
isDegenerate: function() {
return this.members.length === 0;
},
// Return an array of members, each extended with an 'index' property whose value
// is the member index.
indexedMembers: function() {
var result = new Array(this.members.length);
for (var i = 0; i < this.members.length; i++) {
result[i] = Object.assign({}, this.members[i], {index: i});
}
return result;
},
// Return the first member with the given role. A copy of the member object
// is returned, extended with an 'index' property whose value is the member index.
memberByRole: function(role) {
for (var i = 0; i < this.members.length; i++) {
if (this.members[i].role === role) {
return Object.assign({}, this.members[i], {index: i});
}
}
},
// Same as memberByRole, but returns all members with the given role
membersByRole: function(role) {
var result = [];
for (var i = 0; i < this.members.length; i++) {
if (this.members[i].role === role) {
result.push(Object.assign({}, this.members[i], {index: i}));
}
}
return result;
},
// Return the first member with the given id. A copy of the member object
// is returned, extended with an 'index' property whose value is the member index.
memberById: function(id) {
for (var i = 0; i < this.members.length; i++) {
if (this.members[i].id === id) {
return Object.assign({}, this.members[i], {index: i});
}
}
},
// Return the first member with the given id and role. A copy of the member object
// is returned, extended with an 'index' property whose value is the member index.
memberByIdAndRole: function(id, role) {
for (var i = 0; i < this.members.length; i++) {
if (this.members[i].id === id && this.members[i].role === role) {
return Object.assign({}, this.members[i], {index: i});
}
}
},
addMember: function(member, index) {
var members = this.members.slice();
members.splice(index === undefined ? members.length : index, 0, member);
return this.update({members: members});
},
updateMember: function(member, index) {
var members = this.members.slice();
members.splice(index, 1, Object.assign({}, members[index], member));
return this.update({members: members});
},
removeMember: function(index) {
var members = this.members.slice();
members.splice(index, 1);
return this.update({members: members});
},
removeMembersWithID: function(id) {
var members = this.members.filter(function(m) { return m.id !== id; });
return this.update({members: members});
},
moveMember: function(fromIndex, toIndex) {
var members = this.members.slice();
members.splice(toIndex, 0, members.splice(fromIndex, 1)[0]);
return this.update({members: members});
},
// Wherever a member appears with id `needle.id`, replace it with a member
// with id `replacement.id`, type `replacement.type`, and the original role,
// 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;
var members = [];
for (var i = 0; i < this.members.length; i++) {
var member = this.members[i];
if (member.id !== needle.id) {
members.push(member);
} else if (keepDuplicates || !this.memberByIdAndRole(replacement.id, member.role)) {
members.push({ id: replacement.id, type: replacement.type, role: member.role });
}
}
return this.update({ members: members });
},
asJXON: function(changeset_id) {
var r = {
relation: {
'@id': this.osmId(),
'@version': this.version || 0,
member: this.members.map(function(member) {
return {
keyAttributes: {
type: member.type,
role: member.role,
ref: osmEntity.id.toOSM(member.id)
}
};
}, this),
tag: Object.keys(this.tags).map(function(k) {
return { keyAttributes: { k: k, v: this.tags[k] } };
}, this)
}
};
if (changeset_id) {
r.relation['@changeset'] = changeset_id;
}
return r;
},
asGeoJSON: function(resolver) {
return resolver.transient(this, 'GeoJSON', function () {
if (this.isMultipolygon()) {
return {
type: 'MultiPolygon',
coordinates: this.multipolygon(resolver)
};
} else {
return {
type: 'FeatureCollection',
properties: this.tags,
features: this.members.map(function (member) {
return Object.assign({role: member.role}, resolver.entity(member.id).asGeoJSON(resolver));
})
};
}
});
},
area: function(resolver) {
return resolver.transient(this, 'area', function() {
return d3_geoArea(this.asGeoJSON(resolver));
});
},
isMultipolygon: function() {
return this.tags.type === 'multipolygon';
},
isComplete: function(resolver) {
for (var i = 0; i < this.members.length; i++) {
if (!resolver.hasEntity(this.members[i].id)) {
return false;
}
}
return true;
},
hasFromViaTo: function() {
return (
this.members.some(function(m) { return m.role === 'from'; }) &&
this.members.some(function(m) { return m.role === 'via'; }) &&
this.members.some(function(m) { return m.role === 'to'; })
);
},
isRestriction: function() {
return !!(this.tags.type && this.tags.type.match(/^restriction:?/));
},
isValidRestriction: function() {
if (!this.isRestriction()) return false;
var froms = this.members.filter(function(m) { return m.role === 'from'; });
var vias = this.members.filter(function(m) { return m.role === 'via'; });
var tos = this.members.filter(function(m) { return m.role === 'to'; });
if (froms.length !== 1 && this.tags.restriction !== 'no_entry') return false;
if (froms.some(function(m) { return m.type !== 'way'; })) return false;
if (tos.length !== 1 && this.tags.restriction !== 'no_exit') return false;
if (tos.some(function(m) { return m.type !== 'way'; })) return false;
if (vias.length === 0) return false;
if (vias.length > 1 && vias.some(function(m) { return m.type !== 'way'; })) return false;
return true;
},
isConnectivity: function() {
return !!(this.tags.type && this.tags.type.match(/^connectivity:?/));
},
// Returns an array [A0, ... An], each Ai being an array of node arrays [Nds0, ... Ndsm],
// where Nds0 is an outer ring and subsequent Ndsi's (if any i > 0) being inner rings.
//
// This corresponds to the structure needed for rendering a multipolygon path using a
// `evenodd` fill rule, as well as the structure of a GeoJSON MultiPolygon geometry.
//
// In the case of invalid geometries, this function will still return a result which
// includes the nodes of all way members, but some Nds may be unclosed and some inner
// rings not matched with the intended outer ring.
//
multipolygon: function(resolver) {
var outers = this.members.filter(function(m) { return 'outer' === (m.role || 'outer'); });
var inners = this.members.filter(function(m) { return 'inner' === m.role; });
outers = osmJoinWays(outers, resolver);
inners = osmJoinWays(inners, resolver);
var sequenceToLineString = function(sequence) {
if (sequence.nodes.length > 2 &&
sequence.nodes[0] !== sequence.nodes[sequence.nodes.length - 1]) {
// close unclosed parts to ensure correct area rendering - #2945
sequence.nodes.push(sequence.nodes[0]);
}
return sequence.nodes.map(function(node) { return node.loc; });
};
outers = outers.map(sequenceToLineString);
inners = inners.map(sequenceToLineString);
var result = outers.map(function(o) {
// Heuristic for detecting counterclockwise winding order. Assumes
// that OpenStreetMap polygons are not hemisphere-spanning.
return [d3_geoArea({ type: 'Polygon', coordinates: [o] }) > 2 * Math.PI ? o.reverse() : o];
});
function findOuter(inner) {
var o, outer;
for (o = 0; o < outers.length; o++) {
outer = outers[o];
if (geomPolygonContainsPolygon(outer, inner)) {
return o;
}
}
for (o = 0; o < outers.length; o++) {
outer = outers[o];
if (geomPolygonIntersectsPolygon(outer, inner, false)) {
return o;
}
}
}
for (var i = 0; i < inners.length; i++) {
var inner = inners[i];
if (d3_geoArea({ type: 'Polygon', coordinates: [inner] }) < 2 * Math.PI) {
inner = inner.reverse();
}
var o = findOuter(inners[i]);
if (o !== undefined) {
result[o].push(inners[i]);
} else {
result.push([inners[i]]); // Invalid geometry
}
}
return result;
}
});