From 5fe22be7a05678781e0a0d654e0c4ff8151e4e33 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Fri, 28 Dec 2012 18:42:21 -0800 Subject: [PATCH] Introduce real Entity subclasses --- API.md | 2 +- js/id/graph/entity.js | 135 +++++++++++++++++++++--------------- js/id/renderer/map.js | 4 +- test/index.html | 4 +- test/index_packaged.html | 4 +- test/spec/graph/entity.js | 134 +++-------------------------------- test/spec/graph/node.js | 33 +++++++++ test/spec/graph/relation.js | 37 ++++++++++ test/spec/graph/way.js | 81 ++++++++++++++++++---- 9 files changed, 237 insertions(+), 197 deletions(-) create mode 100644 test/spec/graph/node.js create mode 100644 test/spec/graph/relation.js 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); }); }); });