diff --git a/Makefile b/Makefile index d95dd85a0..4a4084631 100644 --- a/Makefile +++ b/Makefile @@ -45,7 +45,6 @@ $(BUILDJS_TARGETS): $(BUILDJS_SOURCES) build.js MODULE_TARGETS = \ js/lib/id/index.js \ js/lib/id/behavior.js \ - js/lib/id/geo.js \ js/lib/id/modes.js \ js/lib/id/operations.js \ js/lib/id/presets.js \ @@ -68,14 +67,6 @@ js/lib/id/behavior.js: $(shell find modules/behavior -type f) @rm -f $@ node_modules/.bin/rollup -f umd -n iD.behavior modules/behavior/index.js --no-strict -o $@ -js/lib/id/core.js: $(shell find modules/core -type f) - @rm -f $@ - node_modules/.bin/rollup -f umd -n iD modules/core/index.js --no-strict -o $@ - -js/lib/id/geo.js: $(shell find modules/geo -type f) - @rm -f $@ - node_modules/.bin/rollup -f umd -n iD.geo modules/geo/index.js --no-strict -o $@ - js/lib/id/modes.js: $(shell find modules/modes -type f) @rm -f $@ node_modules/.bin/rollup -f umd -n iD.modes modules/modes/index.js --no-strict -o $@ diff --git a/index.html b/index.html index 64befdbcb..82a725d0e 100644 --- a/index.html +++ b/index.html @@ -34,10 +34,7 @@ - - - diff --git a/js/lib/id/geo.js b/js/lib/id/geo.js index be373b0cc..000fb46de 100644 --- a/js/lib/id/geo.js +++ b/js/lib/id/geo.js @@ -115,6 +115,798 @@ }); + function interestingTag(key) { + return key !== 'attribution' && + key !== 'created_by' && + key !== 'source' && + key !== 'odbl' && + key.indexOf('tiger:') !== 0; + + } + + var oneWayTags = { + 'aerialway': { + 'chair_lift': true, + 'mixed_lift': true, + 't-bar': true, + 'j-bar': true, + 'platter': true, + 'rope_tow': true, + 'magic_carpet': true, + 'yes': true + }, + 'highway': { + 'motorway': true, + 'motorway_link': true + }, + 'junction': { + 'roundabout': true + }, + 'man_made': { + 'piste:halfpipe': true + }, + 'piste:type': { + 'downhill': true, + 'sled': true, + 'yes': true + }, + 'waterway': { + 'river': true, + 'stream': true + } + }; + + function Entity(attrs) { + // For prototypal inheritance. + if (this instanceof Entity) return; + + // Create the appropriate subtype. + if (attrs && attrs.type) { + return Entity[attrs.type].apply(this, arguments); + } else if (attrs && attrs.id) { + return Entity[Entity.id.type(attrs.id)].apply(this, arguments); + } + + // Initialize a generic Entity (used only in tests). + return (new Entity()).initialize(arguments); + } + + Entity.id = function(type) { + return Entity.id.fromOSM(type, Entity.id.next[type]--); + }; + + Entity.id.next = {node: -1, way: -1, relation: -1}; + + Entity.id.fromOSM = function(type, id) { + return type[0] + id; + }; + + Entity.id.toOSM = function(id) { + return id.slice(1); + }; + + Entity.id.type = function(id) { + return {'n': 'node', 'w': 'way', 'r': 'relation'}[id[0]]; + }; + + // A function suitable for use as the second argument to d3.selection#data(). + Entity.key = function(entity) { + return entity.id + 'v' + (entity.v || 0); + }; + + Entity.prototype = { + tags: {}, + + initialize: function(sources) { + for (var i = 0; i < sources.length; ++i) { + var source = sources[i]; + for (var prop in source) { + if (Object.prototype.hasOwnProperty.call(source, prop)) { + if (source[prop] === undefined) { + delete this[prop]; + } else { + this[prop] = source[prop]; + } + } + } + } + + if (!this.id && this.type) { + this.id = Entity.id(this.type); + } + if (!this.hasOwnProperty('visible')) { + this.visible = true; + } + + if (iD.debug) { + Object.freeze(this); + Object.freeze(this.tags); + + if (this.loc) Object.freeze(this.loc); + if (this.nodes) Object.freeze(this.nodes); + if (this.members) Object.freeze(this.members); + } + + return this; + }, + + copy: function(resolver, copies) { + if (copies[this.id]) + return copies[this.id]; + + var copy = Entity(this, {id: undefined, user: undefined, version: undefined}); + copies[this.id] = copy; + + return copy; + }, + + osmId: function() { + return Entity.id.toOSM(this.id); + }, + + isNew: function() { + return this.osmId() < 0; + }, + + update: function(attrs) { + return Entity(this, attrs, {v: 1 + (this.v || 0)}); + }, + + mergeTags: function(tags) { + var merged = _.clone(this.tags), changed = false; + for (var k in tags) { + var t1 = merged[k], + t2 = tags[k]; + if (!t1) { + changed = true; + merged[k] = t2; + } else if (t1 !== t2) { + changed = true; + merged[k] = _.union(t1.split(/;\s*/), t2.split(/;\s*/)).join(';'); + } + } + return changed ? this.update({tags: merged}) : this; + }, + + intersects: function(extent, resolver) { + return this.extent(resolver).intersects(extent); + }, + + isUsed: function(resolver) { + return _.without(Object.keys(this.tags), 'area').length > 0 || + resolver.parentRelations(this).length > 0; + }, + + hasInterestingTags: function() { + return _.keys(this.tags).some(interestingTag); + }, + + isHighwayIntersection: function() { + return false; + }, + + deprecatedTags: function() { + var tags = _.toPairs(this.tags); + var deprecated = {}; + + iD.data.deprecated.forEach(function(d) { + var match = _.toPairs(d.old)[0]; + tags.forEach(function(t) { + if (t[0] === match[0] && + (t[1] === match[1] || match[1] === '*')) { + deprecated[t[0]] = t[1]; + } + }); + }); + + return deprecated; + } + }; + + function Way() { + if (!(this instanceof Way)) { + return (new Way()).initialize(arguments); + } else if (arguments.length) { + this.initialize(arguments); + } + } + + Entity.way = Way; + + Way.prototype = Object.create(Entity.prototype); + + _.extend(Way.prototype, { + type: 'way', + nodes: [], + + copy: function(resolver, copies) { + if (copies[this.id]) + return copies[this.id]; + + var copy = Entity.prototype.copy.call(this, resolver, copies); + + var nodes = this.nodes.map(function(id) { + return resolver.entity(id).copy(resolver, copies).id; + }); + + copy = copy.update({nodes: nodes}); + copies[this.id] = copy; + + return copy; + }, + + extent: function(resolver) { + return resolver.transient(this, 'extent', function() { + var extent = Extent(); + for (var i = 0; i < this.nodes.length; i++) { + var node = resolver.hasEntity(this.nodes[i]); + if (node) { + extent._extend(node.extent()); + } + } + return extent; + }); + }, + + first: function() { + return this.nodes[0]; + }, + + last: function() { + return this.nodes[this.nodes.length - 1]; + }, + + contains: function(node) { + return this.nodes.indexOf(node) >= 0; + }, + + affix: function(node) { + if (this.nodes[0] === node) return 'prefix'; + if (this.nodes[this.nodes.length - 1] === node) return 'suffix'; + }, + + layer: function() { + // explicit layer tag, clamp between -10, 10.. + if (this.tags.layer !== undefined) { + return Math.max(-10, Math.min(+(this.tags.layer), 10)); + } + + // implied layer tag.. + if (this.tags.location === 'overground') return 1; + if (this.tags.location === 'underground') return -1; + if (this.tags.location === 'underwater') return -10; + + if (this.tags.power === 'line') return 10; + if (this.tags.power === 'minor_line') return 10; + if (this.tags.aerialway) return 10; + if (this.tags.bridge) return 1; + if (this.tags.cutting) return -1; + if (this.tags.tunnel) return -1; + if (this.tags.waterway) return -1; + if (this.tags.man_made === 'pipeline') return -10; + if (this.tags.boundary) return -10; + return 0; + }, + + isOneWay: function() { + // explicit oneway tag.. + if (['yes', '1', '-1'].indexOf(this.tags.oneway) !== -1) { return true; } + if (['no', '0'].indexOf(this.tags.oneway) !== -1) { return false; } + + // implied oneway tag.. + for (var key in this.tags) { + if (key in oneWayTags && (this.tags[key] in oneWayTags[key])) + return true; + } + return false; + }, + + isClosed: function() { + return this.nodes.length > 0 && this.first() === this.last(); + }, + + isConvex: function(resolver) { + if (!this.isClosed() || this.isDegenerate()) return null; + + var nodes = _.uniq(resolver.childNodes(this)), + coords = _.map(nodes, 'loc'), + curr = 0, prev = 0; + + for (var i = 0; i < coords.length; i++) { + var o = coords[(i+1) % coords.length], + a = coords[i], + b = coords[(i+2) % coords.length], + res = cross(o, a, b); + + curr = (res > 0) ? 1 : (res < 0) ? -1 : 0; + if (curr === 0) { + continue; + } else if (prev && curr !== prev) { + return false; + } + prev = curr; + } + return true; + }, + + isArea: function() { + if (this.tags.area === 'yes') + return true; + if (!this.isClosed() || this.tags.area === 'no') + return false; + for (var key in this.tags) + if (key in iD.areaKeys && !(this.tags[key] in iD.areaKeys[key])) + return true; + return false; + }, + + isDegenerate: function() { + return _.uniq(this.nodes).length < (this.isArea() ? 3 : 2); + }, + + areAdjacent: function(n1, n2) { + for (var i = 0; i < this.nodes.length; i++) { + if (this.nodes[i] === n1) { + if (this.nodes[i - 1] === n2) return true; + if (this.nodes[i + 1] === n2) return true; + } + } + return false; + }, + + geometry: function(graph) { + return graph.transient(this, 'geometry', function() { + return this.isArea() ? 'area' : 'line'; + }); + }, + + addNode: function(id, index) { + var nodes = this.nodes.slice(); + nodes.splice(index === undefined ? nodes.length : index, 0, id); + return this.update({nodes: nodes}); + }, + + updateNode: function(id, index) { + var nodes = this.nodes.slice(); + nodes.splice(index, 1, id); + return this.update({nodes: nodes}); + }, + + replaceNode: function(needle, replacement) { + if (this.nodes.indexOf(needle) < 0) + return this; + + var nodes = this.nodes.slice(); + for (var i = 0; i < nodes.length; i++) { + if (nodes[i] === needle) { + nodes[i] = replacement; + } + } + return this.update({nodes: nodes}); + }, + + removeNode: function(id) { + var nodes = []; + + for (var i = 0; i < this.nodes.length; i++) { + var node = this.nodes[i]; + if (node !== id && nodes[nodes.length - 1] !== node) { + nodes.push(node); + } + } + + // Preserve circularity + if (this.nodes.length > 1 && this.first() === id && this.last() === id && nodes[nodes.length - 1] !== nodes[0]) { + nodes.push(nodes[0]); + } + + return this.update({nodes: nodes}); + }, + + asJXON: function(changeset_id) { + var r = { + way: { + '@id': this.osmId(), + '@version': this.version || 0, + nd: _.map(this.nodes, function(id) { + return { keyAttributes: { ref: Entity.id.toOSM(id) } }; + }), + tag: _.map(this.tags, function(v, k) { + return { keyAttributes: { k: k, v: v } }; + }) + } + }; + if (changeset_id) r.way['@changeset'] = changeset_id; + return r; + }, + + asGeoJSON: function(resolver) { + return resolver.transient(this, 'GeoJSON', function() { + var coordinates = _.map(resolver.childNodes(this), 'loc'); + if (this.isArea() && this.isClosed()) { + return { + type: 'Polygon', + coordinates: [coordinates] + }; + } else { + return { + type: 'LineString', + coordinates: coordinates + }; + } + }); + }, + + area: function(resolver) { + return resolver.transient(this, 'area', function() { + var nodes = resolver.childNodes(this); + + var json = { + type: 'Polygon', + coordinates: [_.map(nodes, 'loc')] + }; + + if (!this.isClosed() && nodes.length) { + json.coordinates[0].push(nodes[0].loc); + } + + var area = d3.geo.area(json); + + // Heuristic for detecting counterclockwise winding order. Assumes + // that OpenStreetMap polygons are not hemisphere-spanning. + if (area > 2 * Math.PI) { + json.coordinates[0] = json.coordinates[0].reverse(); + area = d3.geo.area(json); + } + + return isNaN(area) ? 0 : area; + }); + } + }); + + function Relation() { + if (!(this instanceof Relation)) { + return (new Relation()).initialize(arguments); + } else if (arguments.length) { + this.initialize(arguments); + } + } + Entity.relation = Relation; + + Relation.prototype = Object.create(Entity.prototype); + + Relation.creationOrder = function(a, b) { + var aId = parseInt(Entity.id.toOSM(a.id), 10); + var bId = parseInt(Entity.id.toOSM(b.id), 10); + + if (aId < 0 || bId < 0) return aId - bId; + return bId - aId; + }; + + _.extend(Relation.prototype, { + type: 'relation', + members: [], + + copy: function(resolver, copies) { + if (copies[this.id]) + return copies[this.id]; + + var copy = Entity.prototype.copy.call(this, resolver, copies); + + var members = this.members.map(function(member) { + return _.extend({}, 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 Extent(); + memo = memo || {}; + memo[this.id] = true; + + var extent = Extent(); + for (var i = 0; i < this.members.length; i++) { + var member = resolver.hasEntity(this.members[i].id); + if (member) { + 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] = _.extend({}, 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 _.extend({}, this.members[i], {index: i}); + } + } + }, + + // 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 _.extend({}, 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 _.extend({}, 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, _.extend({}, 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 = _.reject(this.members, function(m) { return m.id === id; }); + 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, + // unless a member already exists with that id and role. Return an updated + // relation. + replaceMember: function(needle, replacement) { + 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 (!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: _.map(this.members, function(member) { + return { keyAttributes: { type: member.type, role: member.role, ref: Entity.id.toOSM(member.id) } }; + }), + tag: _.map(this.tags, function(v, k) { + return { keyAttributes: { k: k, v: v } }; + }) + } + }; + 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 _.extend({role: member.role}, resolver.entity(member.id).asGeoJSON(resolver)); + }) + }; + } + }); + }, + + area: function(resolver) { + return resolver.transient(this, 'area', function() { + return d3.geo.area(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; + }, + + isRestriction: function() { + return !!(this.tags.type && this.tags.type.match(/^restriction:?/)); + }, + + // 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'); }), + inners = this.members.filter(function(m) { return 'inner' === m.role; }); + + outers = joinWays(outers, resolver); + inners = joinWays(inners, resolver); + + outers = outers.map(function(outer) { return _.map(outer.nodes, 'loc'); }); + inners = inners.map(function(inner) { return _.map(inner.nodes, 'loc'); }); + + var result = outers.map(function(o) { + // Heuristic for detecting counterclockwise winding order. Assumes + // that OpenStreetMap polygons are not hemisphere-spanning. + return [d3.geo.area({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 (polygonContainsPolygon(outer, inner)) + return o; + } + + for (o = 0; o < outers.length; o++) { + outer = outers[o]; + if (polygonIntersectsPolygon(outer, inner)) + return o; + } + } + + for (var i = 0; i < inners.length; i++) { + var inner = inners[i]; + + if (d3.geo.area({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; + } + }); + + function Node() { + if (!(this instanceof Node)) { + return (new Node()).initialize(arguments); + } else if (arguments.length) { + this.initialize(arguments); + } + } + + Entity.node = Node; + + Node.prototype = Object.create(Entity.prototype); + + _.extend(Node.prototype, { + type: 'node', + + extent: function() { + return new Extent(this.loc); + }, + + geometry: function(graph) { + return graph.transient(this, 'geometry', function() { + return graph.isPoi(this) ? 'point' : 'vertex'; + }); + }, + + move: function(loc) { + return this.update({loc: loc}); + }, + + isIntersection: function(resolver) { + return resolver.transient(this, 'isIntersection', function() { + return resolver.parentWays(this).filter(function(parent) { + return (parent.tags.highway || + parent.tags.waterway || + parent.tags.railway || + parent.tags.aeroway) && + parent.geometry(resolver) === 'line'; + }).length > 1; + }); + }, + + isHighwayIntersection: function(resolver) { + return resolver.transient(this, 'isHighwayIntersection', function() { + return resolver.parentWays(this).filter(function(parent) { + return parent.tags.highway && parent.geometry(resolver) === 'line'; + }).length > 1; + }); + }, + + asJXON: function(changeset_id) { + var r = { + node: { + '@id': this.osmId(), + '@lon': this.loc[0], + '@lat': this.loc[1], + '@version': (this.version || 0), + tag: _.map(this.tags, function(v, k) { + return { keyAttributes: { k: k, v: v } }; + }) + } + }; + if (changeset_id) r.node['@changeset'] = changeset_id; + return r; + }, + + asGeoJSON: function() { + return { + type: 'Point', + coordinates: this.loc + }; + } + }); + function Turn(turn) { if (!(this instanceof Turn)) return new Turn(turn); @@ -155,14 +947,14 @@ var splitIndex, wayA, wayB, indexA, indexB; if (isClosingNode) { splitIndex = Math.ceil(way.nodes.length / 2); // split at midpoint - wayA = iD.Way({id: way.id + '-a', tags: way.tags, nodes: way.nodes.slice(0, splitIndex)}); - wayB = iD.Way({id: way.id + '-b', tags: way.tags, nodes: way.nodes.slice(splitIndex)}); + wayA = Way({id: way.id + '-a', tags: way.tags, nodes: way.nodes.slice(0, splitIndex)}); + wayB = Way({id: way.id + '-b', tags: way.tags, nodes: way.nodes.slice(splitIndex)}); indexA = 1; indexB = way.nodes.length - 2; } else { splitIndex = _.indexOf(way.nodes, vertex.id, 1); // split at vertexid - wayA = iD.Way({id: way.id + '-a', tags: way.tags, nodes: way.nodes.slice(0, splitIndex + 1)}); - wayB = iD.Way({id: way.id + '-b', tags: way.tags, nodes: way.nodes.slice(splitIndex)}); + wayA = Way({id: way.id + '-a', tags: way.tags, nodes: way.nodes.slice(0, splitIndex + 1)}); + wayB = Way({id: way.id + '-b', tags: way.tags, nodes: way.nodes.slice(splitIndex)}); indexA = splitIndex - 1; indexB = splitIndex + 1; } diff --git a/js/lib/id/index.js b/js/lib/id/index.js index c8811b6e9..949055cba 100644 --- a/js/lib/id/index.js +++ b/js/lib/id/index.js @@ -121,454 +121,6 @@ }); - function inferRestriction(graph, from, via, to, projection) { - var fromWay = graph.entity(from.way), - fromNode = graph.entity(from.node), - toWay = graph.entity(to.way), - toNode = graph.entity(to.node), - viaNode = graph.entity(via.node), - fromOneWay = (fromWay.tags.oneway === 'yes' && fromWay.last() === via.node) || - (fromWay.tags.oneway === '-1' && fromWay.first() === via.node), - toOneWay = (toWay.tags.oneway === 'yes' && toWay.first() === via.node) || - (toWay.tags.oneway === '-1' && toWay.last() === via.node), - angle = getAngle(viaNode, fromNode, projection) - - getAngle(viaNode, toNode, projection); - - angle = angle * 180 / Math.PI; - - while (angle < 0) - angle += 360; - - if (fromNode === toNode) - return 'no_u_turn'; - if ((angle < 23 || angle > 336) && fromOneWay && toOneWay) - return 'no_u_turn'; - if (angle < 158) - return 'no_right_turn'; - if (angle > 202) - return 'no_left_turn'; - - return 'no_straight_on'; - } - - // For fixing up rendering of multipolygons with tags on the outer member. - // https://github.com/openstreetmap/iD/issues/613 - function isSimpleMultipolygonOuterMember(entity, graph) { - if (entity.type !== 'way') - return false; - - var parents = graph.parentRelations(entity); - if (parents.length !== 1) - return false; - - var parent = parents[0]; - if (!parent.isMultipolygon() || Object.keys(parent.tags).length > 1) - return false; - - var members = parent.members, member; - for (var i = 0; i < members.length; i++) { - member = members[i]; - if (member.id === entity.id && member.role && member.role !== 'outer') - return false; // Not outer member - if (member.id !== entity.id && (!member.role || member.role === 'outer')) - return false; // Not a simple multipolygon - } - - return parent; - } - - // Join `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. - // - // Members of `array` must have, at minimum, `type` and `id` properties. - // Thus either an array of `iD.Way`s or a relation member array may be - // used. - // - // If an member has a `tags` property, its tags will be reversed via - // `iD.actions.Reverse` in the output. - // - // Incomplete members (those for which `graph.hasEntity(element.id)` returns - // false) and non-way members are ignored. - // - function joinWays(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); - }); - - function resolve(member) { - return graph.childNodes(graph.entity(member.id)); - } - - function reverse(member) { - return member.tags ? iD.actions.Reverse(member.id, {reverseOneway: true})(graph).entity(member.id) : member; - } - - while (array.length) { - member = array.shift(); - current = [member]; - current.nodes = nodes = resolve(member).slice(); - joined.push(current); - - while (array.length && _.first(nodes) !== _.last(nodes)) { - first = _.first(nodes); - last = _.last(nodes); - - for (i = 0; i < array.length; i++) { - member = array[i]; - what = resolve(member); - - if (last === _.first(what)) { - how = nodes.push; - what = what.slice(1); - break; - } else if (last === _.last(what)) { - how = nodes.push; - what = what.slice(0, -1).reverse(); - member = reverse(member); - break; - } else if (first === _.last(what)) { - how = nodes.unshift; - what = what.slice(0, -1); - break; - } else if (first === _.first(what)) { - how = nodes.unshift; - what = what.slice(1).reverse(); - member = reverse(member); - break; - } else { - what = how = null; - } - } - - if (!what) - break; // No more joinable ways. - - how.apply(current, [member]); - how.apply(nodes, what); - - array.splice(i, 1); - } - } - - return joined; - } - - function interp(p1, p2, t) { - return [p1[0] + (p2[0] - p1[0]) * t, - p1[1] + (p2[1] - p1[1]) * t]; - } - - // 2D cross product of OA and OB vectors, i.e. z-component of their 3D cross product. - // Returns a positive value, if OAB makes a counter-clockwise turn, - // negative for clockwise turn, and zero if the points are collinear. - function cross(o, a, b) { - return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]); - } - - // http://jsperf.com/id-dist-optimization - function euclideanDistance(a, b) { - var x = a[0] - b[0], y = a[1] - b[1]; - return Math.sqrt((x * x) + (y * y)); - } - - // using WGS84 polar radius (6356752.314245179 m) - // const = 2 * PI * r / 360 - function latToMeters(dLat) { - return dLat * 110946.257617; - } - - // using WGS84 equatorial radius (6378137.0 m) - // const = 2 * PI * r / 360 - function lonToMeters(dLon, atLat) { - return Math.abs(atLat) >= 90 ? 0 : - dLon * 111319.490793 * Math.abs(Math.cos(atLat * (Math.PI/180))); - } - - // using WGS84 polar radius (6356752.314245179 m) - // const = 2 * PI * r / 360 - function metersToLat(m) { - return m / 110946.257617; - } - - // using WGS84 equatorial radius (6378137.0 m) - // const = 2 * PI * r / 360 - function metersToLon(m, atLat) { - return Math.abs(atLat) >= 90 ? 0 : - m / 111319.490793 / Math.abs(Math.cos(atLat * (Math.PI/180))); - } - - // Equirectangular approximation of spherical distances on Earth - function sphericalDistance(a, b) { - var x = lonToMeters(a[0] - b[0], (a[1] + b[1]) / 2), - y = latToMeters(a[1] - b[1]); - return Math.sqrt((x * x) + (y * y)); - } - - function edgeEqual(a, b) { - return (a[0] === b[0] && a[1] === b[1]) || - (a[0] === b[1] && a[1] === b[0]); - } - - // Return the counterclockwise angle in the range (-pi, pi) - // between the positive X axis and the line intersecting a and b. - function getAngle(a, b, projection) { - a = projection(a.loc); - b = projection(b.loc); - return Math.atan2(b[1] - a[1], b[0] - a[0]); - } - - // Choose the edge with the minimal distance from `point` to its orthogonal - // projection onto that edge, if such a projection exists, or the distance to - // the closest vertex on that edge. Returns an object with the `index` of the - // chosen edge, the chosen `loc` on that edge, and the `distance` to to it. - function chooseEdge(nodes, point, projection) { - var dist = euclideanDistance, - points = nodes.map(function(n) { return projection(n.loc); }), - min = Infinity, - idx, loc; - - function dot(p, q) { - return p[0] * q[0] + p[1] * q[1]; - } - - for (var i = 0; i < points.length - 1; i++) { - var o = points[i], - s = [points[i + 1][0] - o[0], - points[i + 1][1] - o[1]], - v = [point[0] - o[0], - point[1] - o[1]], - proj = dot(v, s) / dot(s, s), - p; - - if (proj < 0) { - p = o; - } else if (proj > 1) { - p = points[i + 1]; - } else { - p = [o[0] + proj * s[0], o[1] + proj * s[1]]; - } - - var d = dist(p, point); - if (d < min) { - min = d; - idx = i + 1; - loc = projection.invert(p); - } - } - - return { - index: idx, - distance: min, - loc: loc - }; - } - - // Return the intersection point of 2 line segments. - // From https://github.com/pgkelley4/line-segments-intersect - // This uses the vector cross product approach described below: - // http://stackoverflow.com/a/565282/786339 - function lineIntersection(a, b) { - function subtractPoints(point1, point2) { - return [point1[0] - point2[0], point1[1] - point2[1]]; - } - function crossProduct(point1, point2) { - return point1[0] * point2[1] - point1[1] * point2[0]; - } - - var p = [a[0][0], a[0][1]], - p2 = [a[1][0], a[1][1]], - q = [b[0][0], b[0][1]], - q2 = [b[1][0], b[1][1]], - r = subtractPoints(p2, p), - s = subtractPoints(q2, q), - uNumerator = crossProduct(subtractPoints(q, p), r), - denominator = crossProduct(r, s); - - if (uNumerator && denominator) { - var u = uNumerator / denominator, - t = crossProduct(subtractPoints(q, p), s) / denominator; - - if ((t >= 0) && (t <= 1) && (u >= 0) && (u <= 1)) { - return interp(p, p2, t); - } - } - - return null; - } - - function pathIntersections(path1, path2) { - var intersections = []; - for (var i = 0; i < path1.length - 1; i++) { - for (var j = 0; j < path2.length - 1; j++) { - var a = [ path1[i], path1[i+1] ], - b = [ path2[j], path2[j+1] ], - hit = lineIntersection(a, b); - if (hit) intersections.push(hit); - } - } - return intersections; - } - - // Return whether point is contained in polygon. - // - // `point` should be a 2-item array of coordinates. - // `polygon` should be an array of 2-item arrays of coordinates. - // - // From https://github.com/substack/point-in-polygon. - // ray-casting algorithm based on - // http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html - // - function pointInPolygon(point, polygon) { - var x = point[0], - y = point[1], - inside = false; - - for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { - var xi = polygon[i][0], yi = polygon[i][1]; - var xj = polygon[j][0], yj = polygon[j][1]; - - var intersect = ((yi > y) !== (yj > y)) && - (x < (xj - xi) * (y - yi) / (yj - yi) + xi); - if (intersect) inside = !inside; - } - - return inside; - } - - function polygonContainsPolygon(outer, inner) { - return _.every(inner, function(point) { - return pointInPolygon(point, outer); - }); - } - - function polygonIntersectsPolygon(outer, inner, checkSegments) { - function testSegments(outer, inner) { - for (var i = 0; i < outer.length - 1; i++) { - for (var j = 0; j < inner.length - 1; j++) { - var a = [ outer[i], outer[i+1] ], - b = [ inner[j], inner[j+1] ]; - if (lineIntersection(a, b)) return true; - } - } - return false; - } - - function testPoints(outer, inner) { - return _.some(inner, function(point) { - return pointInPolygon(point, outer); - }); - } - - return testPoints(outer, inner) || (!!checkSegments && testSegments(outer, inner)); - } - - function pathLength(path) { - var length = 0, - dx, dy; - for (var i = 0; i < path.length - 1; i++) { - dx = path[i][0] - path[i + 1][0]; - dy = path[i][1] - path[i + 1][1]; - length += Math.sqrt(dx * dx + dy * dy); - } - return length; - } - - function AddMember(relationId, member, memberIndex) { - return function(graph) { - var relation = graph.entity(relationId); - - if (isNaN(memberIndex) && member.type === 'way') { - var members = relation.indexedMembers(); - members.push(member); - - var joined = joinWays(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; - - if (j === 0) { - memberIndex = segment[j + 1].index; - } else if (j === segment.length - 1) { - memberIndex = segment[j - 1].index + 1; - } else { - memberIndex = Math.min(segment[j - 1].index + 1, segment[j + 1].index + 1); - } - } - } - } - - return graph.replace(relation.addMember(member, memberIndex)); - }; - } - - function AddMidpoint(midpoint, node) { - return function(graph) { - graph = graph.replace(node.move(midpoint.loc)); - - var parents = _.intersection( - graph.parentWays(graph.entity(midpoint.edge[0])), - graph.parentWays(graph.entity(midpoint.edge[1]))); - - parents.forEach(function(way) { - for (var i = 0; i < way.nodes.length - 1; i++) { - if (edgeEqual([way.nodes[i], way.nodes[i + 1]], midpoint.edge)) { - graph = graph.replace(graph.entity(way.id).addNode(node.id, i + 1)); - - // Add only one midpoint on doubled-back segments, - // turning them into self-intersections. - return; - } - } - }); - - return graph; - }; - } - - // https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/AddNodeToWayAction.as - function AddVertex(wayId, nodeId, index) { - return function(graph) { - return graph.replace(graph.entity(wayId).addNode(nodeId, index)); - }; - } - - function ChangeMember(relationId, member, memberIndex) { - return function(graph) { - return graph.replace(graph.entity(relationId).updateMember(member, memberIndex)); - }; - } - - function ChangePreset(entityId, oldPreset, newPreset) { - return function(graph) { - var entity = graph.entity(entityId), - geometry = entity.geometry(graph), - tags = entity.tags; - - if (oldPreset) tags = oldPreset.removeTags(tags, geometry); - if (newPreset) tags = newPreset.applyTags(tags, geometry); - - return graph.replace(entity.update({tags: tags})); - }; - } - - function ChangeTags(entityId, tags) { - return function(graph) { - var entity = graph.entity(entityId); - return graph.replace(entity.update({tags: tags})); - }; - } - function interestingTag(key) { return key !== 'attribution' && key !== 'created_by' && @@ -2961,6 +2513,753 @@ return d3.rebind(history, dispatch, 'on'); } + function Turn(turn) { + if (!(this instanceof Turn)) + return new Turn(turn); + _.extend(this, turn); + } + + function Intersection(graph, vertexId) { + var vertex = graph.entity(vertexId), + parentWays = graph.parentWays(vertex), + coincident = [], + highways = {}; + + function addHighway(way, adjacentNodeId) { + if (highways[adjacentNodeId]) { + coincident.push(adjacentNodeId); + } else { + highways[adjacentNodeId] = way; + } + } + + // Pre-split ways that would need to be split in + // order to add a restriction. The real split will + // happen when the restriction is added. + parentWays.forEach(function(way) { + if (!way.tags.highway || way.isArea() || way.isDegenerate()) + return; + + var isFirst = (vertexId === way.first()), + isLast = (vertexId === way.last()), + isAffix = (isFirst || isLast), + isClosingNode = (isFirst && isLast); + + if (isAffix && !isClosingNode) { + var index = (isFirst ? 1 : way.nodes.length - 2); + addHighway(way, way.nodes[index]); + + } else { + var splitIndex, wayA, wayB, indexA, indexB; + if (isClosingNode) { + splitIndex = Math.ceil(way.nodes.length / 2); // split at midpoint + wayA = Way({id: way.id + '-a', tags: way.tags, nodes: way.nodes.slice(0, splitIndex)}); + wayB = Way({id: way.id + '-b', tags: way.tags, nodes: way.nodes.slice(splitIndex)}); + indexA = 1; + indexB = way.nodes.length - 2; + } else { + splitIndex = _.indexOf(way.nodes, vertex.id, 1); // split at vertexid + wayA = Way({id: way.id + '-a', tags: way.tags, nodes: way.nodes.slice(0, splitIndex + 1)}); + wayB = Way({id: way.id + '-b', tags: way.tags, nodes: way.nodes.slice(splitIndex)}); + indexA = splitIndex - 1; + indexB = splitIndex + 1; + } + graph = graph.replace(wayA).replace(wayB); + addHighway(wayA, way.nodes[indexA]); + addHighway(wayB, way.nodes[indexB]); + } + }); + + // remove any ways from this intersection that are coincident + // (i.e. any adjacent node used by more than one intersecting way) + coincident.forEach(function (n) { + delete highways[n]; + }); + + + var intersection = { + highways: highways, + ways: _.values(highways), + graph: graph + }; + + intersection.adjacentNodeId = function(fromWayId) { + return _.find(_.keys(highways), function(k) { + return highways[k].id === fromWayId; + }); + }; + + intersection.turns = function(fromNodeId) { + var start = highways[fromNodeId]; + if (!start) + return []; + + if (start.first() === vertex.id && start.tags.oneway === 'yes') + return []; + if (start.last() === vertex.id && start.tags.oneway === '-1') + return []; + + function withRestriction(turn) { + graph.parentRelations(graph.entity(turn.from.way)).forEach(function(relation) { + if (relation.tags.type !== 'restriction') + return; + + var f = relation.memberByRole('from'), + t = relation.memberByRole('to'), + v = relation.memberByRole('via'); + + if (f && f.id === turn.from.way && + v && v.id === turn.via.node && + t && t.id === turn.to.way) { + turn.restriction = relation.id; + } else if (/^only_/.test(relation.tags.restriction) && + f && f.id === turn.from.way && + v && v.id === turn.via.node && + t && t.id !== turn.to.way) { + turn.restriction = relation.id; + turn.indirect_restriction = true; + } + }); + + return Turn(turn); + } + + var from = { + node: fromNodeId, + way: start.id.split(/-(a|b)/)[0] + }, + via = { node: vertex.id }, + turns = []; + + _.each(highways, function(end, adjacentNodeId) { + if (end === start) + return; + + // backward + if (end.first() !== vertex.id && end.tags.oneway !== 'yes') { + turns.push(withRestriction({ + from: from, + via: via, + to: { + node: adjacentNodeId, + way: end.id.split(/-(a|b)/)[0] + } + })); + } + + // forward + if (end.last() !== vertex.id && end.tags.oneway !== '-1') { + turns.push(withRestriction({ + from: from, + via: via, + to: { + node: adjacentNodeId, + way: end.id.split(/-(a|b)/)[0] + } + })); + } + + }); + + // U-turn + if (start.tags.oneway !== 'yes' && start.tags.oneway !== '-1') { + turns.push(withRestriction({ + from: from, + via: via, + to: from, + u: true + })); + } + + return turns; + }; + + return intersection; + } + + + function inferRestriction(graph, from, via, to, projection) { + var fromWay = graph.entity(from.way), + fromNode = graph.entity(from.node), + toWay = graph.entity(to.way), + toNode = graph.entity(to.node), + viaNode = graph.entity(via.node), + fromOneWay = (fromWay.tags.oneway === 'yes' && fromWay.last() === via.node) || + (fromWay.tags.oneway === '-1' && fromWay.first() === via.node), + toOneWay = (toWay.tags.oneway === 'yes' && toWay.first() === via.node) || + (toWay.tags.oneway === '-1' && toWay.last() === via.node), + angle = getAngle(viaNode, fromNode, projection) - + getAngle(viaNode, toNode, projection); + + angle = angle * 180 / Math.PI; + + while (angle < 0) + angle += 360; + + if (fromNode === toNode) + return 'no_u_turn'; + if ((angle < 23 || angle > 336) && fromOneWay && toOneWay) + return 'no_u_turn'; + if (angle < 158) + return 'no_right_turn'; + if (angle > 202) + return 'no_left_turn'; + + return 'no_straight_on'; + } + + // For fixing up rendering of multipolygons with tags on the outer member. + // https://github.com/openstreetmap/iD/issues/613 + function isSimpleMultipolygonOuterMember(entity, graph) { + if (entity.type !== 'way') + return false; + + var parents = graph.parentRelations(entity); + if (parents.length !== 1) + return false; + + var parent = parents[0]; + if (!parent.isMultipolygon() || Object.keys(parent.tags).length > 1) + return false; + + var members = parent.members, member; + for (var i = 0; i < members.length; i++) { + member = members[i]; + if (member.id === entity.id && member.role && member.role !== 'outer') + return false; // Not outer member + if (member.id !== entity.id && (!member.role || member.role === 'outer')) + return false; // Not a simple multipolygon + } + + return parent; + } + + function simpleMultipolygonOuterMember(entity, graph) { + if (entity.type !== 'way') + return false; + + var parents = graph.parentRelations(entity); + if (parents.length !== 1) + return false; + + var parent = parents[0]; + if (!parent.isMultipolygon() || Object.keys(parent.tags).length > 1) + return false; + + var members = parent.members, member, outerMember; + for (var i = 0; i < members.length; i++) { + member = members[i]; + if (!member.role || member.role === 'outer') { + if (outerMember) + return false; // Not a simple multipolygon + outerMember = member; + } + } + + return outerMember && graph.hasEntity(outerMember.id); + } + + // Join `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. + // + // Members of `array` must have, at minimum, `type` and `id` properties. + // Thus either an array of `iD.Way`s or a relation member array may be + // used. + // + // If an member has a `tags` property, its tags will be reversed via + // `iD.actions.Reverse` in the output. + // + // Incomplete members (those for which `graph.hasEntity(element.id)` returns + // false) and non-way members are ignored. + // + function joinWays(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); + }); + + function resolve(member) { + return graph.childNodes(graph.entity(member.id)); + } + + function reverse(member) { + return member.tags ? iD.actions.Reverse(member.id, {reverseOneway: true})(graph).entity(member.id) : member; + } + + while (array.length) { + member = array.shift(); + current = [member]; + current.nodes = nodes = resolve(member).slice(); + joined.push(current); + + while (array.length && _.first(nodes) !== _.last(nodes)) { + first = _.first(nodes); + last = _.last(nodes); + + for (i = 0; i < array.length; i++) { + member = array[i]; + what = resolve(member); + + if (last === _.first(what)) { + how = nodes.push; + what = what.slice(1); + break; + } else if (last === _.last(what)) { + how = nodes.push; + what = what.slice(0, -1).reverse(); + member = reverse(member); + break; + } else if (first === _.last(what)) { + how = nodes.unshift; + what = what.slice(0, -1); + break; + } else if (first === _.first(what)) { + how = nodes.unshift; + what = what.slice(1).reverse(); + member = reverse(member); + break; + } else { + what = how = null; + } + } + + if (!what) + break; // No more joinable ways. + + how.apply(current, [member]); + how.apply(nodes, what); + + array.splice(i, 1); + } + } + + return joined; + } + + /* + Bypasses features of D3's default projection stream pipeline that are unnecessary: + * Antimeridian clipping + * Spherical rotation + * Resampling + */ + function RawMercator() { + var project = d3.geo.mercator.raw, + k = 512 / Math.PI, // scale + x = 0, y = 0, // translate + clipExtent = [[0, 0], [0, 0]]; + + function projection(point) { + point = project(point[0] * Math.PI / 180, point[1] * Math.PI / 180); + return [point[0] * k + x, y - point[1] * k]; + } + + projection.invert = function(point) { + point = project.invert((point[0] - x) / k, (y - point[1]) / k); + return point && [point[0] * 180 / Math.PI, point[1] * 180 / Math.PI]; + }; + + projection.scale = function(_) { + if (!arguments.length) return k; + k = +_; + return projection; + }; + + projection.translate = function(_) { + if (!arguments.length) return [x, y]; + x = +_[0]; + y = +_[1]; + return projection; + }; + + projection.clipExtent = function(_) { + if (!arguments.length) return clipExtent; + clipExtent = _; + return projection; + }; + + projection.stream = d3.geo.transform({ + point: function(x, y) { + x = projection([x, y]); + this.stream.point(x[0], x[1]); + } + }).stream; + + return projection; + } + + function roundCoords(c) { + return [Math.floor(c[0]), Math.floor(c[1])]; + } + + function interp(p1, p2, t) { + return [p1[0] + (p2[0] - p1[0]) * t, + p1[1] + (p2[1] - p1[1]) * t]; + } + + // 2D cross product of OA and OB vectors, i.e. z-component of their 3D cross product. + // Returns a positive value, if OAB makes a counter-clockwise turn, + // negative for clockwise turn, and zero if the points are collinear. + function cross(o, a, b) { + return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]); + } + + // http://jsperf.com/id-dist-optimization + function euclideanDistance(a, b) { + var x = a[0] - b[0], y = a[1] - b[1]; + return Math.sqrt((x * x) + (y * y)); + } + + // using WGS84 polar radius (6356752.314245179 m) + // const = 2 * PI * r / 360 + function latToMeters(dLat) { + return dLat * 110946.257617; + } + + // using WGS84 equatorial radius (6378137.0 m) + // const = 2 * PI * r / 360 + function lonToMeters(dLon, atLat) { + return Math.abs(atLat) >= 90 ? 0 : + dLon * 111319.490793 * Math.abs(Math.cos(atLat * (Math.PI/180))); + } + + // using WGS84 polar radius (6356752.314245179 m) + // const = 2 * PI * r / 360 + function metersToLat(m) { + return m / 110946.257617; + } + + // using WGS84 equatorial radius (6378137.0 m) + // const = 2 * PI * r / 360 + function metersToLon(m, atLat) { + return Math.abs(atLat) >= 90 ? 0 : + m / 111319.490793 / Math.abs(Math.cos(atLat * (Math.PI/180))); + } + + function offsetToMeters(offset) { + var equatRadius = 6356752.314245179, + polarRadius = 6378137.0, + tileSize = 256; + + return [ + offset[0] * 2 * Math.PI * equatRadius / tileSize, + -offset[1] * 2 * Math.PI * polarRadius / tileSize + ]; + } + + function metersToOffset(meters) { + var equatRadius = 6356752.314245179, + polarRadius = 6378137.0, + tileSize = 256; + + return [ + meters[0] * tileSize / (2 * Math.PI * equatRadius), + -meters[1] * tileSize / (2 * Math.PI * polarRadius) + ]; + } + + // Equirectangular approximation of spherical distances on Earth + function sphericalDistance(a, b) { + var x = lonToMeters(a[0] - b[0], (a[1] + b[1]) / 2), + y = latToMeters(a[1] - b[1]); + return Math.sqrt((x * x) + (y * y)); + } + + function edgeEqual(a, b) { + return (a[0] === b[0] && a[1] === b[1]) || + (a[0] === b[1] && a[1] === b[0]); + } + + // Return the counterclockwise angle in the range (-pi, pi) + // between the positive X axis and the line intersecting a and b. + function getAngle(a, b, projection) { + a = projection(a.loc); + b = projection(b.loc); + return Math.atan2(b[1] - a[1], b[0] - a[0]); + } + + // Choose the edge with the minimal distance from `point` to its orthogonal + // projection onto that edge, if such a projection exists, or the distance to + // the closest vertex on that edge. Returns an object with the `index` of the + // chosen edge, the chosen `loc` on that edge, and the `distance` to to it. + function chooseEdge(nodes, point, projection) { + var dist = euclideanDistance, + points = nodes.map(function(n) { return projection(n.loc); }), + min = Infinity, + idx, loc; + + function dot(p, q) { + return p[0] * q[0] + p[1] * q[1]; + } + + for (var i = 0; i < points.length - 1; i++) { + var o = points[i], + s = [points[i + 1][0] - o[0], + points[i + 1][1] - o[1]], + v = [point[0] - o[0], + point[1] - o[1]], + proj = dot(v, s) / dot(s, s), + p; + + if (proj < 0) { + p = o; + } else if (proj > 1) { + p = points[i + 1]; + } else { + p = [o[0] + proj * s[0], o[1] + proj * s[1]]; + } + + var d = dist(p, point); + if (d < min) { + min = d; + idx = i + 1; + loc = projection.invert(p); + } + } + + return { + index: idx, + distance: min, + loc: loc + }; + } + + // Return the intersection point of 2 line segments. + // From https://github.com/pgkelley4/line-segments-intersect + // This uses the vector cross product approach described below: + // http://stackoverflow.com/a/565282/786339 + function lineIntersection(a, b) { + function subtractPoints(point1, point2) { + return [point1[0] - point2[0], point1[1] - point2[1]]; + } + function crossProduct(point1, point2) { + return point1[0] * point2[1] - point1[1] * point2[0]; + } + + var p = [a[0][0], a[0][1]], + p2 = [a[1][0], a[1][1]], + q = [b[0][0], b[0][1]], + q2 = [b[1][0], b[1][1]], + r = subtractPoints(p2, p), + s = subtractPoints(q2, q), + uNumerator = crossProduct(subtractPoints(q, p), r), + denominator = crossProduct(r, s); + + if (uNumerator && denominator) { + var u = uNumerator / denominator, + t = crossProduct(subtractPoints(q, p), s) / denominator; + + if ((t >= 0) && (t <= 1) && (u >= 0) && (u <= 1)) { + return interp(p, p2, t); + } + } + + return null; + } + + function pathIntersections(path1, path2) { + var intersections = []; + for (var i = 0; i < path1.length - 1; i++) { + for (var j = 0; j < path2.length - 1; j++) { + var a = [ path1[i], path1[i+1] ], + b = [ path2[j], path2[j+1] ], + hit = lineIntersection(a, b); + if (hit) intersections.push(hit); + } + } + return intersections; + } + + // Return whether point is contained in polygon. + // + // `point` should be a 2-item array of coordinates. + // `polygon` should be an array of 2-item arrays of coordinates. + // + // From https://github.com/substack/point-in-polygon. + // ray-casting algorithm based on + // http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html + // + function pointInPolygon(point, polygon) { + var x = point[0], + y = point[1], + inside = false; + + for (var i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { + var xi = polygon[i][0], yi = polygon[i][1]; + var xj = polygon[j][0], yj = polygon[j][1]; + + var intersect = ((yi > y) !== (yj > y)) && + (x < (xj - xi) * (y - yi) / (yj - yi) + xi); + if (intersect) inside = !inside; + } + + return inside; + } + + function polygonContainsPolygon(outer, inner) { + return _.every(inner, function(point) { + return pointInPolygon(point, outer); + }); + } + + function polygonIntersectsPolygon(outer, inner, checkSegments) { + function testSegments(outer, inner) { + for (var i = 0; i < outer.length - 1; i++) { + for (var j = 0; j < inner.length - 1; j++) { + var a = [ outer[i], outer[i+1] ], + b = [ inner[j], inner[j+1] ]; + if (lineIntersection(a, b)) return true; + } + } + return false; + } + + function testPoints(outer, inner) { + return _.some(inner, function(point) { + return pointInPolygon(point, outer); + }); + } + + return testPoints(outer, inner) || (!!checkSegments && testSegments(outer, inner)); + } + + function pathLength(path) { + var length = 0, + dx, dy; + for (var i = 0; i < path.length - 1; i++) { + dx = path[i][0] - path[i + 1][0]; + dy = path[i][1] - path[i + 1][1]; + length += Math.sqrt(dx * dx + dy * dy); + } + return length; + } + + + var geo = Object.freeze({ + roundCoords: roundCoords, + interp: interp, + cross: cross, + euclideanDistance: euclideanDistance, + latToMeters: latToMeters, + lonToMeters: lonToMeters, + metersToLat: metersToLat, + metersToLon: metersToLon, + offsetToMeters: offsetToMeters, + metersToOffset: metersToOffset, + sphericalDistance: sphericalDistance, + edgeEqual: edgeEqual, + angle: getAngle, + chooseEdge: chooseEdge, + lineIntersection: lineIntersection, + pathIntersections: pathIntersections, + pointInPolygon: pointInPolygon, + polygonContainsPolygon: polygonContainsPolygon, + polygonIntersectsPolygon: polygonIntersectsPolygon, + pathLength: pathLength, + Extent: Extent, + Intersection: Intersection, + Turn: Turn, + inferRestriction: inferRestriction, + isSimpleMultipolygonOuterMember: isSimpleMultipolygonOuterMember, + simpleMultipolygonOuterMember: simpleMultipolygonOuterMember, + joinWays: joinWays, + RawMercator: RawMercator + }); + + function AddMember(relationId, member, memberIndex) { + return function(graph) { + var relation = graph.entity(relationId); + + if (isNaN(memberIndex) && member.type === 'way') { + var members = relation.indexedMembers(); + members.push(member); + + var joined = joinWays(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; + + if (j === 0) { + memberIndex = segment[j + 1].index; + } else if (j === segment.length - 1) { + memberIndex = segment[j - 1].index + 1; + } else { + memberIndex = Math.min(segment[j - 1].index + 1, segment[j + 1].index + 1); + } + } + } + } + + return graph.replace(relation.addMember(member, memberIndex)); + }; + } + + function AddMidpoint(midpoint, node) { + return function(graph) { + graph = graph.replace(node.move(midpoint.loc)); + + var parents = _.intersection( + graph.parentWays(graph.entity(midpoint.edge[0])), + graph.parentWays(graph.entity(midpoint.edge[1]))); + + parents.forEach(function(way) { + for (var i = 0; i < way.nodes.length - 1; i++) { + if (edgeEqual([way.nodes[i], way.nodes[i + 1]], midpoint.edge)) { + graph = graph.replace(graph.entity(way.id).addNode(node.id, i + 1)); + + // Add only one midpoint on doubled-back segments, + // turning them into self-intersections. + return; + } + } + }); + + return graph; + }; + } + + // https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/AddNodeToWayAction.as + function AddVertex(wayId, nodeId, index) { + return function(graph) { + return graph.replace(graph.entity(wayId).addNode(nodeId, index)); + }; + } + + function ChangeMember(relationId, member, memberIndex) { + return function(graph) { + return graph.replace(graph.entity(relationId).updateMember(member, memberIndex)); + }; + } + + function ChangePreset(entityId, oldPreset, newPreset) { + return function(graph) { + var entity = graph.entity(entityId), + geometry = entity.geometry(graph), + tags = entity.tags; + + if (oldPreset) tags = oldPreset.removeTags(tags, geometry); + if (newPreset) tags = newPreset.applyTags(tags, geometry); + + return graph.replace(entity.update({tags: tags})); + }; + } + + function ChangeTags(entityId, tags) { + return function(graph) { + var entity = graph.entity(entityId); + return graph.replace(entity.update({tags: tags})); + }; + } + function Circularize(wayId , projection, maxAngle) { maxAngle = (maxAngle || 20) * Math.PI / 180; @@ -5050,6 +5349,7 @@ }); exports.actions = actions; + exports.geo = geo; exports.Connection = Connection; exports.Difference = Difference; exports.Entity = Entity; diff --git a/js/lib/id/ui/intro.js b/js/lib/id/ui/intro.js index 465216d98..512396d5e 100644 --- a/js/lib/id/ui/intro.js +++ b/js/lib/id/ui/intro.js @@ -247,40 +247,6 @@ return d3.rebind(step, event, 'on'); } - - function pointBox(point, context) { - var rect = context.surfaceRect(); - point = context.projection(point); - return { - left: point[0] + rect.left - 30, - top: point[1] + rect.top - 50, - width: 60, - height: 70 - }; - } - - function pad(box, padding, context) { - if (box instanceof Array) { - var rect = context.surfaceRect(); - box = context.projection(box); - box = { - left: box[0] + rect.left, - top: box[1] + rect.top - }; - } - return { - left: box.left - padding, - top: box.top - padding, - width: (box.width || 0) + 2 * padding, - height: (box.width || 0) + 2 * padding - }; - } - - function icon(name, svgklass) { - return '' + - ''; - } - function navigation(context, reveal) { var event = d3.dispatch('done'), timeouts = []; @@ -607,9 +573,6 @@ exports.area = area; exports.line = line; - exports.pad = pad; - exports.pointBox = pointBox; - exports.icon = icon; exports.navigation = navigation; exports.point = point; exports.startEditing = startEditing; diff --git a/modules/geo/intersection.js b/modules/geo/intersection.js index f1c3c00ef..38c17c7a4 100644 --- a/modules/geo/intersection.js +++ b/modules/geo/intersection.js @@ -1,4 +1,5 @@ import { angle as getAngle } from './index'; +import { Way } from '../core/index'; export function Turn(turn) { if (!(this instanceof Turn)) @@ -40,14 +41,14 @@ export function Intersection(graph, vertexId) { var splitIndex, wayA, wayB, indexA, indexB; if (isClosingNode) { splitIndex = Math.ceil(way.nodes.length / 2); // split at midpoint - wayA = iD.Way({id: way.id + '-a', tags: way.tags, nodes: way.nodes.slice(0, splitIndex)}); - wayB = iD.Way({id: way.id + '-b', tags: way.tags, nodes: way.nodes.slice(splitIndex)}); + wayA = Way({id: way.id + '-a', tags: way.tags, nodes: way.nodes.slice(0, splitIndex)}); + wayB = Way({id: way.id + '-b', tags: way.tags, nodes: way.nodes.slice(splitIndex)}); indexA = 1; indexB = way.nodes.length - 2; } else { splitIndex = _.indexOf(way.nodes, vertex.id, 1); // split at vertexid - wayA = iD.Way({id: way.id + '-a', tags: way.tags, nodes: way.nodes.slice(0, splitIndex + 1)}); - wayB = iD.Way({id: way.id + '-b', tags: way.tags, nodes: way.nodes.slice(splitIndex)}); + wayA = Way({id: way.id + '-a', tags: way.tags, nodes: way.nodes.slice(0, splitIndex + 1)}); + wayB = Way({id: way.id + '-b', tags: way.tags, nodes: way.nodes.slice(splitIndex)}); indexA = splitIndex - 1; indexB = splitIndex + 1; } diff --git a/modules/index.js b/modules/index.js index 352e9f8d8..227c4d95b 100644 --- a/modules/index.js +++ b/modules/index.js @@ -1,4 +1,5 @@ import * as actions from './actions/index'; +import * as geo from './geo/index'; export { Connection } from './core/connection'; export { Difference } from './core/difference'; @@ -12,5 +13,6 @@ export { Tree } from './core/tree'; export { Way } from './core/way'; export { - actions + actions, + geo }; diff --git a/replaceStuff.js b/replaceStuff.js new file mode 100644 index 000000000..fa722e78c --- /dev/null +++ b/replaceStuff.js @@ -0,0 +1,68 @@ +var fs = require('fs'); +var args = require('minimist')(process.argv.slice(2)); +var global = []; +function readFiles(dirname, outdir) { + var filenames = fs.readdirSync(dirname); + filenames.forEach(function(filename) { + var fileData = fs.readFileSync(dirname + filename).toString().split('\n'); + processFile(fileData, outdir + filename); + }); +} +var POSSIBLE_MODULES = [ 'actions', 'geo', 'modes', 'util', 'core', 'behavior' ]; +function findData(data) { + var modules = { 'actions': [], 'geo': [], 'modes': [], 'util': [], 'core': [], 'behavior': [] }; + var cores = [ 'Entity', 'Way', 'Relation', 'Node', 'Graph', 'Tree', 'Difference', 'History' ]; + var ret = data.map(function(lineArg) { + var line = lineArg; + POSSIBLE_MODULES.forEach(function(mod) { + cores.forEach(function(c) { + while (line.indexOf('iD.' + c) > -1 ) { + var start = line.indexOf('iD.' + c); + var prefix = 3; + line = line.slice(0, start) + line.slice(start + prefix); + if (modules.core.indexOf(c) === -1) { + modules.core.push(c); + console.log(c); + } + } + }); + + while (line.indexOf('iD.' + mod + '.') > -1 ) { + var start = line.indexOf('iD.' + mod + '.'); + var prefix = 3 + mod.length + 1; + var end = line.indexOf('(', start); + var foo = line.slice(start + prefix, end); + if (modules[mod].indexOf(foo) === -1) { + modules[mod].push(foo); + } + line = line.slice(0, start) + line.slice(start + prefix); + } + }); + return line; + }); + POSSIBLE_MODULES.forEach(function(mod) { + if (modules[mod].length > 0) { + var importStuff = modules[mod].join(', '); + ret.unshift(`import { ${importStuff} } from '../${mod}/index';`); + /*eslint-disable */ + /*eslint-enable */ + } + }); + + return ret; +} + +function processFile(fd, name) { + global.push({ + name: name, + data: fd + }); +} + +readFiles(args.dir, args.out); + + +global.forEach(function (f) { + var processedData = findData(f.data); + fs.writeFile(f.name, processedData.join('\n'), function (e) {if (e) console.log(e);}); +}); diff --git a/test/index.html b/test/index.html index a64636118..d118c0f38 100644 --- a/test/index.html +++ b/test/index.html @@ -43,7 +43,6 @@ -