Split out and test Way

This commit is contained in:
Tom MacWright
2012-10-16 16:40:54 -04:00
parent a9fa9daf1b
commit 2c9b377b5e
4 changed files with 144 additions and 316 deletions

View File

@@ -1,178 +1,12 @@
// iD/Entity.js
// Entity classes for iD
/*
define(['dojo/_base/declare','dojo/_base/array','dojo/_base/lang',
'iD/actions/AddNodeToWayAction','iD/actions/MoveNodeAction'
], function(declare,array,lang){
// ----------------------------------------------------------------------
// Entity base class
declare("iD.Entity", null, {
connection: null,
id: NaN,
loaded: false,
tags: null,
entityType: '',
parents: null,
modified: false,
deleted: false,
MAINKEYS: ['highway','amenity','railway','waterway'],
constructor:function() {
// summary: The base class for an entity (way, node or relation).
this.tags={};
this.parents=new Hashtable();
},
isType:function(type) {
// summary: Is this entity of the specified type ('node','way','relation')?
return this.entityType==type; // Boolean
},
toString:function() {
return this.entityType+"."+this.id;
},
// --------------------------------
// Provoke redraw and other changes
refresh:function() {
// summary: Ask the connection to provoke redraw and other changes.
this.connection.refreshEntity(this);
},
// ---------------
// Clean and dirty
_markClean:function() {
// summary: Mark entity as clean. Should only be called from UndoableEntityAction.
this.modified=false;
},
_markDirty:function() {
// summary: Mark entity as dirty. Should only be called from UndoableEntityAction.
this.modified=true;
},
isDirty:function() {
// summary: Is the entity dirty?
return this.modified; // Boolean
},
// --------
// Deletion
setDeletedState:function(isDeleted) {
// summary: Mark entity as deleted or not.
this.deleted=isDeleted;
},
// -------------------------------------
// Bounding box check (to be overridden)
within:function(left,right,top,bottom) {
// summary: Is the entity within the specified bbox?
return !this.deleted; // Boolean
},
// -------------
// Tag functions
getTagsHash:function() {
// summary: Tag getter.
// returns: The tags hash (reference to the actual object property, not a copy).
return this.tags; // Object
},
numTags:function() {
// summary: Count how many tags this entity has.
var c=0;
for (var i in this.tags) { c++; }
return c; // int
},
friendlyName:function() {
// summary: Rough-and-ready function to return a human-friendly name
// for the object. Really just a placeholder for something better.
// returns: A string such as 'river' or 'Fred's House'.
if (this.numTags()===0) { return ''; }
var n=[];
if (this.tags.name) { n.push(this.tags.name); }
if (this.tags.ref) { n.push(this.tags.ref); }
if (n.length===0) {
for (var i=0; i<this.MAINKEYS.length; i++) {
if (this.tags[this.MAINKEYS[i]]) { n.push(this.tags[this.MAINKEYS[i]]); break; }
}
}
return n.length===0 ? 'unknown' : n.join('; '); // String
},
// ---------------
// Parent-handling
addParent:function(entity) {
// summary: Record a parent (a relation or way which contains this entity).
this.parents.put(entity,true);
},
removeParent:function(entity) {
// summary: Remove a parent (e.g. when node removed from a way).
this.parents.remove(_entity);
},
hasParent:function(entity) {
// summary: Does this entity have the specified parent (e.g. is it in a certain relation)?
return this.parents.containsKey(entity); // Boolean
},
parentObjects:function() {
// summary: List of all parents of this entity.
return this.parents.keys(); // Boolean
},
hasParentWays:function() {
// summary: Does this entity have any parents which are ways?
var p=this.parentObjects();
for (var i in p) {
if (p[i].entityType=='way') { return true; }
}
return false; // Boolean
},
parentWays:function() {
// summary: Return an array of all ways that this entity is a member of.
return this._parentObjectsOfClass('way'); // Array
},
parentRelations:function() {
// summary: Return an array of all relations that this entity is a member of.
return this._parentObjectsOfClass('relation'); // Array
},
_parentObjectsOfClass:function(_class) {
var p=this.parentObjects(), c=[];
for (var i in p) {
if (p[i].entityType==_class) { c.push(p[i]); }
}
return c;
}
// Halcyon also implements:
// removeFromParents()
// hasParents()
// findParentRelationsOfType(type,role)
// getRelationMemberships()
// countParentObjects(within)
// memberships()
});
// ----------------------------------------------------------------------
// End of module
});
*/
if (typeof iD === 'undefined') iD = {};
iD.Entity = function() {
this.tags = {};
this.parents = new Hashtable();
this.connection = null;
this.id =  NaN;
this.loaded = false;
this.tags = null;
this.id = NaN;
this.loaded = false;
this.entityType = '';
this.parents = null;
this.modified = false;
@@ -187,7 +21,7 @@ iD.Entity.prototype = {
},
toString:function() {
return this.entityType+"."+this.id;
return this.entityType + " . " + this.id;
},
// --------------------------------
@@ -207,18 +41,9 @@ iD.Entity.prototype = {
// -------------
// Tag functions
getTagsHash:function() {
// summary: Tag getter.
// returns: The tags hash (reference to the actual object property, not a copy).
return this.tags; // Object
},
numTags:function() {
// summary: Count how many tags this entity has.
var c=0;
for (var i in this.tags) { c++; }
return c; // int
// summary: Count how many tags this entity has.
return Object.keys(this.tags).length;
},
friendlyName:function() {

View File

@@ -1,153 +1,139 @@
// iD/Way.js
define(['dojo/_base/declare','dojo/_base/array','dojo/_base/lang',
'iD/Entity','iD/actions/AddNodeToWayAction','iD/actions/MoveNodeAction'
], function(declare,array,lang){
if (typeof iD === 'undefined') iD = {};
iD.Way = function(conn, id, nodes, tags, loaded) {
// summary: An OSM way.
this.connection = conn;
this.entityType = 'way';
this.id = id;
this.nodes = nodes || [];
this.tags = tags || {};
this.loaded = (loaded === undefined) ? true : loaded;
this.modified = this.id < 0;
_.each(nodes, _.bind(function(node) {
node.addParent(this);
}, this));
this._calculateBbox();
};
// ----------------------------------------------------------------------
// Way class
iD.Way.prototype = {
length:function() {
// summary: Return the number of nodes in the way.
return this.nodes.length;
},
declare("iD.Way", [iD.Entity], {
nodes: null,
entityType: "way",
edgel: NaN,
edger: NaN,
edget: NaN,
edgeb: NaN,
isClosed:function() {
// summary: Is this a closed way (first and last nodes the same)?
return this.nodes[this.nodes.length-1]==this.nodes[0]; // Boolean
},
constructor:function(conn,id,nodes,tags,loaded) {
// summary: An OSM way.
this.connection=conn;
this.id=Number(id);
this.nodes=nodes;
this.tags=tags;
this.loaded=(loaded==undefined) ? true : loaded;
this.modified=this.id<0;
_.each(nodes, _.bind(function(node) {
node.addParent(this);
}, this));
this._calculateBbox();
},
length:function() {
// summary: Return the number of nodes in the way.
return this.nodes.length; // int
},
isClosed:function() {
// summary: Is this a closed way (first and last nodes the same)?
return this.nodes[this.nodes.length-1]==this.nodes[0]; // Boolean
},
isType:function(_type) {
// summary: Is this a 'way' (always true), an 'area' (closed) or a 'line' (unclosed)?
switch (_type) {
case 'way': return true;
case 'area': return this.isClosed;
case 'line': return !(this.isClosed);
}
return false; // Boolean
},
isType:function(_type) {
// summary: Is this a 'way' (always true), an 'area' (closed) or a 'line' (unclosed)?
switch (_type) {
case 'way': return true;
case 'area': return this.isClosed;
case 'line': return !(this.isClosed);
}
return false; // Boolean
},
getNode:function(index) {
// summary: Return the node at the given position.
return this.nodes[index]; // iD.Node
},
getFirstNode:function() {
// summary: Return the first node in the way.
return this.nodes[0]; // iD.Node
},
getLastNode:function() {
// summary: Return the last node in the way.
return this.nodes[this.nodes.length-1]; // iD.Node
},
getNode:function(index) {
// summary: Return the node at the given position.
return this.nodes[index]; // iD.Node
},
getFirstNode:function() {
// summary: Return the first node in the way.
return this.nodes[0]; // iD.Node
},
getLastNode:function() {
// summary: Return the last node in the way.
return this.nodes[this.nodes.length-1]; // iD.Node
},
// ---------------------
// Bounding-box handling
// ---------------------
// Bounding-box handling
within:function(left,right,top,bottom) {
if (!this.edgel ||
(this.edgel<left && this.edger<left ) ||
(this.edgel>right && this.edger>right ) ||
(this.edgeb<bottom && this.edget<bottom) ||
(this.edgeb>top && this.edgeb>top ) || this.deleted) { return false; }
return true;
},
within:function(left,right,top,bottom) {
if (!this.edgel ||
(this.edgel<left && this.edger<left ) ||
(this.edgel>right && this.edger>right ) ||
(this.edgeb<bottom && this.edget<bottom) ||
(this.edgeb>top && this.edgeb>top ) || this.deleted) { return false; }
return true;
},
_calculateBbox:function() {
this.edgel = 999999; this.edger = -999999;
this.edgeb = 999999; this.edget = -999999;
for (var i in this.nodes) { this.expandBbox(this.nodes[i]); }
},
_calculateBbox:function() {
this.edgel=999999; this.edger=-999999;
this.edgeb=999999; this.edget=-999999;
for (var i in this.nodes) { this.expandBbox(this.nodes[i]); }
},
expandBbox:function(node) {
// summary: Enlarge the way's bounding box to make sure it includes the co-ordinates of a supplied node.
this.edgel=Math.min(this.edgel,node.lon);
this.edger=Math.max(this.edger,node.lon);
this.edgeb=Math.min(this.edgeb,node.lat);
this.edget=Math.max(this.edget,node.lat);
},
expandBbox:function(node) {
// summary: Enlarge the way's bounding box to make sure it
// includes the co-ordinates of a supplied node.
this.edgel=Math.min(this.edgel,node.lon);
this.edger=Math.max(this.edger,node.lon);
this.edgeb=Math.min(this.edgeb,node.lat);
this.edget=Math.max(this.edget,node.lat);
},
// --------------
// Action callers
// --------------
// Action callers
doAppendNode:function(node, performAction) {
// summary: Add a node to the end of the way, using an undo stack.
// returns: New length of the way.
if (node!=this.getLastNode()) performAction(new iD.actions.AddNodeToWayAction(this, node, this.nodes, -1, true));
return this.nodes.length + 1; // int
},
doAppendNode:function(node, performAction) {
// summary: Add a node to the end of the way, using an undo stack.
// returns: New length of the way.
if (node!=this.getLastNode()) performAction(new iD.actions.AddNodeToWayAction(this, node, this.nodes, -1, true));
return this.nodes.length + 1; // int
},
doPrependNode:function(node, performAction) {
// summary: Add a node to the start of the way, using an undo stack.
// returns: New length of the way.
if (node!=this.getFirstNode()) performAction(new iD.actions.AddNodeToWayAction(this, node, this.nodes, 0, true));
return this.nodes.length + 1; // int
},
doPrependNode:function(node, performAction) {
// summary: Add a node to the start of the way, using an undo stack.
// returns: New length of the way.
if (node!=this.getFirstNode()) performAction(new iD.actions.AddNodeToWayAction(this, node, this.nodes, 0, true));
return this.nodes.length + 1; // int
},
doInsertNode:function(index, node, performAction) {
// summary: Add a node at a given index within the way, using an undo stack.
if (index>0 && this.getNode(index-1)==node) return;
if (index<this.nodes.length-1 && this.getNode(index)==node) return;
performAction(new iD.actions.AddNodeToWayAction(this, node, this.nodes, index, false));
},
doInsertNodeAtClosestPosition:function(newNode, isSnap, performAction) {
// summary: Add a node into whichever segment of the way is nearest, using an undo stack.
// isSnap: Boolean Should the node position be snapped to be exactly on the segment?
// returns: The index at which the node was inserted.
var closestProportion = 1;
var newIndex = 0;
var snapped;
doInsertNode:function(index, node, performAction) {
// summary: Add a node at a given index within the way, using an undo stack.
if (index > 0 && this.getNode(index - 1)==node) return;
if (index < this.nodes.length - 1 && this.getNode(index)==node) return;
performAction(new iD.actions.AddNodeToWayAction(this, node, this.nodes, index, false));
},
for (var i=0; i<this.nodes.length-1; i++) {
var node1 = this.getNode(i);
var node2 = this.getNode(i+1);
var directDist = this._pythagoras(node1, node2);
var viaNewDist = this._pythagoras(node1, newNode) + this._pythagoras(node2, newNode);
var proportion = Math.abs(viaNewDist/directDist - 1);
if (proportion < closestProportion) {
newIndex = i+1;
closestProportion = proportion;
snapped = this._calculateSnappedPoint(node1, node2, newNode);
}
}
doInsertNodeAtClosestPosition:function(newNode, isSnap, performAction) {
// summary: Add a node into whichever segment of the way is nearest, using an undo stack.
// isSnap: Boolean Should the node position be snapped to be exactly on the segment?
// returns: The index at which the node was inserted.
var closestProportion = 1,
newIndex = 0,
snapped;
// splice in new node
if (isSnap) { newNode.doSetLonLatp(snapped.x, snapped.y, performAction); }
this.doInsertNode(newIndex, newNode, performAction);
return newIndex; // int
},
_pythagoras:function(node1, node2) { return (Math.sqrt(Math.pow(node1.lon-node2.lon,2)+Math.pow(node1.latp-node2.latp,2))); },
_calculateSnappedPoint:function(node1, node2, newNode) {
var w = node2.lon - node1.lon;
var h = node2.latp - node1.latp;
var u = ((newNode.lon-node1.lon) * w + (newNode.latp-node1.latp) * h) / (w*w + h*h);
return { x: node1.lon + u*w, y: node1.latp + u*h };
},
});
for (var i = 0; i < this.nodes.length - 1; i++) {
var node1 = this.getNode(i);
var node2 = this.getNode(i+1);
var directDist = this._pythagoras(node1, node2);
var viaNewDist = this._pythagoras(node1, newNode) +
this._pythagoras(node2, newNode);
var proportion = Math.abs(viaNewDist/directDist - 1);
if (proportion < closestProportion) {
newIndex = i+1;
closestProportion = proportion;
snapped = this._calculateSnappedPoint(node1, node2, newNode);
}
}
// ----------------------------------------------------------------------
// End of module
});
// splice in new node
if (isSnap) { newNode.doSetLonLatp(snapped.x, snapped.y, performAction); }
this.doInsertNode(newIndex, newNode, performAction);
return newIndex; // int
},
_pythagoras:function(node1, node2) { return (Math.sqrt(Math.pow(node1.lon-node2.lon,2)+Math.pow(node1.latp-node2.latp,2))); },
_calculateSnappedPoint:function(node1, node2, newNode) {
var w = node2.lon - node1.lon;
var h = node2.latp - node1.latp;
var u = ((newNode.lon-node1.lon) * w + (newNode.latp-node1.latp) * h) / (w*w + h*h);
return { x: node1.lon + u*w, y: node1.latp + u*h };
}
};

View File

@@ -14,10 +14,12 @@
<script type="text/javascript" src="../js/lib/jshashtable.js"></script>
<script type="text/javascript" src="../js/iD/Node.js"></script>
<script type="text/javascript" src="../js/iD/Entity.js"></script>
<script type="text/javascript" src="../js/iD/Way.js"></script>
<!-- include spec files here... -->
<script type="text/javascript" src="spec/Node.js"></script>
<script type="text/javascript" src="spec/Entity.js"></script>
<script type="text/javascript" src="spec/Way.js"></script>
<script type="text/javascript">
(function() {

15
test/spec/Way.js Normal file
View File

@@ -0,0 +1,15 @@
describe('Way', function() {
var way;
beforeEach(function() {
way = new iD.Way();
});
it('is a way', function() {
expect(way.entityType).toEqual('way');
});
it('has zero nodes by default', function() {
expect(way.length()).toEqual(0);
});
});