summaryrefslogtreecommitdiff
path: root/cad/src/dna/commands/DnaStrand/DnaStrand_EditCommand.py
blob: 682bd4d007863e06ff3ccae0f4d4f4619a2cf8df (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
# Copyright 2008-2009 Nanorex, Inc.  See LICENSE file for details. 
"""
@author: Ninad
@version: $Id$
@copyright: 2008-2009 Nanorex, Inc.  See LICENSE file for details.

History:
Created on 2008-02-14

TODO: as of 2008-02-14
- Post dna data model implementation:    
  - We need to make sure that the strand doesn't form a ring  ( in other words
    no handles when strand is a ring). The current code ignores that. 
  - implement/ revise methods such as self.modifyStructure(), 
    self._gather_parameters() etc. 
  - self.updateHandlePositions() may need revision if the current supporting 
    methods in class dna_model.DnaSegment change. 

"""
import foundation.env as env
from utilities.debug import print_compact_stack
from utilities.constants import gensym
from utilities.constants import purple
from utilities.Comparison import same_vals

from geometry.VQT import  V
from geometry.VQT import  vlen
from geometry.VQT import  norm
from Numeric import dot

from exprs.State_preMixin import State_preMixin
from exprs.attr_decl_macros import Instance, State
from exprs.__Symbols__ import _self
from exprs.Exprs import call_Expr
from widgets.prefs_widgets import ObjAttr_StateRef
from exprs.ExprsConstants import Width, Point, ORIGIN, Color
from exprs.ExprsConstants import Vector

from model.chunk import Chunk
from model.chem import Atom
from model.bonds import Bond

from dna.commands.DnaStrand.DnaStrand_GraphicsMode import DnaStrand_GraphicsMode
from dna.commands.DnaStrand.DnaStrand_ResizeHandle import DnaStrand_ResizeHandle
from dna.model.Dna_Constants import getNumberOfBasePairsFromDuplexLength
from dna.model.Dna_Constants import getDuplexLength
from dna.generators.B_Dna_PAM3_SingleStrand_Generator import B_Dna_PAM3_SingleStrand_Generator

from command_support.EditCommand import EditCommand 

from utilities.constants import noop
from utilities.constants import red, black

from utilities.Comparison import same_vals

from utilities.prefs_constants import dnaStrandEditCommand_cursorTextCheckBox_changedBases_prefs_key
from utilities.prefs_constants import dnaStrandEditCommand_cursorTextCheckBox_numberOfBases_prefs_key
from utilities.prefs_constants import dnaStrandEditCommand_showCursorTextCheckBox_prefs_key
from utilities.prefs_constants import cursorTextColor_prefs_key

from dna.commands.DnaStrand.DnaStrand_PropertyManager import DnaStrand_PropertyManager


CYLINDER_WIDTH_DEFAULT_VALUE = 0.0
HANDLE_RADIUS_DEFAULT_VALUE = 1.5

ORIGIN = V(0,0,0)

_superclass = EditCommand

class DnaStrand_EditCommand(State_preMixin, EditCommand):
    """
    Command to edit a DnaStrand (chunk object as of 2008-02-14)

    To edit a Strand, first enter BuildDna_EditCommand (accessed using 
    Build> Dna) then, select a strand chunk of an existing DnaSegment within the 
    DnaGroup you are editing. When you select the strand chunk, it enters 
    DnaStrand_Editcommand, shows its property manager and also shows the 
    resize handles if any.
    """
        
    #Graphics Mode 
    GraphicsMode_class = DnaStrand_GraphicsMode
    
    #Property Manager
    PM_class = DnaStrand_PropertyManager
    
    cmd              =  'Dna Strand'
    prefix           =  'Strand '   # used for gensym
    cmdname          = "DNA_STRAND"
    
    commandName       = 'DNA_STRAND'
    featurename       = "Edit Dna Strand"
    from utilities.constants import CL_SUBCOMMAND
    command_level = CL_SUBCOMMAND
    command_parent = 'BUILD_DNA'

    command_should_resume_prevMode = True
    command_has_its_own_PM = True
    
    # Generators for DNA, nanotubes and graphene have their MT name 
    # generated (in GeneratorBaseClass) from the prefix.
    create_name_from_prefix  =  True 

    call_makeMenus_for_each_event = True 

    

    #@see: self.updateHandlePositions for details on how these variables are 
    #used in computation. 
    #@see: DnaStrand_ResizeHandle and handle declaration in this class 
    #definition
    handlePoint1 = State( Point, ORIGIN)
    handlePoint2 = State( Point, ORIGIN)    

    handleDirection1 = State(Vector, ORIGIN)
    handleDirection2 = State(Vector, ORIGIN)

    #See self._update_resizeHandle_radius where this gets changed. 
    #also see DnaSegment_ResizeHandle to see how its implemented. 
    handleSphereRadius1 = State(Width, HANDLE_RADIUS_DEFAULT_VALUE)
    handleSphereRadius2 = State(Width, HANDLE_RADIUS_DEFAULT_VALUE)

    #handleColors 
    handleColor1 = State(Color, purple)
    handleColor2 = State(Color, purple)

    #TODO: 'cylinderWidth attr used for resize handles -- needs to be renamed 
    #along with 'height_ref attr in exprs.DraggableHandle_AlongLine
    cylinderWidth = State(Width, CYLINDER_WIDTH_DEFAULT_VALUE) 
    cylinderWidth2 = State(Width, CYLINDER_WIDTH_DEFAULT_VALUE) 

    #Resize Handles for Strand. See self.updateHandlePositions()
    leftHandle = Instance(         
        DnaStrand_ResizeHandle( 
            command = _self,
            height_ref = call_Expr( ObjAttr_StateRef, _self, 'cylinderWidth'),
            origin = handlePoint1,
            fixedEndOfStructure = handlePoint2,
            direction = handleDirection1,
            sphereRadius = handleSphereRadius1,
            handleColor = handleColor1
        ))


    rightHandle = Instance( 
        DnaStrand_ResizeHandle(
            command = _self,
            height_ref = call_Expr( ObjAttr_StateRef, _self, 'cylinderWidth2'),
            origin = handlePoint2,
            fixedEndOfStructure = handlePoint1,
            direction = handleDirection2,
            sphereRadius = handleSphereRadius2,
            handleColor = handleColor2
        ))


    def __init__(self, commandSequencer):
        """
        Constructor for InsertDna_EditCommand
        """

        glpane = commandSequencer.assy.glpane
        State_preMixin.__init__(self, glpane)        
        EditCommand.__init__(self, commandSequencer)
        
        
        #It uses BuildDna_EditCommand.flyoutToolbar ( in other words, that 
        #toolbar is still accessible and not changes while in this command)
        flyoutToolbar = None

        #Graphics handles for editing the structure . 
        self.handles = []        
        self.grabbedHandle = None

        #This is used for comarison purpose in model_changed method to decide
        #whether to update the sequence. 
        self._previousNumberOfBases = None                
        
    #New Command API method -- implemented on 2008-08-27
    
    def command_update_UI(self):
        """
        Extends superclass method. 
        @see: baseCommand.command_update_UI()
        """     
        #This MAY HAVE BUG. WHEN --
        #debug pref 'call model_changed only when needed' is ON
        #See related bug 2729 for details. 

        #The following code that updates te handle positions and the strand 
        #sequence fixes bugs like 2745 and updating the handle positions
        #updating handle positions in command_update_UI instead of in 
        #self.graphicsMode._draw_handles() is also a minor optimization
        #This can be further optimized by debug pref 
        #'call command_update_UI only when needed' but its NOT done because of
        #an issue mentioned in bug 2729   - Ninad 2008-04-07
        
        _superclass.command_update_UI(self)
            
        if self.grabbedHandle is not None:
            return

        #For Rattlesnake, PAM5 segment resizing  is not supported. 
        #@see: self.hasResizableStructure()        
        if self.hasValidStructure():
            isStructResizable, why_not = self.hasResizableStructure()
            if not isStructResizable:
                self.handles = []
                return
            elif len(self.handles) == 0:
                self._updateHandleList()

            self.updateHandlePositions()
            #NOTE: The following also updates self._previousParams
            self._updateStrandSequence_if_needed()
            
    def keep_empty_group(self, group):
        """
        Returns True if the empty group should not be automatically deleted. 
        otherwise returns False. The default implementation always returns 
        False. Subclasses should override this method if it needs to keep the
        empty group for some reasons. Note that this method will only get called
        when a group has a class constant autdelete_when_empty set to True. 
        (and as of 2008-03-06, it is proposed that dna_updater calls this method
        when needed. 
        @see: Command.keep_empty_group() which is overridden here. 
        """

        bool_keep = EditCommand.keep_empty_group(self, group)

        if not bool_keep:     
            if self.hasValidStructure():                
                if group is self.struct:
                    bool_keep = True
                elif group is self.struct.parent_node_of_class(self.assy.DnaGroup):
                    bool_keep = True

        return bool_keep

    
    def _gatherParameters(self):
        """
        Return the parameters from the property manager UI. Delegates this to
        self.propMgr
        """     
        return self.propMgr.getParameters()

    def _createStructure(self):
        """
        Creates and returns the structure -- TO BE IMPLEMENTED ONCE 
        DNA_MODEL IS READY FOR USE.
        @return : DnaStrand.
        @rtype: L{Group}  
        @note: This needs to return a DNA object once that model is implemented        
        """
        pass

    def _modifyStructure(self, params):
        """
        Modify the structure based on the parameters specified. 
        Overrides EditCommand._modifystructure. This method removes the old 
        structure and creates a new one using self._createStructure. This 
        was needed for the structures like this (Dna, Nanotube etc) . .
        See more comments in the method.
        """        

        #It could happen that the self.struct is killed before this method 
        #is called. For example: Enter Edit Dna strand, select the strand, hit 
        #delete and then hit Done to exit strand edit. Whenever you hit Done, 
        #modify structure gets called (if old params don't match new ones) 
        #so it needs to return safely if the structure was not valid due
        #to some previous operation 
        if not self.hasValidStructure():
            return 
        
        # parameters have changed, update existing structure
        self._revertNumber()


        # self.name needed for done message
        if self.create_name_from_prefix:
            # create a new name
            name = self.name = gensym(self.prefix, self.assy) # (in _build_struct)
            self._gensym_data_for_reusing_name = (self.prefix, name)
        else:
            # use externally created name
            self._gensym_data_for_reusing_name = None
                # (can't reuse name in this case -- not sure what prefix it was
                #  made with)
            name = self.name

        self.dna = B_Dna_PAM3_SingleStrand_Generator()

        numberOfBases, \
                     dnaForm, \
                     dnaModel, \
                     color_junk, \
                     name_junk = params
        #see a note about color_junk in DnaSegment_EditCommand._modifyStructure()

        numberOfBasesToAddOrRemove =  self._determine_numberOfBases_to_change()

        if numberOfBasesToAddOrRemove != 0: 
            resizeEndStrandAtom, resizeEndAxisAtom = \
                               self.get_strand_and_axis_endAtoms_at_resize_end()

            if resizeEndAxisAtom:
                dnaSegment = resizeEndAxisAtom.molecule.parent_node_of_class(
                    self.assy.DnaSegment)
                
                if dnaSegment:
                    #A DnaStrand can have multiple DNA Segments with different 
                    #basesPerTurn and duplexRise so make sure that while 
                    #resizing the strand, use the dna segment of the 
                    #resizeEndAxisAtom. Fixes bug 2922 - Ninad 2008-08-04
                    basesPerTurn = dnaSegment.getBasesPerTurn()                
                    duplexRise = dnaSegment.getDuplexRise() 
                                            
                    resizeEnd_final_position = self._get_resizeEnd_final_position(
                        resizeEndAxisAtom, 
                        abs(numberOfBasesToAddOrRemove),
                        duplexRise )
    
                    self.dna.modify(dnaSegment, 
                                    resizeEndAxisAtom,
                                    numberOfBasesToAddOrRemove, 
                                    basesPerTurn, 
                                    duplexRise,
                                    resizeEndAxisAtom.posn(),
                                    resizeEnd_final_position,
                                    resizeEndStrandAtom = resizeEndStrandAtom )                        

        return  

    def _finalizeStructure(self):
        """
        Overrides EditCommand._finalizeStructure. 
        @see: EditCommand.preview_or_finalize_structure
        """     
        if self.struct is not None:
            #@TODO 2008-03-19:Should we always do this even when strand sequence
            #is not changed?? Should it waste time in comparing the current 
            #sequence with a previous one? Assigning sequence while leaving the 
            #command will be slow for large files.Need to be optimized if 
            #problematic.
            #What if a flag is set in self.propMgr.updateSequence() when it 
            #updates the seq for the second time and we check that here. 
            #Thats  not good if the method gets called multiple times for some 
            #reason other than the user entered sequence. So not doing here. 
            #Better fix would be to check if sequence gets changed (textChanged)
            #in DnaSequence editor class .... Need to be revised
            EditCommand._finalizeStructure(self) 
            self._updateStrandSequence_if_needed()
            self.assignStrandSequence()

    def _previewStructure(self):        
        EditCommand._previewStructure(self)
        self.updateHandlePositions()
        self._updateStrandSequence_if_needed()


    def _updateStrandSequence_if_needed(self):
        if self.hasValidStructure():            
            new_numberOfBases = self.struct.getNumberOfBases()

            #@TODO Update self._previousParams again? 
            #This NEEDS TO BE REVISED. BUG MENTIONED BELOW----
            #we already update this in EditCommand class. But, it doesn't work for 
            #the following bug -- 1. create a duplex with 5 basepairs, 2. resize 
            #red strand to 2 bases. 3. Undo 4. Redo 5. Try to resize it again to 
            #2 bases -- it doesn't work because self.previousParams stil stores 
            #bases as 2 and thinks nothing got changed! Can we declare 
            #self._previewStructure as a undiable state? Or better self._previousParams
            #should store self.struct and should check method return value 
            #like self.struct.getNumberOfBases()--Ninad 2008-04-07
            if self.previousParams is not None:
                if new_numberOfBases != self.previousParams[0]:
                    self.propMgr.numberOfBasesSpinBox.setValue(new_numberOfBases)
                    self.previousParams = self._gatherParameters()
            if not same_vals(new_numberOfBases, self._previousNumberOfBases):
                self.propMgr.updateSequence()
                self._previousNumberOfBases = new_numberOfBases    

    def _get_resizeEnd_final_position(self, 
                                      resizeEndAxisAtom, 
                                      numberOfBases, 
                                      duplexRise):

        final_position = None   
        if self.grabbedHandle:
            final_position = self.grabbedHandle.currentPosition
        else:
            dnaSegment = resizeEndAxisAtom.molecule.parent_node_of_class(self.assy.DnaSegment)
            other_axisEndAtom = dnaSegment.getOtherAxisEndAtom(resizeEndAxisAtom)
            axis_vector = resizeEndAxisAtom.posn() - other_axisEndAtom.posn()
            segment_length_to_add = getDuplexLength('B-DNA', 
                                                    numberOfBases, 
                                                    duplexRise = duplexRise)

            final_position = resizeEndAxisAtom.posn() + norm(axis_vector)*segment_length_to_add

        return final_position


    def getStructureName(self):
        """
        Returns the name string of self.struct if there is a valid structure. 
        Otherwise returns None. This information is used by the name edit field 
        of  this command's PM when we call self.propMgr.show()
        @see: DnaStrand_PropertyManager.show()        
        @see: self.setStructureName
        """
        if self.hasValidStructure():
            return self.struct.name
        else:
            return None

    def setStructureName(self, name):
        """
        Sets the name of self.struct to param <name> (if there is a valid 
        structure). 
        The PM of this command callss this method while closing itself 
        @param name: name of the structure to be set.
        @type name: string
        @see: DnaStrand_PropertyManager.close()
        @see: self.getStructureName()
        @see: DnaSegment_GraphicsMode.leftUp , 
              DnaSegment_editCommand.setStructureName for comments about some 
              issues.         
        """

        if self.hasValidStructure():
            self.struct.name = name
        return

    def assignStrandSequence(self):
        """
        Assigns the sequence typed in the sequence editor text field to 
        the selected strand chunk. The method it invokes also assigns 
        complimentary bases to the mate strand(s).
        @see: DnaStrand.setStrandSequence
        """
        if not self.hasValidStructure(): 
            #Fixes bug 2923            
            return
        
        sequenceString = self.propMgr.sequenceEditor.getPlainSequence()
        sequenceString = str(sequenceString)     
        
        #assign strand sequence only if it not the same as the current sequence
        seq = self.struct.getStrandSequence()
        
        if seq != sequenceString:
            self.struct.setStrandSequence(sequenceString)
        return

    def editStructure(self, struct = None):
        """
        Edit the structure 
        @param struct: structure to be edited (in this case its a strand chunk)
        @type struct: chunk or None (this will change post dna data model)
        """
        EditCommand.editStructure(self, struct)        
        if self.hasValidStructure():
            self._updatePropMgrParams()   
            #For Rattlesnake, we do not support resizing of PAM5 model. 
            #So don't append the exprs handles to the handle list (and thus 
            #don't draw those handles. See self.model_changed() 
            isStructResizable, why_not = self.hasResizableStructure()
            if not isStructResizable:
                self.handles = []
            else:
                self._updateHandleList()
                self.updateHandlePositions()
                
    def _updatePropMgrParams(self):
        """
        Subclasses may override this method. 
        Update some property manager parameters with the parameters of
        self.struct (which is being edited)
        @see: self.editStructure()
        """

        #Format in which params need to be provided to the Property manager
        
                #numberOfBases, 
                #dnaForm,
                #dnaModel,
                #color

        self._previousNumberOfBases = self.struct.getNumberOfBases()
        numberOfBases = self._previousNumberOfBases        
        color = self.struct.getColor()
        name = self.getStructureName()

        params_for_propMgr = ( numberOfBases,
                               None, 
                               None,                          
                               color,
                               name )

        #TODO 2008-03-25: better to get all parameters from self.struct and
        #set it in propMgr?  This will mostly work except that reverse is 
        #not true. i.e. we can not specify same set of params for 
        #self.struct.setProps ...because endPoint1 and endPoint2 are derived.
        #by the structure when needed. Commenting out following line of code
        #UPDATE 2008-05-06 Fixes a bug due to which the parameters in propMGr
        #of DnaSegment_EditCommand are not same as the original structure
        #(e.g. bases per turn and duplexrise)
        self.propMgr.setParameters(params_for_propMgr)
        return

    def _getStructureType(self):
        """
        Subclasses override this method to define their own structure type. 
        Returns the type of the structure this editCommand supports. 
        This is used in isinstance test. 
        @see: EditCommand._getStructureType() (overridden here)
        """
        return self.win.assy.DnaStrand
    
    def updateSequence(self):
        """
        Public method provided for convenience. If any callers outside of this 
        command need to update the sequence in the sequence editor, they can  
        simply call currentCommand.updateSequence() rather than 
        currentCommand.propMgr.updateSequence().
        @see: Ui_DnaSequenceEditor.updateSequence() which does the actual 
        job of updating the sequence string in the sequence editor.
        """
        if self.propMgr is not None:
            self.propMgr.updateSequence()
        return

    def hasResizableStructure(self):
        """
        For Rattlesnake release, we dont support strand resizing for PAM5 
        models. If the structure is not resizable, the handles won't be drawn
        @see:self.model_changed()
        @see:DnaStrand_PropertyManager.model_changed()
        @see: self.editStructure()
        @see: DnaSegment.is_PAM3_DnaStrand()
        """
        #Note: This method fixes bugs similar to (and including) bug 2812 but 
        #the changes didn't made it to Rattlesnake rc2 -- Ninad 2008-04-16
        isResizable = True
        why_not = ''

        if not self.hasValidStructure():
            isResizable = False
            why_not     = 'It is invalid.'
            return isResizable, why_not        

        isResizable = self.struct.is_PAM3_DnaStrand()

        if not isResizable:
            why_not = 'It needs to be converted to PAM3 model.'
            return isResizable, why_not

        #The following fixes bug 2812
        strandEndBaseAtom1, strandEndBaseAtom2 = self.struct.get_strand_end_base_atoms()

        if strandEndBaseAtom1 is strandEndBaseAtom2:
            isResizable = False
            why_not = "It is probably a \'closed loop\'."
            return isResizable, why_not
        
        return True, ''


    def _updateHandleList(self):
        """        
        Updates the list of handles (self.handles) 
        @see: self.editStructure
        @see: DnaSegment_GraphicsMode._drawHandles()
        """        
        # note: if handlePoint1 and/or handlePoint2 can change more often than 
        # this runs, we'll need to rerun the two assignments above whenever they
        # change and before the handle is drawn. An easy way would be to rerun
        # these assignments in the draw method of our GM. [bruce 080128]
        self.handles = [] # guess, but seems like a good idea [bruce 080128]
        self.handles.append(self.leftHandle)
        self.handles.append(self.rightHandle)

    def updateHandlePositions(self):
        """
        Update handle positions
        """
        if len(self.handles) == 0:
            #No handles are appended to self.handles list. 
            #@See self.model_changed() and self._updateHandleList()
            return

        self.cylinderWidth = CYLINDER_WIDTH_DEFAULT_VALUE
        self.cylinderWidth2 = CYLINDER_WIDTH_DEFAULT_VALUE 

        axisAtom1 = None
        axisAtom2 = None
        strandEndBaseAtom1 = None
        strandEndBaseAtom2 = None  

        #It could happen (baecause of bugs) that standEndBaseAtom1 and 
        #strandEndBaseAtom2 are one and the same! i.e strand end atom is 
        #not bonded. If this happens, we should throw a traceback bug 
        #exit gracefully. The possible cause of this bug may be ---
        #(just an example): For some reason, the modify code is unable to 
        #determine the correct bond direction of the resized structure
        #and therefore, while fusing the original strand with the new one created
        #by DnaDuplex.modify, it is unable to find bondable pairs! 
        # [- Ninad 2008-03-31]
        strandEndBaseAtom1, strandEndBaseAtom2 = self.struct.get_strand_end_base_atoms()
        if strandEndBaseAtom1 is strandEndBaseAtom2:
            print_compact_stack("bug in updating handle positions, some resized DnaStrand " \
                                "has only one atom")
            return


        if strandEndBaseAtom1:
            axisAtom1 = strandEndBaseAtom1.axis_neighbor()
            if axisAtom1:
                self.handleDirection1 = self._get_handle_direction(axisAtom1, 
                                                                   strandEndBaseAtom1)                
                self.handlePoint1 = axisAtom1.posn()                

        if strandEndBaseAtom2:
            axisAtom2 = strandEndBaseAtom2.axis_neighbor()

            if axisAtom2:
                self.handleDirection2 = self._get_handle_direction(axisAtom2, 
                                                                   strandEndBaseAtom2)
                self.handlePoint2 = axisAtom2.posn()   


        # UPDATE 2008-04-15:
        # Before 2008-04-15 the state attrs for exprs handles were always reset to None
        #at the beginning of the method. But it is calling model_changed signal
        #recursively. (se also bug 2729) So, reset thos state attrs only when 
        #needed  -- Ninad  [ this fix was not in RattleSnake rc1] 

        # Set handlePoints (i.e. their origins) and the handle directions to 
        # None if the atoms used to compute these state attrs are missing. 
        # The GraphicsMode checks if the handles have valid placement 
        # attributes set before drawing it.    

        if strandEndBaseAtom1 is None and strandEndBaseAtom2 is None:
            #probably a ring
            self.handles = []


        if strandEndBaseAtom1 is None or axisAtom1 is None:            
            self.handleDirection1 = None
            self.handlePoint1 = None

        if strandEndBaseAtom2 is None or axisAtom2 is None:            
            self.handleDirection2 = None
            self.handlePoint2 = None

        #update the radius of resize handle 
        self._update_resizeHandle_radius(axisAtom1, axisAtom2)

        #update the display color of the handles
        self._update_resizeHandle_color(strandEndBaseAtom1, strandEndBaseAtom2)

    def _update_resizeHandle_radius(self, axisAtom1, axisAtom2):
        """
        Finds out the sphere radius to use for the resize handles, based on 
        atom /chunk or glpane display (whichever decides the display of the end 
        atoms.  The default  value is 1.2.


        @see: self.updateHandlePositions()
        @see: B{Atom.drawing_radius()}
        """               

        if axisAtom1 is not None:
            self.handleSphereRadius1 = max(1.005*axisAtom1.drawing_radius(), 
                                           1.005*HANDLE_RADIUS_DEFAULT_VALUE)
        if axisAtom2 is not None: 
            self.handleSphereRadius2 =  max(1.005*axisAtom2.drawing_radius(), 
                                            1.005*HANDLE_RADIUS_DEFAULT_VALUE) 

    def _update_resizeHandle_color(self, 
                                   strandEndBaseAtom1, 
                                   strandEndBaseAtom2):
        """
        Update the color of resize handles using the current color 
        of the *chunk* of the corresponding strand end atoms. 
        @Note: If no specific color is set to the chunk, it uses 'purple' as
        the default color (and doesn't use the atom.element.color) . This 
        can be changed if needed.
        """
        if strandEndBaseAtom1 is not None:
            color = strandEndBaseAtom1.molecule.color 
            if color: 
                self.handleColor1 = color

        if strandEndBaseAtom2 is not None:
            color = strandEndBaseAtom2.molecule.color 
            if color: 
                self.handleColor2 = color

    def _get_handle_direction(self, axisAtom, strandAtom):
        """
        Get the handle direction.
        """

        handle_direction = None

        strand_rail = strandAtom.molecule.get_ladder_rail() 

        for bond_direction in (1, -1):
            next_strand_atom = strandAtom.strand_next_baseatom(bond_direction)
            if next_strand_atom:
                break

        if next_strand_atom:
            next_axis_atom = next_strand_atom.axis_neighbor()
            if next_axis_atom:
                handle_direction = norm(axisAtom.posn() - next_axis_atom.posn())

        return handle_direction

    def getDnaRibbonParams(self):
        """
        Returns parameters for drawing the dna ribbon. 

        If the dna rubberband line should NOT be drawn (example when you are 
        removing bases from the strand or  if its unable to get dnaSegment) , 
        it retuns None. So the caller should check if the method return value
        is not None. 
        @see: DnaStrand_GraphicsMode._draw_handles()
        """

        if self.grabbedHandle is None:
            return None

        if self.grabbedHandle.origin is None:
            return None

        direction_of_drag = norm(self.grabbedHandle.currentPosition - \
                                 self.grabbedHandle.origin)

        #If the strand bases are being removed (determined by checking the 
        #direction of drag) , no need to draw the rubberband line. 
        if dot(self.grabbedHandle.direction, direction_of_drag) < 0:
            return None

        strandEndAtom, axisEndAtom = self.get_strand_and_axis_endAtoms_at_resize_end()

        #DnaStrand.get_DnaSegment_with_content_atom saely handles the case where
        #strandEndAtom is None. 
        dnaSegment = self.struct.get_DnaSegment_with_content_atom(strandEndAtom)

        ribbon1_direction = None

        if dnaSegment:     
            basesPerTurn = dnaSegment.getBasesPerTurn()
            duplexRise = dnaSegment.getDuplexRise()        
            ribbon1_start_point = strandEndAtom.posn()

            if strandEndAtom:
                ribbon1_start_point = strandEndAtom.posn()
                for bond_direction, neighbor in strandEndAtom.bond_directions_to_neighbors():
                    if neighbor and neighbor.is_singlet():
                        ribbon1_direction = bond_direction
                        break

            ribbon1Color = strandEndAtom.molecule.color
            if not ribbon1Color:
                ribbon1Color = strandEndAtom.element.color

            return (self.grabbedHandle.origin,
                    self.grabbedHandle.currentPosition,
                    basesPerTurn, 
                    duplexRise, 
                    ribbon1_start_point,
                    ribbon1_direction,
                    ribbon1Color
                )

        return None



    def get_strand_and_axis_endAtoms_at_resize_end(self):
        resizeEndStrandAtom = None        
        resizeEndAxisAtom = None

        strandEndAtom1, strandEndAtom2 = self.struct.get_strand_end_base_atoms()

        if self.grabbedHandle is not None:
            for atm in (strandEndAtom1, strandEndAtom2):
                axisEndAtom = atm.axis_neighbor()
                if axisEndAtom:
                    if same_vals(axisEndAtom.posn(), self.grabbedHandle.origin):
                        resizeEndStrandAtom = atm
                        resizeEndAxisAtom = axisEndAtom


        return (resizeEndStrandAtom, resizeEndAxisAtom)


    def getCursorText(self):
        """
        Used by DnaStrand_GraphicsMode._draw_handles()
        @TODO: It also updates the number of bases
        """
        if self.grabbedHandle is None:
            return

        #@TODO: This updates the PM as the cursor moves. 
        #Need to rename this method so that you that it also does more things 
        #than just to return a textString. Even better if its called in 
        #self.model_changed but at the moment there is a bug thats why we 
        #are doing this update by calling getCursorText in the 
        #GraphicscMode._draw_handles-- Ninad 2008-04-05
        self.update_numberOfBases()
        
        text = ""
        textColor = env.prefs[cursorTextColor_prefs_key] # Mark 2008-08-28
        
        if not env.prefs[dnaStrandEditCommand_showCursorTextCheckBox_prefs_key]:
            return text, textColor
        
        numberOfBases = self.propMgr.numberOfBasesSpinBox.value()
        
        numberOfBasesString = self._getCursorText_numberOfBases(
            numberOfBases)
        
        changedBases = self._getCursorText_changedBases(
            numberOfBases)
        
        text = numberOfBasesString 
        
        if text and changedBases:
            text += " " # Removed comma. Mark 2008-07-03
        
        text += changedBases                   

        return (text , textColor)
    
    def _getCursorText_numberOfBases(self, numberOfBases):
        """
        Return the cursor textstring that gives information about the number 
        of bases if the corresponding prefs_key returns True.
        """
        numberOfBasesString = ''

        if env.prefs[
            dnaStrandEditCommand_cursorTextCheckBox_numberOfBases_prefs_key]:
            numberOfBasesString = "%db"%numberOfBases

        return numberOfBasesString

    def _getCursorText_changedBases(self, numberOfBases):
        """
        @see: self.getCursorText()
        """
        changedBasesString = ''

        if env.prefs[
            dnaStrandEditCommand_cursorTextCheckBox_changedBases_prefs_key]:
            
            original_numberOfBases = self.struct.getNumberOfBases()
            changed_bases = numberOfBases - original_numberOfBases            

            if changed_bases > 0:
                changedBasesString = "(" + "+" + str(changed_bases) + ")"
            else:
                changedBasesString = "(" + str(changed_bases) + ")"

        return changedBasesString


    def modifyStructure(self):
        """

        Called when a resize handle is dragged to change the length of the 
        segment. (Called upon leftUp) . This method assigns the new parameters 
        for the segment after it is resized and calls 
        preview_or_finalize_structure which does the rest of the job. 
        Note that Client should call this public method and should never call
        the private method self._modifyStructure. self._modifyStructure is 
        called only by self.preview_or_finalize_structure

        @see: B{DnaStrand_ResizeHandle.on_release} (the caller)
        @see: B{SelectChunks_GraphicsMode.leftUp} (which calls the 
              the relevent method in DragHandler API. )
        @see: B{exprs.DraggableHandle_AlongLine}, B{exprs.DragBehavior}
        @see: B{self.preview_or_finalize_structure }
        @see: B{self._modifyStructure}         
        """
        #TODO: need to cleanup this and may be use use something like
        #self.previousParams = params in the end -- 2008-03-24 (midnight)


        if self.grabbedHandle is None:
            return   

        #TODO: Important note: How does NE1 know that structure is modified? 
        #Because number of base pairs parameter in the PropMgr changes as you 
        #drag the handle . This is done in self.getCursorText() ... not the 
        #right place to do it. OR that method needs to be renamed to reflect
        #this as suggested in that method -- Ninad 2008-03-25        
        self.preview_or_finalize_structure(previewing = True) 

        self.glpane.gl_update()

    def update_numberOfBases(self):
        """
        Updates the numberOfBases in the PM while a resize handle is being 
        dragged. 
        @see: self.getCursorText() where it is called. 
        """
        #@Note: originally (before 2008-04-05, it was called in 
        #DnaStrand_ResizeHandle.on_drag() but that 'may' have some bugs 
        #(not verified) also see self.getCursorText() to know why it is
        #called there (basically a workaround for bug 2729
        if self.grabbedHandle is None:
            return

        currentPosition = self.grabbedHandle.currentPosition
        resize_end = self.grabbedHandle.origin

        new_duplexLength = vlen( currentPosition - resize_end )

        numberOfBasePairs_to_change = getNumberOfBasePairsFromDuplexLength('B-DNA', 
                                                                           new_duplexLength)

        original_numberOfBases = self.struct.getNumberOfBases()
        #If the dot product of handle direction and the direction in which it 
        #is dragged is negative, this means we need to subtract bases
        direction_of_drag = norm(currentPosition - resize_end)
        if dot(self.grabbedHandle.direction, direction_of_drag ) < 0:
            total_number_of_bases = original_numberOfBases - numberOfBasePairs_to_change
            self.propMgr.numberOfBasesSpinBox.setValue(total_number_of_bases)
        else:
            total_number_of_bases = original_numberOfBases + numberOfBasePairs_to_change
            self.propMgr.numberOfBasesSpinBox.setValue(total_number_of_bases - 1)


    def _determine_numberOfBases_to_change(self):
        """
        """       
        #The Property manager will be showing the current number 
        #of base pairs (w. May be we can use that number directly here? 
        #The following is  safer to do so lets just recompute the 
        #number of base pairs. (if it turns out to be slow, we will consider
        #using the already computed calue from the property manager

        original_numberOfBases = self.struct.getNumberOfBases()

        numberOfBasesToAddOrRemove = self.propMgr.numberOfBasesSpinBox.value()\
                                   - original_numberOfBases 

        if numberOfBasesToAddOrRemove > 0:
            #dna.modify will remove the first base pair it creates 
            #(that basepair will only be used for proper alignment of the 
            #duplex with the existing structure) So we need to compensate for
            #this basepair by adding 1 to the new number of base pairs. 
            numberOfBasesToAddOrRemove += 1


        return numberOfBasesToAddOrRemove


    def makeMenus(self): 
        """
        Create context menu for this command.
        """
        if not hasattr(self, 'graphicsMode'):
            return

        selobj = self.glpane.selobj

        if selobj is None:
            return

        self.Menu_spec = []

        highlightedChunk = None
        if isinstance(selobj, Chunk):
            highlightedChunk = selobj
        if isinstance(selobj, Atom):
            highlightedChunk = selobj.molecule
        elif isinstance(selobj, Bond):
            chunk1 = selobj.atom1.molecule
            chunk2 = selobj.atom2.molecule
            if chunk1 is chunk2 and chunk1 is not None:
                highlightedChunk = chunk1

        if highlightedChunk is None:
            return

        if self.hasValidStructure():
            ### REVIEW: these early returns look wrong, since they skip running
            # the subsequent call of highlightedChunk.make_glpane_cmenu_items
            # for no obvious reason. [bruce 090114 comment, in two commands]
            if (self.struct is highlightedChunk) or \
               (self.struct is highlightedChunk.parent_node_of_class(
                   self.assy.DnaStrand)):
                item = (("Currently editing %r"%self.struct.name), 
                        noop, 'disabled')
                self.Menu_spec.append(item)
                return	 
            #following should be self.struct.getDnaGroup or self.struct.getDnaGroup
            #need to formalize method name and then make change.
            dnaGroup = self.struct.parent_node_of_class(self.assy.DnaGroup)
            if dnaGroup is None:
                return            
            if not dnaGroup is highlightedChunk.parent_node_of_class(self.assy.DnaGroup):                
                item = ("Edit unavailable: Member of a different DnaGroup",
                        noop, 'disabled')
                self.Menu_spec.append(item)
                return

        highlightedChunk.make_glpane_cmenu_items(self.Menu_spec, self)