summaryrefslogtreecommitdiff
path: root/cad/src/analysis/ESP/ESPImage.py
blob: f2c541896ad3378f5c4686936926bafef0159cfe (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
# Copyright 2004-2009 Nanorex, Inc.  See LICENSE file for details.
"""
ESPImage.py - display an Electrostatic Potential Image (or any OpenGL texture)

Note: keep this around, even if ESPImage per se becomes obsolete,
since it's also our only working Node for directly displaying an image in
the 3d model space.

@author: Huaicai, Bruce
@version: $Id$
@copyright: 2004-2009 Nanorex, Inc.  See LICENSE file for details.

History:

bruce 071215 split class ESPImage into its own file, out of jigs_planes.py.

TODO: refactor into a general texture image jig (and perhaps
some texture helpers, but merge those with the ones in texture_*.py
which were partly derived from these methods),
and a specific ESPImage jig.

Module classification: model, though contains operations/simulation
and perhaps other code. Probably part of an ESP package.
"""

import os
import math
import shutil

from OpenGL.GL import glPushMatrix
from OpenGL.GL import glTranslatef
from OpenGL.GL import glRotatef
from OpenGL.GL import glPopMatrix
from OpenGL.GL import glPushName, glPopName

from OpenGL.GL import GL_CLAMP
from OpenGL.GL import GL_TEXTURE_WRAP_S
from OpenGL.GL import glTexParameterf
from OpenGL.GL import GL_TEXTURE_WRAP_T
from OpenGL.GL import GL_REPEAT
from OpenGL.GL import GL_LINEAR
from OpenGL.GL import GL_TEXTURE_MAG_FILTER
from OpenGL.GL import GL_LINEAR_MIPMAP_LINEAR
from OpenGL.GL import GL_TEXTURE_MIN_FILTER
from OpenGL.GL import GL_NEAREST
from OpenGL.GL import GL_DECAL
from OpenGL.GL import GL_TEXTURE_ENV
from OpenGL.GL import GL_TEXTURE_ENV_MODE
from OpenGL.GL import glTexEnvf
from OpenGL.GL import glGenTextures
from OpenGL.GL import GL_TEXTURE_2D
from OpenGL.GL import glBindTexture
from OpenGL.GL import GL_UNPACK_ALIGNMENT
from OpenGL.GL import glPixelStorei
from OpenGL.GL import GL_RGBA
from OpenGL.GL import GL_UNSIGNED_BYTE
from OpenGL.GL import glTexImage2D
from OpenGL.GLU import gluBuild2DMipmaps

from PyQt4.Qt import QMessageBox
from PyQt4.Qt import QFileDialog

import foundation.env as env
from utilities import debug_flags
from foundation.state_utils import DataMixin

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

from graphics.drawing.drawers import drawPlane
from graphics.drawing.drawers import drawwirecube
from graphics.drawing.drawers import drawLineLoop

from geometry.VQT import V, Q, A
from graphics.rendering.povray.povheader import povpoint
from graphics.behaviors.shape import SelectionShape
from geometry.Slab import Slab

from utilities.Log import redmsg, greenmsg
from utilities.debug_prefs import debug_pref, Choice_boolean_False
from model.jigs import Jig # REVIEW: all uses of this are suspicious!
from graphics.images.ImageUtils import nEImageOps

from utilities.constants import black
from utilities.constants import ave_colors
from utilities.constants import green
from utilities.constants import START_NEW_SELECTION
from utilities.constants import SUBTRACT_FROM_SELECTION

from model.jigs_planes import RectGadget
from model.jigs_planes import povStrVec

from analysis.ESP.ESPImageProp import ESPImageProp
from platform_dependent.PlatformDependent import find_or_make_Nanorex_subdir

from analysis.ESP.NanoHiveUtils import get_nh_espimage_filename
from analysis.ESP.NanoHiveUtils import run_nh_simulation
from analysis.ESP.NanoHive_SimParameters import NanoHive_SimParameters
from utilities.debug import print_compact_traceback
# ==

class ESPImage(RectGadget):
    """
    Electrostatic potential image, displayed as a translucent square in 3D.
    """
    #bruce 060212 use separate own_mutable_attrs and mutable_attrs to work
    #around design flaws in attrlist inheritance scheme (also including
    #superclass mutable_attrs center,quat -- might fix some bugs -- and adding
    #image_mods)
    own_mutable_attrs = ('fill_color', 'image_mods', )
    mutable_attrs = RectGadget.mutable_attrs + own_mutable_attrs
    copyable_attrs = RectGadget.copyable_attrs + own_mutable_attrs + \
                   ('resolution', 'opacity', 'show_esp_bbox', 'image_offset',
                    'edge_offset', 'espimage_file', 'highlightChecked',
                    'xaxis_orient', 'yaxis_orient', 'multiplicity' )
        #bruce 060212 added 'espimage_file', 'highlightChecked',
        #'xaxis_orient', 'yaxis_orient', 'multiplicity' (not sure adding
        #'multiplicity' is correct)

    sym = "ESPImage" #bruce 070604 removed space (per Mark decision)
    icon_names = ["modeltree/ESP_Image.png", "modeltree/ESP_Image-hide.png"]
    mmp_record_name = "espimage"
    featurename = "ESP Image" #Renamed from ESP Window. mark 060108


    def __init__(self, assy, list1, READ_FROM_MMP = False):
        RectGadget.__init__(self, assy, list1, READ_FROM_MMP)
        self.assy = assy
        self.color = black # Border color
        self.normcolor = black
        self.fill_color = 85/255.0, 170/255.0, 255/255.0 # a nice blue

        # This specifies the resolution of the ESP Image. The total number of
        # ESP data points in the image will number resolution^2.
        self.resolution = 32
            # Keep it small so sim run doesn't take so long. Mark 050930.
        self.show_esp_bbox = True
            # Show/Hide ESP Image Volume (Bbox). All atoms inside this volume
            # are used by the MPQC ESP Plane plug-in to calculate the ESP
            # points.
        self.image_offset = 1.0
            # the perpendicular (front and back) image offset used to create
            # the depth of the bbox
        self.edge_offset = 1.0
            # the edge offset used to create the edge boundary of the bbox
        self.opacity = 0.6 # float, from 0.0 (transparent) to 1.0 (opaque)
        self.image_obj = None
            # helper object for texture image, or None if no texture is ready
            # [bruce 060207 revised comment]
        self.image_mods = image_mod_record()
            # accumulated modifications to the file's image [bruce 060210
            # bugfix] ##e need to use self.image_mods in writepov, too, perhaps
            # via a temporary image file
        self.tex_name = None # OpenGL texture name for image_obj, if we have one
            # [bruce 060207 for fixing bug 1059]
        self.espimage_file = '' # ESP Image (png) filename
        self.highlightChecked = False # Flag if highlight is turned on or off
            ###e does this need storing in mmp file? same Q for xaxis_orient,
            ###etc. [bruce 060212 comment]
        self.xaxis_orient = 0 # ESP Image X Axis orientation
            # [bruce comment 060212: used by external code in files_nh.py]
        self.yaxis_orient = 0 # ESP Image Y Axis orientation
        self.multiplicity = 1 # Multiplicity of atoms within self's bbox volume

        self.pickCheckOnly = False #This is used to notify drawing code if
            # it's just for picking purpose
            ### REVIEW/TODO: understanding how self.pickCheckOnly might be
            # left over from one drawing call to another (potentially causing
            # bugs) is a mess. It needs to be refactored so that it's just an
            # argument to all methods it's passed through. This involves some
            # superclass methods; maybe they can be overridden so the argument
            # is only needed in local methods, I don't know. This is done in
            # several Node classes, so I added this comment to all of them.
            # [bruce 090310 comment]
        return

    def _initTextureEnv(self): # called during draw method
        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP)
            # [looks like a bug that we overwrite clamp with repeat, just
            # below? bruce 060212 comment]
        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP)
        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT)
        if debug_pref("smoother textures", Choice_boolean_False,
                      prefs_key = True):
            #bruce 060212 new feature (only visible in debug version so far);
            # ideally it'd be controllable per-jig for side-by-side comparison;
            # also, changing its menu item ought to gl_update but doesn't ##e
            glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
            if self.have_mipmaps:
                glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
                                GL_LINEAR_MIPMAP_LINEAR)
            else:
                glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
        else:
            glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
            glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
        glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_DECAL)
        return

    def _create_PIL_image_obj_from_espimage_file(self):
        """
        Creates a PIL image object from the jig's ESP image (png) file.
        """
        if self.espimage_file:
            self.image_obj = nEImageOps(self.espimage_file)
            self.image_mods.do_to( self.image_obj)
                #bruce 060210 bugfix: stored image_mods in mmp file, so we can
                # reuse them here
        return

    def _loadTexture(self):
        """
        Load texture data from current image object
        """
        ix, iy, image = self.image_obj.getTextureData()

        # allocate texture object if never yet done
        # [bruce 060207 revised all related code, to fix bug 1059]
        if self.tex_name is None:
            self.tex_name = glGenTextures(1)
            # note: by experiment (iMac G5 Panther), this returns a single
            # number (1L, 2L, ...), not a list or tuple, but for an argument
            # >1 it returns a list of longs. We depend on this behavior here.
            # [bruce 060207]

        # initialize texture data
        glBindTexture(GL_TEXTURE_2D, self.tex_name) # 2d texture (x and y size)

        glPixelStorei(GL_UNPACK_ALIGNMENT, 1)
        self.have_mipmaps = False
        if debug_pref("smoother tiny textures", Choice_boolean_False,
                      prefs_key = True):
            #bruce 060212 new feature; only takes effect when image is
            # reloaded for some reason (like "load image" button)
            gluBuild2DMipmaps(GL_TEXTURE_2D, GL_RGBA, ix, iy, GL_RGBA,
                              GL_UNSIGNED_BYTE, image)
            self.have_mipmaps = True
        else:
            glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, ix, iy, 0, GL_RGBA,
                         GL_UNSIGNED_BYTE, image)
                # 0 is mipmap level, GL_RGBA is internal format, ix, iy is
                # size, 0 is borderwidth, and (GL_RGBA, GL_UNSIGNED_BYTE,
                # image) describe the external image data.
                # [bruce 060212 comment]

        ## self._initTextureEnv() #bruce 060207 do this in draw method, not here
        self.assy.o.gl_update()
        return

    def setProps(self, name, border_color, width, height, resolution,
                 center, wxyz, trans, fill_color, show_bbox,
                 win_offset, edge_offset ):
        """
        Set the properties for an ESP Image read from a (MMP) file.
        """
        self.name = name
        self.color = self.normcolor = border_color
        self.width = width
        self.height = height
        self.resolution = resolution
        self.center = center
        self.quat = Q(wxyz[0], wxyz[1], wxyz[2], wxyz[3])
        self.opacity = trans
        self.fill_color = fill_color
        self.show_esp_bbox = show_bbox
        self.image_offset = win_offset
        self.edge_offset = edge_offset

    def _getinfo(self):

        c = self.center * 1e-10
        ctr_pt = (float(c[0]), float(c[1]), float(c[2]))
        centerPoint = '%1.2e %1.2e %1.2e' % ctr_pt

        n = c + (self.planeNorm * 1e-10)
        np = (float(n[0]), float(n[1]), float(n[2]))
        normalPoint = '%1.2e %1.2e %1.2e' % np

        return  "[Object: ESP Image] [Name: " + str(self.name) + "] " + \
                "[centerPoint = " + centerPoint + "] " + \
                "[normalPoint = " + normalPoint + "]"

    def getstatistics(self, stats):
        stats.num_espimage += 1

    def set_cntl(self):
        self.cntl = ESPImageProp(self, self.assy.o)

    def _createShape(self, selSense = START_NEW_SELECTION):
        """
        """
        # REVIEW: this is used during drawing. Is that too slow?
        # [bruce 090310 question]

        hw = self.width/2.0
        wo = self.image_offset
        eo = self.edge_offset

        shape = SelectionShape(self.right, self.up, self.planeNorm)
        slab = Slab(self.center - self.planeNorm * wo, self.planeNorm, 2 * wo)
        pos = [V(-hw - eo, hw + eo, 0.0), V(hw + eo, -hw - eo, 0.0)]
        p3d = []
        for p in pos:
            p3d += [self.quat.rot(p) + self.center]

        # selSense used to highlight (not select) atoms inside the jig's volume.
        shape.pickrect(p3d[0], p3d[1], self.center, selSense, slab = slab)

        return shape

    def pickSelected(self, pick):
        """
        Select atoms inside the ESP Image bounding box. (Actually this works
        for chunk too.)
        """
        # selSense is used to highlight (not select) atoms inside the jig's
        # volume.
        if not pick:
            selSense = SUBTRACT_FROM_SELECTION
        else:
            selSense = START_NEW_SELECTION

        shape = self._createShape(selSense)
        shape.select(self.assy)
        return

    def _findObjsInside(self):
        """
        Find objects [atoms or chunks] inside the shape
        """
        # note: this is used during drawing. [bruce 090310 comment]
        shape = self._createShape()
        return shape.findObjInside(self.assy)

    def _highlightObjsInside(self):
        """
        highlight atoms covered by self
        """
        # Note: I refactored how this is called [and also renamed it
        # from highlightAtomChunks], so it doesn't slow down
        # every draw of every model by a complete scan of nodes just for
        # this (even when this rarely-used Node class is not present
        # in the model). It used to be called by its own model scanner,
        # _highlightAtoms, in SelectAtoms_basicGraphicsMode, called during
        # graphicsMode.Draw. Now it's called directly by self.draw (which is
        # also called during .Draw, or after today's refactoring of .Draw,
        # by .Draw_model).
        #
        # This is predicted to cause one observable change: we'll call this
        # during all modes, not only subclasses of SelectAtoms mode.
        # This is not obviously bad, and is hard to change, and this Node
        # is no longer considered very important, so I'm not worrying about
        # it for now. If you want to change it, don't reintroduce
        # anything that slows down all drawing, like the old code did!
        #
        # [bruce 090310]

        if not self.highlightChecked:
            return

        color = ave_colors( 0.8, green, black)
        atomChunks = self._findObjsInside() # atoms or chunks
        for m in atomChunks:
            if isinstance(m, Chunk):
                for a in m.atoms.itervalues():
                    a.overdraw_with_special_color(color)
            else:
                m.overdraw_with_special_color(color)
        return

    def edit(self): # in class ESPImage
        """
        Force into 'Build' mode before opening the dialog
        """
        #bruce 060403 changes: force Build, not Select Atoms as before; only
        # do this if current mode is not Build. (But why do we need to force it
        # into any particular mode? I don't know. [bruce 070608])
        commandSequencer = self.assy.w.commandSequencer #bruce 071008
        commandSequencer.userEnterCommand('DEPOSIT')
        Jig.edit(self)

    def make_selobj_cmenu_items(self, menu_spec):
        """
        Add ESP Image specific context menu items to <menu_spec> list when
        self is the selobj. Currently not working since ESP Image jigs do not
        get highlighted.
        """
        # mark 060312
        item = ('Hide', self.Hide)
        menu_spec.append(item)
        menu_spec.append(None) # Separator
        item = ('Properties...', self.edit)
        menu_spec.append(item)
        item = ('Calculate ESP', self.__CM_Calculate_ESP)
        menu_spec.append(item)
        item = ('Load ESP Image', self.__CM_Load_ESP_Image)
        menu_spec.append(item)
        return

    def writepov(self, file, dispdef):
        if self.hidden or self.is_disabled():
            return

        hw = self.width/2.0
        ## wo = self.image_offset
        ## eo = self.edge_offset
        corners_pos = [V(-hw,  hw, 0.0),
                       V(-hw, -hw, 0.0),
                       V( hw, -hw, 0.0),
                       V( hw,  hw, 0.0)]
        povPlaneCorners = []
        for v in corners_pos:
            povPlaneCorners += [self.quat.rot(v) + self.center]
        strPts = ' %s, %s, %s, %s ' % tuple(map(povpoint, povPlaneCorners))
        if self.image_obj:
            imgName = os.path.basename(self.espimage_file)
            imgPath = os.path.dirname(self.espimage_file)
            file.write('\n // Before you render, please set this '
                       'command option: Library_Path="%s"\n\n' % (imgPath,))
            file.write('esp_plane_texture(' + strPts + ', "'+ imgName + '") \n')
        else:
            color = '%s %f>' % (povStrVec(self.fill_color), self.opacity)
            file.write('esp_plane_color(' + strPts + ', ' + color + ') \n')
        return

    def draw(self, glpane, dispdef):
        """
        Most drawing is done after the main drawing code is
        done. (i.e. in self.draw_after_highlighting()).
        """
        #bruce 090310 revised this; it used to do nothing.
        # See comment in the following method for details.
        self._highlightObjsInside()

    def draw_after_highlighting(self,
                                glpane,
                                dispdef,
                                pickCheckOnly = False):
        """
        For ESPImage class, this does all the drawing. (Does it after main
        drawing code is finished drawing.) This method ensures that the
        ESP image jig gets selected even when you click inside the
        rectangular box (i.e. not just along the edges of the box).
        """
        anythingDrawn = False
        if self.hidden:
            return anythingDrawn

        self.pickCheckOnly = pickCheckOnly
        anythingDrawn = True
        glPushName(self.glname)
        try:
            self._draw(glpane, dispdef) #calls self._draw_jig()
        except:
            anythingDrawn = False
            msg = "ignoring exception when drawing Jig %r" % self
            print_compact_traceback(msg + ": ")
        glPopName()

        return anythingDrawn

    def _draw_jig(self, glpane, color, highlighted = False):
        """
        Draw an ESPImage jig (self) as a plane with an edge and a bounding box.

        @note: this is not called during graphicsMode.Draw_model as with most
            Jigs, but during graphicsMode.Draw_after_highlighting.
        """
        glPushMatrix()

        glTranslatef( self.center[0], self.center[1], self.center[2])
        q = self.quat
        glRotatef( q.angle*180.0/math.pi, q.x, q.y, q.z)

        #bruce 060207 extensively revised texture code re fixing bug 1059
        if self.tex_name is not None and self.image_obj:
            # self.image_obj cond is needed, for clear_esp_image() to work
            textureReady = True
            glBindTexture(GL_TEXTURE_2D, self.tex_name)
                # review: maybe this belongs in draw_plane instead?
            self._initTextureEnv() # sets texture params the way we want them
        else:
            textureReady = False
        drawPlane(self.fill_color, self.width, self.width, textureReady,
                  self.opacity, SOLID = True,
                  pickCheckOnly = self.pickCheckOnly )

        hw = self.width/2.0
        corners_pos = [V(-hw,  hw, 0.0),
                       V(-hw, -hw, 0.0),
                       V( hw, -hw, 0.0),
                       V( hw,  hw, 0.0)]
        drawLineLoop(color, corners_pos)

        # Draw the ESP Image bbox.
        if self.show_esp_bbox:
            wo = self.image_offset
            eo = self.edge_offset
            drawwirecube(color, V(0.0, 0.0, 0.0), V(hw + eo, hw + eo, wo), 1.0)
                #drawwirebox

            # This is for debugging purposes. This draws a green normal vector
            # using local space coords. [Mark 050930]
            if 0:
                from graphics.drawing.CS_draw_primitives import drawline
                drawline(green, V(0.0, 0.0, 0.0), V(0.0, 0.0, 1.0), 0, 3)

        glpane.kluge_reset_texture_mode_to_work_around_renderText_bug()

        glPopMatrix()

        # This is for debugging purposes. This draws a yellow normal vector
        # using model space coords. [Mark 050930]
        if 0:
            from graphics.drawing.CS_draw_primitives import drawline
            from utilities.constants import yellow
            drawline(yellow, self.center, self.center + self.planeNorm, 0, 3)

    def writemmp(self, mapping):
        """
        [extends Jig method]
        """
        _super = Jig
        _super.writemmp(self, mapping)
        # Write espimage "info" record.
        line = "info espimage espimage_file = " + self.espimage_file + "\n"
        mapping.write(line)
        #bruce 060210 bugfix: write image_mods if we have any
        if self.image_mods:
            line = "info espimage image_mods = %s\n" % (self.image_mods,)
            mapping.write(line)
        return

    def mmp_record_jigspecific_midpart(self):
        color = map(int, A(self.fill_color) * 255)

        dataline = "%.2f %.2f %d (%f, %f, %f) (%f, %f, %f, %f) " \
                   "%.2f (%d, %d, %d) %d %.2f %.2f" % \
                   (self.width, self.height, self.resolution,
                    self.center[0], self.center[1], self.center[2],
                    self.quat.w, self.quat.x, self.quat.y, self.quat.z,
                    self.opacity, color[0], color[1], color[2],
                    self.show_esp_bbox, self.image_offset, self.edge_offset )
        return " " + dataline

    def readmmp_info_espimage_setitem( self, key, val, interp ):
        """
        This is called when reading an mmp file, for each "info espimage"
        record which occurs right after this node is read and no other
        (espimage jig) node has been read.

        Key is a list of words, val a string; the entire record format is
        presently [060108] "info espimage <key> = <val>", and there is exactly
        one word in <key>, "espimage_file". <val> is the espimage filename.
        <interp> is not currently used.
        """
        if len(key) != 1:
            if debug_flags.atom_debug:
                print "atom_debug: fyi: info espimage with unrecognized " \
                      "key %r (not an error)" % (key,)
            return
        if key[0] == 'espimage_file':
            if val:
                if os.path.exists(val):
                    self.espimage_file = val
                    self.image_mods.reset()
                        # also might be done in load_espimage_file, but needed
                        # here even if it's not
                    self.load_espimage_file()
                else:
                    msg = redmsg("info espimage espimage_file = " + val +
                                 ". File does not exist.  No image loaded.")
                    env.history.message(msg)
            # BUG: I think it's a bug to go on to interpret image_mods if the
            # file doesn't exist. Not sure how to fix that. [bruce 060210]
            pass
        elif key[0] == 'image_mods': #bruce 060210
            try:
                self.image_mods.set_to_str(val)
            except ValueError:
                print "mmp syntax error in esp image modifications:", val
            else:
                if self.image_obj:
                    self.image_mods.do_to( self.image_obj)
                    self._loadTexture()
            pass
        return

    def get_sim_parms(self):
        sim_parms = NanoHive_SimParameters()

        sim_parms.desc = 'ESP Calculation from MT Context Menu for ' + self.name
        sim_parms.iterations = 1
        sim_parms.spf = 1e-17 # Steps per Frame
        sim_parms.temp = 300 # Room temp

        # Get updated multiplicity from this ESP image jig bbox
        atomList = self._findObjsInside()
        self.multiplicity = getMultiplicity(atomList)

        sim_parms.esp_image = self

        return sim_parms

    def calculate_esp(self):

        cmd = greenmsg("Calculate ESP: ")

        errmsgs = [ # warning: their indices in this list matter.
            "Error: Nano-Hive plug-in not enabled.",
            "Error: Nano-Hive Plug-in path is empty.",
            "Error: Nano-Hive plug-in path points to a file that does not exist.",
            "Error: Nano-Hive plug-in is not Version 1.2b.",
            "Error: Couldn't connect to Nano-Hive instance.",
            "Error: Load command failed.",
            "Error: Run command failed.",
            "Simulation Aborted"
         ]

        sim_parms = self.get_sim_parms()
        sims_to_run = ["MPQC_ESP"]
        results_to_save = [] # Results info included in write_nh_mpqc_esp_rec()

        # Temporary file name of ESP image file.
        nhdir = find_or_make_Nanorex_subdir("Nano-Hive")
        tmp_espimage_file = os.path.join(nhdir, "%s.png" % (self.name))

        # Destination (permanent) file name of ESP image file.
        espimage_file = get_nh_espimage_filename(self.assy, self.name)

        msg = "Running ESP calculation on [%s]. " \
              "Results will be written to: [%s]" % (self.name, espimage_file)
        env.history.message( cmd + msg )

        r = run_nh_simulation(self.assy, 'CalcESP',
                              sim_parms, sims_to_run, results_to_save)

        if r:
            msg = redmsg(errmsgs[r - 1])
            env.history.message( cmd + msg )
            return

        msg = "ESP calculation on [%s] finished." % (self.name)
        env.history.message( cmd + msg )

        # Move tmp file to permanent location. Make sure the tmp file is there.
        if os.path.exists(tmp_espimage_file):
            shutil.move(tmp_espimage_file, espimage_file)
        else:
            print "Temporary ESP Image file ", tmp_espimage_file, \
                  " does not exist. Image not loaded."
            return

        self.espimage_file = espimage_file
        self.load_espimage_file()
        self.assy.changed()
        self.assy.w.win_update()

        return

    def __CM_Calculate_ESP(self):
        """
        Method for "Calculate ESP" context menu
        """
        self.calculate_esp()

    def __CM_Load_ESP_Image(self):
        """
        Method for "Load ESP Image" context menu
        """
        self.load_espimage_file()

    def load_espimage_file(self, choose_new_image = False, parent = None):
        """
        Load the ESP (.png) image file pointed to by self.espimage_file. If
        the file does not exist, or if choose_new_image is True, the user will
        be prompted to choose a new image, and if the file chooser dialog is
        not cancelled, the new image will be loaded and its pathname stored in
        self.espimage_file.

        Return value is None if user cancels the file chooser, but is
        self.espimage_file (which has just been reloaded, and a history
        message emitted, whether or not it was newly chosen) in all other
        cases.

        If self.espimage_file is changed (to a different value), this marks
        assy as changed.
        """
        #bruce 060207 revised docstring. I suspect this routine has several
        # distinct purposes which should not be so intimately mixed (i.e. it
        # should be several routines). Nothing presently uses the return
        # value.

        old_espimage_file = self.espimage_file

        if not parent:
            parent = self.assy.w

        cmd = greenmsg("Load ESP Image: ")

        ## print "load_espimage_file(): espimage_file = ", self.espimage_file

        if choose_new_image or not self.espimage_file:
            choose_new_image = True

        elif not os.path.exists(self.espimage_file):
            msg = "The ESP image file:\n" + self.espimage_file + \
                  "\ndoes not exist.\n\nWould you like to select one?"
            choose_new_image = True
            QMessageBox.warning( parent, "Choose ESP Image", \
                                 msg, QMessageBox.Ok, QMessageBox.Cancel)
            #bruce 060207 question: shouldn't we check here whether they said
            #ok or cancel?? Looks like a bug. ####@@@@

        if choose_new_image:
            cwd = self.assy.get_cwd()

            fn = QFileDialog.getOpenFileName(
                parent,
                "Load an ESP Image File",
                cwd,
                "Portable Network Graphics (*.png);;All Files (*.*);;"
                ) #bruce 060212 added All Files option


            if not fn:
                env.history.message(cmd + "Cancelled.")
                return None

            self.espimage_file = str(fn)
            if old_espimage_file != self.espimage_file:
                self.changed() #bruce 060207 fix of perhaps-unreported bug
            pass

        if self.image_mods:
            self.image_mods.reset()
            self.changed()

        self._create_PIL_image_obj_from_espimage_file()
        self._loadTexture()
            #bruce 060212 comment: this does gl_update, but when we're called
            # from dialog's open file button, the glpane doesn't show the new
            # texture until the dialog is closed (which is a bug, IMHO), even
            # if we call env.call_qApp_processEvents() before returning from
            # this method (load_espimage_file). I don't know why.

        # Bug fix 1041-1.  Mark 051003
        msg = "ESP image loaded: [" + self.espimage_file + "]"
        env.history.message(cmd + msg)

        return self.espimage_file


    def clear_esp_image(self):
        """
        Clears the image in the ESP Image.
        """
        self.image_obj = None
        # don't self.image_mods.reset(); but when we load again, that might
        # clear it
        self.assy.o.gl_update()

    def flip_esp_image(self): # slot method
        if self.image_obj:
            self.image_obj.flip()
            self.image_mods.flip() #bruce 060210
            self.changed() #bruce 060210
            self._loadTexture()

    def mirror_esp_image(self):
        if self.image_obj:
            self.image_obj.mirror()
            self.image_mods.mirror() #bruce 060210
            self.changed() #bruce 060210
            self._loadTexture()

    def rotate_esp_image(self, deg):
        if self.image_obj:
            self.image_obj.rotate(deg)
            self.image_mods.rotate(deg) #bruce 060210
            self.changed() #bruce 060210
            self._loadTexture()

    pass # end of class ESPImage


