11var inherit = require ( './inherit' ) ;
22var object = require ( './object' ) ;
33var util = require ( './util' ) ;
4+ var Mousetrap = require ( 'mousetrap' ) ;
45
56//////////////////////////////////////////////////////////////////////////////
67/**
@@ -29,10 +30,11 @@ var mapInteractor = function (args) {
2930 var actionMatch = require ( './util' ) . actionMatch ;
3031 var quadFeature = require ( './quadFeature' ) ;
3132
32- var m_options = args || { } ,
33+ var m_options ,
3334 m_this = this ,
3435 m_mouse ,
35- m_keyboard ,
36+ m_keyHandler ,
37+ m_boundKeys ,
3638 m_state ,
3739 m_queue ,
3840 $node ,
@@ -147,6 +149,50 @@ var mapInteractor = function (args) {
147149 cancelOnMove : true
148150 } ,
149151
152+ keyboard : {
153+ actions : {
154+ /* Specific actions can be disabled by removing them from this object
155+ * or stting an empty list as the key bindings. Additional actions
156+ * can be added to the dictionary, each of which gets a list of key
157+ * bindings. See Mousetrap documentation for special key names. */
158+ 'zoom.in' : [ 'plus' , 'shift+plus' , 'shift+ctrl+plus' , '=' , 'shift+=' , 'shift+ctrl+=' ] ,
159+ 'zoom.out' : [ '-' , 'shift+-' , 'shift+ctrl+-' , '_' , 'shift+_' , 'shift+ctrl+_' ] ,
160+ 'zoom.0' : [ '1' ] ,
161+ 'zoom.3' : [ '2' ] ,
162+ 'zoom.6' : [ '3' ] ,
163+ 'zoom.9' : [ '4' ] ,
164+ 'zoom.12' : [ '5' ] ,
165+ 'zoom.15' : [ '6' ] ,
166+ 'zoom.18' : [ '7' ] ,
167+ 'pan.left' : [ 'left' , 'shift+left' , 'shift+ctrl+left' ] ,
168+ 'pan.right' : [ 'right' , 'shift+right' , 'shift+ctrl+right' ] ,
169+ 'pan.up' : [ 'up' , 'shift+up' , 'shift+ctrl+up' ] ,
170+ 'pan.down' : [ 'down' , 'shift+down' , 'shift+ctrl+down' ] ,
171+ 'rotate.ccw' : [ '<' , 'shift+<' , 'shift+ctrl+<' , '.' , 'shift+.' , 'shift+ctrl+.' ] ,
172+ 'rotate.cw' : [ '>' , 'shift+>' , 'shift+ctrl+>' , ',' , 'shift+,' , 'shift+ctrl+,' ] ,
173+ 'rotate.0' : [ '0' ]
174+ } ,
175+ meta : {
176+ /* the metakeys that are down during a key event determine the
177+ * magnitude of the action, where 0 is the default small action
178+ * (1-pixel pan, small zoom, small rotation), 1 is a middle-sized
179+ * action, and 2 is the largest action. Metakeys that aren't listed
180+ * are ignored. Metakeys include shift, ctrl, alt, and meta (alt is
181+ * either the alt or option key, and meta is either windows or
182+ * command). */
183+ 0 : { shift : false , ctrl : false } ,
184+ 1 : { shift : true , ctrl : true } ,
185+ 2 : { shift : true , ctrl : false }
186+ } ,
187+ /* if focusHighlight is truthy, then a class is added to the map such
188+ * that when the map gets focus, it is indicated inside the border of
189+ * the map -- browsers usually show focus on the outside, which isn't
190+ * useful if the map is full window. It might be desireable to change
191+ * this so it is only present if the focus is reached via the keyboard
192+ * (which would propably require detecting keyup events). */
193+ focusHighlight : true
194+ } ,
195+
150196 wheelScaleX : 1 ,
151197 wheelScaleY : 1 ,
152198 zoomScale : 1 ,
@@ -169,10 +215,10 @@ var mapInteractor = function (args) {
169215 ease : function ( t ) { return ( 2 - t ) * t ; }
170216 }
171217 } ,
172- m_options
218+ args || { }
173219 ) ;
174- /* We don't want to merge the original arrays array with a array passed in
175- * the args, so override that as necessary for actions. */
220+ /* We don't want to merge the original arrays with arrays passed in the args,
221+ * so override that as necessary for actions. */
176222 if ( args && args . actions ) {
177223 m_options . actions = $ . extend ( true , [ ] , args . actions ) ;
178224 }
@@ -357,11 +403,6 @@ var mapInteractor = function (args) {
357403 }
358404 } ;
359405
360- // default keyboard object
361- // (keyboard events not implemented yet)
362- m_keyboard = {
363- } ;
364-
365406 // The interactor state determines what actions are taken in response to
366407 // core browser events.
367408 //
@@ -409,6 +450,119 @@ var mapInteractor = function (args) {
409450 */
410451 m_queue = { } ;
411452
453+ /**
454+ * Process keys that we've captured. Metakeys determine the magnitude of
455+ * the action.
456+ *
457+ * @param {string } action: the basic action to take.
458+ * @param {object } evt: the event with metakeys.
459+ * @param {object } keys: keys used to trigger the event. keys.simualted is
460+ * true if artificially triggered.
461+ */
462+ this . _handleKeys = function ( action , evt , keys ) {
463+ if ( keys && keys . simulated === true ) {
464+ evt = keys ;
465+ }
466+ var meta = m_options . keyboard . meta || { 0 : { } } ,
467+ map = m_this . map ( ) ,
468+ mapSize = map . size ( ) ,
469+ actionBase = action , actionValue = '' ,
470+ value , factor , move = { } ;
471+
472+ for ( value in meta ) {
473+ if ( meta . hasOwnProperty ( value ) ) {
474+ if ( ( meta [ value ] . shift === undefined || evt . shiftKey === ! ! meta [ value ] . shift ) &&
475+ ( meta [ value ] . ctrl === undefined || evt . ctrlKey === ! ! meta [ value ] . ctrl ) &&
476+ ( meta [ value ] . alt === undefined || evt . altKey === ! ! meta [ value ] . alt ) &&
477+ ( meta [ value ] . meta === undefined || evt . metaKey === ! ! meta [ value ] . meta ) ) {
478+ factor = value ;
479+ }
480+ }
481+ }
482+ if ( factor === undefined ) {
483+ /* metakeys don't match, so don't trigger an event. */
484+ return ;
485+ }
486+
487+ evt . stopPropagation ( ) ;
488+ evt . preventDefault ( ) ;
489+
490+ if ( action . indexOf ( '.' ) >= 0 ) {
491+ actionBase = action . substr ( 0 , action . indexOf ( '.' ) ) ;
492+ actionValue = action . substr ( action . indexOf ( '.' ) + 1 ) ;
493+ }
494+ switch ( actionBase ) {
495+ case 'zoom' :
496+ switch ( actionValue ) {
497+ case 'in' :
498+ move . zoomDelta = [ 0.05 , 0.25 , 1 ] [ factor ] ;
499+ break ;
500+ case 'out' :
501+ move . zoomDelta = - [ 0.05 , 0.25 , 1 ] [ factor ] ;
502+ break ;
503+ default :
504+ if ( ! isNaN ( parseFloat ( actionValue ) ) ) {
505+ move . zoom = parseFloat ( actionValue ) ;
506+ }
507+ break ;
508+ }
509+ break ;
510+ case 'pan' :
511+ switch ( actionValue ) {
512+ case 'down' :
513+ move . panY = - Math . max ( 1 , [ 0 , 0.05 , 0.5 ] [ factor ] * mapSize . height ) ;
514+ break ;
515+ case 'left' :
516+ move . panX = Math . max ( 1 , [ 0 , 0.05 , 0.5 ] [ factor ] * mapSize . width ) ;
517+ break ;
518+ case 'right' :
519+ move . panX = - Math . max ( 1 , [ 0 , 0.05 , 0.5 ] [ factor ] * mapSize . width ) ;
520+ break ;
521+ case 'up' :
522+ move . panY = Math . max ( 1 , [ 0 , 0.05 , 0.5 ] [ factor ] * mapSize . height ) ;
523+ break ;
524+ }
525+ break ;
526+ case 'rotate' :
527+ switch ( actionValue ) {
528+ case 'ccw' :
529+ move . rotationDelta = [ 1 , 5 , 90 ] [ factor ] * Math . PI / 180 ;
530+ break ;
531+ case 'cw' :
532+ move . rotationDelta = - [ 1 , 5 , 90 ] [ factor ] * Math . PI / 180 ;
533+ break ;
534+ default :
535+ if ( ! isNaN ( parseFloat ( actionValue ) ) ) {
536+ move . rotation = parseFloat ( actionValue ) ;
537+ }
538+ break ;
539+ }
540+ break ;
541+ }
542+ map . geoTrigger ( geo_event . keyaction , {
543+ move : move ,
544+ action : action ,
545+ factor : factor ,
546+ event : evt
547+ } ) ;
548+ if ( move . cancel ) {
549+ return ;
550+ }
551+ if ( move . zoom !== undefined ) {
552+ map . zoom ( move . zoom ) ;
553+ } else if ( move . zoomDelta ) {
554+ map . zoom ( map . zoom ( ) + move . zoomDelta ) ;
555+ }
556+ if ( move . rotation !== undefined ) {
557+ map . rotation ( move . rotation ) ;
558+ } else if ( move . rotationDelta ) {
559+ map . rotation ( map . rotation ( ) + move . rotationDelta ) ;
560+ }
561+ if ( move . panX || move . panY ) {
562+ map . pan ( { x : move . panX || 0 , y : move . panY || 0 } ) ;
563+ }
564+ } ;
565+
412566 ////////////////////////////////////////////////////////////////////////////
413567 /**
414568 * Connects events to a map. If the map is not set, then this does nothing.
@@ -443,6 +597,28 @@ var mapInteractor = function (args) {
443597 } ) ) {
444598 $node . on ( 'contextmenu.geojs' , function ( ) { return false ; } ) ;
445599 }
600+
601+ if ( m_options . keyboard && m_options . keyboard . actions ) {
602+ m_keyHandler = Mousetrap ( $node [ 0 ] ) ;
603+ var bound = [ ] ;
604+ for ( var keyAction in m_options . keyboard . actions ) {
605+ if ( m_options . keyboard . actions . hasOwnProperty ( keyAction ) ) {
606+ m_keyHandler . bind (
607+ m_options . keyboard . actions [ keyAction ] ,
608+ ( function ( action ) {
609+ return function ( evt , keys ) {
610+ m_this . _handleKeys ( action , evt , keys ) ;
611+ } ;
612+ } ) ( keyAction )
613+ ) ;
614+ bound = bound . concat ( m_options . keyboard . actions [ keyAction ] ) ;
615+ }
616+ }
617+ m_boundKeys = bound ;
618+ }
619+ $node . toggleClass ( 'highlight-focus' ,
620+ m_boundKeys && m_boundKeys . length && m_options . keyboard . focusHighlight ) ;
621+
446622 return m_this ;
447623 } ;
448624
@@ -453,12 +629,20 @@ var mapInteractor = function (args) {
453629 */
454630 ////////////////////////////////////////////////////////////////////////////
455631 this . _disconnectEvents = function ( ) {
632+ if ( m_boundKeys ) {
633+ if ( m_keyHandler ) {
634+ m_boundKeys . every ( m_keyHandler . unbind , m_keyHandler ) ;
635+ }
636+ m_boundKeys = null ;
637+ m_keyHandler = null ;
638+ }
456639 if ( $node ) {
457640 $node . off ( '.geojs' ) ;
458641 $node = null ;
459642 }
460643 m_this . _handleMouseWheel = function ( ) { } ;
461644 m_callZoom = function ( ) { } ;
645+
462646 return m_this ;
463647 } ;
464648
@@ -1454,6 +1638,9 @@ var mapInteractor = function (args) {
14541638 ////////////////////////////////////////////////////////////////////////////
14551639 this . destroy = function ( ) {
14561640 m_this . _disconnectEvents ( ) ;
1641+ if ( m_this . map ( ) ) {
1642+ $ ( m_this . map ( ) . node ( ) ) . removeClass ( 'highlight-focus' ) ;
1643+ }
14571644 m_this . map ( null ) ;
14581645 } ;
14591646
@@ -1468,11 +1655,14 @@ var mapInteractor = function (args) {
14681655
14691656 ////////////////////////////////////////////////////////////////////////////
14701657 /**
1471- * Get current keyboard information
1658+ * Get/set current keyboard information
14721659 */
14731660 ////////////////////////////////////////////////////////////////////////////
1474- this . keyboard = function ( ) {
1475- return $ . extend ( true , { } , m_keyboard ) ;
1661+ this . keyboard = function ( newValue ) {
1662+ if ( newValue === undefined ) {
1663+ return $ . extend ( true , { } , m_options . keyboard || { } ) ;
1664+ }
1665+ return m_this . options ( { keyboard : newValue } ) ;
14761666 } ;
14771667
14781668 ////////////////////////////////////////////////////////////////////////////
@@ -1589,6 +1779,24 @@ var mapInteractor = function (args) {
15891779 return m_this ;
15901780 }
15911781
1782+ if ( type === 'keyboard' && m_keyHandler ) {
1783+ /* Mousetrap passes through the keys we send, but not an event object,
1784+ * so we construct an artifical event object as the keys, and use that.
1785+ */
1786+ var keys = {
1787+ shiftKey : options . shift || options . shiftKey || false ,
1788+ ctrlKey : options . ctrl || options . ctrlKey || false ,
1789+ altKey : options . alt || options . altKey || false ,
1790+ metaKey : options . meta || options . metaKey || false ,
1791+ toString : function ( ) { return options . keys ; } ,
1792+ stopPropagation : function ( ) { } ,
1793+ preventDefault : function ( ) { } ,
1794+ simulated : true
1795+ } ;
1796+ m_keyHandler . trigger ( keys ) ;
1797+ return ;
1798+ }
1799+
15921800 page = options . page || { } ;
15931801
15941802 if ( options . map ) {
0 commit comments