diff --git a/API.md b/API.md
index fcc2c89ec..d92a69628 100644
--- a/API.md
+++ b/API.md
@@ -39,7 +39,7 @@ A **line** is a way that is not an area. Elements representing lines have a `.li
class. Since a line is also a way, they also have a `.way` class.
An **area** is a way that is circular, has certain tags, or lacks certain other
-tags (see `iD.Way.isArea` for the exact definition). Elements representing areas
+tags (see `iD.Way#isArea` for the exact definition). Elements representing areas
have an `.area` class. Since an area is also a way, they also have a `.way` class.
### Tag classes
diff --git a/js/id/graph/entity.js b/js/id/graph/entity.js
index a7faee1b9..f6be3905c 100644
--- a/js/id/graph/entity.js
+++ b/js/id/graph/entity.js
@@ -1,30 +1,12 @@
-iD.Entity = function(a, b, c) {
- if (!(this instanceof iD.Entity)) return new iD.Entity(a, b, c);
+iD.Entity = function(attrs) {
+ // For prototypal inheritance.
+ if (this instanceof iD.Entity) return;
- this.tags = {};
+ // Create the appropriate subtype.
+ if (attrs && attrs.type) return iD.Entity[attrs.type].apply(this, arguments);
- var sources = [a, b, c], source;
- for (var i = 0; i < sources.length; ++i) {
- source = sources[i];
- for (var prop in source) {
- if (Object.prototype.hasOwnProperty.call(source, prop)) {
- this[prop] = source[prop];
- }
- }
- }
-
- if (!this.id && this.type) {
- this.id = iD.Entity.id(this.type);
- this._updated = true;
- }
-
- if (iD.debug) {
- Object.freeze(this);
- Object.freeze(this.tags);
-
- if (this.nodes) Object.freeze(this.nodes);
- if (this.members) Object.freeze(this.members);
- }
+ // Initialize a generic Entity (used only in tests).
+ return (new iD.Entity()).initialize(arguments);
};
iD.Entity.id = function (type) {
@@ -42,6 +24,34 @@ iD.Entity.id.toOSM = function (id) {
};
iD.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)) {
+ this[prop] = source[prop];
+ }
+ }
+ }
+
+ if (!this.id && this.type) {
+ this.id = iD.Entity.id(this.type);
+ this._updated = true;
+ }
+
+ if (iD.debug) {
+ Object.freeze(this);
+ Object.freeze(this.tags);
+
+ if (this.nodes) Object.freeze(this.nodes);
+ if (this.members) Object.freeze(this.members);
+ }
+
+ return this;
+ },
+
osmId: function() {
return iD.Entity.id.toOSM(this.id);
},
@@ -97,39 +107,54 @@ iD.Entity.prototype = {
}
};
-iD.Node = function(attrs) {
- return iD.Entity(attrs || {}, {type: 'node'});
+iD.Entity.extend = function(properties) {
+ var Subclass = function() {
+ if (this instanceof Subclass) return;
+ return (new Subclass()).initialize(arguments);
+ };
+
+ Subclass.prototype = new iD.Entity();
+ _.extend(Subclass.prototype, properties);
+ iD.Entity[properties.type] = Subclass;
+
+ return Subclass;
};
-iD.Way = function(attrs) {
- return iD.Entity({nodes: []}, attrs || {}, {type: 'way'});
-};
+iD.Node = iD.Entity.extend({
+ type: "node"
+});
-iD.Way.isOneWay = function(d) {
- return !!(d.tags.oneway && d.tags.oneway === 'yes');
-};
+iD.Way = iD.Entity.extend({
+ type: "way",
+ nodes: [],
-iD.Way.isClosed = function(d) {
- return (!d.nodes.length) || d.nodes[d.nodes.length - 1].id === d.nodes[0].id;
-};
+ isOneWay: function() {
+ return !!(this.tags.oneway && this.tags.oneway === 'yes');
+ },
-// a way is an area if:
-//
-// - area=yes
-// - closed and
-// - doesn't have area=no
-// - doesn't have highway tag
-iD.Way.isArea = function(d) {
- return (d.tags.area && d.tags.area === 'yes') ||
- (iD.Way.isClosed(d) &&
- // area-ness is disabled
- (!d.tags.area || d.tags.area !== 'no') &&
- // Tags that disable area-ness unless they are accompanied by
- // area=yes
- !d.tags.highway &&
- !d.tags.barrier);
-};
+ isClosed: function() {
+ return (!this.nodes.length) || this.nodes[this.nodes.length - 1] === this.nodes[0];
+ },
-iD.Relation = function(attrs) {
- return iD.Entity({members: []}, attrs || {}, {type: 'relation'});
-};
+ // a way is an area if:
+ //
+ // - area=yes
+ // - closed and
+ // - doesn't have area=no
+ // - doesn't have highway tag
+ isArea: function() {
+ return (this.tags.area && this.tags.area === 'yes') ||
+ (this.isClosed() &&
+ // area-ness is disabled
+ (!this.tags.area || this.tags.area !== 'no') &&
+ // Tags that disable area-ness unless they are accompanied by
+ // area=yes
+ !this.tags.highway &&
+ !this.tags.barrier);
+ }
+});
+
+iD.Relation = iD.Entity.extend({
+ type: "relation",
+ members: []
+});
diff --git a/js/id/renderer/map.js b/js/id/renderer/map.js
index 059361438..4e0703b1f 100644
--- a/js/id/renderer/map.js
+++ b/js/id/renderer/map.js
@@ -104,7 +104,7 @@ iD.Map = function() {
if (a.type === 'way') {
a._line = nodeline(a);
ways.push(a);
- if (iD.Way.isArea(a)) areas.push(a);
+ if (a.isArea()) areas.push(a);
else lines.push(a);
} else if (a._poi) {
points.push(a);
@@ -235,7 +235,7 @@ iD.Map = function() {
// Determine the lengths of oneway paths
var lengths = {},
- oneways = strokes.filter(iD.Way.isOneWay).each(function(d) {
+ oneways = strokes.filter(function (d) { return d.isOneWay(); }).each(function(d) {
lengths[d.id] = Math.floor(this.getTotalLength() / alength);
}).data();
diff --git a/test/index.html b/test/index.html
index a9887a49e..2ed56cb74 100644
--- a/test/index.html
+++ b/test/index.html
@@ -111,8 +111,10 @@
-
+
+
+
diff --git a/test/index_packaged.html b/test/index_packaged.html
index 5b99099b2..c7fa9aa9a 100644
--- a/test/index_packaged.html
+++ b/test/index_packaged.html
@@ -43,8 +43,10 @@
-
+
+
+
diff --git a/test/spec/graph/entity.js b/test/spec/graph/entity.js
index 0010696fe..e2790d815 100644
--- a/test/spec/graph/entity.js
+++ b/test/spec/graph/entity.js
@@ -1,4 +1,10 @@
describe('iD.Entity', function () {
+ it("returns a subclass of the appropriate type", function () {
+ expect(iD.Entity({type: 'way'})).be.an.instanceOf(iD.Way);
+ expect(iD.Entity({type: 'node'})).be.an.instanceOf(iD.Node);
+ expect(iD.Entity({type: 'relation'})).be.an.instanceOf(iD.Relation);
+ });
+
if (iD.debug) {
it("is frozen", function () {
expect(Object.isFrozen(iD.Entity())).to.be.true;
@@ -41,6 +47,11 @@ describe('iD.Entity', function () {
expect(e.tags).to.equal(tags);
});
+ it("preserves existing attributes", function () {
+ var e = iD.Entity({id: 'w1'}).update({});
+ expect(e.id).to.equal('w1');
+ });
+
it("tags the entity as updated", function () {
var tags = {foo: 'bar'},
e = iD.Entity().update({tags: tags});
@@ -112,126 +123,3 @@ describe('iD.Entity', function () {
});
});
});
-
-describe('iD.Node', function () {
- it("returns a node", function () {
- expect(iD.Node().type).to.equal("node");
- });
-
- it("returns a created Entity if no ID is specified", function () {
- expect(iD.Node().created()).to.be.ok;
- });
-
- it("returns an unmodified Entity if ID is specified", function () {
- expect(iD.Node({id: 'n1234'}).created()).not.to.be.ok;
- expect(iD.Node({id: 'n1234'}).modified()).not.to.be.ok;
- });
-
- it("defaults tags to an empty object", function () {
- expect(iD.Node().tags).to.eql({});
- });
-
- it("sets tags as specified", function () {
- expect(iD.Node({tags: {foo: 'bar'}}).tags).to.eql({foo: 'bar'});
- });
-
- describe("#intersects", function () {
- it("returns true for a node within the given extent", function () {
- expect(iD.Node({loc: [0, 0]}).intersects([[-180, 90], [180, -90]])).to.equal(true);
- });
-
- it("returns false for a node outside the given extend", function () {
- expect(iD.Node({loc: [0, 0]}).intersects([[100, 90], [180, -90]])).to.equal(false);
- });
- });
-});
-
-describe('iD.Way', function () {
- if (iD.debug) {
- it("freezes nodes", function () {
- expect(Object.isFrozen(iD.Way().nodes)).to.be.true;
- });
- }
-
- it("returns a way", function () {
- expect(iD.Way().type).to.equal("way");
- });
-
- it("returns a created Entity if no ID is specified", function () {
- expect(iD.Way().created()).to.be.ok;
- });
-
- it("returns an unmodified Entity if ID is specified", function () {
- expect(iD.Way({id: 'w1234'}).created()).not.to.be.ok;
- expect(iD.Way({id: 'w1234'}).modified()).not.to.be.ok;
- });
-
- it("defaults nodes to an empty array", function () {
- expect(iD.Way().nodes).to.eql([]);
- });
-
- it("sets nodes as specified", function () {
- expect(iD.Way({nodes: ["n-1"]}).nodes).to.eql(["n-1"]);
- });
-
- it("defaults tags to an empty object", function () {
- expect(iD.Way().tags).to.eql({});
- });
-
- it("sets tags as specified", function () {
- expect(iD.Way({tags: {foo: 'bar'}}).tags).to.eql({foo: 'bar'});
- });
-
- describe("#intersects", function () {
- it("returns true for a way with a node within the given extent", function () {
- var node = iD.Node({loc: [0, 0]}),
- way = iD.Way({nodes: [node.id]}),
- graph = iD.Graph([node, way]);
- expect(way.intersects([[-180, 90], [180, -90]], graph)).to.equal(true);
- });
-
- it("returns false for way with no nodes within the given extent", function () {
- var node = iD.Node({loc: [0, 0]}),
- way = iD.Way({nodes: [node.id]}),
- graph = iD.Graph([node, way]);
- expect(way.intersects([[100, 90], [180, -90]], graph)).to.equal(false);
- });
- });
-});
-
-describe('iD.Relation', function () {
- if (iD.debug) {
- it("freezes nodes", function () {
- expect(Object.isFrozen(iD.Relation().members)).to.be.true;
- });
- }
-
- it("returns a relation", function () {
- expect(iD.Relation().type).to.equal("relation");
- });
-
- it("returns a created Entity if no ID is specified", function () {
- expect(iD.Relation().created()).to.be.ok;
- });
-
- it("returns an unmodified Entity if ID is specified", function () {
- expect(iD.Relation({id: 'r1234'}).created()).not.to.be.ok;
- expect(iD.Relation({id: 'r1234'}).modified()).not.to.be.ok;
- });
-
- it("defaults members to an empty array", function () {
- expect(iD.Relation().members).to.eql([]);
- });
-
- it("sets members as specified", function () {
- expect(iD.Relation({members: ["n-1"]}).members).to.eql(["n-1"]);
- });
-
- it("defaults tags to an empty object", function () {
- expect(iD.Relation().tags).to.eql({});
- });
-
- it("sets tags as specified", function () {
- expect(iD.Relation({tags: {foo: 'bar'}}).tags).to.eql({foo: 'bar'});
- });
-});
diff --git a/test/spec/graph/node.js b/test/spec/graph/node.js
new file mode 100644
index 000000000..b364dc0e6
--- /dev/null
+++ b/test/spec/graph/node.js
@@ -0,0 +1,33 @@
+describe('iD.Node', function () {
+ it("returns a node", function () {
+ expect(iD.Node()).to.be.an.instanceOf(iD.Node);
+ expect(iD.Node().type).to.equal("node");
+ });
+
+ it("returns a created Entity if no ID is specified", function () {
+ expect(iD.Node().created()).to.be.ok;
+ });
+
+ it("returns an unmodified Entity if ID is specified", function () {
+ expect(iD.Node({id: 'n1234'}).created()).not.to.be.ok;
+ expect(iD.Node({id: 'n1234'}).modified()).not.to.be.ok;
+ });
+
+ it("defaults tags to an empty object", function () {
+ expect(iD.Node().tags).to.eql({});
+ });
+
+ it("sets tags as specified", function () {
+ expect(iD.Node({tags: {foo: 'bar'}}).tags).to.eql({foo: 'bar'});
+ });
+
+ describe("#intersects", function () {
+ it("returns true for a node within the given extent", function () {
+ expect(iD.Node({loc: [0, 0]}).intersects([[-180, 90], [180, -90]])).to.equal(true);
+ });
+
+ it("returns false for a node outside the given extend", function () {
+ expect(iD.Node({loc: [0, 0]}).intersects([[100, 90], [180, -90]])).to.equal(false);
+ });
+ });
+});
diff --git a/test/spec/graph/relation.js b/test/spec/graph/relation.js
new file mode 100644
index 000000000..00e9addd8
--- /dev/null
+++ b/test/spec/graph/relation.js
@@ -0,0 +1,37 @@
+describe('iD.Relation', function () {
+ if (iD.debug) {
+ it("freezes nodes", function () {
+ expect(Object.isFrozen(iD.Relation().members)).to.be.true;
+ });
+ }
+
+ it("returns a relation", function () {
+ expect(iD.Relation()).to.be.an.instanceOf(iD.Relation);
+ expect(iD.Relation().type).to.equal("relation");
+ });
+
+ it("returns a created Entity if no ID is specified", function () {
+ expect(iD.Relation().created()).to.be.ok;
+ });
+
+ it("returns an unmodified Entity if ID is specified", function () {
+ expect(iD.Relation({id: 'r1234'}).created()).not.to.be.ok;
+ expect(iD.Relation({id: 'r1234'}).modified()).not.to.be.ok;
+ });
+
+ it("defaults members to an empty array", function () {
+ expect(iD.Relation().members).to.eql([]);
+ });
+
+ it("sets members as specified", function () {
+ expect(iD.Relation({members: ["n-1"]}).members).to.eql(["n-1"]);
+ });
+
+ it("defaults tags to an empty object", function () {
+ expect(iD.Relation().tags).to.eql({});
+ });
+
+ it("sets tags as specified", function () {
+ expect(iD.Relation({tags: {foo: 'bar'}}).tags).to.eql({foo: 'bar'});
+ });
+});
diff --git a/test/spec/graph/way.js b/test/spec/graph/way.js
index 1293c5b50..022893d31 100644
--- a/test/spec/graph/way.js
+++ b/test/spec/graph/way.js
@@ -1,24 +1,77 @@
-describe('Way', function() {
- describe('#isClosed', function() {
- it('is not closed with two distinct nodes', function() {
- var open_way = { type: 'way', nodes: [{id: 'n1'}, {id: 'n2'}] };
- expect(iD.Way.isClosed(open_way)).to.equal(false);
+describe('iD.Way', function() {
+ if (iD.debug) {
+ it("freezes nodes", function () {
+ expect(Object.isFrozen(iD.Way().nodes)).to.be.true;
});
- it('is not closed with a node loop', function() {
- var closed_way = { type: 'way', nodes: [{id: 'n1'}, {id: 'n2'}, {id: 'n1'}] };
- expect(iD.Way.isClosed(closed_way)).to.equal(true);
+ }
+
+ it("returns a way", function () {
+ expect(iD.Way()).to.be.an.instanceOf(iD.Way);
+ expect(iD.Way().type).to.equal("way");
+ });
+
+ it("returns a created Entity if no ID is specified", function () {
+ expect(iD.Way().created()).to.be.ok;
+ });
+
+ it("returns an unmodified Entity if ID is specified", function () {
+ expect(iD.Way({id: 'w1234'}).created()).not.to.be.ok;
+ expect(iD.Way({id: 'w1234'}).modified()).not.to.be.ok;
+ });
+
+ it("defaults nodes to an empty array", function () {
+ expect(iD.Way().nodes).to.eql([]);
+ });
+
+ it("sets nodes as specified", function () {
+ expect(iD.Way({nodes: ["n-1"]}).nodes).to.eql(["n-1"]);
+ });
+
+ it("defaults tags to an empty object", function () {
+ expect(iD.Way().tags).to.eql({});
+ });
+
+ it("sets tags as specified", function () {
+ expect(iD.Way({tags: {foo: 'bar'}}).tags).to.eql({foo: 'bar'});
+ });
+
+ describe("#intersects", function () {
+ it("returns true for a way with a node within the given extent", function () {
+ var node = iD.Node({loc: [0, 0]}),
+ way = iD.Way({nodes: [node.id]}),
+ graph = iD.Graph([node, way]);
+ expect(way.intersects([[-180, 90], [180, -90]], graph)).to.equal(true);
+ });
+
+ it("returns false for way with no nodes within the given extent", function () {
+ var node = iD.Node({loc: [0, 0]}),
+ way = iD.Way({nodes: [node.id]}),
+ graph = iD.Graph([node, way]);
+ expect(way.intersects([[100, 90], [180, -90]], graph)).to.equal(false);
+ });
+ });
+
+ describe('#isClosed', function() {
+ it('returns false when the way ends are not equal', function() {
+ expect(iD.Way({nodes: ['n1', 'n2']}).isClosed()).to.equal(false);
+ });
+
+ it('returns true when the way ends are equal', function() {
+ expect(iD.Way({nodes: ['n1', 'n2', 'n1']}).isClosed()).to.equal(true);
});
});
describe('#isOneWay', function() {
- it('is not oneway without any tags', function() {
- expect(iD.Way.isOneWay(iD.Way())).to.eql(false);
+ it('returns false when the way has no tags', function() {
+ expect(iD.Way().isOneWay()).to.eql(false);
});
- it('is not oneway oneway=no', function() {
- expect(iD.Way.isOneWay(iD.Way({ tags: { oneway: 'no' } }))).to.eql(false);
+
+ it('returns false when the way has tag oneway=no', function() {
+ expect(iD.Way({tags: { oneway: 'no' }}).isOneWay()).to.equal(false);
});
- it('is oneway oneway=yes', function() {
- expect(iD.Way.isOneWay(iD.Way({ tags: { oneway: 'yes' } }))).to.eql(true);
+
+ it('returns true when the way has tag oneway=yes', function() {
+ expect(iD.Way({tags: { oneway: 'yes' }}).isOneWay()).to.equal(true);
});
});
});