def getMultiplicity(objList): # only used in this file
    """
    @param objList: A list of Atom/Chunk objects
    @return: If the total number of electron is odd, return 2, otherwise return 1.

    @note: not correct for PAM atoms, but doesn't check for the error of using one.
    """
    #Huaicai 10/04/05 -- see also the test code at EOF
    # [bruce 071120 moved this from chem.py to jigs_planes.py]
    numElectrons = 0
    for m in objList:
        if isinstance(m, Atom):
            numElectrons += m.element.eltnum
        elif isinstance(m, Chunk):
            for a in m.atoms.itervalues():
                numElectrons += a.element.eltnum

    if numElectrons % 2:
        return 2
    else:
        return 1
    pass


class image_mod_record(DataMixin):
    """
    record the mirror/flip/rotate history of an image in a short canonical form,
    and be able to write/read/do this
    """
    #bruce 060210; maybe should be refiled in ImageUtils.py, though only used
    #in this file
    def __init__(self, mirror = False, ccwdeg = 0):
        """
        whether to mirror it, and (then) how much to rotate it
        counterclockwise, in degrees
        """
            #k haven't verified it's ccw and not cw, in terms of how it's
            # used, but this code should work either way
        self.mirrorQ = not not mirror # boolean
        self.rot = ccwdeg % 360 # float or int (I think)

    def reset(self):
        """reset self to default values"""
        self.mirrorQ = False
        self.rot = 0

    def __str__(self):
        """
        [WARNING (kluge, sorry): this format is required by the code,
         which uses it to print parts of mmp records]
        """
        return "%s %s" % (self.mirrorQ, self.rot)

    def __repr__(self):
        return "<%s at %#x; mirrorQ, rot == %r>" % \
               (self.__class__.__name__, id(self), (self.mirrorQ, self.rot))

    def set_to_str(self, str1):
        """
        set self to the values encoded in the given string, which should
        have been produced by str(self); debug print on syntax error
        """
        try:
            mir, rot = str1.split() # e.g. "False", "180"
            ## mir = bool(mir) # wrong -- bool("False") is True!!!
            # mir should be "True" or "False" (unrecognized mirs are
            # treated as False)
            mir = (mir == 'True')
            rot = float(rot)
        except:
            raise ValueError, "syntax error in %r" % (str1,)
                # (note: no guarantee str1 is even a string, in principle)
        else:
            self.mirrorQ = mir
            self.rot = rot
        return

    def mirror(self):
        """
        left-right mirroring
        """
        self.mirrorQ = not self.mirrorQ
        self.rot = (- self.rot) % 360

    def flip(self):
        """
        vertical flip (top-bottom mirroring)
        """
        self.rotate(90)
        self.mirror()
        self.rotate(-90)

    def rotate(self, deg):
        self.rot = (self.rot + deg) % 360

    def do_to(self, similar):
        """
        do your mods to another object that also has mirror/flip/rotate methods
        """
        if self.mirrorQ:
            similar.mirror()
        if self.rot:
            similar.rotate(self.rot)
        return

    def __nonzero__(self):
        # Python requires this to return an int; i think a boolean should be ok
        return not not (self.mirrorQ or self.rot)
            # only correct since we always canonicalize rot by % 360

    def _copyOfObject(self): # (in class image_mod_record [bruce circa 060210])
        """
        [override abstract method of DataMixin]
        """
        return self.__class__(self.mirrorQ, self.rot)

    def __eq__(self, other): #bruce 060222 for Undo; in class image_mod_record
        """
        [override abstract method of DataMixin]
        """
        # note: defining __eq__ is sufficient, but only because we inherit
        # from DataMixin, which defines __ne__ based on __eq__
        return self.__class__ is other.__class__ and \
               (self.mirrorQ, self.rot) == (other.mirrorQ, other.rot)

    pass # end of class image_mod_record

# == test code

if __name__ == '__main__':

    nopos = V(0,0,0)
        #bruce 060308 replaced 'no' with nopos (w/o knowing if it was correct
        #in the first place)

    alist = [Atom('C', nopos, None),
             Atom('C', nopos, None),
             Atom('H', nopos, None),
             Atom('O', nopos, None),
            ]

    assert getMultiplicity(alist) == 2

    alist += [Atom('N', nopos, None), ]
    assert getMultiplicity(alist) == 1

    print "Test succeed, no assertion error."


#end