]>
Commit | Line | Data |
---|---|---|
1 | import wx | |
2 | ||
3 | from Numeric import array,Float,cos,pi,sum,minimum,maximum,Int32 | |
4 | from time import clock, sleep | |
5 | import types | |
6 | import os | |
7 | ||
8 | ID_ZOOM_IN_BUTTON = wx.NewId() | |
9 | ID_ZOOM_OUT_BUTTON = wx.NewId() | |
10 | ID_ZOOM_TO_FIT_BUTTON = wx.NewId() | |
11 | ID_MOVE_MODE_BUTTON = wx.NewId() | |
12 | ID_TEST_BUTTON = wx.NewId() | |
13 | ||
14 | ID_ABOUT_MENU = wx.NewId() | |
15 | ID_EXIT_MENU = wx.NewId() | |
16 | ID_ZOOM_TO_FIT_MENU = wx.NewId() | |
17 | ID_DRAWTEST_MENU = wx.NewId() | |
18 | ID_DRAWMAP_MENU = wx.NewId() | |
19 | ID_CLEAR_MENU = wx.NewId() | |
20 | ||
21 | ID_TEST = wx.NewId() | |
22 | ||
23 | ||
24 | ### These are some functions for bitmaps of icons. | |
25 | import cPickle, zlib | |
26 | ||
27 | def GetHandData(): | |
28 | return cPickle.loads(zlib.decompress( | |
29 | 'x\xda\xd3\xc8)0\xe4\nV72T\x00!\x05Cu\xae\xc4`u=\x85d\x05\xa7\x9c\xc4\xe4l0O\ | |
30 | \x01\xc8S\xb6t\x06A(\x1f\x0b\xa0\xa9\x8c\x9e\x1e6\x19\xa0\xa8\x1e\x88\xd4C\ | |
31 | \x97\xd1\x83\xe8\x80 \x9c2zh\xa6\xc1\x11X\n\xab\x8c\x02\x8a\x0cD!\x92\x12\ | |
32 | \x98\x8c\x1e\x8a\x8b\xd1d\x14\xf4\x90%\x90LC\xf6\xbf\x1e\xba\xab\x91%\xd0\ | |
33 | \xdc\x86C\x06\xd9m\xe8!\xaa\x87S\x86\x1a1\xa7\x07\x00v\x0f[\x17' )) | |
34 | ||
35 | def GetHandBitmap(): | |
36 | return wx.BitmapFromXPMData(GetHandData()) | |
37 | ||
38 | #---------------------------------------------------------------------- | |
39 | def GetPlusData(): | |
40 | return cPickle.loads(zlib.decompress( | |
41 | 'x\xda\xd3\xc8)0\xe4\nV72T\x00!\x05Cu\xae\xc4`u=\x85d\x05\xa7\x9c\xc4\xe4l0O\ | |
42 | \x01\xc8S\xb6t\x06A(\x1f\x0b RF\x0f\x08\xb0\xc9@D\xe1r\x08\x19\xb8j=l2`\r\ | |
43 | \xe82HF\xe9a\xc8\xe8\xe9A\x9c@\x8a\x0c\x0e\xd3p\xbb\x00\x8f\xab\xe1>\xd5\xd3\ | |
44 | \xc3\x15:P)l!\n\x91\xc2\x1a\xd6`)\xec\xb1\x00\x92\xc2\x11?\xb8e\x88\x8fSt\ | |
45 | \x19=\x00\x82\x16[\xf7' )) | |
46 | ||
47 | def GetPlusBitmap(): | |
48 | return wx.BitmapFromXPMData(GetPlusData()) | |
49 | ||
50 | #---------------------------------------------------------------------- | |
51 | def GetMinusData(): | |
52 | return cPickle.loads(zlib.decompress( | |
53 | 'x\xda\xd3\xc8)0\xe4\nV72T\x00!\x05Cu\xae\xc4`u=\x85d\x05\xa7\x9c\xc4\xe4l0O\ | |
54 | \x01\xc8S\xb6t\x06A(\x1f\x0b RF\x0f\x08\xb0\xc9@D\xe1r\x08\x19\xb8j=\xa2e\ | |
55 | \x10\x16@\x99\xc82zz\x10\'\x90"\x83\xc34r\xdc\x86\xf0\xa9\x9e\x1e\xae\xd0\ | |
56 | \x81Ja\x0bQ\x88\x14\xd6\xb0\x06Ka\x8f\x05\x90\x14\x8e\xf8\xc1-C|\x9c\xa2\xcb\ | |
57 | \xe8\x01\x00\xed\x0f[\x87' )) | |
58 | ||
59 | def GetMinusBitmap(): | |
60 | return wx.BitmapFromXPMData(GetMinusData()) | |
61 | ||
62 | ## This is a bunch of stuff for implimenting interactive use: catching | |
63 | ## when objects are clicked on by the mouse, etc. I've made a start, so if | |
64 | ## you are interesed in making that work, let me know, I may have gotten | |
65 | ## it going by then | |
66 | ||
67 | #### I probably want a full set of events someday: | |
68 | ## #### mouse over, right click, left click mouse up etc, etc. | |
69 | ## ##FLOATCANVAS_EVENT_LEFT_DOWN = wx.NewEventType() | |
70 | ## ##FLOATCANVAS_EVENT_LEFT_UP = wx.NewEventType() | |
71 | ## ##FLOATCANVAS_EVENT_RIGHT_DOWN = wx.NewEventType() | |
72 | ## ##FLOATCANVAS_EVENT_RIGHT_UP = wx.NewEventType() | |
73 | ## ##FLOATCANVAS_EVENT_MOUSE_OVER = wx.NewEventType() | |
74 | ||
75 | ##WXFLOATCANVASEVENT = wx.NewEventType() | |
76 | ||
77 | ##def EVT_WXFLOATCANVASEVENT( window, function ): | |
78 | ||
79 | ## """Your documentation here""" | |
80 | ||
81 | ## window.Connect( -1, -1, WXFLOATCANVASEVENT, function ) | |
82 | ||
83 | ##class wxFloatCanvasObjectEvent(wx.PyCommandEvent): | |
84 | ## def __init__(self, WindowID,Object): | |
85 | ## wx.PyCommandEvent.__init__(self, WXFLOATCANVASEVENT, WindowID) | |
86 | ## self.Object = Object | |
87 | ||
88 | ## def Clone( self ): | |
89 | ## self.__class__( self.GetId() ) | |
90 | ||
91 | ##class ColorGenerator: | |
92 | ||
93 | ## """ An instance of this class generates a unique color each time | |
94 | ## GetNextColor() is called. Someday I will use a proper Python | |
95 | ## generator for this class. | |
96 | ||
97 | ## The point of this generator is for the hit-test bitmap, each object | |
98 | ## needs to be a unique color. Also, each system can be running a | |
99 | ## different number of colors, and it doesn't appear to be possible to | |
100 | ## have a wxMemDC with a different colordepth as the screen so this | |
101 | ## generates colors far enough apart that they can be distinguished on | |
102 | ## a 16bit screen. Anything less than 16bits won't work. | |
103 | ## """ | |
104 | ||
105 | ## def __init__(self,depth = 16): | |
106 | ## self.r = 0 | |
107 | ## self.g = 0 | |
108 | ## self.b = 0 | |
109 | ## if depth == 16: | |
110 | ## self.step = 8 | |
111 | ## elif depth >= 24: | |
112 | ## self.step = 1 | |
113 | ## else: | |
114 | ## raise "ColorGenerator does not work with depth = %s"%depth | |
115 | ||
116 | ## def GetNextColor(self): | |
117 | ## step = self.step | |
118 | ## ##r,g,b = self.r,self.g,self.b | |
119 | ## self.r += step | |
120 | ## if self.r > 255: | |
121 | ## self.r = step | |
122 | ## self.g += step | |
123 | ## if self.g > 255: | |
124 | ## self.g = step | |
125 | ## self.b += step | |
126 | ## if self.b > 255: | |
127 | ## ## fixme: this should be a derived exception | |
128 | ## raise "Too many objects for HitTest" | |
129 | ## return (self.r,self.g,self.b) | |
130 | ||
131 | ||
132 | class draw_object: | |
133 | """ | |
134 | This is the base class for all the objects that can be drawn. | |
135 | ||
136 | each object has the following properties; (incomplete) | |
137 | ||
138 | BoundingBox : is of the form: array((min_x,min_y),(max_x,max_y)) | |
139 | Pen | |
140 | Brush | |
141 | ||
142 | """ | |
143 | ||
144 | def __init__(self,Foreground = 0): | |
145 | self.Foreground = Foreground | |
146 | ||
147 | self._Canvas = None | |
148 | ||
149 | # I pre-define all these as class variables to provide an easier | |
150 | # interface, and perhaps speed things up by caching all the Pens | |
151 | # and Brushes, although that may not help, as I think wx now | |
152 | # does that on it's own. Send me a note if you know! | |
153 | ||
154 | BrushList = { | |
155 | ( None,"Transparent") : wx.TRANSPARENT_BRUSH, | |
156 | ("Blue","Solid") : wx.BLUE_BRUSH, | |
157 | ("Green","Solid") : wx.GREEN_BRUSH, | |
158 | ("White","Solid") : wx.WHITE_BRUSH, | |
159 | ("Black","Solid") : wx.BLACK_BRUSH, | |
160 | ("Grey","Solid") : wx.GREY_BRUSH, | |
161 | ("MediumGrey","Solid") : wx.MEDIUM_GREY_BRUSH, | |
162 | ("LightGrey","Solid") : wx.LIGHT_GREY_BRUSH, | |
163 | ("Cyan","Solid") : wx.CYAN_BRUSH, | |
164 | ("Red","Solid") : wx.RED_BRUSH | |
165 | } | |
166 | PenList = { | |
167 | (None,"Transparent",1) : wx.TRANSPARENT_PEN, | |
168 | ("Green","Solid",1) : wx.GREEN_PEN, | |
169 | ("White","Solid",1) : wx.WHITE_PEN, | |
170 | ("Black","Solid",1) : wx.BLACK_PEN, | |
171 | ("Grey","Solid",1) : wx.GREY_PEN, | |
172 | ("MediumGrey","Solid",1) : wx.MEDIUM_GREY_PEN, | |
173 | ("LightGrey","Solid",1) : wx.LIGHT_GREY_PEN, | |
174 | ("Cyan","Solid",1) : wx.CYAN_PEN, | |
175 | ("Red","Solid",1) : wx.RED_PEN | |
176 | } | |
177 | ||
178 | FillStyleList = { | |
179 | "Transparent" : wx.TRANSPARENT, | |
180 | "Solid" : wx.SOLID, | |
181 | "BiDiagonalHatch": wx.BDIAGONAL_HATCH, | |
182 | "CrossDiagHatch" : wx.CROSSDIAG_HATCH, | |
183 | "FDiagonal_Hatch": wx.FDIAGONAL_HATCH, | |
184 | "CrossHatch" : wx.CROSS_HATCH, | |
185 | "HorizontalHatch": wx.HORIZONTAL_HATCH, | |
186 | "VerticalHatch" : wx.VERTICAL_HATCH | |
187 | } | |
188 | ||
189 | LineStyleList = { | |
190 | "Solid" : wx.SOLID, | |
191 | "Transparent": wx.TRANSPARENT, | |
192 | "Dot" : wx.DOT, | |
193 | "LongDash" : wx.LONG_DASH, | |
194 | "ShortDash" : wx.SHORT_DASH, | |
195 | "DotDash" : wx.DOT_DASH, | |
196 | } | |
197 | ||
198 | def SetBrush(self,FillColor,FillStyle): | |
199 | if FillColor is None or FillStyle is None: | |
200 | self.Brush = wx.TRANSPARENT_BRUSH | |
201 | self.FillStyle = "Transparent" | |
202 | else: | |
203 | if not self.BrushList.has_key((FillColor,FillStyle)): | |
204 | self.BrushList[(FillColor,FillStyle)] = wx.Brush(FillColor,self.FillStyleList[FillStyle]) | |
205 | self.Brush = self.BrushList[(FillColor,FillStyle)] | |
206 | ||
207 | def SetPen(self,LineColor,LineStyle,LineWidth): | |
208 | if (LineColor is None) or (LineStyle is None): | |
209 | self.Pen = wx.TRANSPARENT_PEN | |
210 | self.LineStyle = 'Transparent' | |
211 | else: | |
212 | if not self.PenList.has_key((LineColor,LineStyle,LineWidth)): | |
213 | self.PenList[(LineColor,LineStyle,LineWidth)] = wx.Pen(LineColor,LineWidth,self.LineStyleList[LineStyle]) | |
214 | self.Pen = self.PenList[(LineColor,LineStyle,LineWidth)] | |
215 | ||
216 | def SetPens(self,LineColors,LineStyles,LineWidths): | |
217 | """ | |
218 | This method used when an object could have a list of pens, rather than just one | |
219 | It is used for LineSet, and perhaps others in the future. | |
220 | ||
221 | fixme: this is really kludgy, there has got to be a better way! | |
222 | ||
223 | """ | |
224 | ||
225 | length = 1 | |
226 | if type(LineColors) == types.ListType: | |
227 | length = len(LineColors) | |
228 | else: | |
229 | LineColors = [LineColors] | |
230 | ||
231 | if type(LineStyles) == types.ListType: | |
232 | length = len(LineStyles) | |
233 | else: | |
234 | LineStyles = [LineStyles] | |
235 | ||
236 | if type(LineWidths) == types.ListType: | |
237 | length = len(LineWidths) | |
238 | else: | |
239 | LineWidths = [LineWidths] | |
240 | ||
241 | if length > 1: | |
242 | if len(LineColors) == 1: | |
243 | LineColors = LineColors*length | |
244 | if len(LineStyles) == 1: | |
245 | LineStyles = LineStyles*length | |
246 | if len(LineWidths) == 1: | |
247 | LineWidths = LineWidths*length | |
248 | ||
249 | self.Pens = [] | |
250 | for (LineColor,LineStyle,LineWidth) in zip(LineColors,LineStyles,LineWidths): | |
251 | if LineColor is None or LineStyle is None: | |
252 | self.Pens.append(wx.TRANSPARENT_PEN) | |
253 | # what's this for?> self.LineStyle = 'Transparent' | |
254 | if not self.PenList.has_key((LineColor,LineStyle,LineWidth)): | |
255 | Pen = wx.Pen(LineColor,LineWidth,self.LineStyleList[LineStyle]) | |
256 | self.Pens.append(Pen) | |
257 | else: | |
258 | self.Pens.append(self.PenList[(LineColor,LineStyle,LineWidth)]) | |
259 | if length == 1: | |
260 | self.Pens = self.Pens[0] | |
261 | ||
262 | def PutInBackground(self): | |
263 | if self._Canvas and self.Foreground: | |
264 | self._Canvas._TopDrawList.remove(self) | |
265 | self._Canvas._DrawList.append(self) | |
266 | self._Canvas._BackgroundDirty = 1 | |
267 | self.Foreground = 0 | |
268 | ||
269 | def PutInForeground(self): | |
270 | if self._Canvas and (not self.Foreground): | |
271 | self._Canvas._TopDrawList.append(self) | |
272 | self._Canvas._DrawList.remove(self) | |
273 | self._Canvas._BackgroundDirty = 1 | |
274 | self.Foreground = 1 | |
275 | ||
276 | ||
277 | class Polygon(draw_object): | |
278 | ||
279 | """ | |
280 | ||
281 | The Polygon class takes a list of 2-tuples, or a NX2 NumPy array of | |
282 | point coordinates. so that Points[N][0] is the x-coordinate of | |
283 | point N and Points[N][1] is the y-coordinate or Points[N,0] is the | |
284 | x-coordinate of point N and Points[N,1] is the y-coordinate for | |
285 | arrays. | |
286 | ||
287 | """ | |
288 | def __init__(self,Points,LineColor,LineStyle,LineWidth,FillColor,FillStyle,Foreground = 0): | |
289 | draw_object.__init__(self,Foreground) | |
290 | self.Points = array(Points,Float) | |
291 | self.BoundingBox = array(((min(self.Points[:,0]),min(self.Points[:,1])),(max(self.Points[:,0]),max(self.Points[:,1]))),Float) | |
292 | ||
293 | self.LineColor = LineColor | |
294 | self.LineStyle = LineStyle | |
295 | self.LineWidth = LineWidth | |
296 | self.FillColor = FillColor | |
297 | self.FillStyle = FillStyle | |
298 | ||
299 | self.SetPen(LineColor,LineStyle,LineWidth) | |
300 | self.SetBrush(FillColor,FillStyle) | |
301 | ||
302 | ||
303 | def _Draw(self,dc,WorldToPixel,ScaleFunction): | |
304 | Points = WorldToPixel(self.Points) | |
305 | dc.SetPen(self.Pen) | |
306 | dc.SetBrush(self.Brush) | |
307 | #dc.DrawPolygon(map(lambda x: (x[0],x[1]), Points.tolist())) | |
308 | dc.DrawPolygon(Points) | |
309 | ||
310 | class PolygonSet(draw_object): | |
311 | """ | |
312 | The PolygonSet class takes a Geometry.Polygon object. | |
313 | so that Points[N] = (x1,y1) and Points[N+1] = (x2,y2). N must be an even number! | |
314 | ||
315 | it creates a set of line segments, from (x1,y1) to (x2,y2) | |
316 | ||
317 | """ | |
318 | ||
319 | def __init__(self,PolySet,LineColors,LineStyles,LineWidths,FillColors,FillStyles,Foreground = 0): | |
320 | draw_object.__init__(self, Foreground) | |
321 | ||
322 | ##fixme: there should be some error checking for everything being the right length. | |
323 | ||
324 | ||
325 | self.Points = array(Points,Float) | |
326 | self.BoundingBox = array(((min(self.Points[:,0]),min(self.Points[:,1])),(max(self.Points[:,0]),max(self.Points[:,1]))),Float) | |
327 | ||
328 | self.LineColors = LineColors | |
329 | self.LineStyles = LineStyles | |
330 | self.LineWidths = LineWidths | |
331 | self.FillColors = FillColors | |
332 | self.FillStyles = FillStyles | |
333 | ||
334 | self.SetPens(LineColors,LineStyles,LineWidths) | |
335 | ||
336 | def _Draw(self,dc,WorldToPixel,ScaleFunction): | |
337 | Points = WorldToPixel(self.Points) | |
338 | Points.shape = (-1,4) | |
339 | dc.DrawLineList(Points,self.Pens) | |
340 | ||
341 | ||
342 | class Line(draw_object): | |
343 | """ | |
344 | The Line class takes a list of 2-tuples, or a NX2 NumPy array of point coordinates. | |
345 | so that Points[N][0] is the x-coordinate of point N and Points[N][1] is the y-coordinate | |
346 | or Points[N,0] is the x-coordinate of point N and Points[N,1] is the y-coordinate for arrays. | |
347 | ||
348 | It will draw a straight line if there are two points, and a polyline if there are more than two. | |
349 | ||
350 | """ | |
351 | def __init__(self,Points,LineColor,LineStyle,LineWidth,Foreground = 0): | |
352 | draw_object.__init__(self, Foreground) | |
353 | ||
354 | self.Points = array(Points,Float) | |
355 | self.BoundingBox = array(((min(self.Points[:,0]),min(self.Points[:,1])),(max(self.Points[:,0]),max(self.Points[:,1]))),Float) | |
356 | ||
357 | self.LineColor = LineColor | |
358 | self.LineStyle = LineStyle | |
359 | self.LineWidth = LineWidth | |
360 | ||
361 | self.SetPen(LineColor,LineStyle,LineWidth) | |
362 | ||
363 | def SetPoints(self,Points): | |
364 | self.Points = Points | |
365 | self.BoundingBox = array(((min(self.Points[:,0]),min(self.Points[:,1])),(max(self.Points[:,0]),max(self.Points[:,1]))),Float) | |
366 | if self._Canvas: | |
367 | # It looks like this shouldn't be private | |
368 | self._Canvas.BoundingBoxDirty = 1 | |
369 | ||
370 | def _Draw(self,dc,WorldToPixel,ScaleFunction): | |
371 | Points = WorldToPixel(self.Points) | |
372 | dc.SetPen(self.Pen) | |
373 | #dc.DrawLines(map(lambda x: (x[0],x[1]), Points.tolist())) | |
374 | dc.DrawLines(Points) | |
375 | ||
376 | ||
377 | class LineSet(draw_object): | |
378 | """ | |
379 | The LineSet class takes a list of 2-tuples, or a NX2 NumPy array of point coordinates. | |
380 | so that Points[N] = (x1,y1) and Points[N+1] = (x2,y2). N must be an even number! | |
381 | ||
382 | it creates a set of line segments, from (x1,y1) to (x2,y2) | |
383 | ||
384 | """ | |
385 | ||
386 | def __init__(self,Points,LineColors,LineStyles,LineWidths,Foreground = 0): | |
387 | draw_object.__init__(self, Foreground) | |
388 | ||
389 | NumLines = len(Points) / 2 | |
390 | ##fixme: there should be some error checking for everything being the right length. | |
391 | ||
392 | ||
393 | self.Points = array(Points,Float) | |
394 | self.BoundingBox = array(((min(self.Points[:,0]),min(self.Points[:,1])),(max(self.Points[:,0]),max(self.Points[:,1]))),Float) | |
395 | ||
396 | self.LineColors = LineColors | |
397 | self.LineStyles = LineStyles | |
398 | self.LineWidths = LineWidths | |
399 | ||
400 | self.SetPens(LineColors,LineStyles,LineWidths) | |
401 | ||
402 | def _Draw(self,dc,WorldToPixel,ScaleFunction): | |
403 | Points = WorldToPixel(self.Points) | |
404 | Points.shape = (-1,4) | |
405 | dc.DrawLineList(Points,self.Pens) | |
406 | ||
407 | ||
408 | class PointSet(draw_object): | |
409 | """ | |
410 | The PointSet class takes a list of 2-tuples, or a NX2 NumPy array of point coordinates. | |
411 | so that Points[N][0] is the x-coordinate of point N and Points[N][1] is the y-coordinate | |
412 | or Points[N,0] is the x-coordinate of point N and Points[N,1] is the y-coordinate for arrays. | |
413 | ||
414 | Each point will be drawn the same color and Diameter. The Diameter is in screen points, | |
415 | not world coordinates. | |
416 | ||
417 | """ | |
418 | def __init__(self,Points,Color,Diameter,Foreground = 0): | |
419 | draw_object.__init__(self,Foreground) | |
420 | ||
421 | self.Points = array(Points,Float) | |
422 | self.Points.shape = (-1,2) # Make sure it is a NX2 array, even if there is only one point | |
423 | self.BoundingBox = array(((min(self.Points[:,0]),min(self.Points[:,1])),(max(self.Points[:,0]),max(self.Points[:,1]))),Float) | |
424 | ||
425 | self.Color = Color | |
426 | self.Diameter = Diameter | |
427 | ||
428 | self.SetPen(Color,"Solid",1) | |
429 | self.SetBrush(Color,"Solid") | |
430 | ||
431 | def SetPoints(self,Points): | |
432 | self.Points = Points | |
433 | self.BoundingBox = array(((min(self.Points[:,0]),min(self.Points[:,1])),(max(self.Points[:,0]),max(self.Points[:,1]))),Float) | |
434 | if self._Canvas: | |
435 | # It looks like this shouldn't be private | |
436 | self._Canvas.BoundingBoxDirty = 1 | |
437 | ||
438 | def _Draw(self,dc,WorldToPixel,ScaleFunction): | |
439 | dc.SetPen(self.Pen) | |
440 | Points = WorldToPixel(self.Points) | |
441 | if self.Diameter <= 1: | |
442 | dc.DrawPointList(Points) | |
443 | elif self.Diameter <= 2: | |
444 | # A Little optimization for a diameter2 - point | |
445 | dc.DrawPointList(Points) | |
446 | dc.DrawPointList(Points + (1,0)) | |
447 | dc.DrawPointList(Points + (0,1)) | |
448 | dc.DrawPointList(Points + (1,1)) | |
449 | else: | |
450 | dc.SetBrush(self.Brush) | |
451 | radius = int(round(self.Diameter/2)) | |
452 | for (x,y) in Points: | |
453 | dc.DrawEllipse(((x - radius), (y - radius)), (self.Diameter, self.Diameter)) | |
454 | ||
455 | ||
456 | ||
457 | class Dot(draw_object): | |
458 | """ | |
459 | The Dot class takes an x.y coordinate pair, and the Diameter of the circle. | |
460 | The Diameter is in pixels, so it won't change with zoom. | |
461 | ||
462 | Also Fill and line data | |
463 | ||
464 | """ | |
465 | def __init__(self,x,y,Diameter,LineColor,LineStyle,LineWidth,FillColor,FillStyle,Foreground = 0): | |
466 | draw_object.__init__(self,Foreground) | |
467 | ||
468 | self.X = x | |
469 | self.Y = y | |
470 | self.Diameter = Diameter | |
471 | # NOTE: the bounding box does not include the diameter of the dot, as that is in pixel coords. | |
472 | # If this is a problem, perhaps you should use a circle, instead! | |
473 | self.BoundingBox = array(((x,y),(x,y)),Float) | |
474 | ||
475 | self.LineColor = LineColor | |
476 | self.LineStyle = LineStyle | |
477 | self.LineWidth = LineWidth | |
478 | self.FillColor = FillColor | |
479 | self.FillStyle = FillStyle | |
480 | ||
481 | self.SetPen(LineColor,LineStyle,LineWidth) | |
482 | self.SetBrush(FillColor,FillStyle) | |
483 | ||
484 | def _Draw(self,dc,WorldToPixel,ScaleFunction): | |
485 | dc.SetPen(self.Pen) | |
486 | dc.SetBrush(self.Brush) | |
487 | radius = int(round(self.Diameter/2)) | |
488 | (X,Y) = WorldToPixel((self.X,self.Y)) | |
489 | dc.DrawEllipse(((X - radius), (Y - radius)), (self.Diameter, self.Diameter)) | |
490 | ||
491 | ||
492 | class Rectangle(draw_object): | |
493 | def __init__(self,x,y,width,height,LineColor,LineStyle,LineWidth,FillColor,FillStyle,Foreground = 0): | |
494 | draw_object.__init__(self,Foreground) | |
495 | ||
496 | self.X = x | |
497 | self.Y = y | |
498 | self.Width = width | |
499 | self.Height = height | |
500 | self.BoundingBox = array(((x,y),(x+width,y+height)),Float) | |
501 | self.LineColor = LineColor | |
502 | self.LineStyle = LineStyle | |
503 | self.LineWidth = LineWidth | |
504 | self.FillColor = FillColor | |
505 | self.FillStyle = FillStyle | |
506 | ||
507 | self.SetPen(LineColor,LineStyle,LineWidth) | |
508 | self.SetBrush(FillColor,FillStyle) | |
509 | ||
510 | def _Draw(self,dc,WorldToPixel,ScaleFunction): | |
511 | (X,Y) = WorldToPixel((self.X,self.Y)) | |
512 | (Width,Height) = ScaleFunction((self.Width,self.Height)) | |
513 | ||
514 | dc.SetPen(self.Pen) | |
515 | dc.SetBrush(self.Brush) | |
516 | dc.DrawRectangle((X,Y), (Width,Height)) | |
517 | ||
518 | class Ellipse(draw_object): | |
519 | def __init__(self,x,y,width,height,LineColor,LineStyle,LineWidth,FillColor,FillStyle,Foreground = 0): | |
520 | draw_object.__init__(self,Foreground) | |
521 | ||
522 | self.X = x | |
523 | self.Y = y | |
524 | self.Width = width | |
525 | self.Height = height | |
526 | self.BoundingBox = array(((x,y),(x+width,y+height)),Float) | |
527 | self.LineColor = LineColor | |
528 | self.LineStyle = LineStyle | |
529 | self.LineWidth = LineWidth | |
530 | self.FillColor = FillColor | |
531 | self.FillStyle = FillStyle | |
532 | ||
533 | self.SetPen(LineColor,LineStyle,LineWidth) | |
534 | self.SetBrush(FillColor,FillStyle) | |
535 | ||
536 | def _Draw(self,dc,WorldToPixel,ScaleFunction): | |
537 | (X,Y) = WorldToPixel((self.X,self.Y)) | |
538 | (Width,Height) = ScaleFunction((self.Width,self.Height)) | |
539 | ||
540 | dc.SetPen(self.Pen) | |
541 | dc.SetBrush(self.Brush) | |
542 | dc.DrawEllipse((X,Y), (Width,Height)) | |
543 | ||
544 | class Circle(draw_object): | |
545 | def __init__(self,x,y,Diameter,LineColor,LineStyle,LineWidth,FillColor,FillStyle,Foreground = 0): | |
546 | draw_object.__init__(self,Foreground) | |
547 | ||
548 | self.X = x | |
549 | self.Y = y | |
550 | self.Diameter = Diameter | |
551 | self.BoundingBox = array(((x-Diameter/2,y-Diameter/2),(x+Diameter/2,y+Diameter/2)),Float) | |
552 | self.LineColor = LineColor | |
553 | self.LineStyle = LineStyle | |
554 | self.LineWidth = LineWidth | |
555 | self.FillColor = FillColor | |
556 | self.FillStyle = FillStyle | |
557 | ||
558 | self.SetPen(LineColor,LineStyle,LineWidth) | |
559 | self.SetBrush(FillColor,FillStyle) | |
560 | ||
561 | def _Draw(self,dc,WorldToPixel,ScaleFunction): | |
562 | (X,Y) = WorldToPixel((self.X,self.Y)) | |
563 | (Diameter,dummy) = ScaleFunction((self.Diameter,self.Diameter)) | |
564 | ||
565 | dc.SetPen(self.Pen) | |
566 | dc.SetBrush(self.Brush) | |
567 | dc.DrawEllipse((X-Diameter/2,Y-Diameter/2), (Diameter,Diameter)) | |
568 | ||
569 | class Text(draw_object): | |
570 | """ | |
571 | ||
572 | This class creates a text object, placed at the coordinates, | |
573 | x,y. the "Position" argument is a two charactor string, indicating | |
574 | where in relation to the coordinates the string should be oriented. | |
575 | ||
576 | The first letter is: t, c, or b, for top, center and bottom | |
577 | The second letter is: l, c, or r, for left, center and right | |
578 | ||
579 | I've provided arguments for Family, Style, and Weight of font, but | |
580 | have not yet implimented them, so all text is: wx.SWISS, wx.NORMAL, wx.NORMAL. | |
581 | I'd love it if someone would impliment that! | |
582 | ||
583 | The size is fixed, and does not scale with the drawing. | |
584 | ||
585 | """ | |
586 | ||
587 | def __init__(self,String,x,y,Size,ForeGround,BackGround,Family,Style,Weight,Underline,Position,Foreground = 0): | |
588 | draw_object.__init__(self,Foreground) | |
589 | ||
590 | self.String = String | |
591 | self.Size = Size | |
592 | ||
593 | self.ForeGround = ForeGround | |
594 | if BackGround is None: | |
595 | self.BackGround = None | |
596 | else: | |
597 | self.BackGround = BackGround | |
598 | self.Family = Family | |
599 | self.Style = Style | |
600 | self.Weight = Weight | |
601 | self.Underline = Underline | |
602 | self.Position = Position | |
603 | # fixme: this should use the passed in parameters! | |
604 | self.Font = wx.Font(Size, wx.MODERN, wx.NORMAL, wx.NORMAL, Underline, ) | |
605 | ||
606 | self.BoundingBox = array(((x,y),(x,y)),Float) | |
607 | ||
608 | self.X = x | |
609 | self.Y = y | |
610 | self.x_shift = None | |
611 | self.y_shift = None | |
612 | ||
613 | def _Draw(self,dc,WorldToPixel,ScaleFunction): | |
614 | (X,Y) = WorldToPixel((self.X,self.Y)) | |
615 | dc.SetFont(self.Font) | |
616 | dc.SetTextForeground(self.ForeGround) | |
617 | if self.BackGround: | |
618 | dc.SetBackgroundMode(wx.SOLID) | |
619 | dc.SetTextBackground(self.BackGround) | |
620 | else: | |
621 | dc.SetBackgroundMode(wx.TRANSPARENT) | |
622 | ||
623 | # compute the shift, and adjust the coordinates, if neccesary | |
624 | # This had to be put in here, becsuse there is no wx.DC during __init__ | |
625 | if self.x_shift is None or self.y_shift is None: | |
626 | if self.Position == 'tl': | |
627 | x_shift,y_shift = 0,0 | |
628 | else: | |
629 | (w,h) = dc.GetTextExtent(self.String) | |
630 | if self.Position[0] == 't': | |
631 | y_shift = 0 | |
632 | elif self.Position[0] == 'c': | |
633 | y_shift = h/2 | |
634 | elif self.Position[0] == 'b': | |
635 | y_shift = h | |
636 | else: | |
637 | ##fixme: this should be a real derived exception | |
638 | raise "Invalid value for Text Object Position Attribute" | |
639 | if self.Position[1] == 'l': | |
640 | x_shift = 0 | |
641 | elif self.Position[1] == 'c': | |
642 | x_shift = w/2 | |
643 | elif self.Position[1] == 'r': | |
644 | x_shift = w | |
645 | else: | |
646 | ##fixme: this should be a real derived exception | |
647 | raise "Invalid value for Text Object Position Attribute" | |
648 | self.x_shift = x_shift | |
649 | self.y_shift = y_shift | |
650 | dc.DrawText(self.String, (X-self.x_shift, Y-self.y_shift)) | |
651 | ||
652 | ||
653 | #--------------------------------------------------------------------------- | |
654 | ||
655 | class FloatCanvas(wx.Panel): | |
656 | """ | |
657 | FloatCanvas.py | |
658 | ||
659 | This is a high level window for drawing maps and anything else in an | |
660 | arbitrary coordinate system. | |
661 | ||
662 | The goal is to provide a convenient way to draw stuff on the screen | |
663 | without having to deal with handling OnPaint events, converting to pixel | |
664 | coordinates, knowing about wxWindows brushes, pens, and colors, etc. It | |
665 | also provides virtually unlimited zooming and scrolling | |
666 | ||
667 | I am using it for two things: | |
668 | 1) general purpose drawing in floating point coordinates | |
669 | 2) displaying map data in Lat-long coordinates | |
670 | ||
671 | If the projection is set to None, it will draw in general purpose | |
672 | floating point coordinates. If the projection is set to 'FlatEarth', it | |
673 | will draw a FlatEarth projection, centered on the part of the map that | |
674 | you are viewing. You can also pass in your own projection function. | |
675 | ||
676 | It is double buffered, so re-draws after the window is uncovered by something | |
677 | else are very quick. | |
678 | ||
679 | It relies on NumPy, which is needed for speed | |
680 | ||
681 | Bugs and Limitations: | |
682 | Lots: patches, fixes welcome | |
683 | ||
684 | For Map drawing: It ignores the fact that the world is, in fact, a | |
685 | sphere, so it will do strange things if you are looking at stuff near | |
686 | the poles or the date line. so far I don't have a need to do that, so I | |
687 | havn't bothered to add any checks for that yet. | |
688 | ||
689 | Zooming: | |
690 | I have set no zoom limits. What this means is that if you zoom in really | |
691 | far, you can get integer overflows, and get wierd results. It | |
692 | doesn't seem to actually cause any problems other than wierd output, at | |
693 | least when I have run it. | |
694 | ||
695 | Speed: | |
696 | I have done a couple of things to improve speed in this app. The one | |
697 | thing I have done is used NumPy Arrays to store the coordinates of the | |
698 | points of the objects. This allowed me to use array oriented functions | |
699 | when doing transformations, and should provide some speed improvement | |
700 | for objects with a lot of points (big polygons, polylines, pointsets). | |
701 | ||
702 | The real slowdown comes when you have to draw a lot of objects, because | |
703 | you have to call the wx.DC.DrawSomething call each time. This is plenty | |
704 | fast for tens of objects, OK for hundreds of objects, but pretty darn | |
705 | slow for thousands of objects. | |
706 | ||
707 | The solution is to be able to pass some sort of object set to the DC | |
708 | directly. I've used DC.DrawPointList(Points), and it helped a lot with | |
709 | drawing lots of points. I havn't got a LineSet type object, so I havn't | |
710 | used DC.DrawLineList yet. I'd like to get a full set of DrawStuffList() | |
711 | methods implimented, and then I'd also have a full set of Object sets | |
712 | that could take advantage of them. I hope to get to it some day. | |
713 | ||
714 | Copyright: Christopher Barker | |
715 | ||
716 | License: Same as wxPython | |
717 | ||
718 | Please let me know if you're using this!!! | |
719 | ||
720 | Contact me at: | |
721 | ||
722 | Chris.Barker@noaa.gov | |
723 | ||
724 | """ | |
725 | ||
726 | def __init__(self, parent, id = -1, | |
727 | size = wx.DefaultSize, | |
728 | ProjectionFun = None, | |
729 | BackgroundColor = "WHITE", | |
730 | Debug = 0, | |
731 | EnclosingFrame = None, | |
732 | UseToolbar = 1, | |
733 | UseBackground = 0, | |
734 | UseHitTest = 0): | |
735 | ||
736 | wx.Panel.__init__( self, parent, id, wx.DefaultPosition, size) | |
737 | ||
738 | if ProjectionFun == 'FlatEarth': | |
739 | self.ProjectionFun = self.FlatEarthProjection | |
740 | elif type(ProjectionFun) == types.FunctionType: | |
741 | self.ProjectionFun = ProjectionFun | |
742 | elif ProjectionFun is None: | |
743 | self.ProjectionFun = lambda x=None: array( (1,1), Float) | |
744 | else: | |
745 | raise('Projectionfun must be either: "FlatEarth", None, or a function that takes the ViewPortCenter and returns a MapProjectionVector') | |
746 | ||
747 | self.UseBackground = UseBackground | |
748 | self.UseHitTest = UseHitTest | |
749 | ||
750 | self.NumBetweenBlits = 40 | |
751 | ||
752 | ## you can have a toolbar with buttons for zoom-in, zoom-out and | |
753 | ## move. If you don't use the toolbar, you should provide your | |
754 | ## own way of navigating the canvas | |
755 | if UseToolbar: | |
756 | ## Create the vertical sizer for the toolbar and Panel | |
757 | box = wx.BoxSizer(wx.VERTICAL) | |
758 | box.Add(self.BuildToolbar(), 0, wx.ALL | wx.ALIGN_LEFT | wx.GROW, 4) | |
759 | ||
760 | self.DrawPanel = wx.Window(self,-1,wx.DefaultPosition,wx.DefaultSize,wx.SUNKEN_BORDER) | |
761 | box.Add(self.DrawPanel,1,wx.GROW) | |
762 | ||
763 | box.Fit(self) | |
764 | self.SetAutoLayout(True) | |
765 | self.SetSizer(box) | |
766 | else: | |
767 | self.DrawPanel = self | |
768 | ||
769 | self.DrawPanel.BackgroundBrush = wx.Brush(BackgroundColor,wx.SOLID) | |
770 | ||
771 | self.Debug = Debug | |
772 | ||
773 | self.EnclosingFrame = EnclosingFrame | |
774 | ||
775 | wx.EVT_PAINT(self.DrawPanel, self.OnPaint) | |
776 | wx.EVT_SIZE(self.DrawPanel, self.OnSize) | |
777 | ||
778 | wx.EVT_LEFT_DOWN(self.DrawPanel, self.LeftButtonEvent) | |
779 | wx.EVT_LEFT_UP(self.DrawPanel, self.LeftButtonEvent) | |
780 | wx.EVT_RIGHT_DOWN(self.DrawPanel, self.RightButtonEvent) | |
781 | wx.EVT_MOTION(self.DrawPanel, self.LeftButtonEvent) | |
782 | ||
783 | ||
784 | self._DrawList = [] | |
785 | if self.UseBackground: | |
786 | self._TopDrawList = [] | |
787 | self.BoundingBox = None | |
788 | self.BoundingBoxDirty = 0 | |
789 | self.ViewPortCenter= array( (0,0), Float) | |
790 | ||
791 | self.MapProjectionVector = array( (1,1), Float) # No Projection to start! | |
792 | self.TransformVector = array( (1,-1), Float) # default Transformation | |
793 | ||
794 | self.Scale = 1 | |
795 | ||
796 | self.GUIMode = None | |
797 | self.StartRBBox = None | |
798 | self.PrevRBBox = None | |
799 | self.StartMove = None | |
800 | self.PrevMoveBox = None | |
801 | # called just to make sure everything is initialized | |
802 | self.OnSize(None) | |
803 | ||
804 | ||
805 | def BuildToolbar(self): | |
806 | tb = wx.ToolBar(self,-1) | |
807 | self.ToolBar = tb | |
808 | ||
809 | tb.SetToolBitmapSize((23,23)) | |
810 | ||
811 | tb.AddTool(ID_ZOOM_IN_BUTTON, GetPlusBitmap(), isToggle=True,shortHelpString = "Zoom In") | |
812 | wx.EVT_TOOL(self, ID_ZOOM_IN_BUTTON, self.SetMode) | |
813 | ||
814 | tb.AddTool(ID_ZOOM_OUT_BUTTON, GetMinusBitmap(), isToggle=True,shortHelpString = "Zoom Out") | |
815 | wx.EVT_TOOL(self, ID_ZOOM_OUT_BUTTON, self.SetMode) | |
816 | ||
817 | tb.AddTool(ID_MOVE_MODE_BUTTON, GetHandBitmap(), isToggle=True,shortHelpString = "Move") | |
818 | wx.EVT_TOOL(self, ID_MOVE_MODE_BUTTON, self.SetMode) | |
819 | ||
820 | tb.AddSeparator() | |
821 | ||
822 | tb.AddControl(wx.Button(tb, ID_ZOOM_TO_FIT_BUTTON, "Zoom To Fit",wx.DefaultPosition, wx.DefaultSize)) | |
823 | wx.EVT_BUTTON(self, ID_ZOOM_TO_FIT_BUTTON, self.ZoomToFit) | |
824 | ||
825 | tb.Realize() | |
826 | return tb | |
827 | ||
828 | def SetMode(self,event): | |
829 | for id in [ID_ZOOM_IN_BUTTON,ID_ZOOM_OUT_BUTTON,ID_MOVE_MODE_BUTTON]: | |
830 | self.ToolBar.ToggleTool(id,0) | |
831 | self.ToolBar.ToggleTool(event.GetId(),1) | |
832 | if event.GetId() == ID_ZOOM_IN_BUTTON: | |
833 | self.SetGUIMode("ZoomIn") | |
834 | elif event.GetId() == ID_ZOOM_OUT_BUTTON: | |
835 | self.SetGUIMode("ZoomOut") | |
836 | elif event.GetId() == ID_MOVE_MODE_BUTTON: | |
837 | self.SetGUIMode("Move") | |
838 | ||
839 | ||
840 | def SetGUIMode(self,Mode): | |
841 | if Mode in ["ZoomIn","ZoomOut","Move",None]: | |
842 | self.GUIMode = Mode | |
843 | else: | |
844 | raise "Not a valid Mode" | |
845 | ||
846 | def FlatEarthProjection(self,CenterPoint): | |
847 | return array((cos(pi*CenterPoint[1]/180),1),Float) | |
848 | ||
849 | def LeftButtonEvent(self,event): | |
850 | if self.EnclosingFrame: | |
851 | if event.Moving: | |
852 | position = self.PixelToWorld((event.GetX(),event.GetY())) | |
853 | self.EnclosingFrame.SetStatusText("%8.3f, %8.3f"%tuple(position)) | |
854 | if self.GUIMode: | |
855 | if self.GUIMode == "ZoomIn": | |
856 | if event.LeftDown(): | |
857 | self.StartRBBox = (event.GetX(),event.GetY()) | |
858 | self.PrevRBBox = None | |
859 | elif event.Dragging() and event.LeftIsDown() and self.StartRBBox: | |
860 | x0,y0 = self.StartRBBox | |
861 | x1,y1 = event.GetX(),event.GetY() | |
862 | w, h = abs(x1-x0),abs(y1-y0) | |
863 | w = max(w,int(h*self.AspectRatio)) | |
864 | h = int(w/self.AspectRatio) | |
865 | x_c, y_c = (x0+x1)/2 , (y0+y1)/2 | |
866 | dc = wx.ClientDC(self.DrawPanel) | |
867 | dc.BeginDrawing() | |
868 | dc.SetPen(wx.Pen('WHITE', 2,wx.SHORT_DASH)) | |
869 | dc.SetBrush(wx.TRANSPARENT_BRUSH) | |
870 | dc.SetLogicalFunction(wx.XOR) | |
871 | if self.PrevRBBox: | |
872 | dc.DrawRectangleXY(*self.PrevRBBox) | |
873 | dc.DrawRectangleXY(x_c-w/2,y_c-h/2,w,h) | |
874 | self.PrevRBBox = (x_c-w/2,y_c-h/2,w,h) | |
875 | dc.EndDrawing() | |
876 | ||
877 | elif event.LeftUp() and self.StartRBBox : | |
878 | self.PrevRBBox = None | |
879 | EndRBBox = (event.GetX(),event.GetY()) | |
880 | StartRBBox = self.StartRBBox | |
881 | # if mouse has moved less that ten pixels, don't use the box. | |
882 | if abs(StartRBBox[0] - EndRBBox[0]) > 10 and abs(StartRBBox[1] - EndRBBox[1]) > 10: | |
883 | EndRBBox = self.PixelToWorld(EndRBBox) | |
884 | StartRBBox = self.PixelToWorld(StartRBBox) | |
885 | BB = array(((min(EndRBBox[0],StartRBBox[0]), min(EndRBBox[1],StartRBBox[1])), | |
886 | (max(EndRBBox[0],StartRBBox[0]), max(EndRBBox[1],StartRBBox[1]))),Float) | |
887 | self.ZoomToBB(BB) | |
888 | else: | |
889 | Center = self.PixelToWorld(StartRBBox) | |
890 | self.Zoom(1.5,Center) | |
891 | self.StartRBBox = None | |
892 | ||
893 | if self.GUIMode == "ZoomOut": | |
894 | if event.LeftDown(): | |
895 | Center = self.PixelToWorld((event.GetX(),event.GetY())) | |
896 | self.Zoom(1/1.5,Center) | |
897 | elif self.GUIMode == "Move": | |
898 | if event.LeftDown(): | |
899 | self.StartMove = array((event.GetX(),event.GetY())) | |
900 | self.PrevMoveBox = None | |
901 | elif event.Dragging() and event.LeftIsDown() and self.StartMove: | |
902 | x_1,y_1 = event.GetX(),event.GetY() | |
903 | w, h = self.PanelSize | |
904 | x_tl, y_tl = x_1 - self.StartMove[0], y_1 - self.StartMove[1] | |
905 | dc = wx.ClientDC(self.DrawPanel) | |
906 | dc.BeginDrawing() | |
907 | dc.SetPen(wx.Pen('WHITE', 1,)) | |
908 | dc.SetBrush(wx.TRANSPARENT_BRUSH) | |
909 | dc.SetLogicalFunction(wx.XOR) | |
910 | if self.PrevMoveBox: | |
911 | dc.DrawRectangleXY(*self.PrevMoveBox) | |
912 | dc.DrawRectangleXY(x_tl,y_tl,w,h) | |
913 | self.PrevMoveBox = (x_tl,y_tl,w,h) | |
914 | dc.EndDrawing() | |
915 | ||
916 | elif event.LeftUp() and self.StartMove: | |
917 | self.PrevMoveBox = None | |
918 | StartMove = self.StartMove | |
919 | EndMove = array((event.GetX(),event.GetY())) | |
920 | if sum((StartMove-EndMove)**2) > 16: | |
921 | self.Move(StartMove-EndMove,'Pixel') | |
922 | self.StartMove = None | |
923 | ||
924 | def RightButtonEvent(self,event): | |
925 | if self.GUIMode: | |
926 | if self.GUIMode == "ZoomIn": | |
927 | Center = self.PixelToWorld((event.GetX(),event.GetY())) | |
928 | self.Zoom(1/1.5,Center) | |
929 | elif self.GUIMode == "ZoomOut": | |
930 | Center = self.PixelToWorld((event.GetX(),event.GetY())) | |
931 | self.Zoom(1.5,Center) | |
932 | else: | |
933 | event.Skip() | |
934 | event.Skip() | |
935 | ||
936 | def MakeNewBuffers(self): | |
937 | # Make new offscreen bitmap: | |
938 | self._Buffer = wx.EmptyBitmap(self.PanelSize[0],self.PanelSize[1]) | |
939 | if self.UseBackground: | |
940 | self._BackBuffer = wx.EmptyBitmap(self.PanelSize[0],self.PanelSize[1]) | |
941 | self._BackgroundDirty = 1 | |
942 | else: | |
943 | pass | |
944 | ||
945 | def OnSize(self,event): | |
946 | self.PanelSize = array(self.DrawPanel.GetClientSizeTuple(),Int32) | |
947 | try: | |
948 | self.AspectRatio = self.PanelSize[0]/self.PanelSize[1] | |
949 | except ZeroDivisionError: | |
950 | self.AspectRatio = 1.0 | |
951 | self.MakeNewBuffers() | |
952 | self.Draw() | |
953 | ||
954 | def OnPaint(self, event): | |
955 | #dc = wx.BufferedPaintDC(self.DrawPanel, self._Buffer) | |
956 | dc = wx.PaintDC(self.DrawPanel) | |
957 | dc.DrawBitmap(self._Buffer, (0,0)) | |
958 | ||
959 | def Draw(self): | |
960 | """ | |
961 | The Draw method gets pretty complicated because of all the buffers | |
962 | ||
963 | There is a main buffer set up to double buffer the screen, so | |
964 | you can get quick re-draws when the window gets uncovered. | |
965 | ||
966 | If self.UseBackground is set, and an object is set up with the | |
967 | "ForeGround" flag, then it gets drawn to the screen after blitting | |
968 | the background. This is done so that you can have a complicated | |
969 | background, but have something changing on the foreground, | |
970 | without having to wait for the background to get re-drawn. This | |
971 | can be used to support simple animation, for instance. | |
972 | ||
973 | """ | |
974 | if self.Debug: start = clock() | |
975 | ScreenDC = wx.ClientDC(self.DrawPanel) | |
976 | ViewPortWorld = ( self.PixelToWorld((0,0)), self.PixelToWorld(self.PanelSize) ) | |
977 | ViewPortBB = array( ( minimum.reduce(ViewPortWorld), maximum.reduce(ViewPortWorld) ) ) | |
978 | if self.UseBackground: | |
979 | dc = wx.MemoryDC() | |
980 | dc.SelectObject(self._BackBuffer) | |
981 | dc.SetBackground(self.DrawPanel.BackgroundBrush) | |
982 | if self._DrawList: | |
983 | if self._BackgroundDirty: | |
984 | dc.BeginDrawing() | |
985 | dc.Clear() | |
986 | i = 0 | |
987 | for Object in self._DrawList: | |
988 | if self.BBCheck(Object.BoundingBox,ViewPortBB): | |
989 | #print "object is in Bounding Box" | |
990 | i+=1 | |
991 | Object._Draw(dc,self.WorldToPixel,self.ScaleFunction) | |
992 | if i % self.NumBetweenBlits == 0: | |
993 | ScreenDC.Blit((0, 0), self.PanelSize, dc, (0, 0)) | |
994 | dc.EndDrawing() | |
995 | else: | |
996 | dc.Clear() | |
997 | self._BackgroundDirty = 0 | |
998 | dc.SelectObject(self._Buffer) | |
999 | dc.BeginDrawing() | |
1000 | ##Draw Background on Main Buffer: | |
1001 | dc.DrawBitmap(self._BackBuffer,0,0) | |
1002 | #Draw the OnTop stuff | |
1003 | i = 0 | |
1004 | for Object in self._TopDrawList: | |
1005 | i+=1 | |
1006 | Object._Draw(dc,self.WorldToPixel,self.ScaleFunction) | |
1007 | if i % self.NumBetweenBlits == 0: | |
1008 | ScreenDC.Blit((0, 0), self.PanelSize, dc, (0, 0)) | |
1009 | dc.EndDrawing() | |
1010 | else: # not using a Background DC | |
1011 | dc = wx.MemoryDC() | |
1012 | dc.SelectObject(self._Buffer) | |
1013 | dc.SetBackground(self.DrawPanel.BackgroundBrush) | |
1014 | if self._DrawList: | |
1015 | dc.BeginDrawing() | |
1016 | dc.Clear() | |
1017 | i = 0 | |
1018 | for Object in self._DrawList: | |
1019 | if self.BBCheck(Object.BoundingBox,ViewPortBB): | |
1020 | #print "object is in Bounding Box" | |
1021 | i+=1 | |
1022 | Object._Draw(dc,self.WorldToPixel,self.ScaleFunction) | |
1023 | if i % self.NumBetweenBlits == 0: | |
1024 | ScreenDC.Blit((0, 0), self.PanelSize, dc, (0, 0)) | |
1025 | dc.EndDrawing() | |
1026 | else: | |
1027 | dc.Clear() | |
1028 | # now refresh the screen | |
1029 | #ScreenDC.DrawBitmap(self._Buffer,0,0) #NOTE: uisng DrawBitmap didn't work right on MSW | |
1030 | ScreenDC.Blit((0, 0), self.PanelSize, dc, (0, 0)) | |
1031 | ||
1032 | # If the canvas is in the middle of a zoom or move, the Rubber Band box needs to be re-drawn | |
1033 | if self.PrevRBBox: | |
1034 | ScreenDC.SetPen(wx.Pen('WHITE', 2,wx.SHORT_DASH)) | |
1035 | ScreenDC.SetBrush(wx.TRANSPARENT_BRUSH) | |
1036 | ScreenDC.SetLogicalFunction(wx.XOR) | |
1037 | ScreenDC.DrawRectangleXY(*self.PrevRBBox) | |
1038 | elif self.PrevMoveBox: | |
1039 | ScreenDC.SetPen(wx.Pen('WHITE', 1,)) | |
1040 | ScreenDC.SetBrush(wx.TRANSPARENT_BRUSH) | |
1041 | ScreenDC.SetLogicalFunction(wx.XOR) | |
1042 | ScreenDC.DrawRectangleXY(*self.PrevMoveBox) | |
1043 | if self.Debug: print "Drawing took %f seconds of CPU time"%(clock()-start) | |
1044 | ||
1045 | def BBCheck(self, BB1, BB2): | |
1046 | """ | |
1047 | ||
1048 | BBCheck(BB1, BB2) returns True is the Bounding boxes intesect, False otherwise | |
1049 | ||
1050 | """ | |
1051 | if ( (BB1[1,0] > BB2[0,0]) and (BB1[0,0] < BB2[1,0]) and | |
1052 | (BB1[1,1] > BB2[0,1]) and (BB1[0,1] < BB2[1,1]) ): | |
1053 | return True | |
1054 | else: | |
1055 | return False | |
1056 | ||
1057 | def Move(self,shift,CoordType): | |
1058 | """ | |
1059 | move the image in the window. | |
1060 | ||
1061 | shift is an (x,y) tuple, specifying the amount to shift in each direction | |
1062 | ||
1063 | It can be in any of three coordinates: Panel, Pixel, World, | |
1064 | specified by the CoordType parameter | |
1065 | ||
1066 | Panel coordinates means you want to shift the image by some | |
1067 | fraction of the size of the displaed image | |
1068 | ||
1069 | Pixel coordinates means you want to shift the image by some number of pixels | |
1070 | ||
1071 | World coordinates meand you want to shift the image by an amount | |
1072 | in Floating point world coordinates | |
1073 | ||
1074 | """ | |
1075 | ||
1076 | shift = array(shift,Float) | |
1077 | if CoordType == 'Panel':# convert from panel coordinates | |
1078 | shift = shift * array((-1,1),Float) *self.PanelSize/self.TransformVector | |
1079 | elif CoordType == 'Pixel': # convert from pixel coordinates | |
1080 | shift = shift/self.TransformVector | |
1081 | elif CoordType == 'World': # No conversion | |
1082 | pass | |
1083 | else: | |
1084 | raise 'CoordType must be either "Panel", "Pixel", or "World"' | |
1085 | ||
1086 | self.ViewPortCenter = self.ViewPortCenter + shift | |
1087 | self.MapProjectionVector = self.ProjectionFun(self.ViewPortCenter) | |
1088 | self.TransformVector = array((self.Scale,-self.Scale),Float)* self.MapProjectionVector | |
1089 | self._BackgroundDirty = 1 | |
1090 | self.Draw() | |
1091 | ||
1092 | def Zoom(self,factor,center = None): | |
1093 | ||
1094 | """ | |
1095 | Zoom(factor, center) changes the amount of zoom of the image by factor. | |
1096 | If factor is greater than one, the image gets larger. | |
1097 | If factor is less than one, the image gets smaller. | |
1098 | ||
1099 | Center is a tuple of (x,y) coordinates of the center of the viewport, after zooming. | |
1100 | If center is not given, the center will stay the same. | |
1101 | ||
1102 | """ | |
1103 | self.Scale = self.Scale*factor | |
1104 | if center: | |
1105 | self.ViewPortCenter = array(center,Float) | |
1106 | self.MapProjectionVector = self.ProjectionFun(self.ViewPortCenter) | |
1107 | self.TransformVector = array((self.Scale,-self.Scale),Float)* self.MapProjectionVector | |
1108 | self._BackgroundDirty = 1 | |
1109 | self.Draw() | |
1110 | ||
1111 | def ZoomToFit(self,event): | |
1112 | self.ZoomToBB() | |
1113 | ||
1114 | def ZoomToBB(self,NewBB = None,DrawFlag = 1): | |
1115 | ||
1116 | """ | |
1117 | ||
1118 | Zooms the image to the bounding box given, or to the bounding | |
1119 | box of all the objects on the canvas, if none is given. | |
1120 | ||
1121 | """ | |
1122 | ||
1123 | if NewBB: | |
1124 | BoundingBox = NewBB | |
1125 | else: | |
1126 | if self.BoundingBoxDirty: | |
1127 | self._ResetBoundingBox() | |
1128 | BoundingBox = self.BoundingBox | |
1129 | if BoundingBox: | |
1130 | self.ViewPortCenter = array(((BoundingBox[0,0]+BoundingBox[1,0])/2, | |
1131 | (BoundingBox[0,1]+BoundingBox[1,1])/2 ),Float) | |
1132 | self.MapProjectionVector = self.ProjectionFun(self.ViewPortCenter) | |
1133 | # Compute the new Scale | |
1134 | BoundingBox = BoundingBox * self.MapProjectionVector | |
1135 | try: | |
1136 | self.Scale = min((self.PanelSize[0] / (BoundingBox[1,0]-BoundingBox[0,0])), | |
1137 | (self.PanelSize[1] / (BoundingBox[1,1]-BoundingBox[0,1])))*0.95 | |
1138 | except ZeroDivisionError: # this will happen if the BB has zero width or height | |
1139 | try: #width | |
1140 | self.Scale = (self.PanelSize[0] / (BoundingBox[1,0]-BoundingBox[0,0]))*0.95 | |
1141 | except ZeroDivisionError: | |
1142 | try: # height | |
1143 | self.Scale = (self.PanelSize[1] / (BoundingBox[1,1]-BoundingBox[0,1]))*0.95 | |
1144 | except ZeroDivisionError: #zero size! (must be a single point) | |
1145 | self.Scale = 1 | |
1146 | ||
1147 | self.TransformVector = array((self.Scale,-self.Scale),Float)* self.MapProjectionVector | |
1148 | if DrawFlag: | |
1149 | self._BackgroundDirty = 1 | |
1150 | self.Draw() | |
1151 | ||
1152 | def RemoveObjects(self,Objects): | |
1153 | for Object in Objects: | |
1154 | self.RemoveObject(Object,ResetBB = 0) | |
1155 | self.BoundingBoxDirty = 1 | |
1156 | ||
1157 | def RemoveObject(self,Object,ResetBB = 1): | |
1158 | if Object.Foreground: | |
1159 | self._TopDrawList.remove(Object) | |
1160 | else: | |
1161 | self._DrawList.remove(Object) | |
1162 | self._BackgroundDirty = 1 | |
1163 | ||
1164 | if ResetBB: | |
1165 | self.BoundingBoxDirty = 1 | |
1166 | ||
1167 | def Clear(self, ResetBB = True): | |
1168 | self._DrawList = [] | |
1169 | if self.UseBackground: | |
1170 | self._TopDrawList = [] | |
1171 | self._BackgroundDirty = 1 | |
1172 | if ResetBB: | |
1173 | self._ResetBoundingBox() | |
1174 | ||
1175 | def _AddBoundingBox(self,NewBB): | |
1176 | if self.BoundingBox is None: | |
1177 | self.BoundingBox = NewBB | |
1178 | self.ZoomToBB(NewBB,DrawFlag = 0) | |
1179 | else: | |
1180 | self.BoundingBox = array(((min(self.BoundingBox[0,0],NewBB[0,0]), | |
1181 | min(self.BoundingBox[0,1],NewBB[0,1])), | |
1182 | (max(self.BoundingBox[1,0],NewBB[1,0]), | |
1183 | max(self.BoundingBox[1,1],NewBB[1,1]))),Float) | |
1184 | def _ResetBoundingBox(self): | |
1185 | # NOTE: could you remove an item without recomputing the entire bounding box? | |
1186 | self.BoundingBox = None | |
1187 | if self._DrawList: | |
1188 | self.BoundingBox = self._DrawList[0].BoundingBox | |
1189 | for Object in self._DrawList[1:]: | |
1190 | self._AddBoundingBox(Object.BoundingBox) | |
1191 | if self.UseBackground: | |
1192 | for Object in self._TopDrawList: | |
1193 | self._AddBoundingBox(Object.BoundingBox) | |
1194 | if self.BoundingBox is None: | |
1195 | self.ViewPortCenter= array( (0,0), Float) | |
1196 | self.TransformVector = array( (1,-1), Float) | |
1197 | self.MapProjectionVector = array( (1,1), Float) | |
1198 | self.Scale = 1 | |
1199 | self.BoundingBoxDirty = 0 | |
1200 | ||
1201 | def PixelToWorld(self,Points): | |
1202 | """ | |
1203 | Converts coordinates from Pixel coordinates to world coordinates. | |
1204 | ||
1205 | Points is a tuple of (x,y) coordinates, or a list of such tuples, or a NX2 Numpy array of x,y coordinates. | |
1206 | ||
1207 | """ | |
1208 | return (((array(Points,Float) - (self.PanelSize/2))/self.TransformVector) + self.ViewPortCenter) | |
1209 | ||
1210 | def WorldToPixel(self,Coordinates): | |
1211 | """ | |
1212 | This function will get passed to the drawing functions of the objects, | |
1213 | to transform from world to pixel coordinates. | |
1214 | Coordinates should be a NX2 array of (x,y) coordinates, or | |
1215 | a 2-tuple, or sequence of 2-tuples. | |
1216 | """ | |
1217 | return (((array(Coordinates,Float) - self.ViewPortCenter)*self.TransformVector)+(self.PanelSize/2)).astype('i') | |
1218 | ||
1219 | def ScaleFunction(self,Lengths): | |
1220 | """ | |
1221 | This function will get passed to the drawing functions of the objects, | |
1222 | to Change a length from world to pixel coordinates. | |
1223 | ||
1224 | Lengths should be a NX2 array of (x,y) coordinates, or | |
1225 | a 2-tuple, or sequence of 2-tuples. | |
1226 | """ | |
1227 | return (array(Lengths,Float)*self.TransformVector).astype('i') | |
1228 | ||
1229 | ||
1230 | ## This is a set of methods that add objects to the Canvas. It kind | |
1231 | ## of seems like a lot of duplication, but I wanted to be able to | |
1232 | ## instantiate the draw objects separatley form adding them, but | |
1233 | ## also to be able to do add one oin one step. I'm open to better | |
1234 | ## ideas... | |
1235 | def AddRectangle(self,x,y,width,height, | |
1236 | LineColor = "Black", | |
1237 | LineStyle = "Solid", | |
1238 | LineWidth = 1, | |
1239 | FillColor = None, | |
1240 | FillStyle = "Solid", | |
1241 | Foreground = 0): | |
1242 | Object = Rectangle(x,y,width,height,LineColor,LineStyle,LineWidth,FillColor,FillStyle,Foreground) | |
1243 | self.AddObject(Object) | |
1244 | return Object | |
1245 | ||
1246 | def AddEllipse(self,x,y,width,height, | |
1247 | LineColor = "Black", | |
1248 | LineStyle = "Solid", | |
1249 | LineWidth = 1, | |
1250 | FillColor = None, | |
1251 | FillStyle = "Solid", | |
1252 | Foreground = 0): | |
1253 | Object = Ellipse(x,y,width,height,LineColor,LineStyle,LineWidth,FillColor,FillStyle,Foreground) | |
1254 | self.AddObject(Object) | |
1255 | return Object | |
1256 | ||
1257 | def AddCircle(self,x,y,Diameter, | |
1258 | LineColor = "Black", | |
1259 | LineStyle = "Solid", | |
1260 | LineWidth = 1, | |
1261 | FillColor = None, | |
1262 | FillStyle = "Solid", | |
1263 | Foreground = 0): | |
1264 | Object = Circle(x,y,Diameter,LineColor,LineStyle,LineWidth,FillColor,FillStyle,Foreground) | |
1265 | self.AddObject(Object) | |
1266 | return Object | |
1267 | ||
1268 | def AddDot(self,x,y,Diameter, | |
1269 | LineColor = "Black", | |
1270 | LineStyle = "Solid", | |
1271 | LineWidth = 1, | |
1272 | FillColor = None, | |
1273 | FillStyle = "Solid", | |
1274 | Foreground = 0): | |
1275 | Object = Dot(x,y,Diameter,LineColor,LineStyle,LineWidth,FillColor,FillStyle,Foreground) | |
1276 | self.AddObject(Object) | |
1277 | return Object | |
1278 | ||
1279 | def AddPolygon(self,Points, | |
1280 | LineColor = "Black", | |
1281 | LineStyle = "Solid", | |
1282 | LineWidth = 1, | |
1283 | FillColor = None, | |
1284 | FillStyle = "Solid", | |
1285 | Foreground = 0): | |
1286 | ||
1287 | Object = Polygon(Points,LineColor,LineStyle,LineWidth,FillColor,FillStyle,Foreground) | |
1288 | self.AddObject(Object) | |
1289 | return Object | |
1290 | ||
1291 | def AddLine(self,Points, | |
1292 | LineColor = "Black", | |
1293 | LineStyle = "Solid", | |
1294 | LineWidth = 1, | |
1295 | Foreground = 0): | |
1296 | ||
1297 | Object = Line(Points,LineColor,LineStyle,LineWidth,Foreground) | |
1298 | self.AddObject(Object) | |
1299 | return Object | |
1300 | ||
1301 | def AddLineSet(self,Points, | |
1302 | LineColors = "Black", | |
1303 | LineStyles = "Solid", | |
1304 | LineWidths = 1, | |
1305 | Foreground = 0): | |
1306 | ||
1307 | Object = LineSet(Points,LineColors,LineStyles,LineWidths,Foreground) | |
1308 | self.AddObject(Object) | |
1309 | return Object | |
1310 | ||
1311 | def AddPointSet(self,Points, | |
1312 | Color = "Black", | |
1313 | Diameter = 1, | |
1314 | Foreground = 0): | |
1315 | ||
1316 | Object = PointSet(Points,Color,Diameter,Foreground) | |
1317 | self.AddObject(Object) | |
1318 | return Object | |
1319 | ||
1320 | def AddText(self,String,x,y, | |
1321 | Size = 20, | |
1322 | ForeGround = 'Black', | |
1323 | BackGround = None, | |
1324 | Family = 'Swiss', | |
1325 | Style = 'Normal', | |
1326 | Weight = 'Normal', | |
1327 | Underline = 0, | |
1328 | Position = 'tl', | |
1329 | Foreground = 0): | |
1330 | Object = Text(String,x,y,Size,ForeGround,BackGround,Family,Style,Weight,Underline,Position,Foreground) | |
1331 | self.AddObject(Object) | |
1332 | return Object | |
1333 | ||
1334 | def AddObject(self,obj): | |
1335 | # put in a reference to the Canvas, so remove and other stuff can work | |
1336 | obj._Canvas = self | |
1337 | if obj.Foreground and self.UseBackground: | |
1338 | self._TopDrawList.append(obj) | |
1339 | else: | |
1340 | self._DrawList.append(obj) | |
1341 | self._backgrounddirty = 1 | |
1342 | self._AddBoundingBox(obj.BoundingBox) | |
1343 | return None | |
1344 | ||
1345 | ||
1346 | ||
1347 | ||
1348 | ||
1349 | ||
1350 |