diff --git a/css/app.css b/css/app.css
index 0ba223a99..4c149843d 100644
--- a/css/app.css
+++ b/css/app.css
@@ -246,7 +246,6 @@ form.hide {
button {
line-height:20px;
- position: relative;
border:0;
color:#222;
background: white;
@@ -346,16 +345,16 @@ button.save .count {
button.save.has-count .count {
display: block;
position: absolute;
- left: 115%;
top: 0;
bottom: 0;
- background: rgba(255,255,255,.5);
+ background: rgba(255, 255, 255, .5);
color: #333;
padding: 10px;
height: 30px;
line-height: 12px;
border-radius: 4px;
margin: auto;
+ margin-left: 8.3333%;
}
button.save.has-count .count::before {
@@ -929,50 +928,6 @@ div.typeahead a:first-child {
left:0px; right:0px; top:0px; bottom:0px;
}
-.commit-modal .user-info {
- display: inline-block;
-}
-
-.commit-modal .commit-info {
- margin-top: 10px;
-}
-
-.commit-modal .user-info img {
- float: left;
-}
-
-.commit-modal h3 small.count {
- margin-right: 10px;
- text-align: center;
- float: left;
- height: 12px;
- min-width: 12px;
- font-size:12px;
- line-height: 12px;
- border-radius:24px;
- padding:5px;
- background:#7092ff;
- color:#fff;
-}
-
-.commit-modal .changeset-list {
- overflow: auto;
- border:1px solid #ccc;
- background:#fff;
- max-height: 160px;
-}
-
-.commit-modal .warning-section .changeset-list {
- margin-right: 20px;
- overflow-x: visible;
-}
-
-.commit-section.modal-section {
- padding-bottom: 0;
-}
-
-.commit-section.modal-section:last-child { padding-bottom: 20px;}
-
.modal-section {
padding: 20px;
}
@@ -1007,6 +962,56 @@ div.typeahead a:first-child {
display:none;
}
+.loading-modal {
+ text-align: center;
+}
+
+/* Commit Modal
+------------------------------------------------------- */
+
+.commit-modal .user-info {
+ display: inline-block;
+}
+
+.commit-modal .commit-info {
+ margin-top: 10px;
+}
+
+.commit-modal .user-info img {
+ float: left;
+}
+
+.commit-modal h3 small.count {
+ margin-right: 10px;
+ text-align: center;
+ float: left;
+ height: 12px;
+ min-width: 12px;
+ font-size:12px;
+ line-height: 12px;
+ border-radius:24px;
+ padding:5px;
+ background:#7092ff;
+ color:#fff;
+}
+
+.commit-modal .changeset-list {
+ overflow: auto;
+ border:1px solid #ccc;
+ background:#fff;
+ max-height: 160px;
+}
+
+.commit-modal .warning-section .changeset-list button {
+ float: right;
+}
+
+.commit-section.modal-section {
+ padding-bottom: 0;
+}
+
+.commit-section.modal-section:last-child { padding-bottom: 20px;}
+
.changeset-list li {
border-top:1px solid #ccc;
padding:5px 10px;
@@ -1029,10 +1034,6 @@ div.typeahead a:first-child {
font:normal 12px/20px 'Helvetica Neue', Arial, sans-serif;
}
-.loading-modal {
- text-align: center;
-}
-
/* Success
------------------------------------------------------- */
a.success-action {
@@ -1051,7 +1052,8 @@ a.success-action {
text-align:center;
}
-.notice .notice-inner {
+.notice .zoom-to {
+ width:100%;
height: 40px;
border-radius: 5px;
line-height: 40px;
@@ -1060,22 +1062,27 @@ a.success-action {
opacity: 0.9;
}
-.notice .notice-inner .zoom-to {
- width:40px;
- height:40px;
+.notice .zoom-to:hover {
+ background: #bde5aa;
+}
+
+.notice .zoom-to .icon {
+ margin-top:10px;
margin-right:10px;
}
+.icon.zoom-in-invert {
+ background-position: -240px -40px;
+}
+
/* Tooltips
------------------------------------------------------- */
.tooltip {
- white-space: normal;
+ width: 200px;
position: absolute;
- left: 0; right: 0; margin: auto;
z-index: -1000;
height: 0;
- padding: 5px;
opacity: 0;
display: block;
}
@@ -1087,98 +1094,130 @@ a.success-action {
}
.tooltip.top {
- margin-top: -5px;
+ margin-top: -10px;
+ text-align: center;
}
.tooltip.right {
- margin-left: 5px;
+ margin-left: 10px;
+ text-align: left;
}
.tooltip.bottom {
- margin-top: 5px;
+ margin-top: 10px;
+ text-align: center;
}
.tooltip.left {
- margin-left: -5px;
+ margin-left: -10px;
+ text-align: right;
}
.tooltip-inner {
- text-align: left;
- width: 200px;
- font-size: 11px;
- font-weight: bold;
- line-height: 20px;
- padding: 5px 10px;
- color: #333;
- background-color: white;
- -webkit-border-radius: 4px;
- -moz-border-radius: 4px;
- border-radius: 4px;
+ color: #333;
+ display: inline-block;
+ padding: 5px 10px;
+ font-size: 11px;
+ font-weight: bold;
+ line-height: 20px;
+ background-color: white;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+
}
.tooltip-arrow {
- position: absolute;
- width: 0;
- height: 0;
- border-color: transparent;
- border-style: solid;
+ position: absolute;
+ width: 0;
+ height: 0;
+ border-color: transparent;
+ border-style: solid;
}
.tooltip.top .tooltip-arrow {
- bottom: 0;
- left: 50%;
- margin-left: -5px;
- border-top-color: white;
- border-width: 5px 5px 0;
+ bottom: -5px;
+ left: 50%;
+ margin-left: -5px;
+ border-top-color: white;
+ border-width: 5px 5px 0;
}
.tooltip.right .tooltip-arrow {
- top: 50%;
- left: 0;
- margin-top: -5px;
- border-right-color: white;
- border-width: 5px 5px 5px 0;
+ top: 50%;
+ left: -5px;
+ margin-top: -5px;
+ border-right-color: white;
+ border-width: 5px 5px 5px 0;
}
.tooltip.left .tooltip-arrow {
- top: 50%;
- right: 0;
- margin-top: -5px;
- border-left-color: white;
- border-width: 5px 0 5px 5px;
+ top: 50%;
+ right: 5px;
+ margin-top: -5px;
+ border-left-color: white;
+ border-width: 5px 0 5px 5px;
}
.tooltip.bottom .tooltip-arrow {
- top: 0;
- left: 50%;
- margin-left: -5px;
- border-bottom-color: white;
- border-width: 0 5px 5px;
+ top: -5px;
+ left: 50%;
+ margin-left: -5px;
+ border-bottom-color: white;
+ border-width: 0 5px 5px;
}
+.Browse .tooltip {
+ left: -20px !important; }
.Browse .tooltip .tooltip-arrow {
- left: 30px;
+ left: 60px;
+}
+
+.tooltip .keyhint-wrap {
+ padding: 5px 0 5px 0;
}
.tooltip .keyhint {
- float: right;
- background: #eee;
+ display: block;
+ color: #222;
font-size: 10px;
- padding: 0 4px;
- background:#aaa;
- color:#fff;
+ padding: 0px 7px;
+ text-transform: uppercase;
+ font-weight: bold;
+ display: inline-block;
border-radius: 2px;
- margin-left: 4px;
+ border: 1px solid #CCC;
+ position: relative;
+ z-index: 1;
+ text-align: left;
+ clear: both;
+}
+
+.tooltip .keyhint .keyhint-label{
+ display: inline-block;
+}
+
+.tooltip .keyhint::after {
+ content: "";
+ position: absolute;
+ border-radius: 2px;
+ height: 10px;
+ width: 100%;
+ z-index: 0;
+ bottom: -4px;
+ left: -1px;
+ border: 1px solid #CCC;
+ border-top: 0;
}
.tail {
- pointer-events:none;
- position: absolute;
- background: rgba(255, 255, 255, 0.7);
- max-width: 250px;
- margin-top: -15px;
- padding: 5px;
- -webkit-border-radius: 4px;
+ pointer-events:none;
+ position: absolute;
+ background: rgba(255, 255, 255, 0.7);
+ max-width: 250px;
+ margin-top: -15px;
+ padding: 5px;
+ -webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
}
diff --git a/img/source/sprite.svg b/img/source/sprite.svg
index 4cc5abb47..01a28e513 100644
--- a/img/source/sprite.svg
+++ b/img/source/sprite.svg
@@ -39,8 +39,8 @@
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="4"
- inkscape:cx="230.7911"
- inkscape:cy="190.13176"
+ inkscape:cx="332.2911"
+ inkscape:cy="175.13176"
inkscape:document-units="px"
inkscape:current-layer="layer12"
showgrid="false"
@@ -733,7 +733,7 @@
inkscape:export-xdpi="90"
inkscape:export-ydpi="90" />
diff --git a/img/sprite.png b/img/sprite.png
index b0f45f2cd..5922c06c1 100644
Binary files a/img/sprite.png and b/img/sprite.png differ
diff --git a/index.html b/index.html
index 438b54e90..9606fe7b6 100644
--- a/index.html
+++ b/index.html
@@ -75,6 +75,7 @@
+
diff --git a/js/id/actions/add_midpoint.js b/js/id/actions/add_midpoint.js
new file mode 100644
index 000000000..c0cb97f59
--- /dev/null
+++ b/js/id/actions/add_midpoint.js
@@ -0,0 +1,11 @@
+iD.actions.AddMidpoint = function(midpoint, node) {
+ return function(graph) {
+ graph = graph.replace(node.move(midpoint.loc));
+
+ midpoint.ways.forEach(function(way) {
+ graph = graph.replace(graph.entity(way.id).addNode(node.id, way.index));
+ });
+
+ return graph;
+ };
+};
diff --git a/js/id/behavior/add_way.js b/js/id/behavior/add_way.js
index 021377890..fa038d201 100644
--- a/js/id/behavior/add_way.js
+++ b/js/id/behavior/add_way.js
@@ -1,25 +1,17 @@
iD.behavior.AddWay = function(mode) {
var map = mode.map,
- history = mode.history,
controller = mode.controller,
- event = d3.dispatch('startFromNode', 'startFromWay', 'start'),
- draw;
-
- function add(datum) {
- if (datum.type === 'node') {
- event.startFromNode(datum);
- } else if (datum.type === 'way') {
- var choice = iD.geo.chooseIndex(datum, d3.mouse(map.surface.node()), map);
- event.startFromWay(datum, choice.loc, choice.index);
- } else if (datum.midpoint) {
- var way = history.graph().entity(datum.way);
- event.startFromWay(way, datum.loc, datum.index);
- } else {
- event.start(map.mouseCoordinates());
- }
- }
+ event = d3.dispatch('start', 'startFromWay', 'startFromNode', 'startFromMidpoint'),
+ draw = iD.behavior.Draw(map);
var addWay = function(surface) {
+ draw.on('click', event.start)
+ .on('clickWay', event.startFromWay)
+ .on('clickNode', event.startFromNode)
+ .on('clickMidpoint', event.startFromMidpoint)
+ .on('cancel', addWay.cancel)
+ .on('finish', addWay.cancel);
+
map.fastEnable(false)
.minzoom(16)
.dblclickEnable(false);
@@ -43,10 +35,5 @@ iD.behavior.AddWay = function(mode) {
controller.exit();
};
- draw = iD.behavior.Draw()
- .on('add', add)
- .on('cancel', addWay.cancel)
- .on('finish', addWay.cancel);
-
return d3.rebind(addWay, event, 'on');
};
diff --git a/js/id/behavior/drag_midpoint.js b/js/id/behavior/drag_midpoint.js
index 9f73a7115..bd1a40bd9 100644
--- a/js/id/behavior/drag_midpoint.js
+++ b/js/id/behavior/drag_midpoint.js
@@ -1,29 +1,21 @@
iD.behavior.DragMidpoint = function(mode) {
var history = mode.history,
- projection = mode.map.projection,
- behavior = iD.behavior.drag()
+ projection = mode.map.projection;
+
+ var behavior = iD.behavior.drag()
.delegate(".midpoint")
.origin(function(d) {
return projection(d.loc);
})
.on('start', function(d) {
- var w, nds;
- d.node = iD.Node({loc: d.loc});
- var args = [iD.actions.AddNode(d.node)];
- for (var i = 0; i < d.ways.length; i++) {
- w = d.ways[i], nds = w.nodes;
- for (var j = 0; j < nds.length; j++) {
- if ((nds[j] === d.nodes[0] && nds[j + 1] === d.nodes[1]) ||
- (nds[j] === d.nodes[1] && nds[j + 1] === d.nodes[0])) {
- args.push(iD.actions.AddWayNode(w.id, d.node.id, j + 1));
- }
- }
- }
- history.perform.apply(history, args);
- var node = d3.selectAll('.node.vertex')
- .filter(function(data) { return data.id === d.node.id; });
- behavior.target(node.node(), node.datum());
+ var node = iD.Node();
+ history.perform(iD.actions.AddMidpoint(d, node));
+
+ var vertex = d3.selectAll('.vertex')
+ .filter(function(data) { return data.id === node.id; });
+
+ behavior.target(vertex.node(), vertex.datum());
})
.on('move', function(d) {
d3.event.sourceEvent.stopPropagation();
@@ -33,7 +25,8 @@ iD.behavior.DragMidpoint = function(mode) {
.on('end', function() {
history.replace(
iD.actions.Noop(),
- 'added a node to a way');
+ 'Added a node to a way.');
});
+
return behavior;
};
diff --git a/js/id/behavior/draw.js b/js/id/behavior/draw.js
index 4d0153505..bce4470b6 100644
--- a/js/id/behavior/draw.js
+++ b/js/id/behavior/draw.js
@@ -1,5 +1,5 @@
-iD.behavior.Draw = function () {
- var event = d3.dispatch('move', 'add', 'undo', 'cancel', 'finish'),
+iD.behavior.Draw = function(map) {
+ var event = d3.dispatch('move', 'click', 'clickWay', 'clickNode', 'clickMidpoint', 'undo', 'cancel', 'finish'),
keybinding = d3.keybinding('draw'),
down, surface, hover;
@@ -26,7 +26,20 @@ iD.behavior.Draw = function () {
}
function click() {
- event.add(datum());
+ var d = datum();
+ if (d.type === 'way') {
+ var choice = iD.geo.chooseIndex(d, d3.mouse(map.surface.node()), map);
+ event.clickWay(d, choice.loc, choice.index);
+
+ } else if (d.type === 'node') {
+ event.clickNode(d);
+
+ } else if (d.type === 'midpoint') {
+ event.clickMidpoint(d);
+
+ } else {
+ event.click(map.mouseCoordinates());
+ }
}
function keydown() {
diff --git a/js/id/behavior/draw_way.js b/js/id/behavior/draw_way.js
index 63526df7a..21c13d2fa 100644
--- a/js/id/behavior/draw_way.js
+++ b/js/id/behavior/draw_way.js
@@ -1,11 +1,11 @@
-iD.behavior.DrawWay = function(wayId, headId, tailId, index, mode, baseGraph) {
+iD.behavior.DrawWay = function(wayId, index, mode, baseGraph) {
var map = mode.map,
history = mode.history,
controller = mode.controller,
- event = d3.dispatch('add', 'addHead', 'addTail', 'addNode', 'addWay'),
way = mode.history.graph().entity(wayId),
finished = false,
- draw;
+ annotation = 'Added to a way.',
+ draw = iD.behavior.Draw(map);
var node = iD.Node({loc: map.mouseCoordinates()}),
nodeId = node.id;
@@ -17,7 +17,7 @@ iD.behavior.DrawWay = function(wayId, headId, tailId, index, mode, baseGraph) {
function move(datum) {
var loc = map.mouseCoordinates();
- if (datum.type === 'node' || datum.midpoint) {
+ if (datum.type === 'node' || datum.type === 'midpoint') {
loc = datum.loc;
} else if (datum.type === 'way') {
loc = iD.geo.chooseIndex(datum, d3.mouse(map.surface.node()), map).loc;
@@ -26,29 +26,20 @@ iD.behavior.DrawWay = function(wayId, headId, tailId, index, mode, baseGraph) {
history.replace(iD.actions.MoveNode(nodeId, loc));
}
- function add(datum) {
- if (datum.id === headId) {
- event.addHead(datum);
- } else if (datum.id === tailId) {
- event.addTail(datum);
- } else if (datum.type === 'node' && datum.id !== nodeId) {
- event.addNode(datum);
- } else if (datum.type === 'way') {
- var choice = iD.geo.chooseIndex(datum, d3.mouse(map.surface.node()), map);
- event.addWay(datum, choice.loc, choice.index);
- } else if (datum.midpoint) {
- var way = history.graph().entity(datum.way);
- event.addWay(way, datum.loc, datum.index);
- } else {
- event.add(map.mouseCoordinates());
- }
- }
-
function undone() {
controller.enter(iD.modes.Browse());
}
var drawWay = function(surface) {
+ draw.on('move', move)
+ .on('click', drawWay.add)
+ .on('clickWay', drawWay.addWay)
+ .on('clickNode', drawWay.addNode)
+ .on('clickMidpoint', drawWay.addMidpoint)
+ .on('undo', history.undo)
+ .on('cancel', drawWay.cancel)
+ .on('finish', drawWay.finish);
+
map.fastEnable(false)
.minzoom(16)
.dblclickEnable(false);
@@ -80,6 +71,12 @@ iD.behavior.DrawWay = function(wayId, headId, tailId, index, mode, baseGraph) {
history.on('undone.draw', null);
};
+ drawWay.annotation = function(_) {
+ if (!arguments.length) return annotation;
+ annotation = _;
+ return drawWay;
+ };
+
function ReplaceTemporaryNode(newNode) {
return function(graph) {
return graph
@@ -88,10 +85,13 @@ iD.behavior.DrawWay = function(wayId, headId, tailId, index, mode, baseGraph) {
}
}
- // Connect the way to an existing node and continue drawing.
- drawWay.addNode = function(node, annotation) {
- history.perform(
- ReplaceTemporaryNode(node),
+ // Accept the current position of the temporary node and continue drawing.
+ drawWay.add = function(loc) {
+ var newNode = iD.Node({loc: loc});
+
+ history.replace(
+ iD.actions.AddNode(newNode),
+ ReplaceTemporaryNode(newNode),
annotation);
finished = true;
@@ -99,7 +99,7 @@ iD.behavior.DrawWay = function(wayId, headId, tailId, index, mode, baseGraph) {
};
// Connect the way to an existing way.
- drawWay.addWay = function(way, loc, wayIndex, annotation) {
+ drawWay.addWay = function(way, loc, wayIndex) {
var newNode = iD.Node({loc: loc});
history.perform(
@@ -112,13 +112,23 @@ iD.behavior.DrawWay = function(wayId, headId, tailId, index, mode, baseGraph) {
controller.enter(mode);
};
- // Accept the current position of the temporary node and continue drawing.
- drawWay.add = function(loc, annotation) {
- var newNode = iD.Node({loc: loc});
+ // Connect the way to an existing node and continue drawing.
+ drawWay.addNode = function(node) {
+ history.perform(
+ ReplaceTemporaryNode(node),
+ annotation);
- history.replace(
- iD.actions.AddNode(newNode),
- ReplaceTemporaryNode(newNode),
+ finished = true;
+ controller.enter(mode);
+ };
+
+ // Add a midpoint, connect the way to it, and continue drawing.
+ drawWay.addMidpoint = function(midpoint) {
+ var node = iD.Node();
+
+ history.perform(
+ iD.actions.AddMidpoint(midpoint, node),
+ ReplaceTemporaryNode(node),
annotation);
finished = true;
@@ -143,18 +153,11 @@ iD.behavior.DrawWay = function(wayId, headId, tailId, index, mode, baseGraph) {
drawWay.cancel = function() {
history.perform(
d3.functor(baseGraph),
- 'cancelled drawing');
+ 'Cancelled drawing.');
finished = true;
controller.enter(iD.modes.Browse());
};
- draw = iD.behavior.Draw()
- .on('move', move)
- .on('add', add)
- .on('undo', history.undo)
- .on('cancel', drawWay.cancel)
- .on('finish', drawWay.finish);
-
return d3.rebind(drawWay, event, 'on');
};
diff --git a/js/id/connection.js b/js/id/connection.js
index 07cd332b7..d91d3388d 100644
--- a/js/id/connection.js
+++ b/js/id/connection.js
@@ -125,7 +125,7 @@ iD.Connection = function() {
}
}
- return iD.Graph(entities);
+ return entities;
}
function authenticated() {
diff --git a/js/id/graph/graph.js b/js/id/graph/graph.js
index 120fa2fe7..5e865b9ec 100644
--- a/js/id/graph/graph.js
+++ b/js/id/graph/graph.js
@@ -1,19 +1,30 @@
-iD.Graph = function(entities, mutable) {
- if (!(this instanceof iD.Graph)) return new iD.Graph(entities, mutable);
+iD.Graph = function(other, mutable) {
+ if (!(this instanceof iD.Graph)) return new iD.Graph(other, mutable);
+
+ if (other instanceof iD.Graph) {
+ var base = other.base();
+ this.entities = _.assign(Object.create(base.entities), other.entities);
+ this._parentWays = _.assign(Object.create(base.parentWays), other._parentWays);
+ this._parentRels = _.assign(Object.create(base.parentRels), other._parentRels);
+ this.inherited = true;
- if (_.isArray(entities)) {
- this.entities = {};
- for (var i = 0; i < entities.length; i++) {
- this.entities[entities[i].id] = entities[i];
- }
} else {
- this.entities = entities || {};
+ if (_.isArray(other)) {
+ var entities = {};
+ for (var i = 0; i < other.length; i++) {
+ entities[other[i].id] = other[i];
+ }
+ other = entities;
+ }
+ this.entities = Object.create({});
+ this._parentWays = Object.create({});
+ this._parentRels = Object.create({});
+ this.rebase(other || {});
}
this.transients = {};
- this._parentWays = {};
- this._parentRels = {};
this._childNodes = {};
+ this.getEntity = _.bind(this.entity, this);
if (!mutable) {
this.freeze();
@@ -38,51 +49,21 @@ iD.Graph.prototype = {
},
parentWays: function(entity) {
- var ent, id, parents;
-
- if (!this._parentWays.calculated) {
- for (var i in this.entities) {
- ent = this.entities[i];
- if (ent && ent.type === 'way') {
- for (var j = 0; j < ent.nodes.length; j++) {
- id = ent.nodes[j];
- parents = this._parentWays[id] = this._parentWays[id] || [];
- if (parents.indexOf(ent) < 0) {
- parents.push(ent);
- }
- }
- }
- }
- this._parentWays.calculated = true;
- }
-
- return this._parentWays[entity.id] || [];
+ return _.map(this._parentWays[entity.id], this.getEntity);
},
isPoi: function(entity) {
- return this.parentWays(entity).length === 0;
+ var parentWays = this._parentWays[entity.id];
+ return !parentWays || parentWays.length === 0;
+ },
+
+ isShared: function(entity) {
+ var parentWays = this._parentWays[entity.id];
+ return parentWays && parentWays.length > 1;
},
parentRelations: function(entity) {
- var ent, id, parents;
-
- if (!this._parentRels.calculated) {
- for (var i in this.entities) {
- ent = this.entities[i];
- if (ent && ent.type === 'relation') {
- for (var j = 0; j < ent.members.length; j++) {
- id = ent.members[j].id;
- parents = this._parentRels[id] = this._parentRels[id] || [];
- if (parents.indexOf(ent) < 0) {
- parents.push(ent);
- }
- }
- }
- }
- this._parentRels.calculated = true;
- }
-
- return this._parentRels[entity.id] || [];
+ return _.map(this._parentRels[entity.id], this.getEntity);
},
childNodes: function(entity) {
@@ -97,30 +78,132 @@ iD.Graph.prototype = {
return (this._childNodes[entity.id] = nodes);
},
- merge: function(graph) {
- return this.update(function () {
- _.defaults(this.entities, graph.entities);
- });
+ base: function() {
+ return {
+ 'entities': iD.util.getPrototypeOf(this.entities),
+ 'parentWays': iD.util.getPrototypeOf(this._parentWays),
+ 'parentRels': iD.util.getPrototypeOf(this._parentRels)
+ };
+ },
+
+ // Unlike other graph methods, rebase mutates in place. This is because it
+ // is used only during the history operation that merges newly downloaded
+ // data into each state. To external consumers, it should appear as if the
+ // graph always contained the newly downloaded data.
+ rebase: function(entities) {
+ var base = this.base(),
+ i, k, child, id, keys;
+ // Merging of data only needed if graph is the base graph
+ if (!this.inherited) {
+ for (i in entities) {
+ if (!base.entities[i]) {
+ base.entities[i] = entities[i];
+ this._updateCalculated(undefined, entities[i],
+ base.parentWays, base.parentRels);
+ }
+ }
+ }
+
+ keys = Object.keys(this._parentWays);
+ for (i = 0; i < keys.length; i++) {
+ child = keys[i];
+ if (base.parentWays[child]) {
+ for (k = 0; k < base.parentWays[child].length; k++) {
+ id = base.parentWays[child][k];
+ if (this.entity(id) && !_.contains(this._parentWays[child], id)) {
+ this._parentWays[child].push(id);
+ }
+ }
+ }
+ }
+
+ keys = Object.keys(this._parentRels);
+ for (i = 0; i < keys.length; i++) {
+ child = keys[i];
+ if (base.parentRels[child]) {
+ for (k = 0; k < base.parentRels[child].length; k++) {
+ id = base.parentRels[child][k];
+ if (this.entity(id) && !_.contains(this._parentRels[child], id)) {
+ this._parentRels[child].push(id);
+ }
+ }
+ }
+ }
+ },
+
+ // Updates calculated properties (parentWays, parentRels) for the specified change
+ _updateCalculated: function(oldentity, entity, parentWays, parentRels) {
+
+ parentWays = parentWays || this._parentWays;
+ parentRels = parentRels || this._parentRels;
+
+ var type = entity && entity.type || oldentity && oldentity.type,
+ removed, added, ways, rels, i;
+
+
+ if (type === 'way') {
+
+ // Update parentWays
+ if (oldentity && entity) {
+ removed = _.difference(oldentity.nodes, entity.nodes);
+ added = _.difference(entity.nodes, oldentity.nodes);
+ } else if (oldentity) {
+ removed = oldentity.nodes;
+ added = [];
+ } else if (entity) {
+ removed = [];
+ added = entity.nodes;
+ }
+ for (i = 0; i < removed.length; i++) {
+ parentWays[removed[i]] = _.without(parentWays[removed[i]], oldentity.id);
+ }
+ for (i = 0; i < added.length; i++) {
+ ways = _.without(parentWays[added[i]], entity.id);
+ ways.push(entity.id);
+ parentWays[added[i]] = ways;
+ }
+ } else if (type === 'node') {
+
+ } else if (type === 'relation') {
+
+ // Update parentRels
+ if (oldentity && entity) {
+ removed = _.difference(oldentity.members, entity.members);
+ added = _.difference(entity.members, oldentity);
+ } else if (oldentity) {
+ removed = oldentity.members;
+ added = [];
+ } else if (entity) {
+ removed = [];
+ added = entity.members;
+ }
+ for (i = 0; i < removed.length; i++) {
+ parentRels[removed[i].id] = _.without(parentRels[removed[i].id], oldentity.id);
+ }
+ for (i = 0; i < added.length; i++) {
+ rels = _.without(parentRels[added[i].id], entity.id);
+ rels.push(entity.id);
+ parentRels[added[i].id] = rels;
+ }
+ }
},
replace: function(entity) {
return this.update(function () {
+ this._updateCalculated(this.entities[entity.id], entity);
this.entities[entity.id] = entity;
});
},
remove: function(entity) {
return this.update(function () {
- if (entity.created()) {
- delete this.entities[entity.id];
- } else {
- this.entities[entity.id] = undefined;
- }
+ this._updateCalculated(entity, undefined);
+ this.entities[entity.id] = undefined;
});
},
update: function() {
- var graph = this.frozen ? iD.Graph(_.clone(this.entities), true) : this;
+ var graph = this.frozen ? iD.Graph(this, true) : this;
for (var i = 0; i < arguments.length; i++) {
arguments[i].call(graph, graph);
@@ -133,7 +216,6 @@ iD.Graph.prototype = {
this.frozen = true;
if (iD.debug) {
- Object.freeze(this);
Object.freeze(this.entities);
}
@@ -153,9 +235,12 @@ iD.Graph.prototype = {
},
difference: function (graph) {
- var result = [], entity, oldentity, id;
+ var result = [],
+ keys = Object.keys(this.entities),
+ entity, oldentity, id, i;
- for (id in this.entities) {
+ for (i = 0; i < keys.length; i++) {
+ id = keys[i];
entity = this.entities[id];
oldentity = graph.entities[id];
if (entity !== oldentity) {
@@ -177,7 +262,9 @@ iD.Graph.prototype = {
}
}
- for (id in graph.entities) {
+ keys = Object.keys(graph.entities);
+ for (i = 0; i < keys.length; i++) {
+ id = keys[i];
entity = graph.entities[id];
if (entity && !this.entities.hasOwnProperty(id)) {
result.push(id);
@@ -189,25 +276,25 @@ iD.Graph.prototype = {
},
modified: function() {
- var result = [];
+ var result = [], base = this.base().entities;
_.each(this.entities, function(entity, id) {
- if (entity && entity.modified()) result.push(id);
+ if (entity && base[id]) result.push(id);
});
return result;
},
created: function() {
- var result = [];
+ var result = [], base = this.base().entities;
_.each(this.entities, function(entity, id) {
- if (entity && entity.created()) result.push(id);
+ if (entity && !base[id]) result.push(id);
});
return result;
},
deleted: function() {
- var result = [];
+ var result = [], base = this.base().entities;
_.each(this.entities, function(entity, id) {
- if (!entity) result.push(id);
+ if (!entity && base[id]) result.push(id);
});
return result;
}
diff --git a/js/id/graph/history.js b/js/id/graph/history.js
index 29b652554..42e1b1604 100644
--- a/js/id/graph/history.js
+++ b/js/id/graph/history.js
@@ -29,9 +29,9 @@ iD.History = function() {
return stack[index].graph;
},
- merge: function (graph) {
+ merge: function (entities) {
for (var i = 0; i < stack.length; i++) {
- stack[i].graph = stack[i].graph.merge(graph);
+ stack[i].graph.rebase(entities);
}
},
diff --git a/js/id/id.js b/js/id/id.js
index ab6dd37f2..69e64e5fe 100644
--- a/js/id/id.js
+++ b/js/id/id.js
@@ -22,7 +22,7 @@ window.iD = function(container) {
}
function hintprefix(x, y) {
- return '' + x + ' ' + y;
+ return '' + y + '' + '
' + x + '
';
}
var m = container.append('div')
@@ -44,10 +44,10 @@ window.iD = function(container) {
.enter().append('button')
.attr('tabindex', -1)
.attr('class', function (mode) { return mode.title + ' add-button col3'; })
+ .call(bootstrap.tooltip().placement('bottom').html(true))
.attr('data-original-title', function (mode) {
return hintprefix(mode.key, mode.description);
})
- .call(bootstrap.tooltip().placement('bottom').html(true))
.on('click.editor', function (mode) { controller.enter(mode); });
function disableTooHigh() {
@@ -207,12 +207,12 @@ window.iD = function(container) {
limiter.select('#undo')
.property('disabled', !undo)
- .attr('data-original-title', hintprefix('⌘Z', undo))
+ .attr('data-original-title', hintprefix('⌘ + Z', undo))
.call(refreshTooltip);
limiter.select('#redo')
.property('disabled', !redo)
- .attr('data-original-title', hintprefix('⌘⇧Z', redo))
+ .attr('data-original-title', hintprefix('⌘ + ⇧ + Z', redo))
.call(refreshTooltip);
});
diff --git a/js/id/modes/add_area.js b/js/id/modes/add_area.js
index d1a3efb08..614c66677 100644
--- a/js/id/modes/add_area.js
+++ b/js/id/modes/add_area.js
@@ -15,11 +15,13 @@ iD.modes.AddArea = function() {
history = mode.history,
controller = mode.controller;
- function startFromNode(node) {
+ function start(loc) {
var graph = history.graph(),
+ node = iD.Node({loc: loc}),
way = iD.Way({tags: defaultTags});
history.perform(
+ iD.actions.AddNode(node),
iD.actions.AddWay(way),
iD.actions.AddWayNode(way.id, node.id),
iD.actions.AddWayNode(way.id, node.id));
@@ -42,13 +44,25 @@ iD.modes.AddArea = function() {
controller.enter(iD.modes.DrawArea(way.id, graph));
}
- function start(loc) {
+ function startFromNode(node) {
var graph = history.graph(),
- node = iD.Node({loc: loc}),
way = iD.Way({tags: defaultTags});
history.perform(
- iD.actions.AddNode(node),
+ iD.actions.AddWay(way),
+ iD.actions.AddWayNode(way.id, node.id),
+ iD.actions.AddWayNode(way.id, node.id));
+
+ controller.enter(iD.modes.DrawArea(way.id, graph));
+ }
+
+ function startFromMidpoint(midpoint) {
+ var graph = history.graph(),
+ node = iD.Node(),
+ way = iD.Way({tags: defaultTags});
+
+ history.perform(
+ iD.actions.AddMidpoint(midpoint, node),
iD.actions.AddWay(way),
iD.actions.AddWayNode(way.id, node.id),
iD.actions.AddWayNode(way.id, node.id));
@@ -57,9 +71,10 @@ iD.modes.AddArea = function() {
}
behavior = iD.behavior.AddWay(mode)
- .on('startFromNode', startFromNode)
+ .on('start', start)
.on('startFromWay', startFromWay)
- .on('start', start);
+ .on('startFromNode', startFromNode)
+ .on('startFromMidpoint', startFromMidpoint);
mode.map.surface.call(behavior);
mode.map.tail('Click on the map to start drawing an area, like a park, lake, or building.', true);
diff --git a/js/id/modes/add_line.js b/js/id/modes/add_line.js
index bf419d2bb..90adac738 100644
--- a/js/id/modes/add_line.js
+++ b/js/id/modes/add_line.js
@@ -15,6 +15,33 @@ iD.modes.AddLine = function() {
history = mode.history,
controller = mode.controller;
+ function start(loc) {
+ var graph = history.graph(),
+ node = iD.Node({loc: loc}),
+ way = iD.Way({tags: defaultTags});
+
+ history.perform(
+ iD.actions.AddNode(node),
+ iD.actions.AddWay(way),
+ iD.actions.AddWayNode(way.id, node.id));
+
+ controller.enter(iD.modes.DrawLine(way.id, 'forward', graph));
+ }
+
+ function startFromWay(other, loc, index) {
+ var graph = history.graph(),
+ node = iD.Node({loc: loc}),
+ way = iD.Way({tags: defaultTags});
+
+ history.perform(
+ iD.actions.AddNode(node),
+ iD.actions.AddWay(way),
+ iD.actions.AddWayNode(way.id, node.id),
+ iD.actions.AddWayNode(other.id, node.id, index));
+
+ controller.enter(iD.modes.DrawLine(way.id, 'forward', graph));
+ }
+
function startFromNode(node) {
var graph = history.graph(),
parent = graph.parentWays(node)[0],
@@ -37,27 +64,13 @@ iD.modes.AddLine = function() {
}
}
- function startFromWay(other, loc, index) {
+ function startFromMidpoint(midpoint) {
var graph = history.graph(),
- node = iD.Node({loc: loc}),
+ node = iD.Node(),
way = iD.Way({tags: defaultTags});
history.perform(
- iD.actions.AddNode(node),
- iD.actions.AddWay(way),
- iD.actions.AddWayNode(way.id, node.id),
- iD.actions.AddWayNode(other.id, node.id, index));
-
- controller.enter(iD.modes.DrawLine(way.id, 'forward', graph));
- }
-
- function start(loc) {
- var graph = history.graph(),
- node = iD.Node({loc: loc}),
- way = iD.Way({tags: defaultTags});
-
- history.perform(
- iD.actions.AddNode(node),
+ iD.actions.AddMidpoint(midpoint, node),
iD.actions.AddWay(way),
iD.actions.AddWayNode(way.id, node.id));
@@ -65,9 +78,10 @@ iD.modes.AddLine = function() {
}
behavior = iD.behavior.AddWay(mode)
- .on('startFromNode', startFromNode)
+ .on('start', start)
.on('startFromWay', startFromWay)
- .on('start', start);
+ .on('startFromNode', startFromNode)
+ .on('startFromMidpoint', startFromMidpoint);
mode.map.surface.call(behavior);
mode.map.tail('Click on the map to start drawing an road, path, or route.', true);
diff --git a/js/id/modes/add_point.js b/js/id/modes/add_point.js
index 1a51c314d..1b9781a9d 100644
--- a/js/id/modes/add_point.js
+++ b/js/id/modes/add_point.js
@@ -16,22 +16,33 @@ iD.modes.AddPoint = function() {
map.tail('Click on the map to add a point.', true);
- function add() {
- var node = iD.Node({loc: map.mouseCoordinates()});
+ function add(loc) {
+ var node = iD.Node({loc: loc});
history.perform(
iD.actions.AddNode(node),
- 'added a point');
+ 'Added a point.');
controller.enter(iD.modes.Select(node, true));
}
+ function addWay(way, loc, index) {
+ add(loc);
+ }
+
+ function addNode(node) {
+ add(node.loc);
+ }
+
function cancel() {
controller.exit();
}
- behavior = iD.behavior.Draw()
- .on('add', add)
+ behavior = iD.behavior.Draw(map)
+ .on('click', add)
+ .on('clickWay', addWay)
+ .on('clickNode', addNode)
+ .on('clickMidpoint', addNode)
.on('cancel', cancel)
.on('finish', cancel)
(surface);
diff --git a/js/id/modes/browse.js b/js/id/modes/browse.js
index 9782bac27..ab6bc80cb 100644
--- a/js/id/modes/browse.js
+++ b/js/id/modes/browse.js
@@ -3,7 +3,7 @@ iD.modes.Browse = function() {
button: 'browse',
id: 'browse',
title: 'Browse',
- description: 'Pan and zoom the map',
+ description: 'Pan and zoom the map.',
key: 'b'
};
diff --git a/js/id/modes/draw_area.js b/js/id/modes/draw_area.js
index 721452bae..8423ff436 100644
--- a/js/id/modes/draw_area.js
+++ b/js/id/modes/draw_area.js
@@ -8,33 +8,21 @@ iD.modes.DrawArea = function(wayId, baseGraph) {
mode.enter = function() {
var way = mode.history.graph().entity(wayId),
- index = -1,
headId = way.nodes[way.nodes.length - 2],
- tailId = way.first(),
- annotation = way.isDegenerate() ? 'started an area' : 'continued an area';
+ tailId = way.first();
- function addHeadTail() {
- behavior.finish();
- }
+ behavior = iD.behavior.DrawWay(wayId, -1, mode, baseGraph)
+ .annotation(way.isDegenerate() ? 'started an area' : 'continued an area');
- function addNode(node) {
- behavior.addNode(node, annotation);
- }
+ var addNode = behavior.addNode;
- function addWay(way, loc, index) {
- behavior.addWay(way, loc, index, annotation);
- }
-
- function add(loc) {
- behavior.add(loc, annotation);
- }
-
- behavior = iD.behavior.DrawWay(wayId, headId, tailId, index, mode, baseGraph)
- .on('addHead', addHeadTail)
- .on('addTail', addHeadTail)
- .on('addNode', addNode)
- .on('addWay', addWay)
- .on('add', add);
+ behavior.addNode = function(node) {
+ if (node.id === headId || node.id === tailId) {
+ behavior.finish();
+ } else {
+ addNode(node);
+ }
+ };
mode.map.surface.call(behavior);
mode.map.tail('Click to add points to your area. Click the first point to finish the area.', true);
diff --git a/js/id/modes/draw_line.js b/js/id/modes/draw_line.js
index 289d91c14..ae54ab2e1 100644
--- a/js/id/modes/draw_line.js
+++ b/js/id/modes/draw_line.js
@@ -9,41 +9,20 @@ iD.modes.DrawLine = function(wayId, direction, baseGraph) {
mode.enter = function() {
var way = mode.history.graph().entity(wayId),
index = (direction === 'forward') ? undefined : 0,
- headId = (direction === 'forward') ? way.last() : way.first(),
- tailId = (direction === 'forward') ? way.first() : way.last(),
- annotation = way.isDegenerate() ? 'started a line' : 'continued a line';
+ headId = (direction === 'forward') ? way.last() : way.first();
- function addHead() {
- behavior.finish();
- }
+ behavior = iD.behavior.DrawWay(wayId, index, mode, baseGraph)
+ .annotation(way.isDegenerate() ? 'Started a line.' : 'Continued a line.');
- function addTail(node) {
- // connect the way in a loop
- if (way.nodes.length > 2) {
- behavior.addNode(node, annotation);
+ var addNode = behavior.addNode;
+
+ behavior.addNode = function(node) {
+ if (node.id === headId) {
+ behavior.finish();
} else {
- behavior.cancel();
+ addNode(node);
}
- }
-
- function addNode(node) {
- behavior.addNode(node, annotation);
- }
-
- function addWay(way, loc, index) {
- behavior.addWay(way, loc, index, annotation);
- }
-
- function add(loc) {
- behavior.add(loc, annotation);
- }
-
- behavior = iD.behavior.DrawWay(wayId, headId, tailId, index, mode, baseGraph)
- .on('addHead', addHead)
- .on('addTail', addTail)
- .on('addNode', addNode)
- .on('addWay', addWay)
- .on('add', add);
+ };
mode.map.surface.call(behavior);
mode.map.tail('Click to add more points to the line. ' +
diff --git a/js/id/modes/move_way.js b/js/id/modes/move_way.js
index 1f559da82..18c6ee619 100644
--- a/js/id/modes/move_way.js
+++ b/js/id/modes/move_way.js
@@ -18,7 +18,7 @@ iD.modes.MoveWay = function(wayId) {
history.perform(
iD.actions.Noop(),
- 'moved a way');
+ 'Moved a way.');
function move() {
var p = d3.mouse(selection.node()),
@@ -29,7 +29,7 @@ iD.modes.MoveWay = function(wayId) {
history.replace(
iD.actions.MoveWay(wayId, delta, projection),
- 'moved a way');
+ 'Moved a way.');
}
function finish() {
diff --git a/js/id/modes/select.js b/js/id/modes/select.js
index 3b1fca4b2..5f33df018 100644
--- a/js/id/modes/select.js
+++ b/js/id/modes/select.js
@@ -14,7 +14,7 @@ iD.modes.Select = function(entity, initial) {
if (!_.isEqual(entity.tags, tags)) {
mode.history.perform(
iD.actions.ChangeEntityTags(d.id, tags),
- 'changed tags');
+ 'Changed tags.');
}
}
@@ -116,7 +116,7 @@ iD.modes.Select = function(entity, initial) {
history.perform(
iD.actions.AddNode(node),
iD.actions.AddWayNode(datum.id, node.id, choice.index),
- 'added a point to a road');
+ 'Added a point to a road.');
d3.event.preventDefault();
d3.event.stopPropagation();
diff --git a/js/id/operations/circularize.js b/js/id/operations/circularize.js
index 49956430e..ce161aef4 100644
--- a/js/id/operations/circularize.js
+++ b/js/id/operations/circularize.js
@@ -10,12 +10,12 @@ iD.operations.Circularize = function(entityId, mode) {
if (geometry === 'line') {
history.perform(
action,
- 'made a line circular');
+ 'Made a line circular.');
} else if (geometry === 'area') {
history.perform(
action,
- 'made an area circular');
+ 'Made an area circular.');
}
};
diff --git a/js/id/operations/delete.js b/js/id/operations/delete.js
index 03d788261..cc3750360 100644
--- a/js/id/operations/delete.js
+++ b/js/id/operations/delete.js
@@ -9,22 +9,22 @@ iD.operations.Delete = function(entityId, mode) {
if (geometry === 'vertex') {
history.perform(
iD.actions.DeleteNode(entityId),
- 'deleted a vertex');
+ 'Deleted a vertex.');
} else if (geometry === 'point') {
history.perform(
iD.actions.DeleteNode(entityId),
- 'deleted a point');
+ 'Deleted a point.');
} else if (geometry === 'line') {
history.perform(
iD.actions.DeleteWay(entityId),
- 'deleted a line');
+ 'Deleted a line.');
} else if (geometry === 'area') {
history.perform(
iD.actions.DeleteWay(entityId),
- 'deleted an area');
+ 'Deleted an area.');
}
};
@@ -41,7 +41,7 @@ iD.operations.Delete = function(entityId, mode) {
operation.id = "delete";
operation.key = "⌫";
operation.title = "Delete";
- operation.description = "Remove this from the map";
+ operation.description = "Remove this from the map.";
return operation;
};
diff --git a/js/id/operations/reverse.js b/js/id/operations/reverse.js
index b36dfd60c..62fd8810b 100644
--- a/js/id/operations/reverse.js
+++ b/js/id/operations/reverse.js
@@ -4,7 +4,7 @@ iD.operations.Reverse = function(entityId, mode) {
var operation = function() {
history.perform(
iD.actions.ReverseWay(entityId),
- 'reversed a line');
+ 'Reversed a line.');
};
operation.available = function() {
@@ -20,7 +20,7 @@ iD.operations.Reverse = function(entityId, mode) {
operation.id = "reverse";
operation.key = "V";
operation.title = "Reverse";
- operation.description = "Make this way go in the opposite direction";
+ operation.description = "Make this way go in the opposite direction.";
return operation;
};
diff --git a/js/id/operations/split.js b/js/id/operations/split.js
index 3dcab7481..40aa55fd0 100644
--- a/js/id/operations/split.js
+++ b/js/id/operations/split.js
@@ -3,7 +3,7 @@ iD.operations.Split = function(entityId, mode) {
action = iD.actions.SplitWay(entityId);
var operation = function() {
- history.perform(action, 'split a way');
+ history.perform(action, 'Split a way.');
};
operation.available = function() {
@@ -20,7 +20,7 @@ iD.operations.Split = function(entityId, mode) {
operation.id = "split";
operation.key = "X";
operation.title = "Split";
- operation.description = "Split this into two ways at this point";
+ operation.description = "Split this into two ways at this point.";
return operation;
};
diff --git a/js/id/operations/unjoin.js b/js/id/operations/unjoin.js
index c18de3029..f38333bb4 100644
--- a/js/id/operations/unjoin.js
+++ b/js/id/operations/unjoin.js
@@ -3,7 +3,7 @@ iD.operations.Unjoin = function(entityId, mode) {
action = iD.actions.UnjoinNode(entityId);
var operation = function() {
- history.perform(action, 'unjoined lines');
+ history.perform(action, 'Unjoined lines.');
};
operation.available = function() {
@@ -20,7 +20,7 @@ iD.operations.Unjoin = function(entityId, mode) {
operation.id = "unjoin";
operation.key = "⇧-J";
operation.title = "Unjoin";
- operation.description = "Disconnect these ways from each other";
+ operation.description = "Disconnect these ways from each other.";
return operation;
};
diff --git a/js/id/renderer/background.js b/js/id/renderer/background.js
index 6476e7a28..752853e6b 100644
--- a/js/id/renderer/background.js
+++ b/js/id/renderer/background.js
@@ -1,4 +1,10 @@
iD.Background = function() {
+
+ var deviceRatio = (window.devicePixelRatio &&
+ window.devicePixelRatio === 2) ? 0.5 : 1;
+ // tileSize = (deviceRatio === 0.5) ? [128,128] : [256,256];
+ tileSize = [256, 256];
+
var tile = d3.geo.tile(),
projection,
cache = {},
@@ -13,7 +19,12 @@ iD.Background = function() {
'-o-transform-origin:0 0;' +
'-webkit-user-select: none;' +
'-webkit-user-drag: none;' +
- '-moz-user-drag: none;';
+ '-moz-user-drag: none;' +
+ 'opacity:0;';
+
+ function tileSizeAtZoom(d, z) {
+ return Math.ceil(tileSize[0] * Math.pow(2, z - d[2])) / tileSize[0];
+ }
function atZoom(t, distance) {
var power = Math.pow(2, distance);
@@ -21,108 +32,101 @@ iD.Background = function() {
Math.floor(t[0] * power),
Math.floor(t[1] * power),
t[2] + distance];
- az.push(source(az));
return az;
}
- function upZoom(t, distance) {
- var az = atZoom(t, distance), tiles = [];
- for (var x = 0; x < 2; x++) {
- for (var y = 0; y < 2; y++) {
- var up = [az[0] + x, az[1] + y, az[2]];
- up.push(source(up));
- tiles.push(up);
- }
- }
- return tiles;
- }
-
- function tileSize(d, z) {
- return Math.ceil(256 * Math.pow(2, z - d[2])) / 256;
- }
-
function lookUp(d) {
for (var up = -1; up > -d[2]; up--) {
if (cache[atZoom(d, up)] !== false) return atZoom(d, up);
}
}
+ function uniqueBy(a, n) {
+ var o = [], seen = {};
+ for (var i = 0; i < a.length; i++) {
+ if (seen[a[i][n]] === undefined) {
+ o.push(a[i]);
+ seen[a[i][n]] = true;
+ }
+ }
+ return o;
+ }
+
+ function addSource(d) {
+ d.push(source(d));
+ return d;
+ }
+
// derive the tiles onscreen, remove those offscreen and position tiles
// correctly for the currentstate of `projection`
function background() {
- var tiles = tile
+ var sel = this,
+ tiles = tile
.scale(projection.scale())
.scaleExtent(source.scaleExtent || [1, 17])
.translate(projection.translate())(),
+ requests = [],
scaleExtent = tile.scaleExtent(),
z = Math.max(Math.log(projection.scale()) / Math.log(2) - 8, 0),
- rz = Math.max(scaleExtent[0], Math.min(scaleExtent[1], Math.floor(z))),
- ts = 256 * Math.pow(2, z - rz),
+ rz = Math.max(scaleExtent[0],
+ Math.min(scaleExtent[1], Math.floor(z))),
+ ts = tileSize[0] * Math.pow(2, z - rz),
tile_origin = [
projection.scale() / 2 - projection.translate()[0],
- projection.scale() / 2 - projection.translate()[1]],
- ups = {};
+ projection.scale() / 2 - projection.translate()[1]];
tiles.forEach(function(d) {
-
- if (cache[d] === true) {
- d.push(source(d));
- } else if (cache[d] === false &&
- cache[atZoom(d, -1)] !== false &&
- !ups[atZoom(d, -1)]) {
-
- ups[atZoom(d, -1)] = true;
- tiles.push(atZoom(d, -1));
-
- } else if (cache[d] === undefined &&
- lookUp(d)) {
-
- var upTile = lookUp(d);
- if (!ups[upTile]) {
- ups[upTile] = true;
- tiles.push(upTile);
- }
-
- } else if (cache[d] === undefined ||
- cache[d] === false) {
- upZoom(d, 1).forEach(function(u) {
- if (cache[u] && !ups[u]) {
- ups[u] = true;
- tiles.push(u);
- }
- });
+ addSource(d);
+ requests.push(d);
+ if (!cache[d[3]] && lookUp(d)) {
+ requests.push(addSource(lookUp(d)));
}
});
- var image = this
- .selectAll('img')
- .data(tiles, function(d) { return d; });
+ requests = uniqueBy(requests, 3);
function load(d) {
- cache[d.slice(0, 3)] = true;
- d3.select(this).on('load', null);
+ cache[d[3]] = true;
+ d3.select(this)
+ .on('load', null)
+ .transition()
+ .style('opacity', 1);
+ background.apply(sel);
}
function error(d) {
- cache[d.slice(0, 3)] = false;
+ cache[d[3]] = false;
+ d3.select(this).on('load', null);
d3.select(this).remove();
+ background.apply(sel);
}
+ function imageTransform(d) {
+ var _ts = tileSize[0] * Math.pow(2, z - d[2]);
+ var scale = tileSizeAtZoom(d, z);
+ return 'translate(' +
+ (Math.round((d[0] * _ts) - tile_origin[0]) + offset[0]) + 'px,' +
+ (Math.round((d[1] * _ts) - tile_origin[1]) + offset[1]) + 'px)' +
+ 'scale(' + scale + ',' + scale + ')';
+ }
+
+ var image = this
+ .selectAll('img')
+ .data(requests, function(d) { return d[3]; });
+
+ image.exit()
+ .style(transformProp, imageTransform)
+ .transition()
+ .style('opacity', 0)
+ .remove();
+
image.enter().append('img')
.attr('style', imgstyle)
.attr('src', function(d) { return d[3]; })
.on('error', error)
.on('load', load);
-
- image.exit().remove();
-
- image.style(transformProp, function(d) {
- var _ts = 256 * Math.pow(2, z - d[2]);
- var scale = tileSize(d, z);
- return 'translate(' +
- (Math.round((d[0] * _ts) - tile_origin[0]) + offset[0]) + 'px,' +
- (Math.round((d[1] * _ts) - tile_origin[1]) + offset[1]) + 'px) scale(' + scale + ',' + scale + ')';
- });
+
+ image.style(transformProp, imageTransform);
if (Object.keys(cache).length > 100) cache = {};
}
diff --git a/js/id/renderer/map.js b/js/id/renderer/map.js
index 487ac736a..08c18e915 100644
--- a/js/id/renderer/map.js
+++ b/js/id/renderer/map.js
@@ -64,7 +64,7 @@ iD.Map = function() {
for (var i = 0; i < parents.length; i++) {
var parent = parents[i];
if (only[parent.id] === undefined) {
- only[parent.id] = graph.entity(parent.id);
+ only[parent.id] = parent;
addParents(graph.parentRelations(parent));
}
}
@@ -92,7 +92,7 @@ iD.Map = function() {
all = _.compact(_.values(only));
filter = function(d) {
- if (d.midpoint) {
+ if (d.type === 'midpoint') {
for (var i = 0; i < d.ways.length; i++) {
if (d.ways[i].id in only) return true;
}
@@ -112,7 +112,7 @@ iD.Map = function() {
.call(areas, graph, all, filter)
.call(multipolygons, graph, all, filter)
.call(midpoints, graph, all, filter)
- .call(labels, graph, all, filter, dimensions);
+ .call(labels, graph, all, filter, dimensions, !difference);
}
dispatch.drawn(map);
}
@@ -123,7 +123,7 @@ iD.Map = function() {
function connectionLoad(err, result) {
history.merge(result);
- redraw(Object.keys(result.entities));
+ redraw(Object.keys(result));
}
function zoomPan() {
@@ -165,7 +165,8 @@ iD.Map = function() {
}
function resetTransform() {
- if (!surface.style(transformProp)) return false;
+ var prop = surface.style(transformProp);
+ if (!prop || prop === 'none') return false;
surface.style(transformProp, '');
tilegroup.style(transformProp, '');
return true;
diff --git a/js/id/svg/labels.js b/js/id/svg/labels.js
index 2fcc2fcc1..3962c4915 100644
--- a/js/id/svg/labels.js
+++ b/js/id/svg/labels.js
@@ -156,7 +156,7 @@ iD.svg.Labels = function(projection) {
for (var i = 0; i < nodes.length - 1; i++) {
var current = segmentLength(i);
var portion;
- if (!start && sofar + current > from) {
+ if (!start && sofar + current >= from) {
portion = (from - sofar) / current;
start = [
nodes[i][0] + portion * (nodes[i + 1][0] - nodes[i][0]),
@@ -164,7 +164,7 @@ iD.svg.Labels = function(projection) {
];
i0 = i + 1;
}
- if (!end && sofar + current > to) {
+ if (!end && sofar + current >= to) {
portion = (to - sofar) / current;
end = [
nodes[i][0] + portion * (nodes[i + 1][0] - nodes[i][0]),
@@ -183,14 +183,26 @@ iD.svg.Labels = function(projection) {
}
- return function drawLabels(surface, graph, entities, filter, dimensions) {
+ var rtree = new RTree(),
+ rectangles = {};
+
+ return function drawLabels(surface, graph, entities, filter, dimensions, fullRedraw) {
+
- var rtree = new RTree();
var hidePoints = !d3.select('.node.point').node();
var labelable = [], i, k, entity;
for (i = 0; i < label_stack.length; i++) labelable.push([]);
+ if (fullRedraw) {
+ rtree = new RTree();
+ rectangles = {};
+ } else {
+ for (i = 0; i < entities.length; i++) {
+ rtree.remove(rectangles[entities[i].id], entities[i].id);
+ }
+ }
+
// Split entities into groups specified by label_stack
for (i = 0; i < entities.length; i++) {
entity = entities[i];
@@ -252,7 +264,7 @@ iD.svg.Labels = function(projection) {
textAnchor: offset[2]
};
var rect = new RTree.Rectangle(p.x - m, p.y - m, width + 2*m, height + 2*m);
- if (tryInsert(rect)) return p;
+ if (tryInsert(rect, entity.id)) return p;
}
@@ -275,7 +287,7 @@ iD.svg.Labels = function(projection) {
Math.abs(sub[0][1] - sub[sub.length - 1][1]) + 30
);
if (rev) sub = sub.reverse();
- if (tryInsert(rect)) return {
+ if (tryInsert(rect, entity.id)) return {
'font-size': height + 2,
lineString: lineString(sub),
startOffset: offset + '%'
@@ -298,16 +310,19 @@ iD.svg.Labels = function(projection) {
height: height
};
var rect = new RTree.Rectangle(p.x - width/2, p.y, width, height);
- if (tryInsert(rect)) return p;
+ if (tryInsert(rect, entity.id)) return p;
}
- function tryInsert(rect) {
+ function tryInsert(rect, id) {
// Check that label is visible
if (rect.x1 < 0 || rect.y1 < 0 || rect.x2 > dimensions[0] ||
rect.y2 > dimensions[1]) return false;
var v = rtree.search(rect, true).length === 0;
- if (v) rtree.insert(rect);
+ if (v) {
+ rtree.insert(rect, id);
+ rectangles[id] = rect;
+ }
return v;
}
diff --git a/js/id/svg/midpoints.js b/js/id/svg/midpoints.js
index 655e35013..ee8d2c5e2 100644
--- a/js/id/svg/midpoints.js
+++ b/js/id/svg/midpoints.js
@@ -15,19 +15,15 @@ iD.svg.Midpoints = function(projection) {
b = nodes[j + 1],
id = [a.id, b.id].sort().join('-');
- if (!midpoints[id] &&
- iD.geo.dist(projection(a.loc), projection(b.loc)) > 40) {
-
- var midpoint_loc = iD.geo.interp(a.loc, b.loc, 0.5),
- parents = _.intersection(graph.parentWays(a),
- graph.parentWays(b));
+ if (midpoints[id]) {
+ midpoints[id].ways.push({id: entity.id, index: j + 1});
+ } else if (iD.geo.dist(projection(a.loc), projection(b.loc)) > 40) {
midpoints[id] = {
- loc: midpoint_loc,
- ways: parents,
- nodes: [a.id, b.id],
+ type: 'midpoint',
id: id,
- midpoint: true
+ loc: iD.geo.interp(a.loc, b.loc, 0.5),
+ ways: [{id: entity.id, index: j + 1}]
};
}
}
@@ -35,7 +31,7 @@ iD.svg.Midpoints = function(projection) {
var groups = surface.select('.layer-hit').selectAll('g.midpoint')
.filter(filter)
- .data(_.values(midpoints), function (d) { return [d.parents, d.id].join(","); });
+ .data(_.values(midpoints), function (d) { return d.id; });
var group = groups.enter()
.insert('g', ':first-child')
diff --git a/js/id/svg/vertices.js b/js/id/svg/vertices.js
index 06be536ec..ba28c968a 100644
--- a/js/id/svg/vertices.js
+++ b/js/id/svg/vertices.js
@@ -36,7 +36,7 @@ iD.svg.Vertices = function(projection) {
groups.attr('transform', iD.svg.PointTransform(projection))
.call(iD.svg.TagClasses())
.call(iD.svg.MemberClasses(graph))
- .classed('shared', function(entity) { return graph.parentWays(entity).length > 1; });
+ .classed('shared', function(entity) { return graph.isShared(entity); });
// Selecting the following implicitly
// sets the data (vertix entity) on the elements
diff --git a/js/id/ui/contributors.js b/js/id/ui/contributors.js
index c0036a677..c64d8a2ac 100644
--- a/js/id/ui/contributors.js
+++ b/js/id/ui/contributors.js
@@ -16,7 +16,7 @@ iD.ui.contributors = function(map) {
var l = selection
.select('.contributor-list')
.selectAll('a.user-link')
- .data(subset);
+ .data(subset, function(d) { return d; });
l.enter().append('a')
@@ -43,6 +43,10 @@ iD.ui.contributors = function(map) {
ext[1][0], ext[1][1]];
})
.text(' and ' + (u.length - limit) + ' others');
+ } else {
+ selection
+ .select('.contributor-count')
+ .html('');
}
if (!u.length) {
diff --git a/js/id/ui/notice.js b/js/id/ui/notice.js
index bcf44b7c9..12b1a2c09 100644
--- a/js/id/ui/notice.js
+++ b/js/id/ui/notice.js
@@ -4,20 +4,17 @@ iD.ui.notice = function(selection) {
notice = {};
var div = selection.append('div')
- .attr('class', 'notice')
- .append('div')
- .attr('class', 'notice-inner');
+ .attr('class', 'notice');
- div.append('button')
- .attr('class', 'zoom-to')
- .on('click', function() {
- event.zoom();
- })
- .append('span')
- .attr('class', 'icon invert zoom-in');
+ var button = div.append('button')
+ .attr('class', 'zoom-to notice')
+ .on('click', event.zoom);
- div.append('span')
- .attr('class', 'notice-text')
+ button.append('span')
+ .attr('class', 'icon zoom-in-invert');
+
+ button.append('span')
+ .attr('class', 'label')
.text(t('zoom_in_edit'));
notice.message = function(_) {
diff --git a/js/id/util.js b/js/id/util.js
index fac92fba0..f5cfd4450 100644
--- a/js/id/util.js
+++ b/js/id/util.js
@@ -94,3 +94,5 @@ iD.util.editDistance = function(a, b) {
}
return matrix[b.length][a.length];
};
+
+iD.util.getPrototypeOf = Object.getPrototypeOf || function(obj) { return obj.__proto__; };
diff --git a/presets/convert_potlatch.py b/presets/convert_potlatch.py
new file mode 100644
index 000000000..521081429
--- /dev/null
+++ b/presets/convert_potlatch.py
@@ -0,0 +1,47 @@
+from xml.dom.minidom import parse
+import json
+
+dom1 = parse('potlatch.xml')
+
+inputSets = dom1.getElementsByTagName('inputSet')
+
+jsonOutput = []
+
+for inputSet in inputSets:
+ setId = inputSet.getAttribute('id')
+ inputs = inputSet.getElementsByTagName('input')
+ for i in inputs:
+ jsonInput = {}
+ inputType = i.getAttribute('type')
+ if inputType == 'choice':
+ choices = i.getElementsByTagName('choice')
+ jsonInput['type'] = 'choice'
+ jsonInput['description'] = i.getAttribute('description')
+ jsonInput['name'] = i.getAttribute('name')
+ jsonInput['key'] = i.getAttribute('key')
+ jsonInput['choices'] = []
+ for c in choices:
+ jsonInput['choices'].append({
+ "value": c.getAttribute('value'),
+ "text": c.getAttribute('text')
+ })
+ elif inputType == 'freetext':
+ jsonInput['type'] = 'freetext'
+ jsonInput['description'] = i.getAttribute('description')
+ jsonInput['name'] = i.getAttribute('name')
+ jsonInput['key'] = i.getAttribute('key')
+ elif inputType == 'checkbox':
+ jsonInput['type'] = 'checkbox'
+ jsonInput['description'] = i.getAttribute('description')
+ jsonInput['name'] = i.getAttribute('name')
+ jsonInput['key'] = i.getAttribute('key')
+ elif inputType == 'number':
+ jsonInput['type'] = 'number'
+ jsonInput['description'] = i.getAttribute('description')
+ jsonInput['name'] = i.getAttribute('name')
+ jsonInput['minimum'] = i.getAttribute('minimum')
+ jsonInput['maximum'] = i.getAttribute('maximum')
+ jsonInput['key'] = i.getAttribute('key')
+ jsonOutput.append(jsonInput)
+
+json.dump(jsonOutput, open('presets_potlatch.json', 'w'), indent=4)
diff --git a/presets/potlatch.xml b/presets/potlatch.xml
new file mode 100644
index 000000000..ed01d3e1a
--- /dev/null
+++ b/presets/potlatch.xml
@@ -0,0 +1,693 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ http://wiki.openstreetmap.org/wiki/Key:cuisine
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${ref}
+ ${name}
+
+
+
+
+
+
+
+ ${ref}
+ ${name}
+
+
+
+
+
+
+
+ ${ref}
+ ${name}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${operator} ${ref}
+
+
+
+
+
+
+
+
+
+ ${operator} ${ref}
+
+
+
+
+
+
+
+
+
+ ${name|operator} (${ref})
+
+
+
+
+
+
+
+
+
+
+
+ ${ref}
+ ${name}
+
+
+
+
+
+
+
+ ${ref}
+ ${name}
+
+
+
+
+
+
+
+ ${ref}
+ ${name}
+
+
+
+
+
+
+ ${ref}
+ ${name}
+
+
+
+
+
+
+
+
+
+
+
+ http://wiki.openstreetmap.org/wiki/Key:access
+
+
+
+
+
+
+
+
+
+
+
+ http://wiki.openstreetmap.org/wiki/Key:cycleway
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ http://wiki.openstreetmap.org/wiki/Key:building
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ advanced
+
+
+
+
+
+
diff --git a/presets/presets_potlatch.json b/presets/presets_potlatch.json
new file mode 100644
index 000000000..ac674d5cc
--- /dev/null
+++ b/presets/presets_potlatch.json
@@ -0,0 +1,1265 @@
+[
+ {
+ "type": "freetext",
+ "description": "The name",
+ "key": "name",
+ "name": "Name"
+ },
+ {
+ "type": "freetext",
+ "description": "A reference number or code used to identify this thing.",
+ "key": "ref",
+ "name": "Reference number"
+ },
+ {
+ "type": "freetext",
+ "description": "Brand, i.e. Acme",
+ "key": "brand",
+ "name": "Brand"
+ },
+ {
+ "type": "freetext",
+ "description": "Operator, i.e. Acme Springfield Ltd",
+ "key": "operator",
+ "name": "Operator"
+ },
+ {
+ "type": "freetext",
+ "description": "The primary source of information for this object (GPS, survey, Bing, ...)",
+ "key": "source",
+ "name": "Source"
+ },
+ {
+ "type": "freetext",
+ "description": "The official designation or classification (if any). Only use this if the organisation that runs it has its own classification system.",
+ "key": "designation",
+ "name": "Official classification"
+ },
+ {
+ "type": "freetext",
+ "description": "The most common name",
+ "key": "name",
+ "name": "Name"
+ },
+ {
+ "type": "freetext",
+ "description": "The internationally recognised name",
+ "key": "int_name",
+ "name": "International Name"
+ },
+ {
+ "type": "freetext",
+ "description": "The historic or previous name",
+ "key": "old_name",
+ "name": "Historical Name"
+ },
+ {
+ "type": "freetext",
+ "description": "An alternative, currently used, name",
+ "key": "alt_name",
+ "name": "Alternative Name"
+ },
+ {
+ "choices": [
+ {
+ "text": "Free",
+ "value": "free"
+ },
+ {
+ "text": "Yes",
+ "value": "yes"
+ },
+ {
+ "text": "No",
+ "value": "no"
+ }
+ ],
+ "type": "choice",
+ "description": "",
+ "key": "wifi",
+ "name": "Wifi"
+ },
+ {
+ "type": "freetext",
+ "description": "The number of the house, e.g. 156 or 10-12",
+ "key": "addr:housenumber",
+ "name": "House Number"
+ },
+ {
+ "type": "freetext",
+ "description": "The name of the house, e.g. Riverbank Cottage",
+ "key": "addr:housename",
+ "name": "Building Name"
+ },
+ {
+ "type": "freetext",
+ "description": "The Street Name (optional)",
+ "key": "addr:street",
+ "name": "Street Name"
+ },
+ {
+ "type": "freetext",
+ "description": "The postcode",
+ "key": "addr:postcode",
+ "name": "Postcode"
+ },
+ {
+ "type": "freetext",
+ "description": "The URL of the website",
+ "key": "website",
+ "name": "Website"
+ },
+ {
+ "choices": [
+ {
+ "text": "Burger",
+ "value": "burger"
+ },
+ {
+ "text": "Chicken",
+ "value": "chicken"
+ },
+ {
+ "text": "Chinese",
+ "value": "chinese"
+ },
+ {
+ "text": "Coffee Shop",
+ "value": "coffee_shop"
+ },
+ {
+ "text": "Greek",
+ "value": "greek"
+ },
+ {
+ "text": "Pizza",
+ "value": "pizza"
+ },
+ {
+ "text": "Sandwich",
+ "value": "sandwich"
+ },
+ {
+ "text": "Sea Food",
+ "value": "seafood"
+ },
+ {
+ "text": "Regional",
+ "value": "regional"
+ },
+ {
+ "text": "Italian",
+ "value": "italian"
+ },
+ {
+ "text": "German",
+ "value": "german"
+ },
+ {
+ "text": "Kebab/souvlaki/gyro",
+ "value": "kebab"
+ },
+ {
+ "text": "Indian",
+ "value": "indian"
+ },
+ {
+ "text": "Asian",
+ "value": "asian"
+ },
+ {
+ "text": "Mexican",
+ "value": "mexican"
+ },
+ {
+ "text": "Thai",
+ "value": "thai"
+ },
+ {
+ "text": "Japanese",
+ "value": "japanese"
+ },
+ {
+ "text": "Ice-cream",
+ "value": "ice_cream"
+ },
+ {
+ "text": "Fish & Chips",
+ "value": "fish_and_chips"
+ },
+ {
+ "text": "Turkish",
+ "value": "turkish"
+ },
+ {
+ "text": "French",
+ "value": "french"
+ },
+ {
+ "text": "Sushi",
+ "value": "sushi"
+ },
+ {
+ "text": "American",
+ "value": "american"
+ },
+ {
+ "text": "Steak House",
+ "value": "steak_house"
+ },
+ {
+ "text": "International",
+ "value": "international"
+ },
+ {
+ "text": "Spanish",
+ "value": "spanish"
+ },
+ {
+ "text": "Vietnamese",
+ "value": "vietnamese"
+ },
+ {
+ "text": "Fish",
+ "value": "fish"
+ },
+ {
+ "text": "Bavarian",
+ "value": "bavarian"
+ },
+ {
+ "text": "Vegetarian",
+ "value": "vegetarian"
+ }
+ ],
+ "type": "choice",
+ "description": "The type of food that they serve",
+ "key": "cuisine",
+ "name": "Cuisine"
+ },
+ {
+ "type": "freetext",
+ "description": "The official reference number",
+ "key": "ref",
+ "name": "Reference"
+ },
+ {
+ "type": "freetext",
+ "description": "The official international reference number",
+ "key": "int_ref",
+ "name": "International Reference"
+ },
+ {
+ "type": "freetext",
+ "description": "The historic or previous reference number",
+ "key": "old_ref",
+ "name": "Old Reference"
+ },
+ {
+ "type": "freetext",
+ "description": "Width of the road",
+ "key": "width",
+ "name": "Width"
+ },
+ {
+ "choices": [
+ {
+ "text": "Unpaved",
+ "value": "unpaved"
+ },
+ {
+ "text": "Paved",
+ "value": "paved"
+ },
+ {
+ "text": "Asphalt",
+ "value": "asphalt"
+ },
+ {
+ "text": "Concrete",
+ "value": "concrete"
+ },
+ {
+ "text": "Paving stones",
+ "value": "paving_stones"
+ },
+ {
+ "text": "Cobblestone",
+ "value": "cobblestone"
+ },
+ {
+ "text": "Sand",
+ "value": "sand"
+ },
+ {
+ "text": "Gravel",
+ "value": "gravel"
+ },
+ {
+ "text": "Dirt",
+ "value": "dirt"
+ },
+ {
+ "text": "Grass",
+ "value": "grass"
+ }
+ ],
+ "type": "choice",
+ "description": "Type of road surface",
+ "key": "surface",
+ "name": "Surface"
+ },
+ {
+ "type": "checkbox",
+ "description": "The way is a large open space, like at a dock, where vehicles can move anywhere within the space, rather than just along the edge.",
+ "key": "area",
+ "name": "Open area"
+ },
+ {
+ "description": "Total number of lanes, counting both directions",
+ "maximum": 10.0,
+ "minimum": 1.0,
+ "key": "lanes",
+ "type": "number",
+ "name": "Lanes"
+ },
+ {
+ "choices": [
+ {
+ "text": "Generic Bridge",
+ "value": "yes"
+ },
+ {
+ "text": "Viaduct",
+ "value": "viaduct"
+ },
+ {
+ "text": "Suspension bridge",
+ "value": "suspension"
+ }
+ ],
+ "type": "choice",
+ "description": "Road goes over a bridge",
+ "key": "bridge",
+ "name": "Bridge"
+ },
+ {},
+ {
+ "choices": [
+ {
+ "text": "Tunnel",
+ "value": "yes"
+ }
+ ],
+ "type": "choice",
+ "description": "Road goes into a tunnel",
+ "key": "tunnel",
+ "name": "Tunnel"
+ },
+ {
+ "choices": [
+ {
+ "text": "Embankment",
+ "value": "yes"
+ }
+ ],
+ "type": "choice",
+ "description": "Road supported on a raised bed of earth and rock.",
+ "key": "embankment",
+ "name": "Embankment"
+ },
+ {
+ "choices": [
+ {
+ "text": "Cutting",
+ "value": "yes"
+ }
+ ],
+ "type": "choice",
+ "description": "Road carved out of hill on one or both sides.",
+ "key": "cutting",
+ "name": "Cutting"
+ },
+ {
+ "choices": [
+ {
+ "text": "Yes",
+ "value": "yes"
+ },
+ {
+ "text": "Overhead line",
+ "value": "contact_line"
+ },
+ {
+ "text": "Third rail",
+ "value": "rail"
+ },
+ {
+ "text": "No",
+ "value": "no"
+ }
+ ],
+ "type": "choice",
+ "description": "Is the track electrified (whether by 3rd rail, overhead wires, etc)?",
+ "key": "electrified",
+ "name": "Electrified"
+ },
+ {
+ "choices": [
+ {
+ "text": "600V",
+ "value": "600"
+ },
+ {
+ "text": "750V",
+ "value": "750"
+ },
+ {
+ "text": "1500V",
+ "value": "1500"
+ },
+ {
+ "text": "3000V",
+ "value": "3000"
+ },
+ {
+ "text": "12kV",
+ "value": "12000"
+ },
+ {
+ "text": "15kV",
+ "value": "15000"
+ },
+ {
+ "text": "25kV",
+ "value": "25000"
+ }
+ ],
+ "type": "choice",
+ "description": "Nominal voltage of electric wires",
+ "key": "voltage",
+ "name": "Voltage"
+ },
+ {
+ "choices": [
+ {
+ "text": "DC",
+ "value": "0"
+ },
+ {
+ "text": "16.67 Hz",
+ "value": "16.67"
+ },
+ {
+ "text": "16.7 Hz",
+ "value": "16.7"
+ },
+ {
+ "text": "25 Hz",
+ "value": "25"
+ },
+ {
+ "text": "50 Hz",
+ "value": "50"
+ },
+ {
+ "text": "60 Hz",
+ "value": "60"
+ }
+ ],
+ "type": "choice",
+ "description": "Frequency in Hertz of alternating current power supply",
+ "key": "frequency",
+ "name": "Frequency"
+ },
+ {
+ "type": "freetext",
+ "description": "The charge/cost of using this amenity",
+ "key": "fee",
+ "name": "Fee"
+ },
+ {
+ "choices": [
+ {
+ "text": "One way",
+ "value": "yes"
+ },
+ {
+ "text": "Two way",
+ "value": "no"
+ },
+ {
+ "text": "One way reverse",
+ "value": "-1"
+ }
+ ],
+ "type": "choice",
+ "description": "Oneway roads",
+ "key": "oneway",
+ "name": "Oneway"
+ },
+ {},
+ {
+ "choices": [
+ {
+ "text": "Yes",
+ "value": "roundabout"
+ }
+ ],
+ "type": "choice",
+ "description": "Whether this road is a roundabout. Make the way face the direction appropriate for the country.",
+ "key": "junction",
+ "name": "Roundabout"
+ },
+ {},
+ {
+ "choices": [
+ {
+ "text": "Yes",
+ "value": "traffic_signals"
+ }
+ ],
+ "type": "choice",
+ "description": "Intersection controlled by traffic lights",
+ "key": "highway",
+ "name": "Traffic signals"
+ },
+ {
+ "choices": [
+ {
+ "text": "Allowed",
+ "value": "yes"
+ },
+ {
+ "text": "Prohibited",
+ "value": "no"
+ },
+ {
+ "text": "Designated",
+ "value": "designated"
+ }
+ ],
+ "type": "choice",
+ "description": "Can pedestrians use this road, including footpaths if any?",
+ "key": "foot",
+ "name": "Pedestrians permitted"
+ },
+ {
+ "choices": [
+ {
+ "text": "Both",
+ "value": "both"
+ },
+ {
+ "text": "Left",
+ "value": "left"
+ },
+ {
+ "text": "Right",
+ "value": "right"
+ },
+ {
+ "text": "Separate",
+ "value": "separate"
+ },
+ {
+ "text": "None",
+ "value": "none"
+ }
+ ],
+ "type": "choice",
+ "description": "Whether there is a sidewalk at the side of the street",
+ "key": "sidewalk",
+ "name": "Sidewalks"
+ },
+ {},
+ {},
+ {},
+ {
+ "type": "freetext",
+ "description": "12 character internal Naptan ID",
+ "key": "naptan:AtcoCode",
+ "name": "Atco Code"
+ },
+ {
+ "choices": [
+ {
+ "text": "N",
+ "value": "N"
+ },
+ {
+ "text": "NE",
+ "value": "NE"
+ },
+ {
+ "text": "E",
+ "value": "E"
+ },
+ {
+ "text": "SE",
+ "value": "SE"
+ },
+ {
+ "text": "S",
+ "value": "S"
+ },
+ {
+ "text": "SW",
+ "value": "SW"
+ },
+ {
+ "text": "W",
+ "value": "W"
+ },
+ {
+ "text": "NW",
+ "value": "NW"
+ }
+ ],
+ "type": "choice",
+ "description": "The eight-point compass bearning",
+ "key": "naptan:Bearing",
+ "name": "Naptan Bearing"
+ },
+ {
+ "type": "freetext",
+ "description": "The naptan common name",
+ "key": "naptan:CommonName",
+ "name": "Naptan Common Name (read-only)"
+ },
+ {
+ "type": "freetext",
+ "description": "",
+ "key": "naptan:Indicator",
+ "name": "Naptan Indicator (read-only)"
+ },
+ {
+ "type": "freetext",
+ "description": "",
+ "key": "naptan:Street",
+ "name": "Naptan Street (read-only)"
+ },
+ {
+ "type": "freetext",
+ "description": "Delete this when the details have been verified on-the-ground",
+ "key": "naptan:verified",
+ "name": "Naptan Verified?"
+ },
+ {
+ "type": "freetext",
+ "description": "The name of the bus stop",
+ "key": "name",
+ "name": "Stop Name"
+ },
+ {
+ "type": "freetext",
+ "description": "The local reference of the stop, usually one or two letters above the main flag, used at bus interchanges, e.g. L, BX",
+ "key": "local_ref",
+ "name": "Local Ref"
+ },
+ {},
+ {},
+ {},
+ {},
+ {},
+ {},
+ {},
+ {
+ "choices": [
+ {
+ "text": "Allowed",
+ "value": "yes"
+ },
+ {
+ "text": "Prohibited",
+ "value": "no"
+ },
+ {
+ "text": "Private",
+ "value": "private"
+ },
+ {
+ "text": "Cyclists dismount",
+ "value": "dismount"
+ },
+ {
+ "text": "Designated",
+ "value": "designated"
+ }
+ ],
+ "type": "choice",
+ "description": "Are bicyles allowed to use this road (regardless of physical suitability)?",
+ "key": "bicycle",
+ "name": "Bicycles permitted"
+ },
+ {
+ "choices": [
+ {
+ "text": "No bike lanes",
+ "value": "no"
+ },
+ {
+ "text": "On-road bike lane",
+ "value": "lane"
+ },
+ {
+ "text": "Parallel track",
+ "value": "track"
+ },
+ {
+ "text": "Contraflow lane",
+ "value": "opposite_lane"
+ },
+ {
+ "text": "Contraflow track",
+ "value": "opposite_track"
+ },
+ {
+ "text": "Contraflow unmarked",
+ "value": "opposite"
+ }
+ ],
+ "type": "choice",
+ "description": "Road has bike lanes within the road surface",
+ "key": "cycleway",
+ "name": "Bike lanes"
+ },
+ {
+ "type": "freetext",
+ "description": "The width in metres",
+ "key": "width",
+ "name": "Width"
+ },
+ {
+ "choices": [
+ {
+ "text": "Allowed",
+ "value": "yes"
+ },
+ {
+ "text": "Prohibited",
+ "value": "no"
+ },
+ {
+ "text": "Permissive",
+ "value": "permissive"
+ },
+ {
+ "text": "Private",
+ "value": "private"
+ },
+ {
+ "text": "Designated",
+ "value": "designated"
+ }
+ ],
+ "type": "choice",
+ "description": "Are boats allowed to use this waterway?",
+ "key": "boat",
+ "name": "Boat permission"
+ },
+ {
+ "choices": [
+ {
+ "text": "Allowed",
+ "value": "yes"
+ },
+ {
+ "text": "Prohibited",
+ "value": "no"
+ },
+ {
+ "text": "Permissive",
+ "value": "permissive"
+ },
+ {
+ "text": "Private",
+ "value": "private"
+ },
+ {
+ "text": "Designated",
+ "value": "designated"
+ }
+ ],
+ "type": "choice",
+ "description": "Is there a general right of access, regardless of mode of transport?",
+ "key": "access",
+ "name": "General access"
+ },
+ {
+ "choices": [
+ {
+ "text": "Allowed",
+ "value": "yes"
+ },
+ {
+ "text": "Prohibited",
+ "value": "no"
+ },
+ {
+ "text": "Permissive",
+ "value": "permissive"
+ },
+ {
+ "text": "Private",
+ "value": "private"
+ },
+ {
+ "text": "Designated",
+ "value": "designated"
+ }
+ ],
+ "type": "choice",
+ "description": "Are cars and other private vehicles allowed?",
+ "key": "motor_vehicle",
+ "name": "Motor vehicles"
+ },
+ {
+ "choices": [
+ {
+ "text": "Allowed",
+ "value": "yes"
+ },
+ {
+ "text": "Prohibited",
+ "value": "no"
+ },
+ {
+ "text": "Permissive",
+ "value": "permissive"
+ },
+ {
+ "text": "Private",
+ "value": "private"
+ },
+ {
+ "text": "Designated",
+ "value": "designated"
+ }
+ ],
+ "type": "choice",
+ "description": "Are horses allowed?",
+ "key": "horse",
+ "name": "Horses"
+ },
+ {
+ "choices": [
+ {
+ "text": "Locality",
+ "value": "locality"
+ },
+ {
+ "text": "Hamlet",
+ "value": "hamlet"
+ },
+ {
+ "text": "Village",
+ "value": "village"
+ },
+ {
+ "text": "Suburb",
+ "value": "suburb"
+ },
+ {
+ "text": "Town",
+ "value": "town"
+ },
+ {
+ "text": "City",
+ "value": "city"
+ },
+ {
+ "text": "County",
+ "value": "county"
+ },
+ {
+ "text": "Region",
+ "value": "region"
+ },
+ {
+ "text": "State",
+ "value": "state"
+ },
+ {
+ "text": "Country",
+ "value": "country"
+ },
+ {
+ "text": "Continent",
+ "value": "continent"
+ },
+ {
+ "text": "Island",
+ "value": "island"
+ },
+ {
+ "text": "Islet",
+ "value": "islet"
+ }
+ ],
+ "type": "choice",
+ "description": "",
+ "key": "place",
+ "name": "Type of Place"
+ },
+ {
+ "choices": [
+ {
+ "text": "Generic building",
+ "value": "yes"
+ },
+ {
+ "text": "Generic residential",
+ "value": "residential"
+ },
+ {
+ "text": "Big apartments house",
+ "value": "apartments"
+ },
+ {
+ "text": "Terraced house",
+ "value": "terrace"
+ },
+ {
+ "text": "Family house",
+ "value": "house"
+ },
+ {
+ "text": "Small hut",
+ "value": "hut"
+ },
+ {
+ "text": "A garage",
+ "value": "garage"
+ },
+ {
+ "text": "Block of garages",
+ "value": "garages"
+ },
+ {
+ "text": "Office building",
+ "value": "office"
+ },
+ {
+ "text": "Public building",
+ "value": "public"
+ },
+ {
+ "text": "Generic industrial",
+ "value": "industrial"
+ },
+ {
+ "text": "Manufacture",
+ "value": "manufacture"
+ },
+ {
+ "text": "Warehouse",
+ "value": "warehouse"
+ },
+ {
+ "text": "Hangar",
+ "value": "hangar"
+ },
+ {
+ "text": "Fluids storage tank",
+ "value": "storage_tank"
+ },
+ {
+ "text": "Retail",
+ "value": "retail"
+ },
+ {
+ "text": "Supermarket",
+ "value": "supermarket"
+ },
+ {
+ "text": "Train station",
+ "value": "train_station"
+ },
+ {
+ "text": "Church",
+ "value": "church"
+ },
+ {
+ "text": "School",
+ "value": "school"
+ },
+ {
+ "text": "Military bunker",
+ "value": "bunker"
+ },
+ {
+ "text": "Collapsed building",
+ "value": "collapsed"
+ },
+ {
+ "text": "Just a roof",
+ "value": "roof"
+ }
+ ],
+ "type": "choice",
+ "description": "",
+ "key": "building",
+ "name": "Building type, if it is one"
+ },
+ {
+ "choices": [
+ {
+ "text": "2",
+ "value": "2"
+ },
+ {
+ "text": "3",
+ "value": "3"
+ },
+ {
+ "text": "4",
+ "value": "4"
+ },
+ {
+ "text": "6",
+ "value": "6"
+ },
+ {
+ "text": "8",
+ "value": "8"
+ },
+ {
+ "text": "10",
+ "value": "10"
+ },
+ {
+ "text": "12",
+ "value": "12"
+ }
+ ],
+ "type": "choice",
+ "description": "",
+ "key": "cables",
+ "name": "Cables"
+ },
+ {
+ "choices": [
+ {
+ "text": "400 V",
+ "value": "400"
+ },
+ {
+ "text": "600 V",
+ "value": "600"
+ },
+ {
+ "text": "750 V",
+ "value": "750"
+ },
+ {
+ "text": "1500 V",
+ "value": "1500"
+ },
+ {
+ "text": "3000 V",
+ "value": "3000"
+ },
+ {
+ "text": "15 kV",
+ "value": "15000"
+ },
+ {
+ "text": "20 kV",
+ "value": "20000"
+ },
+ {
+ "text": "35 kV",
+ "value": "35000"
+ },
+ {
+ "text": "110 kV",
+ "value": "110000"
+ },
+ {
+ "text": "132 kV",
+ "value": "132000"
+ },
+ {
+ "text": "138 kV",
+ "value": "138000"
+ },
+ {
+ "text": "220 kV",
+ "value": "220000"
+ },
+ {
+ "text": "380 kV",
+ "value": "380000"
+ },
+ {
+ "text": "500 kV",
+ "value": "500000"
+ }
+ ],
+ "type": "choice",
+ "description": "",
+ "key": "voltage",
+ "name": "Voltage"
+ },
+ {
+ "choices": [
+ {
+ "text": "9 pin bowling",
+ "value": "9pin"
+ },
+ {
+ "text": "10 pin bowling",
+ "value": "10pin"
+ },
+ {
+ "text": "American football",
+ "value": "american_football"
+ },
+ {
+ "text": "Archery",
+ "value": "archery"
+ },
+ {
+ "text": "Athletics",
+ "value": "athletics"
+ },
+ {
+ "text": "Australian Rules Football",
+ "value": "australian_football"
+ },
+ {
+ "text": "Baseball",
+ "value": "baseball"
+ },
+ {
+ "text": "Basketball",
+ "value": "basketball"
+ },
+ {
+ "text": "Beach volleyball",
+ "value": "beachvolleyball"
+ },
+ {
+ "text": "Boules/petanque/bocci",
+ "value": "boules"
+ },
+ {
+ "text": "Lawn bowls",
+ "value": "bowls"
+ },
+ {
+ "text": "Canadian football",
+ "value": "canadian_football"
+ },
+ {
+ "text": "Chess",
+ "value": "chess"
+ },
+ {
+ "text": "Cricket",
+ "value": "cricket"
+ },
+ {
+ "text": "Cricket nets",
+ "value": "cricket_nets"
+ },
+ {
+ "text": "Croquet",
+ "value": "croquet"
+ },
+ {
+ "text": "Equestrian",
+ "value": "equestrian"
+ },
+ {
+ "text": "Gaelic football",
+ "value": "gaelic_football"
+ },
+ {
+ "text": "Gymnastics",
+ "value": "gymnastics"
+ },
+ {
+ "text": "(Team) handball",
+ "value": "team_handball"
+ },
+ {
+ "text": "(Field) hockey",
+ "value": "hockey"
+ },
+ {
+ "text": "Korball",
+ "value": "korfball"
+ },
+ {
+ "text": "Pelota",
+ "value": "pelota"
+ },
+ {
+ "text": "Rugby league",
+ "value": "rugby_league"
+ },
+ {
+ "text": "Rugby union",
+ "value": "rugby_union"
+ },
+ {
+ "text": "Shooting",
+ "value": "shooting"
+ },
+ {
+ "text": "Ice skating",
+ "value": "skating"
+ },
+ {
+ "text": "Skateboarding",
+ "value": "skateboard"
+ },
+ {
+ "text": "Soccer/football",
+ "value": "soccer"
+ },
+ {
+ "text": "Swimming",
+ "value": "swimming"
+ },
+ {
+ "text": "Table tennis",
+ "value": "table_tennis"
+ },
+ {
+ "text": "Tennis",
+ "value": "tennis"
+ },
+ {
+ "text": "Volleyball",
+ "value": "volleyball"
+ }
+ ],
+ "type": "choice",
+ "description": "The sport that is predominantly played here.",
+ "key": "sport",
+ "name": "Sport"
+ },
+ {
+ "choices": [
+ {
+ "text": "Yes: ramps/elevators/etc",
+ "value": "yes"
+ },
+ {
+ "text": "No: inaccessible to wheelchairs",
+ "value": "no"
+ },
+ {
+ "text": "Limited accessibility",
+ "value": "limited"
+ }
+ ],
+ "type": "choice",
+ "description": "",
+ "key": "wheelchair",
+ "name": "Wheelchair"
+ }
+]
\ No newline at end of file
diff --git a/test/index.html b/test/index.html
index 4bdd4b5ba..d8d3f8f6f 100644
--- a/test/index.html
+++ b/test/index.html
@@ -68,6 +68,7 @@
+
@@ -137,6 +138,7 @@
+
@@ -170,6 +172,7 @@
+
@@ -182,6 +185,7 @@
+
diff --git a/test/index_packaged.html b/test/index_packaged.html
index 7ae72a056..719a5a999 100644
--- a/test/index_packaged.html
+++ b/test/index_packaged.html
@@ -32,6 +32,7 @@
+
@@ -65,6 +66,7 @@
+
@@ -77,6 +79,7 @@
+
diff --git a/test/spec/actions/add_midpoint.js b/test/spec/actions/add_midpoint.js
new file mode 100644
index 000000000..1f749217e
--- /dev/null
+++ b/test/spec/actions/add_midpoint.js
@@ -0,0 +1,22 @@
+describe("iD.actions.AddMidpoint", function () {
+ it("adds the node at the midpoint location", function () {
+ var node = iD.Node(),
+ midpoint = {loc: [1, 2], ways: []},
+ graph = iD.actions.AddMidpoint(midpoint, node)(iD.Graph());
+
+ expect(graph.entity(node.id).loc).to.eql([1, 2]);
+ });
+
+ it("adds the node to all ways at the respective indexes", function () {
+ var node = iD.Node(),
+ a = iD.Node(),
+ b = iD.Node(),
+ w1 = iD.Way(),
+ w2 = iD.Way({nodes: [a.id, b.id]}),
+ midpoint = {loc: [1, 2], ways: [{id: w1.id, index: 0}, {id: w2.id, index: 1}]},
+ graph = iD.actions.AddMidpoint(midpoint, node)(iD.Graph([a, b, w1, w2]));
+
+ expect(graph.entity(w1.id).nodes).to.eql([node.id]);
+ expect(graph.entity(w2.id).nodes).to.eql([a.id, node.id, b.id]);
+ });
+});
diff --git a/test/spec/actions/delete_way.js b/test/spec/actions/delete_way.js
index 863b0fa2f..d550803fa 100644
--- a/test/spec/actions/delete_way.js
+++ b/test/spec/actions/delete_way.js
@@ -31,8 +31,7 @@ describe("iD.actions.DeleteWay", function () {
expect(graph.entity(node.id)).not.to.be.undefined;
});
- // See #508
- xit("deletes multiple member nodes", function () {
+ it("deletes multiple member nodes", function () {
var a = iD.Node(),
b = iD.Node(),
way = iD.Way({nodes: [a.id, b.id]}),
@@ -42,7 +41,7 @@ describe("iD.actions.DeleteWay", function () {
expect(graph.entity(b.id)).to.be.undefined;
});
- xit("deletes a circular way's start/end node", function () {
+ it("deletes a circular way's start/end node", function () {
var a = iD.Node(),
b = iD.Node(),
c = iD.Node(),
diff --git a/test/spec/connection.js b/test/spec/connection.js
index 2f5d93808..652c1419b 100644
--- a/test/spec/connection.js
+++ b/test/spec/connection.js
@@ -26,24 +26,24 @@ describe('iD.Connection', function () {
c.loadFromURL('data/node.xml', done);
});
- it('returns a graph', function (done) {
+ it('returns an object', function (done) {
c.loadFromURL('data/node.xml', function (err, graph) {
expect(err).to.not.be.ok;
- expect(graph).to.be.instanceOf(iD.Graph);
+ expect(typeof graph).to.eql('object');
done();
});
});
it('parses a node', function (done) {
c.loadFromURL('data/node.xml', function (err, graph) {
- expect(graph.entity('n356552551')).to.be.instanceOf(iD.Entity);
+ expect(graph.n356552551).to.be.instanceOf(iD.Entity);
done();
});
});
it('parses a way', function (done) {
c.loadFromURL('data/way.xml', function (err, graph) {
- expect(graph.entity('w19698713')).to.be.instanceOf(iD.Entity);
+ expect(graph.w19698713).to.be.instanceOf(iD.Entity);
done();
});
});
diff --git a/test/spec/geo.js b/test/spec/geo.js
new file mode 100644
index 000000000..113d4795d
--- /dev/null
+++ b/test/spec/geo.js
@@ -0,0 +1,96 @@
+describe('iD.geo', function() {
+ describe('.roundCoords', function() {
+ expect(iD.geo.roundCoords([0.1, 1])).to.eql([0, 1]);
+ expect(iD.geo.roundCoords([0, 1])).to.eql([0, 1]);
+ expect(iD.geo.roundCoords([0, 1.1])).to.eql([0, 1]);
+ });
+
+ describe('.interp', function() {
+ it('interpolates halfway', function() {
+ var a = [0, 0],
+ b = [10, 10];
+ expect(iD.geo.interp(a, b, 0.5)).to.eql([5, 5]);
+ });
+ it('interpolates to one side', function() {
+ var a = [0, 0],
+ b = [10, 10];
+ expect(iD.geo.interp(a, b, 0)).to.eql([0, 0]);
+ });
+ });
+
+ describe('.dist', function() {
+ it('distance between two same points is zero', function() {
+ var a = [0, 0],
+ b = [0, 0];
+ expect(iD.geo.dist(a, b)).to.eql(0);
+ });
+ it('a straight 10 unit line is 10', function() {
+ var a = [0, 0],
+ b = [10, 0];
+ expect(iD.geo.dist(a, b)).to.eql(10);
+ });
+ it('a pythagorean triangle is right', function() {
+ var a = [0, 0],
+ b = [4, 3];
+ expect(iD.geo.dist(a, b)).to.eql(5);
+ });
+ });
+
+ describe('.pointInPolygon', function() {
+ it('says a point in a polygon is on a polygon', function() {
+ var poly = [[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]];
+ var point = [0.5, 0.5];
+ expect(iD.geo.pointInPolygon(point, poly)).to.be.true;
+ });
+ it('says a point outside of a polygon is outside', function() {
+ var poly = [
+ [0, 0],
+ [0, 1],
+ [1, 1],
+ [1, 0],
+ [0, 0]];
+ var point = [0.5, 1.5];
+ expect(iD.geo.pointInPolygon(point, poly)).to.be.false;
+ });
+ });
+
+ describe('.polygonContainsPolygon', function() {
+ it('says a polygon in a polygon is in', function() {
+ var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]];
+ var inner = [[1, 1], [1, 2], [2, 2], [2, 1], [1, 1]];
+ expect(iD.geo.polygonContainsPolygon(outer, inner)).to.be.true;
+ });
+ it('says a polygon outside of a polygon is out', function() {
+ var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]];
+ var inner = [[1, 1], [1, 9], [2, 2], [2, 1], [1, 1]];
+ expect(iD.geo.polygonContainsPolygon(outer, inner)).to.be.false;
+ });
+ });
+
+ describe('.polygonIntersectsPolygon', function() {
+ it('says a polygon in a polygon intersects it', function() {
+ var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]];
+ var inner = [[1, 1], [1, 2], [2, 2], [2, 1], [1, 1]];
+ expect(iD.geo.polygonIntersectsPolygon(outer, inner)).to.be.true;
+ });
+
+ it('says a polygon that partially intersects does', function() {
+ var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]];
+ var inner = [[-1, -1], [1, 2], [2, 2], [2, 1], [1, 1]];
+ expect(iD.geo.polygonIntersectsPolygon(outer, inner)).to.be.true;
+ });
+
+ it('says totally disjoint polygons do not intersect', function() {
+ var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]];
+ var inner = [[-1, -1], [-1, -2], [-2, -2], [-2, -1], [-1, -1]];
+ expect(iD.geo.polygonIntersectsPolygon(outer, inner)).to.be.false;
+ });
+ });
+
+ describe('.pathLength', function() {
+ it('calculates a simple path length', function() {
+ var path = [[0, 0], [0, 1], [3, 5]];
+ expect(iD.geo.pathLength(path)).to.eql(6);
+ });
+ });
+});
diff --git a/test/spec/graph/graph.js b/test/spec/graph/graph.js
index 25382e7c9..327d66295 100644
--- a/test/spec/graph/graph.js
+++ b/test/spec/graph/graph.js
@@ -1,25 +1,164 @@
describe('iD.Graph', function() {
- it("can be constructed with an entities Object", function () {
- var entity = iD.Entity(),
- graph = iD.Graph({'n-1': entity});
- expect(graph.entity('n-1')).to.equal(entity);
- });
-
- it("can be constructed with an entities Array", function () {
- var entity = iD.Entity(),
- graph = iD.Graph([entity]);
- expect(graph.entity(entity.id)).to.equal(entity);
- });
-
- if (iD.debug) {
- it("is frozen", function () {
- expect(Object.isFrozen(iD.Graph())).to.be.true;
+ describe("constructor", function () {
+ it("accepts an entities Object", function () {
+ var entity = iD.Entity(),
+ graph = iD.Graph({'n-1': entity});
+ expect(graph.entity('n-1')).to.equal(entity);
});
- it("freezes entities", function () {
- expect(Object.isFrozen(iD.Graph().entities)).to.be.true;
+ it("accepts an entities Array", function () {
+ var entity = iD.Entity(),
+ graph = iD.Graph([entity]);
+ expect(graph.entity(entity.id)).to.equal(entity);
});
- }
+
+ it("accepts a Graph", function () {
+ var entity = iD.Entity(),
+ graph = iD.Graph(iD.Graph([entity]));
+ expect(graph.entity(entity.id)).to.equal(entity);
+ });
+
+ it("copies other's entities", function () {
+ var entity = iD.Entity(),
+ base = iD.Graph([entity]),
+ graph = iD.Graph(base);
+ expect(graph.entities).not.to.equal(base.entities);
+ });
+
+ it("rebases on other's base", function () {
+ var base = iD.Graph(),
+ graph = iD.Graph(base);
+ expect(graph.base().entities).to.equal(base.base().entities);
+ });
+
+ it("freezes by default", function () {
+ expect(iD.Graph().frozen).to.be.true;
+ });
+
+ it("remains mutable if passed true as second argument", function () {
+ expect(iD.Graph([], true).frozen).not.to.be.true;
+ });
+ });
+
+ describe("#freeze", function () {
+ it("sets the frozen flag", function () {
+ expect(iD.Graph([], true).freeze().frozen).to.be.true;
+ });
+
+ if (iD.debug) {
+ it("freezes entities", function () {
+ expect(Object.isFrozen(iD.Graph().entities)).to.be.true;
+ });
+ }
+ });
+
+ describe("#rebase", function () {
+ it("preserves existing entities", function () {
+ var node = iD.Node({id: 'n'}),
+ graph = iD.Graph([node]);
+ graph.rebase({});
+ expect(graph.entity('n')).to.equal(node);
+ });
+
+ it("includes new entities", function () {
+ var node = iD.Node({id: 'n'}),
+ graph = iD.Graph();
+ graph.rebase({'n': node});
+ expect(graph.entity('n')).to.equal(node);
+ });
+
+ it("gives precedence to existing entities", function () {
+ var a = iD.Node({id: 'n'}),
+ b = iD.Node({id: 'n'}),
+ graph = iD.Graph([a]);
+ graph.rebase({'n': b});
+ expect(graph.entity('n')).to.equal(a);
+ });
+
+ it("inherits entities from base prototypally", function () {
+ var graph = iD.Graph();
+ graph.rebase({'n': iD.Node()});
+ expect(graph.entities).not.to.have.ownProperty('n');
+ });
+
+ it("updates parentWays", function () {
+ var n = iD.Node({id: 'n'}),
+ w1 = iD.Way({id: 'w1', nodes: ['n']}),
+ w2 = iD.Way({id: 'w2', nodes: ['n']}),
+ graph = iD.Graph([n, w1]);
+
+ graph.rebase({ 'w2': w2 });
+ expect(graph.parentWays(n)).to.eql([w1, w2]);
+ expect(graph._parentWays.hasOwnProperty('n')).to.be.false;
+ });
+
+ it("avoids adding duplicate parentWays", function () {
+ var n = iD.Node({id: 'n'}),
+ w1 = iD.Way({id: 'w1', nodes: ['n']}),
+ graph = iD.Graph([n, w1]);
+ graph.rebase({ 'w1': w1 });
+ expect(graph.parentWays(n)).to.eql([w1]);
+ });
+
+ it("updates parentWays for nodes with modified parentWays", function () {
+ var n = iD.Node({id: 'n'}),
+ w1 = iD.Way({id: 'w1', nodes: ['n']}),
+ w2 = iD.Way({id: 'w2', nodes: ['n']}),
+ w3 = iD.Way({id: 'w3', nodes: ['n']}),
+ graph = iD.Graph([n, w1]),
+ graph2 = graph.replace(w2);
+ graph.rebase({ 'w3': w3 });
+ graph2.rebase({ 'w3': w3 });
+
+ expect(graph2.parentWays(n)).to.eql([w1, w2, w3]);
+ });
+
+ it("avoids re-adding removed parentWays", function() {
+ var n = iD.Node({id: 'n'}),
+ w1 = iD.Way({id: 'w1', nodes: ['n']}),
+ graph = iD.Graph([n, w1]),
+ graph2 = graph.remove(w1);
+ graph.rebase({ 'w1': w1 });
+ graph2.rebase({ 'w1': w1 });
+ expect(graph2.parentWays(n)).to.eql([]);
+ });
+
+ it("updates parentRelations", function () {
+ var n = iD.Node({id: 'n'}),
+ r1 = iD.Relation({id: 'r1', members: [{id: 'n'}]}),
+ r2 = iD.Relation({id: 'r2', members: [{id: 'n'}]}),
+ graph = iD.Graph([n, r1]);
+
+ graph.rebase({'r2': r2});
+
+ expect(graph.parentRelations(n)).to.eql([r1, r2]);
+ expect(graph._parentRels.hasOwnProperty('n')).to.be.false;
+ });
+
+ it("avoids re-adding removed parentRels", function() {
+ var n = iD.Node({id: 'n'}),
+ r1 = iD.Relation({id: 'r1', members: [{id: 'n'}]}),
+ graph = iD.Graph([n, r1]),
+ graph2 = graph.remove(r1);
+ graph.rebase({ 'w1': r1 });
+ graph2.rebase({ 'w1': r1 });
+ expect(graph2.parentWays(n)).to.eql([]);
+ });
+
+ it("updates parentRels for nodes with modified parentWays", function () {
+ var n = iD.Node({id: 'n'}),
+ r1 = iD.Relation({id: 'r1', members: [{id: 'n'}]}),
+ r2 = iD.Relation({id: 'r2', members: [{id: 'n'}]}),
+ r3 = iD.Relation({id: 'r3', members: [{id: 'n'}]}),
+ graph = iD.Graph([n, r1]),
+ graph2 = graph.replace(r2);
+
+ graph.rebase({'r3': r3});
+ graph2.rebase({'r3': r3});
+ expect(graph2.parentRelations(n)).to.eql([r1, r2, r3]);
+ });
+
+ });
describe("#remove", function () {
it("returns a new graph", function () {
@@ -40,6 +179,20 @@ describe('iD.Graph', function() {
graph = iD.Graph([node]);
expect(graph.remove(node).entity(node.id)).to.be.undefined;
});
+
+ it("removes the entity as a parentWay", function () {
+ var node = iD.Node({id: 'n' }),
+ w1 = iD.Way({id: 'w', nodes: ['n']}),
+ graph = iD.Graph([node, w1]);
+ expect(graph.remove(w1).parentWays(node)).to.eql([]);
+ });
+
+ it("removes the entity as a parentRelation", function () {
+ var node = iD.Node({id: 'n' }),
+ r1 = iD.Relation({id: 'w', members: [{id: 'n' }]}),
+ graph = iD.Graph([node, r1]);
+ expect(graph.remove(r1).parentRelations(node)).to.eql([]);
+ });
});
describe("#replace", function () {
@@ -62,6 +215,49 @@ describe('iD.Graph', function() {
graph = iD.Graph([node1]);
expect(graph.replace(node2).entity(node2.id)).to.equal(node2);
});
+
+ it("adds parentWays", function () {
+ var node = iD.Node({id: 'n' }),
+ w1 = iD.Way({id: 'w', nodes: ['n']}),
+ graph = iD.Graph([node]);
+ expect(graph.replace(w1).parentWays(node)).to.eql([w1]);
+ });
+
+ it("removes parentWays", function () {
+ var node = iD.Node({id: 'n' }),
+ w1 = iD.Way({id: 'w', nodes: ['n']}),
+ graph = iD.Graph([node, w1]);
+ expect(graph.remove(w1).parentWays(node)).to.eql([]);
+ });
+
+ it("doesn't add duplicate parentWays", function () {
+ var node = iD.Node({id: 'n' }),
+ w1 = iD.Way({id: 'w', nodes: ['n']}),
+ graph = iD.Graph([node, w1]);
+ expect(graph.replace(w1).parentWays(node)).to.eql([w1]);
+ });
+
+ it("adds parentRels", function () {
+ var node = iD.Node({id: 'n' }),
+ r1 = iD.Relation({id: 'w', members: [{id: 'n'}]}),
+ graph = iD.Graph([node]);
+ expect(graph.replace(r1).parentRelations(node)).to.eql([r1]);
+ });
+
+ it("removes parentRelations", function () {
+ var node = iD.Node({id: 'n' }),
+ r1 = iD.Relation({id: 'w', members: [{id: 'n'}]}),
+ graph = iD.Graph([node, r1]);
+ expect(graph.remove(r1).parentRelations(node)).to.eql([]);
+ });
+
+ it("doesn't add duplicate parentRelations", function () {
+ var node = iD.Node({id: 'n' }),
+ r1 = iD.Relation({id: 'w', members: [{id: 'n'}]}),
+ graph = iD.Graph([node, r1]);
+ expect(graph.replace(r1).parentRelations(node)).to.eql([r1]);
+ });
+
});
describe("#update", function () {
@@ -159,18 +355,18 @@ describe('iD.Graph', function() {
describe("#modified", function () {
it("returns an Array of ids of modified entities", function () {
- var node1 = iD.Node({id: 'n1', _updated: true}),
- node2 = iD.Node({id: 'n2'}),
- graph = iD.Graph([node1, node2]);
- expect(graph.modified()).to.eql([node1.id]);
+ var node = iD.Node({id: 'n1'}),
+ node_ = iD.Node({id: 'n1'}),
+ graph = iD.Graph([node]).replace(node_);
+ expect(graph.modified()).to.eql([node.id]);
});
});
describe("#created", function () {
it("returns an Array of ids of created entities", function () {
- var node1 = iD.Node({id: 'n-1', _updated: true}),
+ var node1 = iD.Node({id: 'n-1'}),
node2 = iD.Node({id: 'n2'}),
- graph = iD.Graph([node1, node2]);
+ graph = iD.Graph([node2]).replace(node1);
expect(graph.created()).to.eql([node1.id]);
});
});
@@ -185,7 +381,7 @@ describe('iD.Graph', function() {
it("doesn't include created entities that were subsequently deleted", function () {
var node = iD.Node(),
- graph = iD.Graph([node]).remove(node);
+ graph = iD.Graph().replace(node).remove(node);
expect(graph.deleted()).to.eql([]);
});
});
diff --git a/test/spec/graph/history.js b/test/spec/graph/history.js
index b59190c0f..dc685b446 100644
--- a/test/spec/graph/history.js
+++ b/test/spec/graph/history.js
@@ -153,17 +153,15 @@ describe("iD.History", function () {
it("includes modified entities", function () {
var node1 = iD.Node({id: "n1"}),
- node2 = node1.update({}),
- graph = iD.Graph([node1]);
- history.merge(graph);
+ node2 = node1.update({});
+ history.merge({ n1: node1});
history.perform(function (graph) { return graph.replace(node2); });
expect(history.changes().modified).to.eql([node2]);
});
it("includes deleted entities", function () {
- var node = iD.Node({id: "n1"}),
- graph = iD.Graph([node]);
- history.merge(graph);
+ var node = iD.Node({id: "n1"});
+ history.merge({ n1: node });
history.perform(function (graph) { return graph.remove(node); });
expect(history.changes().deleted).to.eql([node]);
});
@@ -189,7 +187,7 @@ describe("iD.History", function () {
it("is the sum of all types of changes", function() {
var node1 = iD.Node({id: "n1"}),
node2 = iD.Node();
- history.merge(iD.Graph([node1]));
+ history.merge({ n1: node1 });
history.perform(function (graph) { return graph.remove(node1); });
expect(history.numChanges()).to.eql(1);
history.perform(function (graph) { return graph.replace(node2); });
diff --git a/test/spec/svg/midpoints.js b/test/spec/svg/midpoints.js
new file mode 100644
index 000000000..c34ac0350
--- /dev/null
+++ b/test/spec/svg/midpoints.js
@@ -0,0 +1,52 @@
+describe("iD.svg.Midpoints", function () {
+ var surface,
+ projection = Object,
+ filter = d3.functor(true);
+
+ beforeEach(function () {
+ surface = d3.select(document.createElementNS('http://www.w3.org/2000/svg', 'svg'))
+ .call(iD.svg.Surface());
+ });
+
+ it("finds the location of the midpoints", function () {
+ var a = iD.Node({loc: [0, 0]}),
+ b = iD.Node({loc: [50, 0]}),
+ line = iD.Way({nodes: [a.id, b.id]}),
+ graph = iD.Graph([a, b, line]);
+
+ surface.call(iD.svg.Midpoints(projection), graph, [line], filter);
+
+ expect(surface.select('.midpoint').datum().loc).to.eql([25, 0]);
+ });
+
+ it("doesn't create midpoints on segments with pixel length less than 40", function () {
+ var a = iD.Node({loc: [0, 0]}),
+ b = iD.Node({loc: [39, 0]}),
+ line = iD.Way({nodes: [a.id, b.id]}),
+ graph = iD.Graph([a, b, line]);
+
+ surface.call(iD.svg.Midpoints(projection), graph, [line], filter);
+
+ expect(surface.selectAll('.midpoint')[0]).to.have.length(0);
+ });
+
+ it("binds a datum whose 'ways' property lists ways which include the segement", function () {
+ var a = iD.Node({loc: [0, 0]}),
+ b = iD.Node({loc: [50, 0]}),
+ c = iD.Node({loc: [1, 1]}),
+ d = iD.Node({loc: [2, 2]}),
+ l1 = iD.Way({nodes: [a.id, b.id]}),
+ l2 = iD.Way({nodes: [b.id, a.id]}),
+ l3 = iD.Way({nodes: [c.id, a.id, b.id, d.id]}),
+ l4 = iD.Way({nodes: [a.id, d.id, b.id]}),
+ graph = iD.Graph([a, b, c, d, l1, l2, l3, l4]),
+ ab = function (d) { return d.id === [a.id, b.id].sort().join("-"); };
+
+ surface.call(iD.svg.Midpoints(projection), graph, [l1, l2, l3, l4], filter);
+
+ expect(surface.selectAll('.midpoint').filter(ab).datum().ways).to.eql([
+ {id: l1.id, index: 1},
+ {id: l2.id, index: 1},
+ {id: l3.id, index: 2}]);
+ });
+});
diff --git a/test/spec/util.js b/test/spec/util.js
index 9d5cf082b..de3fab735 100644
--- a/test/spec/util.js
+++ b/test/spec/util.js
@@ -1,123 +1,24 @@
describe('iD.Util', function() {
- var util;
-
- it('#trueObj', function() {
+ it('.trueObj', function() {
expect(iD.util.trueObj(['a', 'b', 'c'])).to.eql({ a: true, b: true, c: true });
expect(iD.util.trueObj([])).to.eql({});
});
- it('#tagText', function() {
+ it('.tagText', function() {
expect(iD.util.tagText({})).to.eql('');
expect(iD.util.tagText({tags:{foo:'bar'}})).to.eql('foo: bar');
expect(iD.util.tagText({tags:{foo:'bar',two:'three'}})).to.eql('foo: bar\ntwo: three');
});
- it('#stringQs', function() {
+ it('.stringQs', function() {
expect(iD.util.stringQs('foo=bar')).to.eql({foo: 'bar'});
expect(iD.util.stringQs('foo=bar&one=2')).to.eql({foo: 'bar', one: '2' });
expect(iD.util.stringQs('')).to.eql({});
});
- it('#qsString', function() {
+ it('.qsString', function() {
expect(iD.util.qsString({ foo: 'bar' })).to.eql('foo=bar');
expect(iD.util.qsString({ foo: 'bar', one: 2 })).to.eql('foo=bar&one=2');
expect(iD.util.qsString({})).to.eql('');
});
-
- describe('geo', function() {
- describe('#roundCoords', function() {
- expect(iD.geo.roundCoords([0.1, 1])).to.eql([0, 1]);
- expect(iD.geo.roundCoords([0, 1])).to.eql([0, 1]);
- expect(iD.geo.roundCoords([0, 1.1])).to.eql([0, 1]);
- });
-
- describe('#interp', function() {
- it('interpolates halfway', function() {
- var a = [0, 0],
- b = [10, 10];
- expect(iD.geo.interp(a, b, 0.5)).to.eql([5, 5]);
- });
- it('interpolates to one side', function() {
- var a = [0, 0],
- b = [10, 10];
- expect(iD.geo.interp(a, b, 0)).to.eql([0, 0]);
- });
- });
-
- describe('#dist', function() {
- it('distance between two same points is zero', function() {
- var a = [0, 0],
- b = [0, 0];
- expect(iD.geo.dist(a, b)).to.eql(0);
- });
- it('a straight 10 unit line is 10', function() {
- var a = [0, 0],
- b = [10, 0];
- expect(iD.geo.dist(a, b)).to.eql(10);
- });
- it('a pythagorean triangle is right', function() {
- var a = [0, 0],
- b = [4, 3];
- expect(iD.geo.dist(a, b)).to.eql(5);
- });
- });
-
- describe('#pointInPolygon', function() {
- it('says a point in a polygon is on a polygon', function() {
- var poly = [[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]];
- var point = [0.5, 0.5];
- expect(iD.geo.pointInPolygon(point, poly)).to.be.true;
- });
- it('says a point outside of a polygon is outside', function() {
- var poly = [
- [0, 0],
- [0, 1],
- [1, 1],
- [1, 0],
- [0, 0]];
- var point = [0.5, 1.5];
- expect(iD.geo.pointInPolygon(point, poly)).to.be.false;
- });
- });
-
- describe('#polygonContainsPolygon', function() {
- it('says a polygon in a polygon is in', function() {
- var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]];
- var inner = [[1, 1], [1, 2], [2, 2], [2, 1], [1, 1]];
- expect(iD.geo.polygonContainsPolygon(outer, inner)).to.be.true;
- });
- it('says a polygon outside of a polygon is out', function() {
- var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]];
- var inner = [[1, 1], [1, 9], [2, 2], [2, 1], [1, 1]];
- expect(iD.geo.polygonContainsPolygon(outer, inner)).to.be.false;
- });
- });
-
- describe('#polygonIntersectsPolygon', function() {
- it('says a polygon in a polygon intersects it', function() {
- var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]];
- var inner = [[1, 1], [1, 2], [2, 2], [2, 1], [1, 1]];
- expect(iD.geo.polygonIntersectsPolygon(outer, inner)).to.be.true;
- });
-
- it('says a polygon that partially intersects does', function() {
- var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]];
- var inner = [[-1, -1], [1, 2], [2, 2], [2, 1], [1, 1]];
- expect(iD.geo.polygonIntersectsPolygon(outer, inner)).to.be.true;
- });
-
- it('says totally disjoint polygons do not intersect', function() {
- var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]];
- var inner = [[-1, -1], [-1, -2], [-2, -2], [-2, -1], [-1, -1]];
- expect(iD.geo.polygonIntersectsPolygon(outer, inner)).to.be.false;
- });
- });
-
- describe('#pathLength', function() {
- it('calculates a simple path length', function() {
- var path = [[0, 0], [0, 1], [3, 5]];
- expect(iD.geo.pathLength(path)).to.eql(6);
- });
- });
- });
});