diff --git a/css/map.css b/css/map.css index 77a258d0f..8e3f3290b 100644 --- a/css/map.css +++ b/css/map.css @@ -37,6 +37,7 @@ g.point .shadow { stroke-opacity: 0; } +g.point.related:not(.selected) .shadow, g.point.hover:not(.selected) .shadow { stroke-opacity: 0.5; } @@ -104,7 +105,9 @@ g.vertex.vertex-hover { display: block; } +g.vertex.related:not(.selected) .shadow, g.vertex.hover:not(.selected) .shadow, +g.midpoint.related:not(.selected) .shadow, g.midpoint.hover:not(.selected) .shadow { fill-opacity: 0.5; } @@ -144,6 +147,7 @@ path.shadow { stroke-opacity: 0; } +path.shadow.related:not(.selected), path.shadow.hover:not(.selected) { stroke-opacity: 0.4; } @@ -1612,6 +1616,7 @@ text.gpx { stroke-width: 8; } +.fill-wireframe path.shadow.related:not(.selected), .fill-wireframe path.shadow.hover:not(.selected) { stroke-opacity: 0.4; } diff --git a/modules/lib/d3.keybinding.js b/modules/lib/d3.keybinding.js index c152f6d97..cc98ab242 100644 --- a/modules/lib/d3.keybinding.js +++ b/modules/lib/d3.keybinding.js @@ -58,33 +58,37 @@ export function d3keybinding(namespace) { return keybinding; }; - keybinding.on = function(code, callback, capture) { - var binding = { - event: { - keyCode: 0, - shiftKey: false, - ctrlKey: false, - altKey: false, - metaKey: false - }, - capture: capture, - callback: callback - }; + keybinding.on = function(codes, callback, capture) { + var arr = [].concat(codes); + for (var i = 0; i < arr.length; i++) { + var code = arr[i]; + var binding = { + event: { + keyCode: 0, + shiftKey: false, + ctrlKey: false, + altKey: false, + metaKey: false + }, + capture: capture, + callback: callback + }; - code = code.toLowerCase().match(/(?:(?:[^+⇧⌃⌥⌘])+|[⇧⌃⌥⌘]|\+\+|^\+$)/g); + code = code.toLowerCase().match(/(?:(?:[^+⇧⌃⌥⌘])+|[⇧⌃⌥⌘]|\+\+|^\+$)/g); - for (var i = 0; i < code.length; i++) { - // Normalise matching errors - if (code[i] === '++') code[i] = '+'; + for (var j = 0; j < code.length; j++) { + // Normalise matching errors + if (code[j] === '++') code[i] = '+'; - if (code[i] in d3keybinding.modifierCodes) { - binding.event[d3keybinding.modifierProperties[d3keybinding.modifierCodes[code[i]]]] = true; - } else if (code[i] in d3keybinding.keyCodes) { - binding.event.keyCode = d3keybinding.keyCodes[code[i]]; + if (code[j] in d3keybinding.modifierCodes) { + binding.event[d3keybinding.modifierProperties[d3keybinding.modifierCodes[code[j]]]] = true; + } else if (code[j] in d3keybinding.keyCodes) { + binding.event.keyCode = d3keybinding.keyCodes[code[j]]; + } } - } - bindings.push(binding); + bindings.push(binding); + } return keybinding; }; @@ -92,6 +96,7 @@ export function d3keybinding(namespace) { return keybinding; } + d3keybinding.modifierCodes = { // Shift key, ⇧ '⇧': 16, shift: 16, diff --git a/modules/modes/select.js b/modules/modes/select.js index 064f197df..06d88b544 100644 --- a/modules/modes/select.js +++ b/modules/modes/select.js @@ -29,7 +29,11 @@ import { modeBrowse } from './browse'; import { modeDragNode } from './drag_node'; import * as Operations from '../operations/index'; import { uiRadialMenu, uiSelectionList } from '../ui/index'; -import { utilEntityOrMemberSelector } from '../util/index'; +import { uiCmd } from '../ui/cmd'; +import { utilEntityOrMemberSelector, utilEntitySelector } from '../util/index'; + + +var relatedParent; export function modeSelect(context, selectedIDs) { @@ -52,7 +56,9 @@ export function modeSelect(context, selectedIDs) { inspector, radialMenu, newFeature = false, - suppressMenu = false; + suppressMenu = false, + follow = false; + var wrap = context.container() .select('.inspector-wrap'); @@ -65,6 +71,56 @@ export function modeSelect(context, selectedIDs) { } + // find the common parent ways for nextVertex, previousVertex + function commonParents() { + var graph = context.graph(), + commonParents = []; + + for (var i = 0; i < selectedIDs.length; i++) { + var entity = context.hasEntity(selectedIDs[i]); + if (!entity || entity.geometry(graph) !== 'vertex') { + return []; // selection includes some not vertexes + } + + var currParents = _.map(graph.parentWays(entity), 'id'); + if (!commonParents.length) { + commonParents = currParents; + continue; + } + + commonParents = _.intersection(commonParents, currParents); + if (!commonParents.length) { + return []; + } + } + + return commonParents; + } + + + function singularParent() { + var parents = commonParents(); + if (!parents) { + relatedParent = null; + return null; + } + + // relatedParent is used when we visit a vertex with multiple + // parents, and we want to remember which parent line we started on. + + if (parents.length === 1) { + relatedParent = parents[0]; // remember this parent for later + return relatedParent; + } + + if (parents.indexOf(relatedParent) !== -1) { + return relatedParent; // prefer the previously seen parent + } + + return parents[0]; + } + + function closeMenu() { if (radialMenu) { context.surface().call(radialMenu.close); @@ -139,6 +195,13 @@ export function modeSelect(context, selectedIDs) { }; + mode.follow = function(_) { + if (!arguments.length) return follow; + follow = _; + return mode; + }; + + mode.enter = function() { function update() { @@ -173,17 +236,30 @@ export function modeSelect(context, selectedIDs) { function selectElements(drawn) { - var entity = singular(); + var surface = context.surface(), + entity = singular(); + if (entity && context.geometry(entity.id) === 'relation') { suppressMenu = true; return; } + var parent = singularParent(); + if (parent) { + surface.selectAll('.related') + .classed('related', false); + surface.selectAll(utilEntitySelector([relatedParent])) + .classed('related', true); + } + var selection = context.surface() - .selectAll(utilEntityOrMemberSelector(selectedIDs, context.graph())); + .selectAll(utilEntityOrMemberSelector(selectedIDs, context.graph())); if (selection.empty()) { - if (drawn) { // Exit mode if selected DOM elements have disappeared.. + // Return to browse mode if selected DOM elements have + // disappeared because the user moved them out of view.. + var source = d3.event && d3.event.type === 'zoom' && d3.event.sourceEvent; + if (drawn && source && (source.type === 'mousemove' || source.type === 'touchmove')) { context.enter(modeBrowse(context)); } } else { @@ -200,6 +276,98 @@ export function modeSelect(context, selectedIDs) { } + function firstVertex() { + d3.event.preventDefault(); + var parent = singularParent(); + if (parent) { + var way = context.entity(parent); + context.enter( + modeSelect(context, [way.first()]).follow(true) + ); + } + } + + + function lastVertex() { + d3.event.preventDefault(); + var parent = singularParent(); + if (parent) { + var way = context.entity(parent); + context.enter( + modeSelect(context, [way.last()]).follow(true) + ); + } + } + + + function previousVertex() { + d3.event.preventDefault(); + var parent = singularParent(); + if (!parent) return; + + var way = context.entity(parent), + length = way.nodes.length, + curr = way.nodes.indexOf(selectedIDs[0]), + index = -1; + + if (curr > 0) { + index = curr - 1; + } else if (way.isClosed()) { + index = length - 2; + } + + if (index !== -1) { + context.enter( + modeSelect(context, [way.nodes[index]]).follow(true) + ); + } + } + + + function nextVertex() { + d3.event.preventDefault(); + var parent = singularParent(); + if (!parent) return; + + var way = context.entity(parent), + length = way.nodes.length, + curr = way.nodes.indexOf(selectedIDs[0]), + index = -1; + + if (curr < length - 1) { + index = curr + 1; + } else if (way.isClosed()) { + index = 0; + } + + if (index !== -1) { + context.enter( + modeSelect(context, [way.nodes[index]]).follow(true) + ); + } + } + + + function nextParent() { + d3.event.preventDefault(); + var parents = _.uniq(commonParents()); + if (!parents || parents.length < 2) return; + + var index = parents.indexOf(relatedParent); + if (index < 0 || index > parents.length - 2) { + relatedParent = parents[0]; + } else { + relatedParent = parents[index + 1]; + } + + var surface = context.surface(); + surface.selectAll('.related') + .classed('related', false); + surface.selectAll(utilEntitySelector([relatedParent])) + .classed('related', true); + } + + behaviors.forEach(function(behavior) { context.install(behavior); }); @@ -211,6 +379,11 @@ export function modeSelect(context, selectedIDs) { operations.unshift(Operations.operationDelete(selectedIDs, context)); keybinding + .on(['[','pgup'], previousVertex) + .on([']', 'pgdown'], nextVertex) + .on([uiCmd('⌘['), 'home'], firstVertex) + .on([uiCmd('⌘]'), 'end'], lastVertex) + .on(['\\', 'pause'], nextParent) .on('⎋', esc, true) .on('space', toggleMenu); @@ -248,6 +421,18 @@ export function modeSelect(context, selectedIDs) { positionMenu(); } + if (follow) { + var extent = geoExtent(), + graph = context.graph(); + selectedIDs.forEach(function(id) { + var entity = context.entity(id); + extent._extend(entity.extent(graph)); + }); + + var loc = extent.center(); + context.map().centerEase(loc); + } + timeout = window.setTimeout(function() { if (show) { showMenu(); @@ -281,11 +466,19 @@ export function modeSelect(context, selectedIDs) { .on('undone.select', null) .on('redone.select', null); - context.surface() - .on('dblclick.select', null) + var surface = context.surface(); + + surface + .on('dblclick.select', null); + + surface .selectAll('.selected') .classed('selected', false); + surface + .selectAll('.related') + .classed('related', false); + context.map().on('drawn.select', null); context.ui().sidebar.hide(); }; diff --git a/modules/ui/init.js b/modules/ui/init.js index 801e0c461..4f9d682ac 100644 --- a/modules/ui/init.js +++ b/modules/ui/init.js @@ -256,14 +256,10 @@ export function uiInit(context) { .on('↑', pan([0, pa])) .on('→', pan([-pa, 0])) .on('↓', pan([0, -pa])) - .on('⇧←', pan([mapDimensions[0], 0])) - .on('⇧↑', pan([0, mapDimensions[1]])) - .on('⇧→', pan([-mapDimensions[0], 0])) - .on('⇧↓', pan([0, -mapDimensions[1]])) - .on(uiCmd('⌘←'), pan([mapDimensions[0], 0])) - .on(uiCmd('⌘↑'), pan([0, mapDimensions[1]])) - .on(uiCmd('⌘→'), pan([-mapDimensions[0], 0])) - .on(uiCmd('⌘↓'), pan([0, -mapDimensions[1]])); + .on(['⇧←', uiCmd('⌘←')], pan([mapDimensions[0], 0])) + .on(['⇧↑', uiCmd('⌘↑')], pan([0, mapDimensions[1]])) + .on(['⇧→', uiCmd('⌘→')], pan([-mapDimensions[0], 0])) + .on(['⇧↓', uiCmd('⌘↓')], pan([0, -mapDimensions[1]])); d3.select(document) .call(keybinding); diff --git a/modules/ui/zoom.js b/modules/ui/zoom.js index ae0527693..685671d93 100644 --- a/modules/ui/zoom.js +++ b/modules/ui/zoom.js @@ -72,16 +72,12 @@ export function uiZoom(context) { var keybinding = d3keybinding('zoom'); _.each(['=','ffequals','plus','ffplus'], function(key) { - keybinding.on(key, zoomIn); - keybinding.on('⇧' + key, zoomIn); - keybinding.on(uiCmd('⌘' + key), zoomInFurther); - keybinding.on(uiCmd('⌘⇧' + key), zoomInFurther); + keybinding.on([key, '⇧' + key], zoomIn); + keybinding.on([uiCmd('⌘' + key), uiCmd('⌘⇧' + key)], zoomInFurther); }); _.each(['-','ffminus','_','dash'], function(key) { - keybinding.on(key, zoomOut); - keybinding.on('⇧' + key, zoomOut); - keybinding.on(uiCmd('⌘' + key), zoomOutFurther); - keybinding.on(uiCmd('⌘⇧' + key), zoomOutFurther); + keybinding.on([key, '⇧' + key], zoomOut); + keybinding.on([uiCmd('⌘' + key), uiCmd('⌘⇧' + key)], zoomOutFurther); }); d3.select(document) diff --git a/test/spec/lib/d3.keybinding.js b/test/spec/lib/d3.keybinding.js index 79a43a225..62f45118b 100644 --- a/test/spec/lib/d3.keybinding.js +++ b/test/spec/lib/d3.keybinding.js @@ -38,6 +38,16 @@ describe('d3.keybinding', function() { expect(spy).to.have.been.calledOnce; }); + it('adds multiple bindings given an array of keys', function () { + d3.select(document).call(keybinding.on(['A','B'], spy)); + + happen.keydown(document, {keyCode: 65}); + expect(spy).to.have.been.calledOnce; + + happen.keydown(document, {keyCode: 66}); + expect(spy).to.have.been.calledTwice; + }); + it('does not dispatch when focus is in input elements by default', function () { d3.select(document).call(keybinding.on('A', spy));