From 7267d05e3dd6f387cd3a47f5efec04b596ea29f2 Mon Sep 17 00:00:00 2001
From: Kushan Joshi <0o3ko0@gmail.com>
Date: Sat, 18 Jun 2016 11:46:52 +0530
Subject: [PATCH] external modules for "geo"
---
Makefile | 9 -
index.html | 3 -
js/lib/id/geo.js | 800 ++++++++++++++++++++++-
js/lib/id/index.js | 1196 ++++++++++++++++++++++-------------
js/lib/id/ui/intro.js | 37 --
modules/geo/intersection.js | 9 +-
modules/index.js | 4 +-
replaceStuff.js | 68 ++
test/index.html | 1 -
9 files changed, 1620 insertions(+), 507 deletions(-)
create mode 100644 replaceStuff.js
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 @@
-