From 0cc2d629853a5d51bab5ccc45159d4b6dae4e5a8 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Tue, 5 Feb 2013 12:14:37 -0500 Subject: [PATCH 01/30] Update onway arrow font size --- js/id/svg/lines.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/js/id/svg/lines.js b/js/id/svg/lines.js index 624237d93..1bfac34b1 100644 --- a/js/id/svg/lines.js +++ b/js/id/svg/lines.js @@ -56,7 +56,9 @@ iD.svg.Lines = function(projection) { } if (!alength) { - var arrow = surface.append('text').text(arrowtext); + var arrow = surface.append('text') + .text(arrowtext) + .style('font-size', 7); alength = arrow.node().getComputedTextLength(); arrow.remove(); } From 675c39187c1fb1ad18498efffddf60a8b8590e9c Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Tue, 5 Feb 2013 09:26:43 -0800 Subject: [PATCH 02/30] Fix point dragging --- js/id/behavior/drag_node.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/id/behavior/drag_node.js b/js/id/behavior/drag_node.js index 92318756c..6df1fbf28 100644 --- a/js/id/behavior/drag_node.js +++ b/js/id/behavior/drag_node.js @@ -66,7 +66,7 @@ iD.behavior.DragNode = function(context) { var loc = context.map().mouseCoordinates(); var d = datum(); - if (d.type === 'node') { + if (d.type === 'node' && d.id !== entity.id) { loc = d.loc; } else if (d.type === 'way') { loc = iD.geo.chooseIndex(d, d3.mouse(context.surface().node()), context).loc; From 7086012b98bb85badd93b40b34f99e9a14af88a8 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Tue, 5 Feb 2013 13:05:12 -0500 Subject: [PATCH 03/30] Source oneway arrow styles from css --- js/id/svg/lines.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/js/id/svg/lines.js b/js/id/svg/lines.js index 1bfac34b1..a1151204b 100644 --- a/js/id/svg/lines.js +++ b/js/id/svg/lines.js @@ -56,11 +56,13 @@ iD.svg.Lines = function(projection) { } if (!alength) { - var arrow = surface.append('text') - .text(arrowtext) - .style('font-size', 7); + var container = surface.append('g') + .attr('class', 'oneway'), + arrow = container.append('text') + .attr('class', 'textpath') + .text(arrowtext); alength = arrow.node().getComputedTextLength(); - arrow.remove(); + container.remove(); } var lines = [], From 2d4f4772651750454b8a57843d6b3d2b49f66a6c Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Tue, 5 Feb 2013 14:11:03 -0500 Subject: [PATCH 04/30] dblclick a shared way adds vertex to all ways --- js/id/modes/select.js | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/js/id/modes/select.js b/js/id/modes/select.js index 99fef11ab..6d28413cf 100644 --- a/js/id/modes/select.js +++ b/js/id/modes/select.js @@ -113,9 +113,28 @@ iD.modes.Select = function(context, selection, initial) { d3.mouse(context.surface().node()), context), node = iD.Node({ loc: choice.loc }); - context.perform( - iD.actions.AddEntity(node), - iD.actions.AddVertex(datum.id, node.id, choice.index), + var prev = datum.nodes[choice.index - 1], + next = datum.nodes[choice.index], + prevParents = context.graph().parentWays({ id: prev }); + + context.perform(iD.actions.AddEntity(node)); + + for (var i = 0; i < prevParents.length; i++) { + var p = prevParents[i]; + for (var k = 0; k < p.nodes.length; k++) { + if (p.nodes[k] === prev) { + if (p.nodes[k-1] === next) { + context.perform(iD.actions.AddVertex(p.id, node.id, k)); + break; + } else if (p.nodes[k+1] === next) { + context.perform(iD.actions.AddVertex(p.id, node.id, k+1)); + break; + } + } + } + } + + context.perform(iD.actions.Noop(), t('operations.add.annotation.vertex')); d3.event.preventDefault(); From bc6238c2be05b8f0111f50c7ae5a04addb62bc22 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Tue, 5 Feb 2013 14:52:05 -0500 Subject: [PATCH 05/30] Use AddMidpoint when doubleclicking on way --- js/id/modes/select.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/js/id/modes/select.js b/js/id/modes/select.js index 6d28413cf..3b0ad82dd 100644 --- a/js/id/modes/select.js +++ b/js/id/modes/select.js @@ -115,26 +115,27 @@ iD.modes.Select = function(context, selection, initial) { var prev = datum.nodes[choice.index - 1], next = datum.nodes[choice.index], - prevParents = context.graph().parentWays({ id: prev }); + prevParents = context.graph().parentWays({ id: prev }), + ways = []; - context.perform(iD.actions.AddEntity(node)); for (var i = 0; i < prevParents.length; i++) { var p = prevParents[i]; for (var k = 0; k < p.nodes.length; k++) { if (p.nodes[k] === prev) { if (p.nodes[k-1] === next) { - context.perform(iD.actions.AddVertex(p.id, node.id, k)); + ways.push({ id: p.id, index: k}); break; } else if (p.nodes[k+1] === next) { - context.perform(iD.actions.AddVertex(p.id, node.id, k+1)); + ways.push({ id: p.id, index: k+1}); break; } } } } - context.perform(iD.actions.Noop(), + context.perform(iD.actions.AddEntity(node), + iD.actions.AddMidpoint({ ways: ways, loc: node.loc }, node), t('operations.add.annotation.vertex')); d3.event.preventDefault(); From 67632d638f79fee02bf9abb221a87b7486b53393 Mon Sep 17 00:00:00 2001 From: Tom MacWright Date: Tue, 5 Feb 2013 15:27:02 -0500 Subject: [PATCH 06/30] Soft clicking. Feels pretty nice. Refs #530 --- js/id/behavior/drag.js | 18 ++++++++-------- js/id/behavior/draw.js | 45 ++++++++++++++++++++++++++-------------- js/id/behavior/select.js | 1 + 3 files changed, 39 insertions(+), 25 deletions(-) diff --git a/js/id/behavior/drag.js b/js/id/behavior/drag.js index d88e8a44a..cd6f607bf 100644 --- a/js/id/behavior/drag.js +++ b/js/id/behavior/drag.js @@ -14,7 +14,7 @@ * Delegation is supported via the `delegate` function. */ -iD.behavior.drag = function () { +iD.behavior.drag = function() { function d3_eventCancel() { d3.event.stopPropagation(); d3.event.preventDefault(); @@ -50,21 +50,21 @@ iD.behavior.drag = function () { moved = 0; var w = d3.select(window) - .on(touchId != null ? "touchmove.drag-" + touchId : "mousemove.drag", dragmove) - .on(touchId != null ? "touchend.drag-" + touchId : "mouseup.drag", dragend, true); + .on(touchId !== null ? "touchmove.drag-" + touchId : "mousemove.drag", dragmove) + .on(touchId !== null ? "touchend.drag-" + touchId : "mouseup.drag", dragend, true); if (origin) { offset = origin.apply(target, arguments); - offset = [ offset[0] - origin_[0], offset[1] - origin_[1] ]; + offset = [offset[0] - origin_[0], offset[1] - origin_[1]]; } else { - offset = [ 0, 0 ]; + offset = [0, 0]; } - if (touchId == null) d3_eventCancel(); + if (touchId === null) d3_eventCancel(); function point() { var p = target.parentNode; - return touchId != null ? d3.touches(p).filter(function (p) { + return touchId !== null ? d3.touches(p).filter(function(p) { return p.identifier === touchId; })[0] : d3.mouse(p); } @@ -103,8 +103,8 @@ iD.behavior.drag = function () { if (d3.event.target === eventTarget) w.on("click.drag", click, true); } - w.on(touchId != null ? "touchmove.drag-" + touchId : "mousemove.drag", null) - .on(touchId != null ? "touchend.drag-" + touchId : "mouseup.drag", null); + w.on(touchId !== null ? "touchmove.drag-" + touchId : "mousemove.drag", null) + .on(touchId !== null ? "touchend.drag-" + touchId : "mouseup.drag", null); } function click() { diff --git a/js/id/behavior/draw.js b/js/id/behavior/draw.js index 656eb7988..078e257b2 100644 --- a/js/id/behavior/draw.js +++ b/js/id/behavior/draw.js @@ -1,24 +1,39 @@ iD.behavior.Draw = function(context) { - var event = d3.dispatch('move', 'click', 'clickWay', 'clickNode', 'undo', 'cancel', 'finish'), + var event = d3.dispatch('move', 'click', 'clickWay', + 'clickNode', 'undo', 'cancel', 'finish'), keybinding = d3.keybinding('draw'), - hover = iD.behavior.Hover(); + hover = iD.behavior.Hover(), + tolerance = 8; function datum() { - if (d3.event.altKey) { - return {}; - } else { - return d3.event.target.__data__ || {}; - } + if (d3.event.altKey) return {}; + else return d3.event.target.__data__ || {}; } function mousedown() { - var selection = d3.select(this); - selection.on('mousemove.draw', null); - d3.select(window) - .on('mouseup.draw', function() { - selection.on('mousemove.draw', mousemove); + function point() { + var p = target.node().parentNode; + return touchId !== null ? d3.touches(p).filter(function(p) { + return p.identifier === touchId; + })[0] : d3.mouse(p); + } + + var target = d3.select(this), + touchId = d3.event.touches ? d3.event.changedTouches[0].identifier : null, + time = +new Date(), + pos = point(); + + target.on('mousemove.draw', null); + + d3.select(window).on('mouseup.draw', function() { + target.on('mousemove.draw', mousemove); + if (iD.geo.dist(pos, point()) < tolerance || + (+new Date() - time) < 500) { + click(); + } }); + } function mousemove() { @@ -77,8 +92,7 @@ iD.behavior.Draw = function(context) { selection .on('mousedown.draw', mousedown) - .on('mousemove.draw', mousemove) - .on('click.draw', click); + .on('mousemove.draw', mousemove); d3.select(document) .call(keybinding) @@ -93,8 +107,7 @@ iD.behavior.Draw = function(context) { selection .on('mousedown.draw', null) - .on('mousemove.draw', null) - .on('click.draw', null); + .on('mousemove.draw', null); d3.select(window).on('mouseup.draw', null); diff --git a/js/id/behavior/select.js b/js/id/behavior/select.js index b5276a4d0..f7a388396 100644 --- a/js/id/behavior/select.js +++ b/js/id/behavior/select.js @@ -1,4 +1,5 @@ iD.behavior.Select = function(context) { + function click() { var datum = d3.select(d3.event.target).datum(); if (datum instanceof iD.Entity) { From f65803d99f7d0a97e9ac7dbacceee470b3a4686f Mon Sep 17 00:00:00 2001 From: Tom MacWright Date: Tue, 5 Feb 2013 15:39:58 -0500 Subject: [PATCH 07/30] Make restriction both space and time --- js/id/behavior/draw.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/id/behavior/draw.js b/js/id/behavior/draw.js index 078e257b2..e482138ec 100644 --- a/js/id/behavior/draw.js +++ b/js/id/behavior/draw.js @@ -28,8 +28,8 @@ iD.behavior.Draw = function(context) { d3.select(window).on('mouseup.draw', function() { target.on('mousemove.draw', mousemove); - if (iD.geo.dist(pos, point()) < tolerance || - (+new Date() - time) < 500) { + if (iD.geo.dist(pos, point()) < tolerance && + (+new Date() - time) < 1000) { click(); } }); From 0e0ca2382ebee689db8b68254152edc158d83273 Mon Sep 17 00:00:00 2001 From: Tom MacWright Date: Tue, 5 Feb 2013 15:40:23 -0500 Subject: [PATCH 08/30] Half-second --- js/id/behavior/draw.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/id/behavior/draw.js b/js/id/behavior/draw.js index e482138ec..d4aac636d 100644 --- a/js/id/behavior/draw.js +++ b/js/id/behavior/draw.js @@ -29,7 +29,7 @@ iD.behavior.Draw = function(context) { d3.select(window).on('mouseup.draw', function() { target.on('mousemove.draw', mousemove); if (iD.geo.dist(pos, point()) < tolerance && - (+new Date() - time) < 1000) { + (+new Date() - time) < 500) { click(); } }); From 9743fdf4773c0806af9fc728170a54f71ae52f15 Mon Sep 17 00:00:00 2001 From: Tom MacWright Date: Tue, 5 Feb 2013 15:41:56 -0500 Subject: [PATCH 09/30] Up the radius tolerance to 12px --- js/id/behavior/draw.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/id/behavior/draw.js b/js/id/behavior/draw.js index d4aac636d..b614763b4 100644 --- a/js/id/behavior/draw.js +++ b/js/id/behavior/draw.js @@ -3,7 +3,7 @@ iD.behavior.Draw = function(context) { 'clickNode', 'undo', 'cancel', 'finish'), keybinding = d3.keybinding('draw'), hover = iD.behavior.Hover(), - tolerance = 8; + tolerance = 12; function datum() { if (d3.event.altKey) return {}; From 4a024651b40e668c2cdcdfefa4c316d1bacaa6a7 Mon Sep 17 00:00:00 2001 From: Tom MacWright Date: Tue, 5 Feb 2013 15:44:50 -0500 Subject: [PATCH 10/30] Allow long clicks --- js/id/behavior/draw.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/js/id/behavior/draw.js b/js/id/behavior/draw.js index b614763b4..e68625d05 100644 --- a/js/id/behavior/draw.js +++ b/js/id/behavior/draw.js @@ -3,6 +3,7 @@ iD.behavior.Draw = function(context) { 'clickNode', 'undo', 'cancel', 'finish'), keybinding = d3.keybinding('draw'), hover = iD.behavior.Hover(), + closeTolerance = 4; tolerance = 12; function datum() { @@ -28,8 +29,9 @@ iD.behavior.Draw = function(context) { d3.select(window).on('mouseup.draw', function() { target.on('mousemove.draw', mousemove); - if (iD.geo.dist(pos, point()) < tolerance && - (+new Date() - time) < 500) { + if (iD.geo.dist(pos, point()) < closeTolerance || + (iD.geo.dist(pos, point()) < tolerance && + (+new Date() - time) < 500)) { click(); } }); From 4b76b136fab3a2bc0beb2aba6f21c23d7287b2c4 Mon Sep 17 00:00:00 2001 From: Tom MacWright Date: Tue, 5 Feb 2013 15:58:54 -0500 Subject: [PATCH 11/30] Fix tests for faux click --- js/id/behavior/draw.js | 2 +- test/spec/modes/add_point.js | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/js/id/behavior/draw.js b/js/id/behavior/draw.js index e68625d05..a4fb53a1a 100644 --- a/js/id/behavior/draw.js +++ b/js/id/behavior/draw.js @@ -3,7 +3,7 @@ iD.behavior.Draw = function(context) { 'clickNode', 'undo', 'cancel', 'finish'), keybinding = d3.keybinding('draw'), hover = iD.behavior.Hover(), - closeTolerance = 4; + closeTolerance = 4, tolerance = 12; function datum() { diff --git a/test/spec/modes/add_point.js b/test/spec/modes/add_point.js index 38a614198..62291f8ee 100644 --- a/test/spec/modes/add_point.js +++ b/test/spec/modes/add_point.js @@ -1,7 +1,7 @@ -describe("iD.modes.AddPoint", function () { +describe("iD.modes.AddPoint", function() { var context; - beforeEach(function () { + beforeEach(function() { var container = d3.select(document.createElement('div')); context = iD() @@ -15,20 +15,22 @@ describe("iD.modes.AddPoint", function () { }); describe("clicking the map", function () { - it("adds a node", function () { - happen.click(context.surface().node(), {}); + it("adds a node", function() { + happen.mousedown(context.surface().node(), {}); + happen.mouseup(window, {}); expect(context.changes().created).to.have.length(1); }); - it("selects the node", function () { - happen.click(context.surface().node(), {}); + it("selects the node", function() { + happen.mousedown(context.surface().node(), {}); + happen.mouseup(window, {}); expect(context.mode().id).to.equal('select'); expect(context.mode().selection()).to.eql([context.changes().created[0].id]); }); }); - describe("pressing ⎋", function () { - it("exits to browse mode", function () { + describe("pressing ⎋", function() { + it("exits to browse mode", function() { happen.keydown(document, {keyCode: 27}); expect(context.mode().id).to.equal('browse'); }); From 930ed8922936f3045430518dc2f41013c9aa7ed6 Mon Sep 17 00:00:00 2001 From: Tom MacWright Date: Tue, 5 Feb 2013 17:49:45 -0500 Subject: [PATCH 12/30] Don't trim the cache. Should be fine. Probably. --- js/id/renderer/background.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/js/id/renderer/background.js b/js/id/renderer/background.js index cae44ca09..2c2b35e78 100644 --- a/js/id/renderer/background.js +++ b/js/id/renderer/background.js @@ -78,6 +78,7 @@ iD.Background = function() { tiles.forEach(function(d) { addSource(d); requests.push(d); + console.log(d[3]); if (!cache[d[3]] && lookUp(d)) { requests.push(addSource(lookUp(d))); } @@ -130,8 +131,6 @@ iD.Background = function() { .on('load', load); image.style(transformProp, imageTransform); - - if (Object.keys(cache).length > 100) cache = {}; } background.offset = function(_) { From f12b7d0fcad6506c121ed6894c9437e6a08a888a Mon Sep 17 00:00:00 2001 From: Tom MacWright Date: Tue, 5 Feb 2013 17:52:42 -0500 Subject: [PATCH 13/30] Console... --- js/id/renderer/background.js | 1 - 1 file changed, 1 deletion(-) diff --git a/js/id/renderer/background.js b/js/id/renderer/background.js index 2c2b35e78..1f0834ed1 100644 --- a/js/id/renderer/background.js +++ b/js/id/renderer/background.js @@ -78,7 +78,6 @@ iD.Background = function() { tiles.forEach(function(d) { addSource(d); requests.push(d); - console.log(d[3]); if (!cache[d[3]] && lookUp(d)) { requests.push(addSource(lookUp(d))); } From 356fd6edd57b1909b7f14445f87d95adcf0f0221 Mon Sep 17 00:00:00 2001 From: Tom MacWright Date: Tue, 5 Feb 2013 17:59:26 -0500 Subject: [PATCH 14/30] Back off on replacing the location to 1/5 of previous throttle --- js/id/behavior/hash.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/id/behavior/hash.js b/js/id/behavior/hash.js index c8762fb13..ced648e45 100644 --- a/js/id/behavior/hash.js +++ b/js/id/behavior/hash.js @@ -29,7 +29,7 @@ iD.behavior.Hash = function(context) { var move = _.throttle(function() { var s1 = formatter(context.map()); if (s0 !== s1) location.replace(s0 = s1); // don't recenter the map! - }, 100); + }, 500); function hashchange() { if (location.hash === s0) return; // ignore spurious hashchange events From 72cd6b91fa629e6bb86d461d9720d7979c229894 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Tue, 5 Feb 2013 10:58:01 -0800 Subject: [PATCH 15/30] Relation#multipolygon returns coordinate arrays --- js/id/graph/relation.js | 8 +-- js/id/svg/multipolygons.js | 2 +- test/spec/graph/relation.js | 106 ++++++++++++++++++------------------ 3 files changed, 57 insertions(+), 59 deletions(-) diff --git a/js/id/graph/relation.js b/js/id/graph/relation.js index 1ed9e8ff1..81a5ba93c 100644 --- a/js/id/graph/relation.js +++ b/js/id/graph/relation.js @@ -145,22 +145,20 @@ _.extend(iD.Relation.prototype, { } } - return joined; + return joined.map(function (nodes) { return _.pluck(nodes, 'loc'); }); } function findOuter(inner) { var o, outer; - inner = _.pluck(inner, 'loc'); - for (o = 0; o < outers.length; o++) { - outer = _.pluck(outers[o], 'loc'); + outer = outers[o]; if (iD.geo.polygonContainsPolygon(outer, inner)) return o; } for (o = 0; o < outers.length; o++) { - outer = _.pluck(outers[o], 'loc'); + outer = outers[o]; if (iD.geo.polygonIntersectsPolygon(outer, inner)) return o; } diff --git a/js/id/svg/multipolygons.js b/js/id/svg/multipolygons.js index b4f050ad5..092f65805 100644 --- a/js/id/svg/multipolygons.js +++ b/js/id/svg/multipolygons.js @@ -24,7 +24,7 @@ iD.svg.Multipolygons = function(projection) { multipolygon = _.flatten(multipolygon, true); return (lineStrings[entity.id] = multipolygon.map(function (ring) { - return 'M' + ring.map(function (node) { return projection(node.loc); }).join('L'); + return 'M' + ring.map(projection).join('L'); }).join("")); } diff --git a/test/spec/graph/relation.js b/test/spec/graph/relation.js index 6e00dcacb..e92409fb5 100644 --- a/test/spec/graph/relation.js +++ b/test/spec/graph/relation.js @@ -148,105 +148,105 @@ describe('iD.Relation', function () { describe("#multipolygon", function () { specify("single polygon consisting of a single way", function () { - var a = iD.Node(), - b = iD.Node(), - c = iD.Node(), + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), w = iD.Way({nodes: [a.id, b.id, c.id, a.id]}), r = iD.Relation({members: [{id: w.id, type: 'way'}]}), g = iD.Graph([a, b, c, w, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c, a]]]); + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, a.loc]]]); }); specify("single polygon consisting of multiple ways", function () { - var a = iD.Node(), - b = iD.Node(), - c = iD.Node(), - d = iD.Node(), + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), + d = iD.Node({loc: [4, 4]}), w1 = iD.Way({nodes: [a.id, b.id, c.id]}), w2 = iD.Way({nodes: [c.id, d.id, a.id]}), r = iD.Relation({members: [{id: w2.id, type: 'way'}, {id: w1.id, type: 'way'}]}), g = iD.Graph([a, b, c, d, w1, w2, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c, d, a]]]); // TODO: not the only valid ordering + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, d.loc, a.loc]]]); // TODO: not the only valid ordering }); specify("single polygon consisting of multiple ways, one needing reversal", function () { - var a = iD.Node(), - b = iD.Node(), - c = iD.Node(), - d = iD.Node(), + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), + d = iD.Node({loc: [4, 4]}), w1 = iD.Way({nodes: [a.id, b.id, c.id]}), w2 = iD.Way({nodes: [a.id, d.id, c.id]}), r = iD.Relation({members: [{id: w2.id, type: 'way'}, {id: w1.id, type: 'way'}]}), g = iD.Graph([a, b, c, d, w1, w2, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c, d, a]]]); // TODO: not the only valid ordering + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, d.loc, a.loc]]]); // TODO: not the only valid ordering }); specify("multiple polygons consisting of single ways", function () { - var a = iD.Node(), - b = iD.Node(), - c = iD.Node(), - d = iD.Node(), - e = iD.Node(), - f = iD.Node(), + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), + d = iD.Node({loc: [4, 4]}), + e = iD.Node({loc: [5, 5]}), + f = iD.Node({loc: [6, 6]}), w1 = iD.Way({nodes: [a.id, b.id, c.id, a.id]}), w2 = iD.Way({nodes: [d.id, e.id, f.id, d.id]}), r = iD.Relation({members: [{id: w2.id, type: 'way'}, {id: w1.id, type: 'way'}]}), g = iD.Graph([a, b, c, d, e, f, w1, w2, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c, a]], [[d, e, f, d]]]); + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, a.loc]], [[d.loc, e.loc, f.loc, d.loc]]]); }); specify("invalid geometry: unclosed ring consisting of a single way", function () { - var a = iD.Node(), - b = iD.Node(), - c = iD.Node(), + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), w = iD.Way({nodes: [a.id, b.id, c.id]}), r = iD.Relation({members: [{id: w.id, type: 'way'}]}), g = iD.Graph([a, b, c, w, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c]]]); + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc]]]); }); specify("invalid geometry: unclosed ring consisting of multiple ways", function () { - var a = iD.Node(), - b = iD.Node(), - c = iD.Node(), - d = iD.Node(), + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), + d = iD.Node({loc: [4, 4]}), w1 = iD.Way({nodes: [a.id, b.id, c.id]}), w2 = iD.Way({nodes: [c.id, d.id]}), r = iD.Relation({members: [{id: w2.id, type: 'way'}, {id: w1.id, type: 'way'}]}), g = iD.Graph([a, b, c, d, w1, w2, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c, d]]]); + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, d.loc]]]); }); specify("invalid geometry: unclosed ring consisting of multiple ways, alternate order", function () { - var a = iD.Node(), - b = iD.Node(), - c = iD.Node(), - d = iD.Node(), + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), + d = iD.Node({loc: [4, 4]}), w1 = iD.Way({nodes: [c.id, d.id]}), w2 = iD.Way({nodes: [a.id, b.id, c.id]}), r = iD.Relation({members: [{id: w2.id, type: 'way'}, {id: w1.id, type: 'way'}]}), g = iD.Graph([a, b, c, d, w1, w2, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c, d]]]); + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, d.loc]]]); }); specify("invalid geometry: unclosed ring consisting of multiple ways, one needing reversal", function () { - var a = iD.Node(), - b = iD.Node(), - c = iD.Node(), - d = iD.Node(), + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), + d = iD.Node({loc: [4, 4]}), w1 = iD.Way({nodes: [a.id, b.id, c.id]}), w2 = iD.Way({nodes: [d.id, c.id]}), r = iD.Relation({members: [{id: w2.id, type: 'way'}, {id: w1.id, type: 'way'}]}), g = iD.Graph([a, b, c, d, w1, w2, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c, d]]]); + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, d.loc]]]); }); specify("invalid geometry: unclosed ring consisting of multiple ways, one needing reversal, alternate order", function () { @@ -259,7 +259,7 @@ describe('iD.Relation', function () { r = iD.Relation({members: [{id: w2.id, type: 'way'}, {id: w1.id, type: 'way'}]}), g = iD.Graph([a, b, c, d, w1, w2, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c, d]]]); + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, d.loc]]]); }); specify("single polygon with single single-way inner", function () { @@ -274,7 +274,7 @@ describe('iD.Relation', function () { r = iD.Relation({members: [{id: outer.id, type: 'way'}, {id: inner.id, role: 'inner', type: 'way'}]}), g = iD.Graph([a, b, c, d, e, f, outer, inner, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c, a], [d, e, f, d]]]); + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, a.loc], [d.loc, e.loc, f.loc, d.loc]]]); }); specify("single polygon with single multi-way inner", function () { @@ -293,7 +293,7 @@ describe('iD.Relation', function () { {id: inner1.id, role: 'inner', type: 'way'}]}), graph = iD.Graph([a, b, c, d, e, f, outer, inner1, inner2, r]); - expect(r.multipolygon(graph)).to.eql([[[a, b, c, a], [d, e, f, d]]]); + expect(r.multipolygon(graph)).to.eql([[[a.loc, b.loc, c.loc, a.loc], [d.loc, e.loc, f.loc, d.loc]]]); }); specify("single polygon with multiple single-way inners", function () { @@ -315,7 +315,7 @@ describe('iD.Relation', function () { {id: inner1.id, role: 'inner', type: 'way'}]}), graph = iD.Graph([a, b, c, d, e, f, g, h, i, outer, inner1, inner2, r]); - expect(r.multipolygon(graph)).to.eql([[[a, b, c, a], [d, e, f, d], [g, h, i, g]]]); + expect(r.multipolygon(graph)).to.eql([[[a.loc, b.loc, c.loc, a.loc], [d.loc, e.loc, f.loc, d.loc], [g.loc, h.loc, i.loc, g.loc]]]); }); specify("multiple polygons with single single-way inner", function () { @@ -337,30 +337,30 @@ describe('iD.Relation', function () { {id: inner.id, role: 'inner', type: 'way'}]}), graph = iD.Graph([a, b, c, d, e, f, g, h, i, outer1, outer2, inner, r]); - expect(r.multipolygon(graph)).to.eql([[[a, b, c, a], [d, e, f, d]], [[g, h, i, g]]]); + expect(r.multipolygon(graph)).to.eql([[[a.loc, b.loc, c.loc, a.loc], [d.loc, e.loc, f.loc, d.loc]], [[g.loc, h.loc, i.loc, g.loc]]]); }); specify("invalid geometry: unmatched inner", function () { - var a = iD.Node(), - b = iD.Node(), - c = iD.Node(), + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), w = iD.Way({nodes: [a.id, b.id, c.id, a.id]}), r = iD.Relation({members: [{id: w.id, role: 'inner', type: 'way'}]}), g = iD.Graph([a, b, c, w, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c, a]]]); + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, a.loc]]]); }); specify("incomplete relation", function () { - var a = iD.Node(), - b = iD.Node(), - c = iD.Node(), + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), w1 = iD.Way({nodes: [a.id, b.id, c.id]}), w2 = iD.Way(), r = iD.Relation({members: [{id: w2.id, type: 'way'}, {id: w1.id, type: 'way'}]}), g = iD.Graph([a, b, c, w1, r]); - expect(r.multipolygon(g)).to.eql([[[a, b, c]]]); + expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc]]]); }); }); }); From 8735413974699fd78ffff26da9436cac8769814d Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Tue, 5 Feb 2013 10:58:41 -0800 Subject: [PATCH 16/30] Relation#asGeoJSON --- js/id/graph/relation.js | 25 +++++++++++++++++++++++++ test/spec/graph/relation.js | 28 ++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/js/id/graph/relation.js b/js/id/graph/relation.js index 81a5ba93c..a404d1981 100644 --- a/js/id/graph/relation.js +++ b/js/id/graph/relation.js @@ -83,6 +83,31 @@ _.extend(iD.Relation.prototype, { return r; }, + asGeoJSON: function(resolver) { + if (this.isMultipolygon()) { + return { + type: 'Feature', + properties: this.tags, + geometry: { + type: 'MultiPolygon', + coordinates: this.multipolygon(resolver) + } + }; + } else { + return { + type: 'FeatureCollection', + properties: this.tags, + features: this.members.map(function(member) { + return _.extend({role: member.role}, resolver.entity(member.id).asGeoJSON(resolver)); + }) + }; + } + }, + + isMultipolygon: function() { + return this.tags.type === 'multipolygon'; + }, + isRestriction: function() { return !!(this.tags.type && this.tags.type.match(/^restriction:?/)); }, diff --git a/test/spec/graph/relation.js b/test/spec/graph/relation.js index e92409fb5..b07f9bee7 100644 --- a/test/spec/graph/relation.js +++ b/test/spec/graph/relation.js @@ -146,6 +146,34 @@ describe('iD.Relation', function () { }); }); + describe("#asGeoJSON", function (){ + it('converts a multipolygon to a GeoJSON MultiPolygon feature', function() { + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), + w = iD.Way({nodes: [a.id, b.id, c.id, a.id]}), + r = iD.Relation({tags: {type: 'multipolygon'}, members: [{id: w.id, type: 'way'}]}), + g = iD.Graph([a, b, c, w, r]), + json = r.asGeoJSON(g); + + expect(json.type).to.equal('Feature'); + expect(json.properties).to.eql({type: 'multipolygon'}); + expect(json.geometry.type).to.equal('MultiPolygon'); + expect(json.geometry.coordinates).to.eql([[[[1, 1], [2, 2], [3, 3], [1, 1]]]]); + }); + + it('converts a relation to a GeoJSON FeatureCollection', function() { + var a = iD.Node({loc: [1, 1]}), + r = iD.Relation({tags: {type: 'type'}, members: [{id: a.id, role: 'role'}]}), + g = iD.Graph([a, r]), + json = r.asGeoJSON(g); + + expect(json.type).to.equal('FeatureCollection'); + expect(json.properties).to.eql({type: 'type'}); + expect(json.features).to.eql([_.extend({role: 'role'}, a.asGeoJSON(g))]); + }); + }); + describe("#multipolygon", function () { specify("single polygon consisting of a single way", function () { var a = iD.Node({loc: [1, 1]}), From 13a784bea58b329207d9b08e8f81bada52f2ffbf Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Tue, 5 Feb 2013 14:05:43 -0800 Subject: [PATCH 17/30] Better Way#asGeoJSON --- js/id/graph/way.js | 27 +++++++++++++++++++-------- test/spec/graph/way.js | 16 +++++++++++++++- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/js/id/graph/way.js b/js/id/graph/way.js index aae89a6ce..1f7c46065 100644 --- a/js/id/graph/way.js +++ b/js/id/graph/way.js @@ -103,13 +103,24 @@ _.extend(iD.Way.prototype, { }, asGeoJSON: function(resolver) { - return { - type: 'Feature', - properties: this.tags, - geometry: { - type: 'LineString', - coordinates: _.pluck(resolver.childNodes(this), 'loc') - } - }; + if (this.isArea()) { + return { + type: 'Feature', + properties: this.tags, + geometry: { + type: 'Polygon', + coordinates: [_.pluck(resolver.childNodes(this), 'loc')] + } + }; + } else { + return { + type: 'Feature', + properties: this.tags, + geometry: { + type: 'LineString', + coordinates: _.pluck(resolver.childNodes(this), 'loc') + } + }; + } } }); diff --git a/test/spec/graph/way.js b/test/spec/graph/way.js index 6b6b1543f..39393ab16 100644 --- a/test/spec/graph/way.js +++ b/test/spec/graph/way.js @@ -207,7 +207,7 @@ describe('iD.Way', function() { }); describe("#asGeoJSON", function () { - it("converts to a GeoJSON LineString features", function () { + it("converts a line to a GeoJSON LineString features", function () { var a = iD.Node({loc: [1, 2]}), b = iD.Node({loc: [3, 4]}), w = iD.Way({tags: {highway: 'residential'}, nodes: [a.id, b.id]}), @@ -219,5 +219,19 @@ describe('iD.Way', function() { expect(json.geometry.type).to.equal('LineString'); expect(json.geometry.coordinates).to.eql([[1, 2], [3, 4]]); }); + + it("converts an area to a GeoJSON Polygon features", function () { + var a = iD.Node({loc: [1, 2]}), + b = iD.Node({loc: [3, 4]}), + c = iD.Node({loc: [5, 6]}), + w = iD.Way({tags: {area: 'yes'}, nodes: [a.id, b.id, c.id, a.id]}), + graph = iD.Graph([a, b, c, w]), + json = w.asGeoJSON(graph); + + expect(json.type).to.equal('Feature'); + expect(json.properties).to.eql({area: 'yes'}); + expect(json.geometry.type).to.equal('Polygon'); + expect(json.geometry.coordinates).to.eql([[[1, 2], [3, 4], [5, 6], [1, 2]]]); + }); }); }); From 5eb06442421d53a005d74587608cb3231a3a6698 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Tue, 5 Feb 2013 12:00:32 -0800 Subject: [PATCH 18/30] Improve multipolygon rendering Multipolygon relations report their geometry as 'area' and are rendered as such. However, they do not render a stroke. The stroke rendering will come from the individual lines, which are given the tag classes of their parent relations, allowing them to have a stroke style matching the style of simple areas with the same tags. Untagged circular ways are no longer considered areas. This prevents an untagged inner way of a multipolygon from rendering as an area and is consistent with how P2 and JOSM treat them. In the CSS, it's no longer necessary to deal with multipolygons explicitly in selectors. But keep in mind that area boundaries can now be rendered either as lines or as area strokes. In most cases the selector should be `path.stroke.tag-_____`, i.e. an explicit `.area` or `.line` classes should not be included. Finally, the parent ways of selected multipolygons are given the 'selected' class. --- css/map.css | 92 ++++++++++++---------------------- index.html | 1 - js/id/graph/relation.js | 2 +- js/id/graph/way.js | 1 + js/id/modes/select.js | 11 +++- js/id/renderer/map.js | 2 - js/id/svg.js | 12 +++++ js/id/svg/areas.js | 34 +++++++------ js/id/svg/labels.js | 5 +- js/id/svg/lines.js | 21 +++++--- js/id/svg/multipolygons.js | 55 -------------------- js/id/svg/tag_classes.js | 22 +++++--- test/index.html | 2 - test/index_packaged.html | 1 - test/spec/graph/way.js | 8 ++- test/spec/svg/areas.js | 42 ++++++++++++++++ test/spec/svg/lines.js | 10 ++++ test/spec/svg/multipolygons.js | 43 ---------------- test/spec/svg/tag_classes.js | 7 +++ 19 files changed, 172 insertions(+), 199 deletions(-) delete mode 100644 js/id/svg/multipolygons.js delete mode 100644 test/spec/svg/multipolygons.js diff --git a/css/map.css b/css/map.css index 749aeb7b3..7b35f6bc5 100644 --- a/css/map.css +++ b/css/map.css @@ -181,95 +181,72 @@ path.shadow.selected { } path.area.stroke, -path.multipolygon { +path.line.member-type-multipolygon.stroke { stroke-width:2; - stroke:#fff; } -path.area.fill, -path.multipolygon { - fill:#fff; - fill-opacity:0.3; -} - -path.multipolygon { - fill-rule: evenodd; -} - -path.area.fill.member-type-multipolygon { - fill: none; -} - -path.area.stroke.selected { +path.area.stroke.selected, +path.line.member-type-multipolygon.stroke.selected { stroke-width:4 !important; } -path.area.stroke.tag-natural, -path.multipolygon.tag-natural { +path.area.stroke { + stroke:#fff; +} +path.area.fill { + fill:#fff; + fill-opacity:0.3; + fill-rule: evenodd; +} + +path.stroke.tag-natural { stroke: #b6e199; stroke-width:1; } -path.area.fill.tag-natural, -path.multipolygon.tag-natural { +path.fill.tag-natural { fill: #b6e199; } -path.area.stroke.tag-natural-water, -path.multipolygon.tag-natural-water { +path.stroke.tag-natural-water { stroke: #77d3de; } -path.area.fill.tag-natural-water, -path.multipolygon.tag-natural-water { +path.fill.tag-natural-water { fill: #77d3de; } -path.area.stroke.tag-building, -path.multipolygon.tag-building { +path.stroke.tag-building { stroke: #e06e5f; stroke-width: 1; } -path.area.fill.tag-building, -path.multipolygon.tag-building { +path.fill.tag-building { fill: #e06e5f; } -path.area.stroke.tag-landuse, -path.area.stroke.tag-natural-wood, -path.area.stroke.tag-natural-tree, -path.area.stroke.tag-natural-grassland, -path.area.stroke.tag-leisure-park, -path.multipolygon.tag-landuse, -path.multipolygon.tag-natural-wood, -path.multipolygon.tag-natural-tree, -path.multipolygon.tag-natural-grassland, -path.multipolygon.tag-leisure-park { +path.stroke.tag-landuse, +path.stroke.tag-natural-wood, +path.stroke.tag-natural-tree, +path.stroke.tag-natural-grassland, +path.stroke.tag-leisure-park { stroke: #8cd05f; stroke-width: 1; } -path.area.fill.tag-landuse, -path.area.fill.tag-natural-wood, -path.area.fill.tag-natural-tree, -path.area.fill.tag-natural-grassland, -path.area.fill.tag-leisure-park, -path.multipolygon.tag-landuse, -path.multipolygon.tag-natural-wood, -path.multipolygon.tag-natural-tree, -path.multipolygon.tag-natural-grassland, -path.multipolygon.tag-leisure-park { +path.fill.tag-landuse, +path.fill.tag-natural-wood, +path.fill.tag-natural-tree, +path.fill.tag-natural-grassland, +path.fill.tag-leisure-park { fill: #8cd05f; fill-opacity: 0.2; } -path.area.stroke.tag-amenity-parking, -path.multipolygon.tag-amenity-parking { +path.stroke.tag-amenity-parking { stroke: #aaa; stroke-width: 1; } -path.area.fill.tag-amenity-parking, -path.multipolygon.tag-amenity-parking { +path.fill.tag-amenity-parking { fill: #aaa; } -path.multipolygon.tag-boundary { +path.fill.tag-boundary { fill: none; } @@ -526,7 +503,7 @@ path.casing.tag-railway-subway { /* waterways */ -path.area.fill.tag-waterway { +path.fill.tag-waterway { fill: #77d3de; } @@ -686,9 +663,7 @@ text.point { } .mode-select .area, -.mode-browse .area, -.mode-select .multipolygon, -.mode-browse .multipolygon { +.mode-browse .area { cursor: url(../img/cursor-select-area.png), pointer; } @@ -701,7 +676,6 @@ text.point { .vertex:active, .line:active, .area:active, -.multipolygon:active, .midpoint:active, .mode-select .selected { cursor: url(../img/cursor-select-acting.png), pointer; diff --git a/index.html b/index.html index afcb8e6be..a58aeb8f2 100644 --- a/index.html +++ b/index.html @@ -44,7 +44,6 @@ - diff --git a/js/id/graph/relation.js b/js/id/graph/relation.js index a404d1981..f8540db1f 100644 --- a/js/id/graph/relation.js +++ b/js/id/graph/relation.js @@ -26,7 +26,7 @@ _.extend(iD.Relation.prototype, { }, geometry: function() { - return 'relation'; + return this.isMultipolygon() ? 'area' : 'relation'; }, // Return the first member with the given role. A copy of the member object diff --git a/js/id/graph/way.js b/js/id/graph/way.js index 1f7c46065..1830e61a4 100644 --- a/js/id/graph/way.js +++ b/js/id/graph/way.js @@ -49,6 +49,7 @@ _.extend(iD.Way.prototype, { isArea: function() { return this.tags.area === 'yes' || (this.isClosed() && + !_.isEmpty(this.tags) && this.tags.area !== 'no' && !this.tags.highway && !this.tags.barrier); diff --git a/js/id/modes/select.js b/js/id/modes/select.js index 3b0ad82dd..532faaca8 100644 --- a/js/id/modes/select.js +++ b/js/id/modes/select.js @@ -143,13 +143,22 @@ iD.modes.Select = function(context, selection, initial) { } } + function selected(entity) { + if (!entity) return false; + if (selection.indexOf(entity.id) >= 0) return true; + return d3.select(this).classed('stroke') && + _.any(context.graph().parentRelations(entity), function(parent) { + return selection.indexOf(parent.id) >= 0; + }); + } + d3.select(document) .call(keybinding); context.surface() .on('dblclick.select', dblclick) .selectAll("*") - .filter(function(d) { return d && selection.indexOf(d.id) >= 0; }) + .filter(selected) .classed('selected', true); radialMenu = iD.ui.RadialMenu(operations); diff --git a/js/id/renderer/map.js b/js/id/renderer/map.js index 19354d7f3..5074abd92 100644 --- a/js/id/renderer/map.js +++ b/js/id/renderer/map.js @@ -19,7 +19,6 @@ iD.Map = function(context) { vertices = iD.svg.Vertices(roundedProjection), lines = iD.svg.Lines(roundedProjection), areas = iD.svg.Areas(roundedProjection), - multipolygons = iD.svg.Multipolygons(roundedProjection), midpoints = iD.svg.Midpoints(roundedProjection), labels = iD.svg.Labels(roundedProjection), tail = d3.tail(), @@ -87,7 +86,6 @@ iD.Map = function(context) { .call(vertices, graph, all, filter) .call(lines, graph, all, filter) .call(areas, graph, all, filter) - .call(multipolygons, graph, all, filter) .call(midpoints, graph, all, filter) .call(labels, graph, all, filter, dimensions, !difference); } diff --git a/js/id/svg.js b/js/id/svg.js index aadb6b830..3b662b2e7 100644 --- a/js/id/svg.js +++ b/js/id/svg.js @@ -27,5 +27,17 @@ iD.svg = { return projection(n.loc); }).join('L')); }; + }, + + MultipolygonMemberTags: function (graph) { + return function (entity) { + var tags = entity.tags; + graph.parentRelations(entity).forEach(function (relation) { + if (relation.isMultipolygon()) { + tags = _.extend({}, relation.tags, tags); + } + }); + return tags; + } } }; diff --git a/js/id/svg/areas.js b/js/id/svg/areas.js index 991b5644c..cd9525d4d 100644 --- a/js/id/svg/areas.js +++ b/js/id/svg/areas.js @@ -1,38 +1,39 @@ iD.svg.Areas = function(projection) { return function drawAreas(surface, graph, entities, filter) { - var areas = []; + var path = d3.geo.path().projection(projection), + areas = []; for (var i = 0; i < entities.length; i++) { var entity = entities[i]; if (entity.geometry(graph) === 'area') { - var points = graph.childNodes(entity).map(function(n) { - return projection(n.loc); - }); - areas.push({ entity: entity, - area: entity.isDegenerate() ? 0 : Math.abs(d3.geom.polygon(points).area()) + area: Math.abs(path.area(entity.asGeoJSON(graph))) }); } } areas.sort(function(a, b) { return b.area - a.area; }); - var lineString = iD.svg.LineString(projection, graph); + function drawPaths(group, areas, filter, klass) { + var tagClasses = iD.svg.TagClasses(); + + if (klass === 'stroke') { + tagClasses.tags(iD.svg.MultipolygonMemberTags(graph)); + } - function drawPaths(group, areas, filter, classes) { var paths = group.selectAll('path.area') .filter(filter) .data(areas, iD.Entity.key); paths.enter() .append('path') - .attr('class', classes); + .attr('class', function (d) { return d.type + ' area ' + klass; }); paths .order() - .attr('d', lineString) - .call(iD.svg.TagClasses()) + .attr('d', function (entity) { return path(entity.asGeoJSON(graph)); }) + .call(tagClasses) .call(iD.svg.MemberClasses(graph)); paths.exit() @@ -43,9 +44,14 @@ iD.svg.Areas = function(projection) { areas = _.pluck(areas, 'entity'); + var strokes = areas.filter(function (area) { + return area.type === 'way'; + }); + var fill = surface.select('.layer-fill'), - stroke = surface.select('.layer-stroke'), - fills = drawPaths(fill, areas, filter, 'way area fill'), - strokes = drawPaths(stroke, areas, filter, 'way area stroke'); + stroke = surface.select('.layer-stroke'); + + drawPaths(fill, areas, filter, 'fill'); + drawPaths(stroke, strokes, filter, 'stroke'); }; }; diff --git a/js/id/svg/labels.js b/js/id/svg/labels.js index 9b2cbd1e9..31d2f03d5 100644 --- a/js/id/svg/labels.js +++ b/js/id/svg/labels.js @@ -335,9 +335,8 @@ iD.svg.Labels = function(projection) { } function getAreaLabel(entity, width, height) { - var nodes = _.pluck(graph.childNodes(entity), 'loc') - .map(iD.svg.RoundProjection(projection)), - centroid = d3.geom.polygon(nodes).centroid(), + var path = d3.geo.path().projection(projection), + centroid = path.centroid(entity.asGeoJSON(graph)), extent = entity.extent(graph), entitywidth = projection(extent[1])[0] - projection(extent[0])[0]; diff --git a/js/id/svg/lines.js b/js/id/svg/lines.js index a1151204b..2fd7b9152 100644 --- a/js/id/svg/lines.js +++ b/js/id/svg/lines.js @@ -34,19 +34,25 @@ iD.svg.Lines = function(projection) { } return function drawLines(surface, graph, entities, filter) { - function drawPaths(group, lines, filter, classes, lineString) { + function drawPaths(group, lines, filter, klass, lineString) { + var tagClasses = iD.svg.TagClasses(); + + if (klass === 'stroke') { + tagClasses.tags(iD.svg.MultipolygonMemberTags(graph)); + } + var paths = group.selectAll('path.line') .filter(filter) .data(lines, iD.Entity.key); paths.enter() .append('path') - .attr('class', classes); + .attr('class', 'way line ' + klass); paths .order() .attr('d', lineString) - .call(iD.svg.TagClasses()) + .call(tagClasses) .call(iD.svg.MemberClasses(graph)); paths.exit() @@ -65,8 +71,7 @@ iD.svg.Lines = function(projection) { container.remove(); } - var lines = [], - lineStrings = {}; + var lines = []; for (var i = 0; i < entities.length; i++) { var entity = entities[i]; @@ -84,9 +89,9 @@ iD.svg.Lines = function(projection) { stroke = surface.select('.layer-stroke'), defs = surface.select('defs'), text = surface.select('.layer-text'), - shadows = drawPaths(shadow, lines, filter, 'way line shadow', lineString), - casings = drawPaths(casing, lines, filter, 'way line casing', lineString), - strokes = drawPaths(stroke, lines, filter, 'way line stroke', lineString); + shadows = drawPaths(shadow, lines, filter, 'shadow', lineString), + casings = drawPaths(casing, lines, filter, 'casing', lineString), + strokes = drawPaths(stroke, lines, filter, 'stroke', lineString); // Determine the lengths of oneway paths var lengths = {}, diff --git a/js/id/svg/multipolygons.js b/js/id/svg/multipolygons.js deleted file mode 100644 index 092f65805..000000000 --- a/js/id/svg/multipolygons.js +++ /dev/null @@ -1,55 +0,0 @@ -iD.svg.Multipolygons = function(projection) { - return function(surface, graph, entities, filter) { - var multipolygons = []; - - for (var i = 0; i < entities.length; i++) { - var entity = entities[i]; - if (entity.geometry(graph) === 'relation' && entity.tags.type === 'multipolygon') { - multipolygons.push(entity); - } - } - - var lineStrings = {}; - - function lineString(entity) { - if (lineStrings[entity.id] !== undefined) { - return lineStrings[entity.id]; - } - - var multipolygon = entity.multipolygon(graph); - if (entity.members.length === 0 || !multipolygon) { - return (lineStrings[entity.id] = null); - } - - multipolygon = _.flatten(multipolygon, true); - return (lineStrings[entity.id] = - multipolygon.map(function (ring) { - return 'M' + ring.map(projection).join('L'); - }).join("")); - } - - function drawPaths(group, multipolygons, filter, classes) { - var paths = group.selectAll('path.multipolygon') - .filter(filter) - .data(multipolygons, iD.Entity.key); - - paths.enter() - .append('path') - .attr('class', classes); - - paths - .order() - .attr('d', lineString) - .call(iD.svg.TagClasses()) - .call(iD.svg.MemberClasses(graph)); - - paths.exit() - .remove(); - - return paths; - } - - var fill = surface.select('.layer-fill'), - paths = drawPaths(fill, multipolygons, filter, 'relation multipolygon'); - }; -}; diff --git a/js/id/svg/tag_classes.js b/js/id/svg/tag_classes.js index 5ce585add..dad388ffe 100644 --- a/js/id/svg/tag_classes.js +++ b/js/id/svg/tag_classes.js @@ -3,10 +3,11 @@ iD.svg.TagClasses = function() { 'highway', 'railway', 'waterway', 'power', 'motorway', 'amenity', 'natural', 'landuse', 'building', 'oneway', 'bridge', 'boundary', 'leisure', 'construction' - ]), tagClassRe = /^tag-/; + ]), tagClassRe = /^tag-/, + tags = function(entity) { return entity.tags; }; - return function tagClassesSelection(selection) { - selection.each(function tagClassesEach(d, i) { + var tagClasses = function(selection) { + selection.each(function tagClassesEach(entity) { var classes, value = this.className; if (value.baseVal !== undefined) value = value.baseVal; @@ -15,11 +16,10 @@ iD.svg.TagClasses = function() { return name.length && !tagClassRe.test(name); }).join(' '); - var tags = d.tags; - for (var k in tags) { + var t = tags(entity); + for (var k in t) { if (!keys[k]) continue; - classes += ' tag-' + k + ' ' + - 'tag-' + k + '-' + tags[k]; + classes += ' tag-' + k + ' ' + 'tag-' + k + '-' + t[k]; } classes = classes.trim(); @@ -29,4 +29,12 @@ iD.svg.TagClasses = function() { } }); }; + + tagClasses.tags = function(_) { + if (!arguments.length) return tags; + tags = _; + return tagClasses; + }; + + return tagClasses; }; diff --git a/test/index.html b/test/index.html index 5f7c19b5a..522f26092 100644 --- a/test/index.html +++ b/test/index.html @@ -47,7 +47,6 @@ - @@ -184,7 +183,6 @@ - diff --git a/test/index_packaged.html b/test/index_packaged.html index d21d48ff2..8dd73d666 100644 --- a/test/index_packaged.html +++ b/test/index_packaged.html @@ -66,7 +66,6 @@ - diff --git a/test/spec/graph/way.js b/test/spec/graph/way.js index 39393ab16..05864d5c3 100644 --- a/test/spec/graph/way.js +++ b/test/spec/graph/way.js @@ -95,8 +95,12 @@ describe('iD.Way', function() { expect(iD.Way({tags: { area: 'yes' }}).isArea()).to.equal(true); }); - it('returns true if the way is closed and has no tags', function() { - expect(iD.Way({nodes: ['n1', 'n1']}).isArea()).to.equal(true); + it('returns false if the way is closed and has no tags', function() { + expect(iD.Way({nodes: ['n1', 'n1']}).isArea()).to.equal(false); + }); + + it('returns true if the way is closed and has tags', function() { + expect(iD.Way({nodes: ['n1', 'n1'], tags: {a: 'b'}}).isArea()).to.equal(true); }); it('returns false if the way is closed and has tag area=no', function() { diff --git a/test/spec/svg/areas.js b/test/spec/svg/areas.js index 807a799f4..bbf8674ae 100644 --- a/test/spec/svg/areas.js +++ b/test/spec/svg/areas.js @@ -69,4 +69,46 @@ describe("iD.svg.Areas", function () { expect(surface.select('.area:nth-child(1)')).to.be.classed('tag-landuse-park'); expect(surface.select('.area:nth-child(2)')).to.be.classed('tag-building-yes'); }); + + it("renders fills for multipolygon areas", function () { + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), + w = iD.Way({nodes: [a.id, b.id, c.id, a.id]}), + r = iD.Relation({tags: {type: 'multipolygon'}, members: [{id: w.id, type: 'way'}]}), + graph = iD.Graph([a, b, c, w, r]), + areas = [w, r]; + + surface.call(iD.svg.Areas(projection), graph, areas, filter); + + expect(surface.select('.fill')).to.be.classed('relation'); + }); + + it("renders no strokes for multipolygon areas", function () { + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), + w = iD.Way({nodes: [a.id, b.id, c.id, a.id]}), + r = iD.Relation({tags: {type: 'multipolygon'}, members: [{id: w.id, type: 'way'}]}), + graph = iD.Graph([a, b, c, w, r]), + areas = [w, r]; + + surface.call(iD.svg.Areas(projection), graph, areas, filter); + + expect(surface.selectAll('.stroke')[0].length).to.equal(0); + }); + + it("adds stroke classes for the tags of the parent relation of multipolygon members", function() { + var a = iD.Node({loc: [1, 1]}), + b = iD.Node({loc: [2, 2]}), + c = iD.Node({loc: [3, 3]}), + w = iD.Way({tags: {area: 'yes'}, nodes: [a.id, b.id, c.id, a.id]}), + r = iD.Relation({members: [{id: w.id}], tags: {type: 'multipolygon', natural: 'wood'}}), + graph = iD.Graph([a, b, c, w, r]); + + surface.call(iD.svg.Areas(projection), graph, [w], filter); + + expect(surface.select('.stroke')).to.be.classed('tag-natural-wood'); + expect(surface.select('.fill')).not.to.be.classed('tag-natural-wood'); + }); }); diff --git a/test/spec/svg/lines.js b/test/spec/svg/lines.js index 61c3229a9..989022632 100644 --- a/test/spec/svg/lines.js +++ b/test/spec/svg/lines.js @@ -39,6 +39,16 @@ describe("iD.svg.Lines", function () { expect(surface.select('.line')).to.be.classed('member-type-route'); }); + it("adds stroke classes for the tags of the parent relation of multipolygon members", function() { + var line = iD.Way(), + relation = iD.Relation({members: [{id: line.id}], tags: {type: 'multipolygon', natural: 'wood'}}), + graph = iD.Graph([line, relation]); + + surface.call(iD.svg.Lines(projection), graph, [line], filter); + + expect(surface.select('.stroke')).to.be.classed('tag-natural-wood'); + }); + it("preserves non-line paths", function () { var line = iD.Way(), graph = iD.Graph([line]); diff --git a/test/spec/svg/multipolygons.js b/test/spec/svg/multipolygons.js deleted file mode 100644 index 67f44ebae..000000000 --- a/test/spec/svg/multipolygons.js +++ /dev/null @@ -1,43 +0,0 @@ -describe("iD.svg.Multipolygons", 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("adds relation and multipolygon classes", function () { - var relation = iD.Relation({tags: {type: 'multipolygon'}}), - graph = iD.Graph([relation]); - - surface.call(iD.svg.Multipolygons(projection), graph, [relation], filter); - - expect(surface.select('path')).to.be.classed('relation'); - expect(surface.select('path')).to.be.classed('multipolygon'); - }); - - it("adds tag classes", function () { - var relation = iD.Relation({tags: {type: 'multipolygon', boundary: "administrative"}}), - graph = iD.Graph([relation]); - - surface.call(iD.svg.Multipolygons(projection), graph, [relation], filter); - - expect(surface.select('.relation')).to.be.classed('tag-boundary'); - expect(surface.select('.relation')).to.be.classed('tag-boundary-administrative'); - }); - - it("preserves non-multipolygon paths", function () { - var relation = iD.Relation({tags: {type: 'multipolygon'}}), - graph = iD.Graph([relation]); - - surface.select('.layer-fill') - .append('path') - .attr('class', 'other'); - - surface.call(iD.svg.Multipolygons(projection), graph, [relation], filter); - - expect(surface.selectAll('.other')[0].length).to.equal(1); - }); -}); diff --git a/test/spec/svg/tag_classes.js b/test/spec/svg/tag_classes.js index dd899e08e..048378815 100644 --- a/test/spec/svg/tag_classes.js +++ b/test/spec/svg/tag_classes.js @@ -19,6 +19,13 @@ describe("iD.svg.TagClasses", function () { expect(selection.attr('class')).to.equal('tag-highway tag-highway-primary'); }); + it('adds tags based on the result of the `tags` accessor', function() { + selection + .datum(iD.Entity()) + .call(iD.svg.TagClasses().tags(d3.functor({highway: 'primary'}))); + expect(selection.attr('class')).to.equal('tag-highway tag-highway-primary'); + }); + it('removes classes for tags that are no longer present', function() { selection .attr('class', 'tag-highway tag-highway-primary') From 14fc1d9c0d1fed338a5ea99a279fe74eb1f3bea8 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Tue, 5 Feb 2013 18:47:28 -0500 Subject: [PATCH 19/30] Fix flickering after redrawing active elems --- js/id/behavior/draw_way.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/js/id/behavior/draw_way.js b/js/id/behavior/draw_way.js index 6b648db0e..8c4c4a5cc 100644 --- a/js/id/behavior/draw_way.js +++ b/js/id/behavior/draw_way.js @@ -28,7 +28,13 @@ iD.behavior.DrawWay = function(context, wayId, index, mode, baseGraph) { function move(datum) { var loc = context.map().mouseCoordinates(); - if (datum.type === 'node') { + if (datum.id === end.id || datum.id === segment.id) { + context.surface().selectAll('.way, .node') + .filter(function (d) { + return d.id === end.id || d.id === segment.id; + }) + .classed('active', true); + } else if (datum.type === 'node') { loc = datum.loc; } else if (datum.type === 'way') { loc = iD.geo.chooseIndex(datum, d3.mouse(context.surface().node()), context).loc; From 8f17628190b603f243c81e04fc2efb518be49800 Mon Sep 17 00:00:00 2001 From: Tom MacWright Date: Tue, 5 Feb 2013 19:09:22 -0500 Subject: [PATCH 20/30] Support nudging while moving ways. Fixes #533 --- js/id/modes/move_way.js | 42 ++++++++++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/js/id/modes/move_way.js b/js/id/modes/move_way.js index 986eef5bd..e21dded78 100644 --- a/js/id/modes/move_way.js +++ b/js/id/modes/move_way.js @@ -6,7 +6,8 @@ iD.modes.MoveWay = function(context, wayId) { var keybinding = d3.keybinding('move-way'); mode.enter = function() { - var origin = point(), + var origin = context.map().mouseCoordinates(), + nudgeInterval, annotation = t('operations.move.annotation.' + context.geometry(wayId)); // If intiated via keyboard @@ -16,17 +17,44 @@ iD.modes.MoveWay = function(context, wayId) { iD.actions.Noop(), annotation); + function edge(point, size) { + var pad = [30, 100, 30, 100]; + if (point[0] > size[0] - pad[0]) return [-10, 0]; + else if (point[0] < pad[2]) return [10, 0]; + else if (point[1] > size[1] - pad[1]) return [0, -10]; + else if (point[1] < pad[3]) return [0, 10]; + return null; + } + + function startNudge(nudge) { + if (nudgeInterval) window.clearInterval(nudgeInterval); + nudgeInterval = window.setInterval(function() { + context.map().pan(nudge).redraw(); + }, 50); + } + + function stopNudge() { + if (nudgeInterval) window.clearInterval(nudgeInterval); + nudgeInterval = null; + } + function point() { - return d3.mouse(context.surface().node()); + return d3.mouse(context.map().surface.node()); } function move() { - var p = point(), - delta = origin ? - [p[0] - origin[0], p[1] - origin[1]] : - [0, 0]; + var p = point(); - origin = p; + var delta = origin ? + [p[0] - context.projection(origin)[0], + p[1] - context.projection(origin)[1]] : + [0, 0]; + + var nudge = edge(p, context.map().size()); + if (nudge) startNudge(nudge); + else stopNudge(); + + origin = context.map().mouseCoordinates(); context.replace( iD.actions.MoveWay(wayId, delta, context.projection), From 8bee68dfbd9300326216d8b979c481e5babd8195 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Tue, 5 Feb 2013 16:25:20 -0800 Subject: [PATCH 21/30] Fix includes --- test/rendering.html | 1 - 1 file changed, 1 deletion(-) diff --git a/test/rendering.html b/test/rendering.html index 196122b2c..ff624305a 100644 --- a/test/rendering.html +++ b/test/rendering.html @@ -21,7 +21,6 @@ - From 4dbd8f5efcde8643fd2b7974d7a64bd7a6b99fa4 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Tue, 5 Feb 2013 16:25:26 -0800 Subject: [PATCH 22/30] Fix #634 --- js/id/svg/points.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/id/svg/points.js b/js/id/svg/points.js index 471bc4b4d..a7fdb10fe 100644 --- a/js/id/svg/points.js +++ b/js/id/svg/points.js @@ -79,7 +79,7 @@ iD.svg.Points.imageIndex = [ }, { tags: { man_made: 'lighthouse' }, - icon: 'lighthouselevel_crossing' + icon: 'lighthouse' }, { tags: { natural: 'peak' }, From a4bf7c689fce0d63914b9ecf52740ca130c48f59 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 6 Feb 2013 10:49:58 -0500 Subject: [PATCH 23/30] Merge DragNode and DragMidpoint Adds shared behaviors such as snapping to DragMidpoint --- combobox.html | 1 - index.html | 1 - js/id/behavior/drag_midpoint.js | 29 ---------------------------- js/id/behavior/drag_node.js | 34 +++++++++++++++++++++++++++------ js/id/modes/browse.js | 3 +-- js/id/modes/select.js | 3 +-- test/index.html | 1 - 7 files changed, 30 insertions(+), 42 deletions(-) delete mode 100644 js/id/behavior/drag_midpoint.js diff --git a/combobox.html b/combobox.html index c739f4898..92949ecfa 100644 --- a/combobox.html +++ b/combobox.html @@ -90,7 +90,6 @@ - diff --git a/index.html b/index.html index a58aeb8f2..807e6a413 100644 --- a/index.html +++ b/index.html @@ -93,7 +93,6 @@ - diff --git a/js/id/behavior/drag_midpoint.js b/js/id/behavior/drag_midpoint.js deleted file mode 100644 index 4e39be2c6..000000000 --- a/js/id/behavior/drag_midpoint.js +++ /dev/null @@ -1,29 +0,0 @@ -iD.behavior.DragMidpoint = function(context) { - var behavior = iD.behavior.drag() - .delegate(".midpoint") - .origin(function(d) { - return context.projection(d.loc); - }) - .on('start', function(d) { - var node = iD.Node(); - - context.perform(iD.actions.AddMidpoint(d, node)); - - var vertex = context.surface().selectAll('.vertex') - .filter(function(data) { return data.id === node.id; }); - - behavior.target(vertex.node(), vertex.datum()); - }) - .on('move', function(d) { - d3.event.sourceEvent.stopPropagation(); - context.replace( - iD.actions.MoveNode(d.id, context.projection.invert(d3.event.point))); - }) - .on('end', function() { - context.replace( - iD.actions.Noop(), - t('operations.add.annotation.vertex')); - }); - - return behavior; -}; diff --git a/js/id/behavior/drag_node.js b/js/id/behavior/drag_node.js index 6df1fbf28..acba7919d 100644 --- a/js/id/behavior/drag_node.js +++ b/js/id/behavior/drag_node.js @@ -1,5 +1,6 @@ iD.behavior.DragNode = function(context) { - var nudgeInterval; + var nudgeInterval, + wasMidpoint; function edge(point, size) { var pad = [30, 100, 30, 100]; @@ -35,6 +36,23 @@ iD.behavior.DragNode = function(context) { } function start(entity) { + + wasMidpoint = entity.type === 'midpoint'; + if (wasMidpoint) { + var midpoint = entity; + entity = iD.Node(); + context.perform(iD.actions.AddMidpoint(midpoint, entity)); + + var vertex = context.surface() + .selectAll('.vertex') + .filter(function(d) { return d.id === entity.id; }); + behavior.target(vertex.node(), entity); + + } else { + context.perform( + iD.actions.Noop()); + } + var activeIDs = _.pluck(context.graph().parentWays(entity), 'id'); activeIDs.push(entity.id); @@ -43,9 +61,6 @@ iD.behavior.DragNode = function(context) { .selectAll('.node, .way') .filter(function (d) { return activeIDs.indexOf(d.id) >= 0; }) .classed('active', true); - - context.perform( - iD.actions.Noop()); } function datum() { @@ -96,6 +111,11 @@ iD.behavior.DragNode = function(context) { iD.actions.Connect([entity.id, d.id]), connectAnnotation(d)); + } else if (wasMidpoint) { + context.replace( + iD.actions.Noop(), + t('operations.add.annotation.vertex')); + } else { context.replace( iD.actions.Noop(), @@ -103,10 +123,12 @@ iD.behavior.DragNode = function(context) { } } - return iD.behavior.drag() - .delegate("g.node") + var behavior = iD.behavior.drag() + .delegate("g.node, g.midpoint") .origin(origin) .on('start', start) .on('move', move) .on('end', end); + + return behavior; }; diff --git a/js/id/modes/browse.js b/js/id/modes/browse.js index 33add1262..97f0d6935 100644 --- a/js/id/modes/browse.js +++ b/js/id/modes/browse.js @@ -10,8 +10,7 @@ iD.modes.Browse = function(context) { var behaviors = [ iD.behavior.Hover(), iD.behavior.Select(context), - iD.behavior.DragNode(context), - iD.behavior.DragMidpoint(context)]; + iD.behavior.DragNode(context)]; mode.enter = function() { behaviors.forEach(function(behavior) { diff --git a/js/id/modes/select.js b/js/id/modes/select.js index 532faaca8..0bcfa8c0b 100644 --- a/js/id/modes/select.js +++ b/js/id/modes/select.js @@ -9,8 +9,7 @@ iD.modes.Select = function(context, selection, initial) { behaviors = [ iD.behavior.Hover(), iD.behavior.Select(context), - iD.behavior.DragNode(context), - iD.behavior.DragMidpoint(context)], + iD.behavior.DragNode(context)], radialMenu; function changeTags(d, tags) { diff --git a/test/index.html b/test/index.html index 522f26092..f17829653 100644 --- a/test/index.html +++ b/test/index.html @@ -90,7 +90,6 @@ - From a21da4f15fef4b149d2ea88d5927093ef6ec4e98 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 6 Feb 2013 12:48:42 -0500 Subject: [PATCH 24/30] Saving graph to and reinstating from localStorage --- js/id/graph/graph.js | 45 +++++++++++++++++++++++++++++++++++++++++- js/id/graph/history.js | 45 +++++++++++++++++++++++++++++++++++++++++- js/id/id.js | 15 +++++++------- js/id/ui.js | 3 ++- js/id/ui/splash.js | 4 ++-- 5 files changed, 100 insertions(+), 12 deletions(-) diff --git a/js/id/graph/graph.js b/js/id/graph/graph.js index 2692aeba6..d9118045a 100644 --- a/js/id/graph/graph.js +++ b/js/id/graph/graph.js @@ -227,10 +227,53 @@ iD.Graph.prototype = { var items = []; for (var i in this.entities) { var entity = this.entities[i]; - if (entity && entity.intersects(extent, this)) { + if (entity && this.hasAllChildren(entity) && entity.intersects(extent, this)) { items.push(entity); } } return items; + }, + + hasAllChildren: function(entity) { + // we're only checking changed entities, since we assume fetched data + // must have all children present + if (this.entities.hasOwnProperty(entity.id)) { + if (entity.type === 'way') { + for (i = 0; i < entity.nodes.length; i++) { + if (!this.entities[entity.nodes[i]]) return false; + } + } else if (entity.type === 'relation') { + for (i = 0; i < entity.members.length; i++) { + if (!this.entities[entity.members[i].id]) return false; + } + } + } + return true; + }, + + // Obliterates any existing entities + load: function(entities) { + + var base = this.base(), + i, entity, prefix; + this.entities = Object.create(base.entities); + + for (i in entities) { + entity = entities[i]; + prefix = i[0]; + + if (prefix == 'n') { + this.entities[i] = new iD.Node(entity); + + } else if (prefix == 'w') { + this.entities[i] = new iD.Way(entity); + + } else if (prefix == 'r') { + this.entities[i] = new iD.Relation(entity); + } + this._updateCalculated(base.entities[i], this.entities[i]); + } + return this; } + }; diff --git a/js/id/graph/history.js b/js/id/graph/history.js index a0930203c..dba04ec7e 100644 --- a/js/id/graph/history.js +++ b/js/id/graph/history.js @@ -1,4 +1,4 @@ -iD.History = function() { +iD.History = function(context) { var stack, index, imagery_used = 'Bing', dispatch = d3.dispatch('change', 'undone', 'redone'); @@ -26,6 +26,10 @@ iD.History = function() { return difference; } + function getKey(n) { + return 'iD_' + window.location.origin + '_' + n; + } + var history = { graph: function() { return stack[index].graph; @@ -148,8 +152,47 @@ iD.History = function() { reset: function() { stack = [{graph: iD.Graph()}]; index = 0; + this.load(); dispatch.change(); + }, + + save: function() { + var json = JSON.stringify(stack.map(function(i) { + return _.extend(i, { + graph: i.graph.entities + }); + })); + + context.storage(getKey('history'), json); + context.storage(getKey('nextIDs'), JSON.stringify(iD.Entity.id.next)); + context.storage(getKey('index'), index); + context.storage(getKey('lock'), ''); + }, + + lock: function() { + if (context.storage(getKey('lock'))) return false; + context.storage(getKey('lock'), true); + return true; + }, + + load: function() { + if (!this.lock()) return; + + var json = context.storage(getKey('history')), + nextIDs = context.storage(getKey('nextIDs')), + index_ = context.storage(getKey('index')); + + if (!json) return; + if (nextIDs) iD.Entity.id.next = JSON.parse(nextIDs); + if (index_ !== null) index = parseInt(index_, 10); + + stack = JSON.parse(json).map(function(d) { + d.graph = iD.Graph().load(d.graph); + return d; + }); + } + }; history.reset(); diff --git a/js/id/id.js b/js/id/id.js index 56d3e481a..e20b3b111 100644 --- a/js/id/id.js +++ b/js/id/id.js @@ -1,18 +1,19 @@ window.iD = function () { var context = {}, - history = iD.History(), - storage = localStorage || {}, - dispatch = d3.dispatch('enter', 'exit'), - mode, - container, - ui = iD.ui(context), - map = iD.Map(context); + storage = localStorage || {}; context.storage = function(k, v) { if (arguments.length === 1) return storage[k]; else storage[k] = v; }; + var history = iD.History(context), + dispatch = d3.dispatch('enter', 'exit'), + mode, + container, + ui = iD.ui(context), + map = iD.Map(context); + // the connection requires .storage() to be available on calling. var connection = iD.Connection(context); diff --git a/js/id/ui.js b/js/id/ui.js index ace66703d..dbbff15c8 100644 --- a/js/id/ui.js +++ b/js/id/ui.js @@ -194,6 +194,7 @@ iD.ui = function(context) { history.on('change.editor', function() { window.onbeforeunload = history.hasChanges() ? function() { + history.save(); return 'You have unsaved changes.'; } : null; @@ -252,7 +253,7 @@ iD.ui = function(context) { context.enter(iD.modes.Browse(context)); if (!context.storage('sawSplash')) { - iD.ui.splash(); + iD.ui.splash(context.container()); context.storage('sawSplash', true); } }; diff --git a/js/id/ui/splash.js b/js/id/ui/splash.js index a4d2a915e..bfcbf3e05 100644 --- a/js/id/ui/splash.js +++ b/js/id/ui/splash.js @@ -1,5 +1,5 @@ -iD.ui.splash = function() { - var modal = iD.ui.modal(); +iD.ui.splash = function(selection) { + var modal = iD.ui.modal(selection); modal.select('.modal') .attr('class', 'modal-splash modal'); From 449c4d235da1f974f2b9c660baeb63a3c431c7b9 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 6 Feb 2013 14:03:31 -0500 Subject: [PATCH 25/30] Add option to restore or reset unsaved changes --- index.html | 1 + js/id/graph/history.js | 34 +++++++++++++++++++++++++++------- js/id/id.js | 1 + js/id/ui.js | 5 +++++ js/id/ui/restore.js | 34 ++++++++++++++++++++++++++++++++++ 5 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 js/id/ui/restore.js diff --git a/index.html b/index.html index 807e6a413..18f1b275b 100644 --- a/index.html +++ b/index.html @@ -67,6 +67,7 @@ + diff --git a/js/id/graph/history.js b/js/id/graph/history.js index dba04ec7e..ac36d564f 100644 --- a/js/id/graph/history.js +++ b/js/id/graph/history.js @@ -1,7 +1,8 @@ iD.History = function(context) { var stack, index, imagery_used = 'Bing', - dispatch = d3.dispatch('change', 'undone', 'redone'); + dispatch = d3.dispatch('change', 'undone', 'redone'), + lock = false; function perform(actions) { actions = Array.prototype.slice.call(actions); @@ -152,11 +153,20 @@ iD.History = function(context) { reset: function() { stack = [{graph: iD.Graph()}]; index = 0; - this.load(); dispatch.change(); }, save: function() { + if (!lock) return; + context.storage(getKey('lock'), null); + + if (!stack.length) { + context.storage(getKey('history'), null); + context.storage(getKey('nextIDs'), null); + context.storage(getKey('index'), null); + return; + } + var json = JSON.stringify(stack.map(function(i) { return _.extend(i, { graph: i.graph.entities @@ -166,17 +176,22 @@ iD.History = function(context) { context.storage(getKey('history'), json); context.storage(getKey('nextIDs'), JSON.stringify(iD.Entity.id.next)); context.storage(getKey('index'), index); - context.storage(getKey('lock'), ''); }, lock: function() { if (context.storage(getKey('lock'))) return false; context.storage(getKey('lock'), true); - return true; + lock = true; + return lock; + }, + + restorableChanges: function() { + if (!this.lock()) return false; + return !!context.storage(getKey('history')); }, load: function() { - if (!this.lock()) return; + if (!lock) return; var json = context.storage(getKey('history')), nextIDs = context.storage(getKey('nextIDs')), @@ -186,10 +201,15 @@ iD.History = function(context) { if (nextIDs) iD.Entity.id.next = JSON.parse(nextIDs); if (index_ !== null) index = parseInt(index_, 10); - stack = JSON.parse(json).map(function(d) { - d.graph = iD.Graph().load(d.graph); + context.storage(getKey('history', null)); + context.storage(getKey('nextIDs', null)); + context.storage(getKey('index', null)); + + stack = JSON.parse(json).map(function(d, i) { + d.graph = iD.Graph(stack[0].graph).load(d.graph); return d; }); + dispatch.change(); } diff --git a/js/id/id.js b/js/id/id.js index e20b3b111..d58464ecb 100644 --- a/js/id/id.js +++ b/js/id/id.js @@ -4,6 +4,7 @@ window.iD = function () { context.storage = function(k, v) { if (arguments.length === 1) return storage[k]; + else if (v === null) delete storage[k]; else storage[k] = v; }; diff --git a/js/id/ui.js b/js/id/ui.js index dbbff15c8..c22ca6b83 100644 --- a/js/id/ui.js +++ b/js/id/ui.js @@ -256,5 +256,10 @@ iD.ui = function(context) { iD.ui.splash(context.container()); context.storage('sawSplash', true); } + + if (history.restorableChanges()) { + iD.ui.restore(context.container(), history); + } + }; }; diff --git a/js/id/ui/restore.js b/js/id/ui/restore.js new file mode 100644 index 000000000..3df275d89 --- /dev/null +++ b/js/id/ui/restore.js @@ -0,0 +1,34 @@ +iD.ui.restore = function(selection, history) { + var modal = iD.ui.modal(selection); + + modal.select('.modal') + .attr('class', 'modal-splash modal'); + + var introModal = modal.select('.content') + .append('div') + .attr('class', 'modal-section fillL') + .text('You have unsaved changes from a previous editing session. Do you wish to restore these changes?'); + + buttons = introModal + .append('div') + .attr('class', 'buttons cf') + .append('div') + .attr('class', 'button-wrap joined col4'); + + buttons.append('button') + .attr('class', 'save action button col6') + .text('Restore') + .on('click', function() { + history.load(); + modal.remove(); + }); + + buttons.append('button') + .attr('class', 'cancel button col6') + .text('Reset') + .on('click', function() { + modal.remove(); + }); + + return modal; +}; From 0d70e466de619185d6752a7f609663b513f9e502 Mon Sep 17 00:00:00 2001 From: Tom MacWright Date: Wed, 6 Feb 2013 14:37:04 -0500 Subject: [PATCH 26/30] Add crazyegg, will remove at release --- index.html | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/index.html b/index.html index 18f1b275b..09d73b8f6 100644 --- a/index.html +++ b/index.html @@ -152,6 +152,8 @@ .call(id.ui()) }); + + + + + From 0acab34054131544587b486c35ee6ee5cee73ae7 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 6 Feb 2013 15:06:50 -0500 Subject: [PATCH 27/30] Draw click event triggered by click instead of up --- js/id/behavior/draw.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/js/id/behavior/draw.js b/js/id/behavior/draw.js index a4fb53a1a..b0da771a4 100644 --- a/js/id/behavior/draw.js +++ b/js/id/behavior/draw.js @@ -27,7 +27,7 @@ iD.behavior.Draw = function(context) { target.on('mousemove.draw', null); - d3.select(window).on('mouseup.draw', function() { + d3.select(window).on('click.draw', function() { target.on('mousemove.draw', mousemove); if (iD.geo.dist(pos, point()) < closeTolerance || (iD.geo.dist(pos, point()) < tolerance && @@ -35,7 +35,6 @@ iD.behavior.Draw = function(context) { click(); } }); - } function mousemove() { @@ -111,7 +110,7 @@ iD.behavior.Draw = function(context) { .on('mousedown.draw', null) .on('mousemove.draw', null); - d3.select(window).on('mouseup.draw', null); + d3.select(window).on('click.draw', null); d3.select(document) .call(keybinding.off) From a9632a2c7a05275315e25d3983110e9cf38cad5e Mon Sep 17 00:00:00 2001 From: Tom MacWright Date: Wed, 6 Feb 2013 15:09:40 -0500 Subject: [PATCH 28/30] Do not trigger radial on double click --- js/id/behavior/select.js | 72 +++++++++++++++++++++++++++++++--------- 1 file changed, 57 insertions(+), 15 deletions(-) diff --git a/js/id/behavior/select.js b/js/id/behavior/select.js index f7a388396..3d10db144 100644 --- a/js/id/behavior/select.js +++ b/js/id/behavior/select.js @@ -1,24 +1,66 @@ iD.behavior.Select = function(context) { - function click() { - var datum = d3.select(d3.event.target).datum(); - if (datum instanceof iD.Entity) { - if (d3.event.shiftKey) { - context.enter(iD.modes.Select(context, context.selection().concat([datum.id]))); - } else { - context.enter(iD.modes.Select(context, [datum.id])); - } - } else if (!d3.event.shiftKey) { - context.enter(iD.modes.Browse(context)); - } - } - var behavior = function(selection) { - selection.on('click.select', click); + + var timeout = null, + // the position of the first mousedown + pos = null; + + function click(event) { + d3.event = event; + var datum = d3.select(d3.event.target).datum(); + if (datum instanceof iD.Entity) { + if (d3.event.shiftKey) { + context.enter(iD.modes.Select(context, context.selection().concat([datum.id]))); + } else { + context.enter(iD.modes.Select(context, [datum.id])); + } + } else if (!d3.event.shiftKey) { + context.enter(iD.modes.Browse(context)); + } + } + + function mousedown() { + pos = d3.mouse(context.surface().node()); + selection + .on('mousemove.select', mousemove) + .on('touchmove.select', mousemove); + + // we've seen a mousedown within 400ms of this one, so ignore + // both because they will be a double click + if (timeout !== null) { + window.clearTimeout(timeout); + selection.on('mousemove.select', null); + timeout = null; + } else { + // queue the click handler to fire in 400ms if no other clicks + // are detected + timeout = window.setTimeout((function(event) { + return function() { + click(event); + timeout = null; + selection.on('mousemove.select', null); + }; + // save the event for the click handler + })(d3.event), 400); + } + } + + // allow mousemoves to cancel the click + function mousemove() { + if (iD.geo.dist(d3.mouse(context.surface().node()), pos) > 4) { + window.clearTimeout(timeout); + timeout = null; + } + } + + selection + .on('mousedown.select', mousedown) + .on('touchstart.select', mousedown); }; behavior.off = function(selection) { - selection.on('click.select', null); + selection.on('mousedown.select', null); }; return behavior; From 83224d0f87ec11a62903a0efba7896cf60a13702 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 6 Feb 2013 15:38:20 -0500 Subject: [PATCH 29/30] imagery_used includes full custom template --- js/id/renderer/background_source.js | 1 + js/id/ui/layerswitcher.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/js/id/renderer/background_source.js b/js/id/renderer/background_source.js index cfed23294..e740525c0 100644 --- a/js/id/renderer/background_source.js +++ b/js/id/renderer/background_source.js @@ -24,6 +24,7 @@ iD.BackgroundSource.template = function(template, subdomains, scaleExtent) { }; generator.scaleExtent = scaleExtent; + generator.template = template; return generator; }; diff --git a/js/id/ui/layerswitcher.js b/js/id/ui/layerswitcher.js index 9eb063517..a15cd33c3 100644 --- a/js/id/ui/layerswitcher.js +++ b/js/id/ui/layerswitcher.js @@ -125,7 +125,7 @@ iD.ui.layerswitcher = function(context) { var configured = d.source(); if (!configured) return; d.source = configured; - d.name = 'Custom (configured)'; + d.name = 'Custom (' + d.source.template + ')'; } context.background().source(d.source); context.history().imagery_used(d.name); From fbe3a41d570441f0cc32ae414c5f858db9d885f9 Mon Sep 17 00:00:00 2001 From: Tom MacWright Date: Wed, 6 Feb 2013 15:47:16 -0500 Subject: [PATCH 30/30] Update tests for faux click events in more places. --- test/index.html | 2 +- test/spec/behavior/select.js | 26 ++++++++++++++++++-------- test/spec/modes/add_point.js | 4 ++-- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/test/index.html b/test/index.html index f17829653..3f2eba72e 100644 --- a/test/index.html +++ b/test/index.html @@ -137,7 +137,7 @@ iD.debug = true; mocha.setup({ ui: 'bdd', - globals: ['__onresize.tail-size'] + globals: ['__onresize.tail-size', '__onmousemove.zoom', '__onmouseup.zoom', '__onclick.draw'] }); var expect = chai.expect; diff --git a/test/spec/behavior/select.js b/test/spec/behavior/select.js index 42c568cca..e794cda8e 100644 --- a/test/spec/behavior/select.js +++ b/test/spec/behavior/select.js @@ -29,21 +29,31 @@ describe("iD.behavior.Select", function() { container.remove(); }); - specify("click on entity selects the entity", function() { - happen.click(context.surface().select('.' + a.id).node()); - expect(context.selection()).to.eql([a.id]); + specify("click on entity selects the entity", function(done) { + happen.mousedown(context.surface().select('.' + a.id).node()); + window.setTimeout(function() { + expect(context.selection()).to.eql([a.id]); + done(); + }, 600); }); - specify("click on empty space clears the selection", function() { + specify("click on empty space clears the selection", function(done) { context.enter(iD.modes.Select(context, [a.id])); happen.click(context.surface().node()); - expect(context.selection()).to.eql([]); + happen.mousedown(context.surface().node()); + window.setTimeout(function() { + expect(context.selection()).to.eql([]); + done(); + }, 600); }); - specify("shift-click on entity adds the entity to the selection", function() { + specify("shift-click on entity adds the entity to the selection", function(done) { context.enter(iD.modes.Select(context, [a.id])); - happen.click(context.surface().select('.' + b.id).node(), {shiftKey: true}); - expect(context.selection()).to.eql([a.id, b.id]); + happen.mousedown(context.surface().select('.' + b.id).node(), {shiftKey: true}); + window.setTimeout(function() { + expect(context.selection()).to.eql([a.id, b.id]); + done(); + }, 600); }); specify("shift-click on empty space leaves the selection unchanged", function() { diff --git a/test/spec/modes/add_point.js b/test/spec/modes/add_point.js index 62291f8ee..5807a3c0b 100644 --- a/test/spec/modes/add_point.js +++ b/test/spec/modes/add_point.js @@ -17,13 +17,13 @@ describe("iD.modes.AddPoint", function() { describe("clicking the map", function () { it("adds a node", function() { happen.mousedown(context.surface().node(), {}); - happen.mouseup(window, {}); + happen.click(window, {}); expect(context.changes().created).to.have.length(1); }); it("selects the node", function() { happen.mousedown(context.surface().node(), {}); - happen.mouseup(window, {}); + happen.click(window, {}); expect(context.mode().id).to.equal('select'); expect(context.mode().selection()).to.eql([context.changes().created[0].id]); });