Introduce real Entity subclasses

This commit is contained in:
John Firebaugh
2012-12-28 18:42:21 -08:00
parent 167556ab02
commit 5fe22be7a0
9 changed files with 237 additions and 197 deletions

2
API.md
View File

@@ -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

View File

@@ -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: []
});

View File

@@ -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();

View File

@@ -111,8 +111,10 @@
<script src="spec/graph/graph.js"></script>
<script src="spec/graph/entity.js"></script>
<script src="spec/graph/history.js"></script>
<script src="spec/graph/node.js"></script>
<script src="spec/graph/way.js"></script>
<script src="spec/graph/relation.js"></script>
<script src="spec/graph/history.js"></script>
<script src="spec/modes/add_point.js"></script>

View File

@@ -43,8 +43,10 @@
<script src="spec/graph/graph.js"></script>
<script src="spec/graph/entity.js"></script>
<script src="spec/graph/history.js"></script>
<script src="spec/graph/node.js"></script>
<script src="spec/graph/way.js"></script>
<script src="spec/graph/relation.js"></script>
<script src="spec/graph/history.js"></script>
<script src="spec/modes/add_point.js"></script>

View File

@@ -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'});
});
});

33
test/spec/graph/node.js Normal file
View File

@@ -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);
});
});
});

View File

@@ -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'});
});
});

View File

@@ -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);
});
});
});