From 18c72670107881b27229f50faab34f53ff301dc7 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Thu, 24 Jan 2013 11:04:06 -0500 Subject: [PATCH] Extract Draw behavior Fixed some bugs but introduced others. --- index.html | 2 + js/id/behavior/draw.js | 54 ++++++++++ js/id/behavior/draw_way.js | 146 ++++++++++++++++++++++++++ js/id/graph/history.js | 2 +- js/id/modes/add_area.js | 45 +++++--- js/id/modes/add_line.js | 53 ++++++---- js/id/modes/add_point.js | 26 +++-- js/id/modes/draw_area.js | 146 ++++---------------------- js/id/modes/draw_line.js | 204 +++++++------------------------------ js/id/renderer/map.js | 1 + test/index.html | 2 + 11 files changed, 343 insertions(+), 338 deletions(-) create mode 100644 js/id/behavior/draw.js create mode 100644 js/id/behavior/draw_way.js diff --git a/index.html b/index.html index 369d1f72a..0979fa558 100644 --- a/index.html +++ b/index.html @@ -91,6 +91,8 @@ + + diff --git a/js/id/behavior/draw.js b/js/id/behavior/draw.js new file mode 100644 index 000000000..f09ed79e4 --- /dev/null +++ b/js/id/behavior/draw.js @@ -0,0 +1,54 @@ +iD.behavior.Draw = function () { + var event = d3.dispatch('move', 'add', 'drop', 'cancel', 'finish'), + keybinding = d3.keybinding('draw'); + + function draw(selection) { + function mousemove() { + event.move(); + } + + function click() { + event.add(); + } + + function backspace() { + d3.event.preventDefault(); + event.drop(); + } + + function del() { + d3.event.preventDefault(); + event.cancel(); + } + + function ret() { + d3.event.preventDefault(); + event.finish(); + } + + selection + .on('mousemove.draw', mousemove) + .on('click.draw', click); + + keybinding + .on('⌫', backspace) + .on('⌦', del) + .on('⎋', ret) + .on('↩', ret); + + d3.select(document) + .call(keybinding); + + return draw; + } + + draw.off = function(selection) { + selection + .on('mousemove.draw', null) + .on('click.draw', null); + + keybinding.off(); + }; + + return d3.rebind(draw, event, 'on'); +}; diff --git a/js/id/behavior/draw_way.js b/js/id/behavior/draw_way.js new file mode 100644 index 000000000..f4909b150 --- /dev/null +++ b/js/id/behavior/draw_way.js @@ -0,0 +1,146 @@ +iD.behavior.DrawWay = function(wayId, headId, tailId, index, mode) { + var map = mode.map, + history = mode.history, + controller = mode.controller, + event = d3.dispatch('add', 'addHead', 'addTail', 'addNode', 'addWay'), + way = mode.history.graph().entity(wayId), + nodeId = way.nodes[index], + hover, draw; + + function move() { + history.replace( + iD.actions.MoveNode(nodeId, map.mouseCoordinates()), + history.undoAnnotation()); + } + + function add() { + var datum = d3.select(d3.event.target).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() { + var way = history.graph().entity(wayId); + if (way) { + controller.enter(mode); + } else { + controller.enter(iD.modes.Browse()); + } + } + + var drawWay = function(surface) { + map.fastEnable(false) + .minzoom(16) + .dblclickEnable(false); + + surface.call(hover) + .call(draw) + .selectAll('.way, .node') + .filter(function (d) { return d.id === wayId || d.id === nodeId; }) + .classed('active', true); + + history.on('undone.draw', undone); + }; + + drawWay.off = function(surface) { + map.fastEnable(true) + .minzoom(0) + .tail(false); + + window.setTimeout(function() { + map.dblclickEnable(true); + }, 1000); + + surface.call(hover.off) + .call(draw.off) + .selectAll('.way, .node') + .classed('active', false); + + history.on('undone.draw', null); + }; + + // Connect the way to an existing node. Continue drawing, or enter the optional `newMode`. + drawWay.addNode = function(node, annotation, newMode) { + history.perform( + iD.actions.AddWayNode(wayId, node.id, index), + annotation); + + controller.enter(newMode || mode); + }; + + // Connect the way to an existing way. + drawWay.addWay = function(way, loc, wayIndex, annotation) { + var newNode = iD.Node({loc: loc}); + + history.perform( + iD.actions.AddNode(newNode), + iD.actions.AddWayNode(wayId, newNode.id, index), + iD.actions.AddWayNode(way.id, newNode.id, wayIndex), + annotation); + + 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}); + + history.perform( + iD.actions.AddNode(newNode), + iD.actions.AddWayNode(wayId, newNode.id, index), + annotation); + + controller.enter(mode); + }; + + // Remove the temporary node and the last connected node but continue drawing. + drawWay.drop = function() { + history.undo(); + }; + + // Finish the draw operation, removing the temporary node. If the way has enough + // nodes to be valid, it's selected. Otherwise, return to browse mode. + drawWay.finish = function() { + history.replace( + iD.actions.DeleteNode(nodeId), + history.undoAnnotation()); + + var way = history.graph().entity(wayId); + if (way) { + controller.enter(iD.modes.Select(way, true)); + } else { + controller.enter(iD.modes.Browse()); + } + }; + + // Cancel the draw operation and return to browse, deleting everything drawn. + drawWay.cancel = function() { + history.perform(iD.actions.DeleteWay(wayId), 'cancelled drawing'); + controller.enter(iD.modes.Browse()); + }; + + hover = iD.behavior.Hover(); + + draw = iD.behavior.Draw() + .on('move', move) + .on('add', add) + .on('drop', drawWay.drop) + .on('cancel', drawWay.cancel) + .on('finish', drawWay.finish); + + return d3.rebind(drawWay, event, 'on'); +}; diff --git a/js/id/graph/history.js b/js/id/graph/history.js index 142a9eeca..a1865715e 100644 --- a/js/id/graph/history.js +++ b/js/id/graph/history.js @@ -8,7 +8,7 @@ iD.History = function() { var annotation; - if (_.isString(_.last(actions))) { + if (!_.isFunction(_.last(actions))) { annotation = actions.pop(); } diff --git a/js/id/modes/add_area.js b/js/id/modes/add_area.js index d3936229e..58109befc 100644 --- a/js/id/modes/add_area.js +++ b/js/id/modes/add_area.js @@ -6,30 +6,33 @@ iD.modes.AddArea = function() { description: 'Add parks, buildings, lakes, or other areas to the map.' }; - var keybinding = d3.keybinding('add-area'); + var behavior; mode.enter = function() { var map = mode.map, + surface = map.surface, history = mode.history, controller = mode.controller; map.dblclickEnable(false) .tail('Click on the map to start drawing an area, like a park, lake, or building.'); - map.surface.on('click.addarea', function() { + function add() { var datum = d3.select(d3.event.target).datum() || {}, - way = iD.Way({tags: { area: 'yes' }}); + way = iD.Way({tags: { area: 'yes' }}), + node; if (datum.type === 'node') { // start from an existing node + node = datum; history.perform( iD.actions.AddWay(way), - iD.actions.AddWayNode(way.id, datum.id), - iD.actions.AddWayNode(way.id, datum.id)); + iD.actions.AddWayNode(way.id, node.id), + iD.actions.AddWayNode(way.id, node.id)); } else { // start from a new node - var node = iD.Node({loc: map.mouseCoordinates()}); + node = iD.Node({loc: map.mouseCoordinates()}); history.perform( iD.actions.AddWay(way), iD.actions.AddNode(node), @@ -37,24 +40,36 @@ iD.modes.AddArea = function() { iD.actions.AddWayNode(way.id, node.id)); } + node = iD.Node({loc: node.loc}); + + history.replace( + iD.actions.AddNode(node), + iD.actions.AddWayNode(way.id, node.id, -1), + 'started an area'); + controller.enter(iD.modes.DrawArea(way.id)); - }); + } - keybinding.on('⎋', function() { + function cancel() { controller.exit(); - }); + } - d3.select(document) - .call(keybinding); + behavior = iD.behavior.Draw() + .on('add', add) + .on('cancel', cancel) + .on('finish', cancel) + (surface); }; mode.exit = function() { + var map = mode.map, + surface = map.surface; + window.setTimeout(function() { - mode.map.dblclickEnable(true); + map.dblclickEnable(true); }, 1000); - mode.map.tail(false); - mode.map.surface.on('click.addarea', null); - keybinding.off(); + map.tail(false); + behavior.off(surface); }; return mode; diff --git a/js/id/modes/add_line.js b/js/id/modes/add_line.js index 190bf7e16..6b3a747e1 100644 --- a/js/id/modes/add_line.js +++ b/js/id/modes/add_line.js @@ -6,37 +6,38 @@ iD.modes.AddLine = function() { description: 'Lines can be highways, streets, pedestrian paths, or even canals.' }; - var keybinding = d3.keybinding('add-line'); + var behavior; mode.enter = function() { var map = mode.map, + surface = map.surface, graph = map.history().graph(), - node, history = mode.history, controller = mode.controller; map.dblclickEnable(false) .tail('Click on the map to start drawing an road, path, or route.'); - map.surface.on('click.addline', function() { + function add() { var datum = d3.select(d3.event.target).datum() || {}, way = iD.Way({ tags: { highway: 'residential' } }), - direction = 'forward'; + direction = 'forward', + node; if (datum.type === 'node') { // continue an existing way - var id = datum.id; - var parents = history.graph(graph).parentWays(datum); + node = datum; + var parents = history.graph(graph).parentWays(node); var isLine = parents.length && parents[0].geometry(graph) === 'line'; - if (isLine && parents[0].nodes[0] === id ) { + if (isLine && parents[0].first() === node.id) { way = parents[0]; direction = 'backward'; - } else if (isLine && _.last(parents[0].nodes) === id) { + } else if (isLine && parents[0].last() === node.id) { way = parents[0]; } else { history.perform( iD.actions.AddWay(way), - iD.actions.AddWayNode(way.id, datum.id)); + iD.actions.AddWayNode(way.id, node.id)); } } else if (datum.type === 'way') { @@ -60,22 +61,36 @@ iD.modes.AddLine = function() { iD.actions.AddWayNode(way.id, node.id)); } - controller.enter(iD.modes.DrawLine(way.id, direction)); - }); + var index = (direction === 'forward') ? way.nodes.length : 0, - keybinding.on('⎋', function() { + node = iD.Node({loc: node.loc}); + + history.replace( + iD.actions.AddNode(node), + iD.actions.AddWayNode(way.id, node.id, index), + 'started a line'); + + controller.enter(iD.modes.DrawLine(way.id, direction, node)); + } + + function cancel() { controller.exit(); - }); + } - d3.select(document) - .call(keybinding); + behavior = iD.behavior.Draw() + .on('add', add) + .on('cancel', cancel) + .on('finish', cancel) + (surface); }; mode.exit = function() { - mode.map.dblclickEnable(true); - mode.map.tail(false); - mode.map.surface.on('click.addline', null); - keybinding.off(); + var map = mode.map, + surface = map.surface; + + map.dblclickEnable(true); + map.tail(false); + behavior.off(surface); }; return mode; diff --git a/js/id/modes/add_point.js b/js/id/modes/add_point.js index bacdc1541..40c8e84ad 100644 --- a/js/id/modes/add_point.js +++ b/js/id/modes/add_point.js @@ -5,16 +5,17 @@ iD.modes.AddPoint = function() { description: 'Restaurants, monuments, and postal boxes are points.' }; - var keybinding = d3.keybinding('add-point'); + var behavior; mode.enter = function() { var map = mode.map, + surface = map.surface, history = mode.history, controller = mode.controller; map.tail('Click on the map to add a point.'); - map.surface.on('click.addpoint', function() { + function add() { var node = iD.Node({loc: map.mouseCoordinates()}); history.perform( @@ -22,20 +23,25 @@ iD.modes.AddPoint = function() { 'added a point'); controller.enter(iD.modes.Select(node, true)); - }); + } - keybinding.on('⎋', function() { + function cancel() { controller.exit(); - }); + } - d3.select(document) - .call(keybinding); + behavior = iD.behavior.Draw() + .on('add', add) + .on('cancel', cancel) + .on('finish', cancel) + (surface); }; mode.exit = function() { - mode.map.tail(false); - mode.map.surface.on('click.addpoint', null); - keybinding.off(); + var map = mode.map, + surface = map.surface; + + map.tail(false); + behavior.off(surface); }; return mode; diff --git a/js/id/modes/draw_area.js b/js/id/modes/draw_area.js index 2c779538d..d692ae550 100644 --- a/js/id/modes/draw_area.js +++ b/js/id/modes/draw_area.js @@ -4,147 +4,39 @@ iD.modes.DrawArea = function(wayId) { id: 'draw-area' }; - var keybinding = d3.keybinding('draw-area'); + var behavior; mode.enter = function() { - var map = mode.map, - surface = map.surface, - history = mode.history, - controller = mode.controller, - way = history.graph().entity(wayId), - index = way.nodes.length - 1, + var way = mode.history.graph().entity(wayId), + index = way.nodes.length - 2, headId = way.nodes[index - 1], - tailId = way.first(), - node = iD.Node({loc: map.mouseCoordinates()}); + tailId = way.first(); - map.dblclickEnable(false) - .fastEnable(false); - map.tail('Click to add points to your area. Click the first point to finish the area.'); - - history.perform( - iD.actions.AddNode(node), - iD.actions.AddWayNode(way.id, node.id, index)); - - surface.selectAll('.way, .node') - .filter(function (d) { return d.id === wayId || d.id === node.id; }) - .classed('active', true); - - function ReplaceTemporaryNode(replacementId) { - return function(graph) { - graph = graph.replace(graph.entity(wayId).updateNode(replacementId, index)); - graph = graph.remove(node); - return graph; - } + function addHeadTail() { + behavior.finish(); } - function mousemove() { - history.replace(iD.actions.MoveNode(node.id, map.mouseCoordinates())); + function addNode(node) { + behavior.addNode(node, way.nodes.length > 2 ? 'added to an area' : ''); } - function click() { - var datum = d3.select(d3.event.target).datum() || {}; - - if (datum.id === tailId || datum.id === headId) { - if (way.nodes.length > 3) { - history.undo(); - controller.enter(iD.modes.Select(way, true)); - } else { - // Areas with less than 3 nodes gets deleted - history.replace(iD.actions.DeleteWay(way.id)); - controller.enter(iD.modes.Browse()); - } - - } else if (datum.type === 'node' && datum.id !== node.id) { - // connect the way to an existing node - history.replace( - ReplaceTemporaryNode(datum.id), - way.nodes.length > 2 ? 'added to an area' : ''); - - controller.enter(iD.modes.DrawArea(wayId)); - - } else { - history.replace( - iD.actions.Noop(), - way.nodes.length > 2 ? 'added to an area' : ''); - - controller.enter(iD.modes.DrawArea(wayId)); - } + function add(loc) { + behavior.add(loc, way.nodes.length > 2 ? 'added to an area' : ''); } - function backspace() { - d3.event.preventDefault(); + behavior = iD.behavior.DrawWay(wayId, headId, tailId, index, mode) + .on('addHead', addHeadTail) + .on('addTail', addHeadTail) + .on('addNode', addNode) + .on('addWay', add) + .on('add', add); - history.replace( - iD.actions.DeleteNode(node.id), - iD.actions.DeleteNode(headId)); - - if (history.graph().entity(wayId)) { - controller.enter(iD.modes.DrawArea(wayId)); - } else { - // The way was deleted because it had too few nodes. - controller.enter(iD.modes.Browse()); - } - } - - function del() { - d3.event.preventDefault(); - history.replace(iD.actions.DeleteWay(wayId)); - controller.enter(iD.modes.Browse()); - } - - function ret() { - d3.event.preventDefault(); - - history.replace(iD.actions.DeleteNode(node.id)); - - if (history.graph().entity(wayId)) { - controller.enter(iD.modes.Select(way, true)); - } else { - // The way was deleted because it had too few nodes. - controller.enter(iD.modes.Browse()); - } - } - - surface - .on('mousemove.drawarea', mousemove) - .on('click.drawarea', click); - - keybinding - .on('⌫', backspace) - .on('⌦', del) - .on('⎋', ret) - .on('↩', ret); - - d3.select(document) - .call(keybinding); - - history.on('undone.drawarea', function () { - controller.enter(iD.modes.Browse()); - }); + mode.map.surface.call(behavior); + mode.map.tail('Click to add points to your area. Click the first point to finish the area.'); }; mode.exit = function() { - var map = mode.map, - surface = map.surface, - history = mode.history; - - surface.selectAll('.way, .node') - .classed('active', false); - - map.tail(false); - map.fastEnable(true); - - surface - .on('mousemove.drawarea', null) - .on('click.drawarea', null); - - keybinding.off(); - - history.on('undone.drawarea', null); - - window.setTimeout(function() { - mode.map.dblclickEnable(true); - }, 1000); + mode.map.surface.call(behavior.off); }; return mode; diff --git a/js/id/modes/draw_line.js b/js/id/modes/draw_line.js index b8fabeca5..1e09ab0ac 100644 --- a/js/id/modes/draw_line.js +++ b/js/id/modes/draw_line.js @@ -4,182 +4,54 @@ iD.modes.DrawLine = function(wayId, direction) { id: 'draw-line' }; - var keybinding = d3.keybinding('draw-line'); + var behavior; mode.enter = function() { - var map = mode.map, - surface = map.surface, - history = mode.history, - controller = mode.controller, - way = history.graph().entity(wayId), - node = iD.Node({loc: map.mouseCoordinates()}), - index = (direction === 'forward') ? way.nodes.length : 0, - headId = (direction === 'forward') ? way.last() : way.first(), + var way = mode.history.graph().entity(wayId), + index = (direction === 'forward') ? way.nodes.length - 1 : 0, + headId = (direction === 'forward') ? way.nodes[index - 1] : way.nodes[index + 1], tailId = (direction === 'forward') ? way.first() : way.last(); - iD.behavior.Hover()(surface); + function addHead() { + behavior.finish(); + } - map.dblclickEnable(false) - .fastEnable(false) - .tail('Click to add more points to the line. ' + + function addTail(node) { + // connect the way in a loop + if (way.nodes.length > 2) { + behavior.addNode(node, 'added to a line', iD.modes.Select(way, true)) + } else { + behavior.cancel(); + } + } + + function addNode(node) { + behavior.addNode(node, 'added to a line'); + } + + function addWay(way, loc, index) { + behavior.addWay(way, loc, index, 'added to a line'); + } + + function add(loc) { + behavior.add(loc, 'added to a line'); + } + + behavior = iD.behavior.DrawWay(wayId, headId, tailId, index, mode) + .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. ' + 'Click on other lines to connect to them, and double-click to ' + 'end the line.'); - - map.minzoom(16); - - history.perform( - iD.actions.AddNode(node), - iD.actions.AddWayNode(wayId, node.id, index)); - - surface.selectAll('.way, .node') - .filter(function (d) { return d.id === wayId || d.id === node.id; }) - .classed('active', true); - - function ReplaceTemporaryNode(replacementId) { - return function(graph) { - graph = graph.replace(graph.entity(wayId).updateNode(replacementId, index)); - graph = graph.remove(node); - return graph; - } - } - - function mousemove() { - history.replace(iD.actions.MoveNode(node.id, map.mouseCoordinates())); - } - - function click() { - var datum = d3.select(d3.event.target).datum() || {}; - - if (datum.id === tailId) { - // connect the way in a loop - if (way.nodes.length > 2) { - history.replace( - ReplaceTemporaryNode(tailId), - 'added to a line'); - - controller.enter(iD.modes.Select(way, true)); - - } else { - history.replace(iD.actions.DeleteWay(way.id)); - controller.enter(iD.modes.Browse()); - } - - } else if (datum.id === headId) { - // finish the way - history.undo(); - - controller.enter(iD.modes.Select(way, true)); - - } else if (datum.type === 'node' && datum.id !== node.id) { - // connect the way to an existing node - history.replace( - ReplaceTemporaryNode(datum.id), - 'added to a line'); - - controller.enter(iD.modes.DrawLine(wayId, direction)); - - } else if (datum.type === 'way' || datum.midpoint) { - var choice; - // connect the way to an existing way - if (datum.midpoint) { - // if clicked on midpoint - datum.id = datum.way; - choice = datum; - } else { - choice = iD.geo.chooseIndex(datum, d3.mouse(surface.node()), map); - } - - history.replace( - iD.actions.MoveNode(node.id, choice.loc), - iD.actions.AddWayNode(datum.id, node.id, choice.index), - 'added to a line'); - - controller.enter(iD.modes.DrawLine(wayId, direction)); - - } else { - history.replace( - iD.actions.Noop(), - 'added to a line'); - - controller.enter(iD.modes.DrawLine(wayId, direction)); - } - } - - function backspace() { - d3.event.preventDefault(); - - history.replace( - iD.actions.DeleteNode(node.id), - iD.actions.DeleteNode(headId)); - - if (history.graph().entity(wayId)) { - controller.enter(iD.modes.DrawLine(wayId, direction)); - } else { - // The way was deleted because it had too few nodes. - controller.enter(iD.modes.Browse()); - } - } - - function del() { - d3.event.preventDefault(); - history.replace(iD.actions.DeleteWay(wayId)); - controller.enter(iD.modes.Browse()); - } - - function ret() { - d3.event.preventDefault(); - - history.replace(iD.actions.DeleteNode(node.id)); - - if (history.graph().entity(wayId)) { - controller.enter(iD.modes.Select(way, true)); - } else { - // The way was deleted because it had too few nodes. - controller.enter(iD.modes.Browse()); - } - } - - surface - .on('mousemove.drawline', mousemove) - .on('click.drawline', click); - - keybinding - .on('⌫', backspace) - .on('⌦', del) - .on('⎋', ret) - .on('↩', ret); - - d3.select(document) - .call(keybinding); - - history.on('undone.drawline', function () { - controller.enter(iD.modes.Browse()); - }); }; mode.exit = function() { - var map = mode.map, - surface = map.surface, - history = mode.history; - - surface.selectAll('.way, .node') - .classed('active', false); - - map.tail(false); - map.fastEnable(true); - map.minzoom(0); - - surface - .on('mousemove.drawline', null) - .on('click.drawline', null); - - keybinding.off(); - - history.on('undone.drawline', null); - - window.setTimeout(function() { - mode.map.dblclickEnable(true); - }, 1000); + mode.map.surface.call(behavior.off); }; return mode; diff --git a/js/id/renderer/map.js b/js/id/renderer/map.js index 7cf07790d..fff0deac7 100644 --- a/js/id/renderer/map.js +++ b/js/id/renderer/map.js @@ -341,6 +341,7 @@ iD.Map = function() { map.minzoom = function(_) { if (!arguments.length) return minzoom; minzoom = _; + return map; }; map.history = function (_) { diff --git a/test/index.html b/test/index.html index 8e88c500b..10bba28ea 100644 --- a/test/index.html +++ b/test/index.html @@ -86,6 +86,8 @@ + +