mirror of
https://github.com/FoggedLens/iD.git
synced 2026-02-12 16:52:50 +00:00
Split out and test Way
This commit is contained in:
185
js/iD/Entity.js
185
js/iD/Entity.js
@@ -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() {
|
||||
|
||||
258
js/iD/Way.js
258
js/iD/Way.js
@@ -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 };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
15
test/spec/Way.js
Normal 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user