diff --git a/css/app.css b/css/app.css index 211efc4df..bb86b3c74 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,17 @@ a.success-action { fill: rgba(255,255,255,.5); } +.radial-menu image { + 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/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 0af64aa20..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,17 +19,21 @@ 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"; return operation; }; diff --git a/js/id/operations/delete.js b/js/id/operations/delete.js index 53038dbc4..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,7 +39,9 @@ iD.operations.Delete = function(entityId) { }; operation.id = "delete"; + operation.key = "⌫"; 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..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,7 +15,9 @@ iD.operations.Move = function(entityId, mode) { }; operation.id = "move"; + operation.key = "M"; 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..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,7 +18,9 @@ iD.operations.Reverse = function(entityId) { }; operation.id = "reverse"; + operation.key = "V"; 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..3dcab7481 100644 --- a/js/id/operations/split.js +++ b/js/id/operations/split.js @@ -1,21 +1,26 @@ -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"; return operation; }; diff --git a/js/id/operations/unjoin.js b/js/id/operations/unjoin.js index 985a41697..c18de3029 100644 --- a/js/id/operations/unjoin.js +++ b/js/id/operations/unjoin.js @@ -1,21 +1,26 @@ -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"; return operation; }; diff --git a/js/id/ui/radial_menu.js b/js/id/ui/radial_menu.js index d38ef3679..f06fa362e 100644 --- a/js/id/ui/radial_menu.js +++ b/js/id/ui/radial_menu.js @@ -1,50 +1,90 @@ -iD.ui.RadialMenu = function(entity, mode) { - var arcs; +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); }); + if (!operations.length) + return; function click(operation) { d3.event.stopPropagation(); - operation(history); + operation(); } - 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') + menu = selection.append('g') .attr('class', 'radial-menu') .attr('transform', "translate(" + center + ")") .attr('opacity', 0); - arcs.transition() + menu.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; }); + menu.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 = menu.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(); }) + .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'); + + 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(); }