From 6559b7015fcf8fffb44a7ae906d719f81e0f9e21 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Tue, 29 Jan 2013 14:36:59 -0500 Subject: [PATCH 1/4] Futuristic radial menu (fixes #548) --- css/app.css | 12 ++++++--- js/id/ui/radial_menu.js | 56 +++++++++++++++++++++++++++-------------- 2 files changed, 46 insertions(+), 22 deletions(-) diff --git a/css/app.css b/css/app.css index 211efc4df..e5d04b365 100644 --- a/css/app.css +++ b/css/app.css @@ -1181,10 +1181,13 @@ a.success-action { border-radius: 4px; } +.radial-menu-background { + stroke: #aaa; + stroke-opacity: 0.4; +} + .radial-menu-item { - fill: white; - stroke: black; - stroke-width: 1; + fill: black; cursor:url(../img/cursor-pointer.png) 6 1, auto; } @@ -1202,6 +1205,9 @@ a.success-action { fill: rgba(255,255,255,.5); } +.radial-menu image { + pointer-events: none; +} /* Media Queries ------------------------------------------------------- */ diff --git a/js/id/ui/radial_menu.js b/js/id/ui/radial_menu.js index d38ef3679..72de946d1 100644 --- a/js/id/ui/radial_menu.js +++ b/js/id/ui/radial_menu.js @@ -13,15 +13,7 @@ iD.ui.RadialMenu = function(entity, mode) { operation(history); } - var arc = d3.svg.arc() - .outerRadius(70) - .innerRadius(30) - .startAngle(function (d, i) { return 2 * Math.PI / operations.length * i; }) - .endAngle(function (d, i) { return 2 * Math.PI / operations.length * (i + 1); }); - - arcs = selection.selectAll() - .data(operations) - .enter().append('g') + arcs = selection.append('g') .attr('class', 'radial-menu') .attr('transform', "translate(" + center + ")") .attr('opacity', 0); @@ -29,17 +21,43 @@ iD.ui.RadialMenu = function(entity, mode) { arcs.transition() .attr('opacity', 0.8); - arcs.append('path') - .attr('class', function (d) { return 'radial-menu-item radial-menu-item-' + d.id; }) - .attr('d', arc) - .classed('disabled', function (d) { return !d.enabled(graph); }) - .on('click', click); + var r = 50, + a = Math.PI / 4, + a0 = -Math.PI / 4, + a1 = a0 + (operations.length - 1) * a; - arcs.append('text') - .attr("transform", function(d, i) { return "translate(" + arc.centroid(d, i) + ")"; }) - .attr("dy", ".35em") - .style("text-anchor", "middle") - .text(function(d) { return d.title; }); + arcs.append('path') + .attr('class', 'radial-menu-background') + .attr('d', 'M' + r * Math.sin(a0) + ',' + + r * Math.cos(a0) + + ' A' + r + ',' + r + ' 0 0,0 ' + + r * Math.sin(a1) + ',' + + r * Math.cos(a1)) + .attr('stroke-width', 50) + .attr('stroke-linecap', 'round'); + + var button = arcs.selectAll() + .data(operations) + .enter().append('g') + .attr('transform', function(d, i) { + return 'translate(' + r * Math.sin(a0 + i * a) + ',' + + r * Math.cos(a0 + i * a) + ')'; + }); + + button.append('circle') + .attr('class', function (d) { return 'radial-menu-item radial-menu-item-' + d.id; }) + .attr('r', 15) + .attr('title', function (d) { return d.title; }) + .classed('disabled', function (d) { return !d.enabled(graph); }) + .on('click', click) + .on('mouseover', mouseover) + .on('mouseout', mouseout); + + button.append('image') + .attr('width', 16) + .attr('height', 16) + .attr('transform', 'translate(-8, -8)') + .attr('xlink:href', 'icons/helipad.png'); }; radialMenu.close = function(selection) { From 9b2860d01afca5b224f60027bb1a504e277a925c Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Tue, 29 Jan 2013 15:52:00 -0500 Subject: [PATCH 2/4] Futuristic radial menu tooltips --- css/app.css | 8 ++++++++ js/id/operations/circular.js | 1 + js/id/operations/delete.js | 1 + js/id/operations/move.js | 1 + js/id/operations/reverse.js | 1 + js/id/operations/split.js | 1 + js/id/operations/unjoin.js | 1 + js/id/ui/radial_menu.js | 39 +++++++++++++++++++++++++++++------- 8 files changed, 46 insertions(+), 7 deletions(-) diff --git a/css/app.css b/css/app.css index e5d04b365..bb86b3c74 100644 --- a/css/app.css +++ b/css/app.css @@ -1209,6 +1209,14 @@ a.success-action { pointer-events: none; } +.radial-menu-tooltip { + background: rgba(255, 255, 255, 0.8); + padding: 5px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} + /* Media Queries ------------------------------------------------------- */ diff --git a/js/id/operations/circular.js b/js/id/operations/circular.js index 0af64aa20..d5d449853 100644 --- a/js/id/operations/circular.js +++ b/js/id/operations/circular.js @@ -29,6 +29,7 @@ iD.operations.Circular = function(entityId, mode) { operation.id = "circular"; operation.title = "Circular"; + operation.description = "Make this round"; return operation; }; diff --git a/js/id/operations/delete.js b/js/id/operations/delete.js index 53038dbc4..42fb2034c 100644 --- a/js/id/operations/delete.js +++ b/js/id/operations/delete.js @@ -37,6 +37,7 @@ iD.operations.Delete = function(entityId) { operation.id = "delete"; operation.title = "Delete"; + operation.description = "Remove this from the map"; return operation; }; diff --git a/js/id/operations/move.js b/js/id/operations/move.js index 9806ede67..ca0f0f65b 100644 --- a/js/id/operations/move.js +++ b/js/id/operations/move.js @@ -13,6 +13,7 @@ iD.operations.Move = function(entityId, mode) { operation.id = "move"; operation.title = "Move"; + operation.description = "Move this to a different location"; return operation; }; diff --git a/js/id/operations/reverse.js b/js/id/operations/reverse.js index 941d8a4da..44763502e 100644 --- a/js/id/operations/reverse.js +++ b/js/id/operations/reverse.js @@ -16,6 +16,7 @@ iD.operations.Reverse = function(entityId) { operation.id = "reverse"; operation.title = "Reverse"; + operation.description = "Make this way go in the opposite direction"; return operation; }; diff --git a/js/id/operations/split.js b/js/id/operations/split.js index 838dac88e..3e298f133 100644 --- a/js/id/operations/split.js +++ b/js/id/operations/split.js @@ -16,6 +16,7 @@ iD.operations.Split = function(entityId) { operation.id = "split"; operation.title = "Split"; + operation.description = "Split this into two ways at this point"; return operation; }; diff --git a/js/id/operations/unjoin.js b/js/id/operations/unjoin.js index 985a41697..e4275eff8 100644 --- a/js/id/operations/unjoin.js +++ b/js/id/operations/unjoin.js @@ -16,6 +16,7 @@ iD.operations.Unjoin = function(entityId) { operation.id = "unjoin"; operation.title = "Unjoin"; + operation.description = "Disconnect these ways from each other"; return operation; }; diff --git a/js/id/ui/radial_menu.js b/js/id/ui/radial_menu.js index 72de946d1..136657dbc 100644 --- a/js/id/ui/radial_menu.js +++ b/js/id/ui/radial_menu.js @@ -1,5 +1,5 @@ iD.ui.RadialMenu = function(entity, mode) { - var arcs; + var menu; var radialMenu = function(selection, center) { var history = mode.map.history(), @@ -13,12 +13,12 @@ iD.ui.RadialMenu = function(entity, mode) { operation(history); } - arcs = selection.append('g') + menu = selection.append('g') .attr('class', 'radial-menu') .attr('transform', "translate(" + center + ")") .attr('opacity', 0); - arcs.transition() + menu.transition() .attr('opacity', 0.8); var r = 50, @@ -26,7 +26,7 @@ iD.ui.RadialMenu = function(entity, mode) { a0 = -Math.PI / 4, a1 = a0 + (operations.length - 1) * a; - arcs.append('path') + menu.append('path') .attr('class', 'radial-menu-background') .attr('d', 'M' + r * Math.sin(a0) + ',' + r * Math.cos(a0) + @@ -36,7 +36,7 @@ iD.ui.RadialMenu = function(entity, mode) { .attr('stroke-width', 50) .attr('stroke-linecap', 'round'); - var button = arcs.selectAll() + var button = menu.selectAll() .data(operations) .enter().append('g') .attr('transform', function(d, i) { @@ -58,11 +58,36 @@ iD.ui.RadialMenu = function(entity, mode) { .attr('height', 16) .attr('transform', 'translate(-8, -8)') .attr('xlink:href', 'icons/helipad.png'); + + var tooltip = menu.append('foreignObject') + .style('display', 'none') + .attr('width', 200) + .attr('height', 400); + + tooltip.append('xhtml:div') + .attr('class', 'radial-menu-tooltip'); + + function mouseover(d, i) { + var angle = a0 + i * a, + dx = angle < 0 ? -200 : 0, + dy = 0; + + tooltip + .attr('x', (r + 30) * Math.sin(angle) + dx) + .attr('y', (r + 30) * Math.cos(angle) + dy) + .style('display', 'block') + .select('div') + .text(d.description); + } + + function mouseout() { + tooltip.style('display', 'none'); + } }; radialMenu.close = function(selection) { - if (arcs) { - arcs.transition() + if (menu) { + menu.transition() .attr('opacity', 0) .remove(); } From f31dcd32a267e1ef891b72f233bef238ceb2583e Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Tue, 29 Jan 2013 16:17:56 -0500 Subject: [PATCH 3/4] Add keybindings for all operations --- js/id/modes/select.js | 30 +++++++++++++----------------- js/id/operations/circular.js | 14 +++++++++----- js/id/operations/delete.js | 12 ++++++++---- js/id/operations/move.js | 6 +++++- js/id/operations/reverse.js | 12 ++++++++---- js/id/operations/split.js | 16 ++++++++++------ js/id/operations/unjoin.js | 16 ++++++++++------ js/id/ui/radial_menu.js | 12 +++--------- 8 files changed, 66 insertions(+), 52 deletions(-) diff --git a/js/id/modes/select.js b/js/id/modes/select.js index 06c05b7ea..ad06bda35 100644 --- a/js/id/modes/select.js +++ b/js/id/modes/select.js @@ -10,20 +10,6 @@ iD.modes.Select = function(entity, initial) { behaviors, radialMenu; - function remove() { - if (entity.type === 'way') { - mode.history.perform( - iD.actions.DeleteWay(entity.id), - 'deleted a way'); - } else if (entity.type === 'node') { - mode.history.perform( - iD.actions.DeleteNode(entity.id), - 'deleted a node'); - } - - mode.controller.exit(); - } - function changeTags(d, tags) { if (!_.isEqual(entity.tags, tags)) { mode.history.perform( @@ -49,6 +35,18 @@ iD.modes.Select = function(entity, initial) { behavior(surface); }); + var operations = d3.values(iD.operations) + .map(function (o) { return o(entity.id, mode); }) + .filter(function (o) { return o.available(); }); + + operations.forEach(function(operation) { + keybinding.on(operation.key, function () { + if (operation.enabled()) { + operation(); + } + }); + }); + var q = iD.util.stringQs(location.hash.substring(1)); location.replace('#' + iD.util.qsString(_.assign(q, { id: entity.id @@ -126,8 +124,6 @@ iD.modes.Select = function(entity, initial) { surface.on('click.select', click) .on('dblclick.select', dblclick); - keybinding.on('⌫', remove); - d3.select(document) .call(keybinding); @@ -137,7 +133,7 @@ iD.modes.Select = function(entity, initial) { }) .classed('selected', true); - radialMenu = iD.ui.RadialMenu(entity, mode); + radialMenu = iD.ui.RadialMenu(operations); if (d3.event && !initial) { var loc = map.mouseCoordinates(); diff --git a/js/id/operations/circular.js b/js/id/operations/circular.js index d5d449853..eb94150fc 100644 --- a/js/id/operations/circular.js +++ b/js/id/operations/circular.js @@ -1,7 +1,8 @@ iD.operations.Circular = function(entityId, mode) { - var action = iD.actions.Circular(entityId, mode.map); + var history = mode.map.history(), + action = iD.actions.Circular(entityId, mode.map); - var operation = function(history) { + var operation = function() { var graph = history.graph(), entity = graph.entity(entityId), geometry = entity.geometry(graph); @@ -18,16 +19,19 @@ iD.operations.Circular = function(entityId, mode) { } }; - operation.available = function(graph) { - var entity = graph.entity(entityId); + operation.available = function() { + var graph = history.graph(), + entity = graph.entity(entityId); return entity.geometry(graph) === 'area' || entity.geometry(graph) === 'line'; }; - operation.enabled = function(graph) { + operation.enabled = function() { + var graph = history.graph(); return action.enabled(graph); }; operation.id = "circular"; + operation.key = "O"; operation.title = "Circular"; operation.description = "Make this round"; diff --git a/js/id/operations/delete.js b/js/id/operations/delete.js index 42fb2034c..03d788261 100644 --- a/js/id/operations/delete.js +++ b/js/id/operations/delete.js @@ -1,5 +1,7 @@ -iD.operations.Delete = function(entityId) { - var operation = function(history) { +iD.operations.Delete = function(entityId, mode) { + var history = mode.map.history(); + + var operation = function() { var graph = history.graph(), entity = graph.entity(entityId), geometry = entity.geometry(graph); @@ -26,8 +28,9 @@ iD.operations.Delete = function(entityId) { } }; - operation.available = function(graph) { - var entity = graph.entity(entityId); + operation.available = function() { + var graph = history.graph(), + entity = graph.entity(entityId); return _.contains(['vertex', 'point', 'line', 'area'], entity.geometry(graph)); }; @@ -36,6 +39,7 @@ iD.operations.Delete = function(entityId) { }; operation.id = "delete"; + operation.key = "⌫"; operation.title = "Delete"; operation.description = "Remove this from the map"; diff --git a/js/id/operations/move.js b/js/id/operations/move.js index ca0f0f65b..383ca4206 100644 --- a/js/id/operations/move.js +++ b/js/id/operations/move.js @@ -1,9 +1,12 @@ iD.operations.Move = function(entityId, mode) { + var history = mode.map.history(); + var operation = function() { mode.controller.enter(iD.modes.MoveWay(entityId)); }; - operation.available = function(graph) { + operation.available = function() { + var graph = history.graph(); return graph.entity(entityId).type === 'way'; }; @@ -12,6 +15,7 @@ iD.operations.Move = function(entityId, mode) { }; operation.id = "move"; + operation.key = "M"; operation.title = "Move"; operation.description = "Move this to a different location"; diff --git a/js/id/operations/reverse.js b/js/id/operations/reverse.js index 44763502e..b36dfd60c 100644 --- a/js/id/operations/reverse.js +++ b/js/id/operations/reverse.js @@ -1,12 +1,15 @@ -iD.operations.Reverse = function(entityId) { - var operation = function(history) { +iD.operations.Reverse = function(entityId, mode) { + var history = mode.map.history(); + + var operation = function() { history.perform( iD.actions.ReverseWay(entityId), 'reversed a line'); }; - operation.available = function(graph) { - var entity = graph.entity(entityId); + operation.available = function() { + var graph = history.graph(), + entity = graph.entity(entityId); return entity.geometry(graph) === 'line'; }; @@ -15,6 +18,7 @@ iD.operations.Reverse = function(entityId) { }; operation.id = "reverse"; + operation.key = "V"; operation.title = "Reverse"; operation.description = "Make this way go in the opposite direction"; diff --git a/js/id/operations/split.js b/js/id/operations/split.js index 3e298f133..3dcab7481 100644 --- a/js/id/operations/split.js +++ b/js/id/operations/split.js @@ -1,20 +1,24 @@ -iD.operations.Split = function(entityId) { - var action = iD.actions.SplitWay(entityId); +iD.operations.Split = function(entityId, mode) { + var history = mode.map.history(), + action = iD.actions.SplitWay(entityId); - var operation = function(history) { + var operation = function() { history.perform(action, 'split a way'); }; - operation.available = function(graph) { - var entity = graph.entity(entityId); + operation.available = function() { + var graph = history.graph(), + entity = graph.entity(entityId); return entity.geometry(graph) === 'vertex'; }; - operation.enabled = function(graph) { + operation.enabled = function() { + var graph = history.graph(); return action.enabled(graph); }; operation.id = "split"; + operation.key = "X"; operation.title = "Split"; operation.description = "Split this into two ways at this point"; diff --git a/js/id/operations/unjoin.js b/js/id/operations/unjoin.js index e4275eff8..c18de3029 100644 --- a/js/id/operations/unjoin.js +++ b/js/id/operations/unjoin.js @@ -1,20 +1,24 @@ -iD.operations.Unjoin = function(entityId) { - var action = iD.actions.UnjoinNode(entityId); +iD.operations.Unjoin = function(entityId, mode) { + var history = mode.map.history(), + action = iD.actions.UnjoinNode(entityId); - var operation = function(history) { + var operation = function() { history.perform(action, 'unjoined lines'); }; - operation.available = function(graph) { - var entity = graph.entity(entityId); + operation.available = function() { + var graph = history.graph(), + entity = graph.entity(entityId); return entity.geometry(graph) === 'vertex'; }; - operation.enabled = function(graph) { + operation.enabled = function() { + var graph = history.graph(); return action.enabled(graph); }; operation.id = "unjoin"; + operation.key = "⇧-J"; operation.title = "Unjoin"; operation.description = "Disconnect these ways from each other"; diff --git a/js/id/ui/radial_menu.js b/js/id/ui/radial_menu.js index 136657dbc..dd94c661e 100644 --- a/js/id/ui/radial_menu.js +++ b/js/id/ui/radial_menu.js @@ -1,16 +1,10 @@ -iD.ui.RadialMenu = function(entity, mode) { +iD.ui.RadialMenu = function(operations) { var menu; var radialMenu = function(selection, center) { - var history = mode.map.history(), - graph = history.graph(), - operations = d3.values(iD.operations) - .map(function (o) { return o(entity.id, mode); }) - .filter(function (o) { return o.available(graph); }); - function click(operation) { d3.event.stopPropagation(); - operation(history); + operation(); } menu = selection.append('g') @@ -48,7 +42,7 @@ iD.ui.RadialMenu = function(entity, mode) { .attr('class', function (d) { return 'radial-menu-item radial-menu-item-' + d.id; }) .attr('r', 15) .attr('title', function (d) { return d.title; }) - .classed('disabled', function (d) { return !d.enabled(graph); }) + .classed('disabled', function (d) { return !d.enabled(); }) .on('click', click) .on('mouseover', mouseover) .on('mouseout', mouseout); From 2008fa1d927d0bbd16835a1e2b1a1f32871853fe Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Tue, 29 Jan 2013 17:52:48 -0500 Subject: [PATCH 4/4] Don't show an empty menu --- js/id/ui/radial_menu.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/js/id/ui/radial_menu.js b/js/id/ui/radial_menu.js index dd94c661e..f06fa362e 100644 --- a/js/id/ui/radial_menu.js +++ b/js/id/ui/radial_menu.js @@ -2,6 +2,9 @@ iD.ui.RadialMenu = function(operations) { var menu; var radialMenu = function(selection, center) { + if (!operations.length) + return; + function click(operation) { d3.event.stopPropagation(); operation();