import wx from Numeric import array,Float,cos,pi,sum,minimum,maximum,Int32 from time import clock, sleep import types import os ID_ZOOM_IN_BUTTON = wx.NewId() ID_ZOOM_OUT_BUTTON = wx.NewId() ID_ZOOM_TO_FIT_BUTTON = wx.NewId() ID_MOVE_MODE_BUTTON = wx.NewId() ID_TEST_BUTTON = wx.NewId() ID_ABOUT_MENU = wx.NewId() ID_EXIT_MENU = wx.NewId() ID_ZOOM_TO_FIT_MENU = wx.NewId() ID_DRAWTEST_MENU = wx.NewId() ID_DRAWMAP_MENU = wx.NewId() ID_CLEAR_MENU = wx.NewId() ID_TEST = wx.NewId() ### These are some functions for bitmaps of icons. import cPickle, zlib def GetHandData(): return cPickle.loads(zlib.decompress( 'x\xda\xd3\xc8)0\xe4\nV72T\x00!\x05Cu\xae\xc4`u=\x85d\x05\xa7\x9c\xc4\xe4l0O\ \x01\xc8S\xb6t\x06A(\x1f\x0b\xa0\xa9\x8c\x9e\x1e6\x19\xa0\xa8\x1e\x88\xd4C\ \x97\xd1\x83\xe8\x80 \x9c2zh\xa6\xc1\x11X\n\xab\x8c\x02\x8a\x0cD!\x92\x12\ \x98\x8c\x1e\x8a\x8b\xd1d\x14\xf4\x90%\x90LC\xf6\xbf\x1e\xba\xab\x91%\xd0\ \xdc\x86C\x06\xd9m\xe8!\xaa\x87S\x86\x1a1\xa7\x07\x00v\x0f[\x17' )) def GetHandBitmap(): return wx.BitmapFromXPMData(GetHandData()) #---------------------------------------------------------------------- def GetPlusData(): return cPickle.loads(zlib.decompress( 'x\xda\xd3\xc8)0\xe4\nV72T\x00!\x05Cu\xae\xc4`u=\x85d\x05\xa7\x9c\xc4\xe4l0O\ \x01\xc8S\xb6t\x06A(\x1f\x0b RF\x0f\x08\xb0\xc9@D\xe1r\x08\x19\xb8j=l2`\r\ \xe82HF\xe9a\xc8\xe8\xe9A\x9c@\x8a\x0c\x0e\xd3p\xbb\x00\x8f\xab\xe1>\xd5\xd3\ \xc3\x15:P)l!\n\x91\xc2\x1a\xd6`)\xec\xb1\x00\x92\xc2\x11?\xb8e\x88\x8fSt\ \x19=\x00\x82\x16[\xf7' )) def GetPlusBitmap(): return wx.BitmapFromXPMData(GetPlusData()) #---------------------------------------------------------------------- def GetMinusData(): return cPickle.loads(zlib.decompress( 'x\xda\xd3\xc8)0\xe4\nV72T\x00!\x05Cu\xae\xc4`u=\x85d\x05\xa7\x9c\xc4\xe4l0O\ \x01\xc8S\xb6t\x06A(\x1f\x0b RF\x0f\x08\xb0\xc9@D\xe1r\x08\x19\xb8j=\xa2e\ \x10\x16@\x99\xc82zz\x10\'\x90"\x83\xc34r\xdc\x86\xf0\xa9\x9e\x1e\xae\xd0\ \x81Ja\x0bQ\x88\x14\xd6\xb0\x06Ka\x8f\x05\x90\x14\x8e\xf8\xc1-C|\x9c\xa2\xcb\ \xe8\x01\x00\xed\x0f[\x87' )) def GetMinusBitmap(): return wx.BitmapFromXPMData(GetMinusData()) ## This is a bunch of stuff for implimenting interactive use: catching ## when objects are clicked on by the mouse, etc. I've made a start, so if ## you are interesed in making that work, let me know, I may have gotten ## it going by then #### I probably want a full set of events someday: ## #### mouse over, right click, left click mouse up etc, etc. ## ##FLOATCANVAS_EVENT_LEFT_DOWN = wx.NewEventType() ## ##FLOATCANVAS_EVENT_LEFT_UP = wx.NewEventType() ## ##FLOATCANVAS_EVENT_RIGHT_DOWN = wx.NewEventType() ## ##FLOATCANVAS_EVENT_RIGHT_UP = wx.NewEventType() ## ##FLOATCANVAS_EVENT_MOUSE_OVER = wx.NewEventType() ##WXFLOATCANVASEVENT = wx.NewEventType() ##def EVT_WXFLOATCANVASEVENT( window, function ): ## """Your documentation here""" ## window.Connect( -1, -1, WXFLOATCANVASEVENT, function ) ##class wxFloatCanvasObjectEvent(wx.PyCommandEvent): ## def __init__(self, WindowID,Object): ## wx.PyCommandEvent.__init__(self, WXFLOATCANVASEVENT, WindowID) ## self.Object = Object ## def Clone( self ): ## self.__class__( self.GetId() ) ##class ColorGenerator: ## """ An instance of this class generates a unique color each time ## GetNextColor() is called. Someday I will use a proper Python ## generator for this class. ## The point of this generator is for the hit-test bitmap, each object ## needs to be a unique color. Also, each system can be running a ## different number of colors, and it doesn't appear to be possible to ## have a wxMemDC with a different colordepth as the screen so this ## generates colors far enough apart that they can be distinguished on ## a 16bit screen. Anything less than 16bits won't work. ## """ ## def __init__(self,depth = 16): ## self.r = 0 ## self.g = 0 ## self.b = 0 ## if depth == 16: ## self.step = 8 ## elif depth >= 24: ## self.step = 1 ## else: ## raise "ColorGenerator does not work with depth = %s"%depth ## def GetNextColor(self): ## step = self.step ## ##r,g,b = self.r,self.g,self.b ## self.r += step ## if self.r > 255: ## self.r = step ## self.g += step ## if self.g > 255: ## self.g = step ## self.b += step ## if self.b > 255: ## ## fixme: this should be a derived exception ## raise "Too many objects for HitTest" ## return (self.r,self.g,self.b) class draw_object: """ This is the base class for all the objects that can be drawn. each object has the following properties; (incomplete) BoundingBox : is of the form: array((min_x,min_y),(max_x,max_y)) Pen Brush """ def __init__(self,Foreground = 0): self.Foreground = Foreground self._Canvas = None # I pre-define all these as class variables to provide an easier # interface, and perhaps speed things up by caching all the Pens # and Brushes, although that may not help, as I think wx now # does that on it's own. Send me a note if you know! BrushList = { ( None,"Transparent") : wx.TRANSPARENT_BRUSH, ("Blue","Solid") : wx.BLUE_BRUSH, ("Green","Solid") : wx.GREEN_BRUSH, ("White","Solid") : wx.WHITE_BRUSH, ("Black","Solid") : wx.BLACK_BRUSH, ("Grey","Solid") : wx.GREY_BRUSH, ("MediumGrey","Solid") : wx.MEDIUM_GREY_BRUSH, ("LightGrey","Solid") : wx.LIGHT_GREY_BRUSH, ("Cyan","Solid") : wx.CYAN_BRUSH, ("Red","Solid") : wx.RED_BRUSH } PenList = { (None,"Transparent",1) : wx.TRANSPARENT_PEN, ("Green","Solid",1) : wx.GREEN_PEN, ("White","Solid",1) : wx.WHITE_PEN, ("Black","Solid",1) : wx.BLACK_PEN, ("Grey","Solid",1) : wx.GREY_PEN, ("MediumGrey","Solid",1) : wx.MEDIUM_GREY_PEN, ("LightGrey","Solid",1) : wx.LIGHT_GREY_PEN, ("Cyan","Solid",1) : wx.CYAN_PEN, ("Red","Solid",1) : wx.RED_PEN } FillStyleList = { "Transparent" : wx.TRANSPARENT, "Solid" : wx.SOLID, "BiDiagonalHatch": wx.BDIAGONAL_HATCH, "CrossDiagHatch" : wx.CROSSDIAG_HATCH, "FDiagonal_Hatch": wx.FDIAGONAL_HATCH, "CrossHatch" : wx.CROSS_HATCH, "HorizontalHatch": wx.HORIZONTAL_HATCH, "VerticalHatch" : wx.VERTICAL_HATCH } LineStyleList = { "Solid" : wx.SOLID, "Transparent": wx.TRANSPARENT, "Dot" : wx.DOT, "LongDash" : wx.LONG_DASH, "ShortDash" : wx.SHORT_DASH, "DotDash" : wx.DOT_DASH, } def SetBrush(self,FillColor,FillStyle): if FillColor is None or FillStyle is None: self.Brush = wx.TRANSPARENT_BRUSH self.FillStyle = "Transparent" else: if not self.BrushList.has_key((FillColor,FillStyle)): self.BrushList[(FillColor,FillStyle)] = wx.Brush(FillColor,self.FillStyleList[FillStyle]) self.Brush = self.BrushList[(FillColor,FillStyle)] def SetPen(self,LineColor,LineStyle,LineWidth): if (LineColor is None) or (LineStyle is None): self.Pen = wx.TRANSPARENT_PEN self.LineStyle = 'Transparent' else: if not self.PenList.has_key((LineColor,LineStyle,LineWidth)): self.PenList[(LineColor,LineStyle,LineWidth)] = wx.Pen(LineColor,LineWidth,self.LineStyleList[LineStyle]) self.Pen = self.PenList[(LineColor,LineStyle,LineWidth)] def SetPens(self,LineColors,LineStyles,LineWidths): """ This method used when an object could have a list of pens, rather than just one It is used for LineSet, and perhaps others in the future. fixme: this is really kludgy, there has got to be a better way! """ length = 1 if type(LineColors) == types.ListType: length = len(LineColors) else: LineColors = [LineColors] if type(LineStyles) == types.ListType: length = len(LineStyles) else: LineStyles = [LineStyles] if type(LineWidths) == types.ListType: length = len(LineWidths) else: LineWidths = [LineWidths] if length > 1: if len(LineColors) == 1: LineColors = LineColors*length if len(LineStyles) == 1: LineStyles = LineStyles*length if len(LineWidths) == 1: LineWidths = LineWidths*length self.Pens = [] for (LineColor,LineStyle,LineWidth) in zip(LineColors,LineStyles,LineWidths): if LineColor is None or LineStyle is None: self.Pens.append(wx.TRANSPARENT_PEN) # what's this for?> self.LineStyle = 'Transparent' if not self.PenList.has_key((LineColor,LineStyle,LineWidth)): Pen = wx.Pen(LineColor,LineWidth,self.LineStyleList[LineStyle]) self.Pens.append(Pen) else: self.Pens.append(self.PenList[(LineColor,LineStyle,LineWidth)]) if length == 1: self.Pens = self.Pens[0] def PutInBackground(self): if self._Canvas and self.Foreground: self._Canvas._TopDrawList.remove(self) self._Canvas._DrawList.append(self) self._Canvas._BackgroundDirty = 1 self.Foreground = 0 def PutInForeground(self): if self._Canvas and (not self.Foreground): self._Canvas._TopDrawList.append(self) self._Canvas._DrawList.remove(self) self._Canvas._BackgroundDirty = 1 self.Foreground = 1 class Polygon(draw_object): """ The Polygon class takes a list of 2-tuples, or a NX2 NumPy array of point coordinates. so that Points[N][0] is the x-coordinate of point N and Points[N][1] is the y-coordinate or Points[N,0] is the x-coordinate of point N and Points[N,1] is the y-coordinate for arrays. """ def __init__(self,Points,LineColor,LineStyle,LineWidth,FillColor,FillStyle,Foreground = 0): draw_object.__init__(self,Foreground) self.Points = array(Points,Float) self.BoundingBox = array(((min(self.Points[:,0]),min(self.Points[:,1])),(max(self.Points[:,0]),max(self.Points[:,1]))),Float) self.LineColor = LineColor self.LineStyle = LineStyle self.LineWidth = LineWidth self.FillColor = FillColor self.FillStyle = FillStyle self.SetPen(LineColor,LineStyle,LineWidth) self.SetBrush(FillColor,FillStyle) def _Draw(self,dc,WorldToPixel,ScaleFunction): Points = WorldToPixel(self.Points) dc.SetPen(self.Pen) dc.SetBrush(self.Brush) #dc.DrawPolygon(map(lambda x: (x[0],x[1]), Points.tolist())) dc.DrawPolygon(Points) class PolygonSet(draw_object): """ The PolygonSet class takes a Geometry.Polygon object. so that Points[N] = (x1,y1) and Points[N+1] = (x2,y2). N must be an even number! it creates a set of line segments, from (x1,y1) to (x2,y2) """ def __init__(self,PolySet,LineColors,LineStyles,LineWidths,FillColors,FillStyles,Foreground = 0): draw_object.__init__(self, Foreground) ##fixme: there should be some error checking for everything being the right length. self.Points = array(Points,Float) self.BoundingBox = array(((min(self.Points[:,0]),min(self.Points[:,1])),(max(self.Points[:,0]),max(self.Points[:,1]))),Float) self.LineColors = LineColors self.LineStyles = LineStyles self.LineWidths = LineWidths self.FillColors = FillColors self.FillStyles = FillStyles self.SetPens(LineColors,LineStyles,LineWidths) def _Draw(self,dc,WorldToPixel,ScaleFunction): Points = WorldToPixel(self.Points) Points.shape = (-1,4) dc.DrawLineList(Points,self.Pens) class Line(draw_object): """ The Line class takes a list of 2-tuples, or a NX2 NumPy array of point coordinates. so that Points[N][0] is the x-coordinate of point N and Points[N][1] is the y-coordinate or Points[N,0] is the x-coordinate of point N and Points[N,1] is the y-coordinate for arrays. It will draw a straight line if there are two points, and a polyline if there are more than two. """ def __init__(self,Points,LineColor,LineStyle,LineWidth,Foreground = 0): draw_object.__init__(self, Foreground) self.Points = array(Points,Float) self.BoundingBox = array(((min(self.Points[:,0]),min(self.Points[:,1])),(max(self.Points[:,0]),max(self.Points[:,1]))),Float) self.LineColor = LineColor self.LineStyle = LineStyle self.LineWidth = LineWidth self.SetPen(LineColor,LineStyle,LineWidth) def SetPoints(self,Points): self.Points = Points self.BoundingBox = array(((min(self.Points[:,0]),min(self.Points[:,1])),(max(self.Points[:,0]),max(self.Points[:,1]))),Float) if self._Canvas: # It looks like this shouldn't be private self._Canvas.BoundingBoxDirty = 1 def _Draw(self,dc,WorldToPixel,ScaleFunction): Points = WorldToPixel(self.Points) dc.SetPen(self.Pen) #dc.DrawLines(map(lambda x: (x[0],x[1]), Points.tolist())) dc.DrawLines(Points) class LineSet(draw_object): """ The LineSet class takes a list of 2-tuples, or a NX2 NumPy array of point coordinates. so that Points[N] = (x1,y1) and Points[N+1] = (x2,y2). N must be an even number! it creates a set of line segments, from (x1,y1) to (x2,y2) """ def __init__(self,Points,LineColors,LineStyles,LineWidths,Foreground = 0): draw_object.__init__(self, Foreground) NumLines = len(Points) / 2 ##fixme: there should be some error checking for everything being the right length. self.Points = array(Points,Float) self.BoundingBox = array(((min(self.Points[:,0]),min(self.Points[:,1])),(max(self.Points[:,0]),max(self.Points[:,1]))),Float) self.LineColors = LineColors self.LineStyles = LineStyles self.LineWidths = LineWidths self.SetPens(LineColors,LineStyles,LineWidths) def _Draw(self,dc,WorldToPixel,ScaleFunction): Points = WorldToPixel(self.Points) Points.shape = (-1,4) dc.DrawLineList(Points,self.Pens) class PointSet(draw_object): """ The PointSet class takes a list of 2-tuples, or a NX2 NumPy array of point coordinates. so that Points[N][0] is the x-coordinate of point N and Points[N][1] is the y-coordinate or Points[N,0] is the x-coordinate of point N and Points[N,1] is the y-coordinate for arrays. Each point will be drawn the same color and Diameter. The Diameter is in screen points, not world coordinates. """ def __init__(self,Points,Color,Diameter,Foreground = 0): draw_object.__init__(self,Foreground) self.Points = array(Points,Float) self.Points.shape = (-1,2) # Make sure it is a NX2 array, even if there is only one point self.BoundingBox = array(((min(self.Points[:,0]),min(self.Points[:,1])),(max(self.Points[:,0]),max(self.Points[:,1]))),Float) self.Color = Color self.Diameter = Diameter self.SetPen(Color,"Solid",1) self.SetBrush(Color,"Solid") def SetPoints(self,Points): self.Points = Points self.BoundingBox = array(((min(self.Points[:,0]),min(self.Points[:,1])),(max(self.Points[:,0]),max(self.Points[:,1]))),Float) if self._Canvas: # It looks like this shouldn't be private self._Canvas.BoundingBoxDirty = 1 def _Draw(self,dc,WorldToPixel,ScaleFunction): dc.SetPen(self.Pen) Points = WorldToPixel(self.Points) if self.Diameter <= 1: dc.DrawPointList(Points) elif self.Diameter <= 2: # A Little optimization for a diameter2 - point dc.DrawPointList(Points) dc.DrawPointList(Points + (1,0)) dc.DrawPointList(Points + (0,1)) dc.DrawPointList(Points + (1,1)) else: dc.SetBrush(self.Brush) radius = int(round(self.Diameter/2)) for (x,y) in Points: dc.DrawEllipse(((x - radius), (y - radius)), (self.Diameter, self.Diameter)) class Dot(draw_object): """ The Dot class takes an x.y coordinate pair, and the Diameter of the circle. The Diameter is in pixels, so it won't change with zoom. Also Fill and line data """ def __init__(self,x,y,Diameter,LineColor,LineStyle,LineWidth,FillColor,FillStyle,Foreground = 0): draw_object.__init__(self,Foreground) self.X = x self.Y = y self.Diameter = Diameter # NOTE: the bounding box does not include the diameter of the dot, as that is in pixel coords. # If this is a problem, perhaps you should use a circle, instead! self.BoundingBox = array(((x,y),(x,y)),Float) self.LineColor = LineColor self.LineStyle = LineStyle self.LineWidth = LineWidth self.FillColor = FillColor self.FillStyle = FillStyle self.SetPen(LineColor,LineStyle,LineWidth) self.SetBrush(FillColor,FillStyle) def _Draw(self,dc,WorldToPixel,ScaleFunction): dc.SetPen(self.Pen) dc.SetBrush(self.Brush) radius = int(round(self.Diameter/2)) (X,Y) = WorldToPixel((self.X,self.Y)) dc.DrawEllipse(((X - radius), (Y - radius)), (self.Diameter, self.Diameter)) class Rectangle(draw_object): def __init__(self,x,y,width,height,LineColor,LineStyle,LineWidth,FillColor,FillStyle,Foreground = 0): draw_object.__init__(self,Foreground) self.X = x self.Y = y self.Width = width self.Height = height self.BoundingBox = array(((x,y),(x+width,y+height)),Float) self.LineColor = LineColor self.LineStyle = LineStyle self.LineWidth = LineWidth self.FillColor = FillColor self.FillStyle = FillStyle self.SetPen(LineColor,LineStyle,LineWidth) self.SetBrush(FillColor,FillStyle) def _Draw(self,dc,WorldToPixel,ScaleFunction): (X,Y) = WorldToPixel((self.X,self.Y)) (Width,Height) = ScaleFunction((self.Width,self.Height)) dc.SetPen(self.Pen) dc.SetBrush(self.Brush) dc.DrawRectangle((X,Y), (Width,Height)) class Ellipse(draw_object): def __init__(self,x,y,width,height,LineColor,LineStyle,LineWidth,FillColor,FillStyle,Foreground = 0): draw_object.__init__(self,Foreground) self.X = x self.Y = y self.Width = width self.Height = height self.BoundingBox = array(((x,y),(x+width,y+height)),Float) self.LineColor = LineColor self.LineStyle = LineStyle self.LineWidth = LineWidth self.FillColor = FillColor self.FillStyle = FillStyle self.SetPen(LineColor,LineStyle,LineWidth) self.SetBrush(FillColor,FillStyle) def _Draw(self,dc,WorldToPixel,ScaleFunction): (X,Y) = WorldToPixel((self.X,self.Y)) (Width,Height) = ScaleFunction((self.Width,self.Height)) dc.SetPen(self.Pen) dc.SetBrush(self.Brush) dc.DrawEllipse((X,Y), (Width,Height)) class Circle(draw_object): def __init__(self,x,y,Diameter,LineColor,LineStyle,LineWidth,FillColor,FillStyle,Foreground = 0): draw_object.__init__(self,Foreground) self.X = x self.Y = y self.Diameter = Diameter self.BoundingBox = array(((x-Diameter/2,y-Diameter/2),(x+Diameter/2,y+Diameter/2)),Float) self.LineColor = LineColor self.LineStyle = LineStyle self.LineWidth = LineWidth self.FillColor = FillColor self.FillStyle = FillStyle self.SetPen(LineColor,LineStyle,LineWidth) self.SetBrush(FillColor,FillStyle) def _Draw(self,dc,WorldToPixel,ScaleFunction): (X,Y) = WorldToPixel((self.X,self.Y)) (Diameter,dummy) = ScaleFunction((self.Diameter,self.Diameter)) dc.SetPen(self.Pen) dc.SetBrush(self.Brush) dc.DrawEllipse((X-Diameter/2,Y-Diameter/2), (Diameter,Diameter)) class Text(draw_object): """ This class creates a text object, placed at the coordinates, x,y. the "Position" argument is a two charactor string, indicating where in relation to the coordinates the string should be oriented. The first letter is: t, c, or b, for top, center and bottom The second letter is: l, c, or r, for left, center and right I've provided arguments for Family, Style, and Weight of font, but have not yet implimented them, so all text is: wx.SWISS, wx.NORMAL, wx.NORMAL. I'd love it if someone would impliment that! The size is fixed, and does not scale with the drawing. """ def __init__(self,String,x,y,Size,ForeGround,BackGround,Family,Style,Weight,Underline,Position,Foreground = 0): draw_object.__init__(self,Foreground) self.String = String self.Size = Size self.ForeGround = ForeGround if BackGround is None: self.BackGround = None else: self.BackGround = BackGround self.Family = Family self.Style = Style self.Weight = Weight self.Underline = Underline self.Position = Position # fixme: this should use the passed in parameters! self.Font = wx.Font(Size, wx.MODERN, wx.NORMAL, wx.NORMAL, Underline, ) self.BoundingBox = array(((x,y),(x,y)),Float) self.X = x self.Y = y self.x_shift = None self.y_shift = None def _Draw(self,dc,WorldToPixel,ScaleFunction): (X,Y) = WorldToPixel((self.X,self.Y)) dc.SetFont(self.Font) dc.SetTextForeground(self.ForeGround) if self.BackGround: dc.SetBackgroundMode(wx.SOLID) dc.SetTextBackground(self.BackGround) else: dc.SetBackgroundMode(wx.TRANSPARENT) # compute the shift, and adjust the coordinates, if neccesary # This had to be put in here, becsuse there is no wx.DC during __init__ if self.x_shift is None or self.y_shift is None: if self.Position == 'tl': x_shift,y_shift = 0,0 else: (w,h) = dc.GetTextExtent(self.String) if self.Position[0] == 't': y_shift = 0 elif self.Position[0] == 'c': y_shift = h/2 elif self.Position[0] == 'b': y_shift = h else: ##fixme: this should be a real derived exception raise "Invalid value for Text Object Position Attribute" if self.Position[1] == 'l': x_shift = 0 elif self.Position[1] == 'c': x_shift = w/2 elif self.Position[1] == 'r': x_shift = w else: ##fixme: this should be a real derived exception raise "Invalid value for Text Object Position Attribute" self.x_shift = x_shift self.y_shift = y_shift dc.DrawText(self.String, (X-self.x_shift, Y-self.y_shift)) #--------------------------------------------------------------------------- class FloatCanvas(wx.Panel): """ FloatCanvas.py This is a high level window for drawing maps and anything else in an arbitrary coordinate system. The goal is to provide a convenient way to draw stuff on the screen without having to deal with handling OnPaint events, converting to pixel coordinates, knowing about wxWindows brushes, pens, and colors, etc. It also provides virtually unlimited zooming and scrolling I am using it for two things: 1) general purpose drawing in floating point coordinates 2) displaying map data in Lat-long coordinates If the projection is set to None, it will draw in general purpose floating point coordinates. If the projection is set to 'FlatEarth', it will draw a FlatEarth projection, centered on the part of the map that you are viewing. You can also pass in your own projection function. It is double buffered, so re-draws after the window is uncovered by something else are very quick. It relies on NumPy, which is needed for speed Bugs and Limitations: Lots: patches, fixes welcome For Map drawing: It ignores the fact that the world is, in fact, a sphere, so it will do strange things if you are looking at stuff near the poles or the date line. so far I don't have a need to do that, so I havn't bothered to add any checks for that yet. Zooming: I have set no zoom limits. What this means is that if you zoom in really far, you can get integer overflows, and get wierd results. It doesn't seem to actually cause any problems other than wierd output, at least when I have run it. Speed: I have done a couple of things to improve speed in this app. The one thing I have done is used NumPy Arrays to store the coordinates of the points of the objects. This allowed me to use array oriented functions when doing transformations, and should provide some speed improvement for objects with a lot of points (big polygons, polylines, pointsets). The real slowdown comes when you have to draw a lot of objects, because you have to call the wx.DC.DrawSomething call each time. This is plenty fast for tens of objects, OK for hundreds of objects, but pretty darn slow for thousands of objects. The solution is to be able to pass some sort of object set to the DC directly. I've used DC.DrawPointList(Points), and it helped a lot with drawing lots of points. I havn't got a LineSet type object, so I havn't used DC.DrawLineList yet. I'd like to get a full set of DrawStuffList() methods implimented, and then I'd also have a full set of Object sets that could take advantage of them. I hope to get to it some day. Copyright: Christopher Barker License: Same as wxPython Please let me know if you're using this!!! Contact me at: Chris.Barker@noaa.gov """ def __init__(self, parent, id = -1, size = wx.DefaultSize, ProjectionFun = None, BackgroundColor = "WHITE", Debug = 0, EnclosingFrame = None, UseToolbar = 1, UseBackground = 0, UseHitTest = 0): wx.Panel.__init__( self, parent, id, wx.DefaultPosition, size) if ProjectionFun == 'FlatEarth': self.ProjectionFun = self.FlatEarthProjection elif type(ProjectionFun) == types.FunctionType: self.ProjectionFun = ProjectionFun elif ProjectionFun is None: self.ProjectionFun = lambda x=None: array( (1,1), Float) else: raise('Projectionfun must be either: "FlatEarth", None, or a function that takes the ViewPortCenter and returns a MapProjectionVector') self.UseBackground = UseBackground self.UseHitTest = UseHitTest self.NumBetweenBlits = 40 ## you can have a toolbar with buttons for zoom-in, zoom-out and ## move. If you don't use the toolbar, you should provide your ## own way of navigating the canvas if UseToolbar: ## Create the vertical sizer for the toolbar and Panel box = wx.BoxSizer(wx.VERTICAL) box.Add(self.BuildToolbar(), 0, wx.ALL | wx.ALIGN_LEFT | wx.GROW, 4) self.DrawPanel = wx.Window(self,-1,wx.DefaultPosition,wx.DefaultSize,wx.SUNKEN_BORDER) box.Add(self.DrawPanel,1,wx.GROW) box.Fit(self) self.SetAutoLayout(True) self.SetSizer(box) else: self.DrawPanel = self self.DrawPanel.BackgroundBrush = wx.Brush(BackgroundColor,wx.SOLID) self.Debug = Debug self.EnclosingFrame = EnclosingFrame wx.EVT_PAINT(self.DrawPanel, self.OnPaint) wx.EVT_SIZE(self.DrawPanel, self.OnSize) wx.EVT_LEFT_DOWN(self.DrawPanel, self.LeftButtonEvent) wx.EVT_LEFT_UP(self.DrawPanel, self.LeftButtonEvent) wx.EVT_RIGHT_DOWN(self.DrawPanel, self.RightButtonEvent) wx.EVT_MOTION(self.DrawPanel, self.LeftButtonEvent) self._DrawList = [] if self.UseBackground: self._TopDrawList = [] self.BoundingBox = None self.BoundingBoxDirty = 0 self.ViewPortCenter= array( (0,0), Float) self.MapProjectionVector = array( (1,1), Float) # No Projection to start! self.TransformVector = array( (1,-1), Float) # default Transformation self.Scale = 1 self.GUIMode = None self.StartRBBox = None self.PrevRBBox = None self.StartMove = None self.PrevMoveBox = None # called just to make sure everything is initialized self.OnSize(None) def BuildToolbar(self): tb = wx.ToolBar(self,-1) self.ToolBar = tb tb.SetToolBitmapSize((23,23)) tb.AddTool(ID_ZOOM_IN_BUTTON, GetPlusBitmap(), isToggle=True,shortHelpString = "Zoom In") wx.EVT_TOOL(self, ID_ZOOM_IN_BUTTON, self.SetMode) tb.AddTool(ID_ZOOM_OUT_BUTTON, GetMinusBitmap(), isToggle=True,shortHelpString = "Zoom Out") wx.EVT_TOOL(self, ID_ZOOM_OUT_BUTTON, self.SetMode) tb.AddTool(ID_MOVE_MODE_BUTTON, GetHandBitmap(), isToggle=True,shortHelpString = "Move") wx.EVT_TOOL(self, ID_MOVE_MODE_BUTTON, self.SetMode) tb.AddSeparator() tb.AddControl(wx.Button(tb, ID_ZOOM_TO_FIT_BUTTON, "Zoom To Fit",wx.DefaultPosition, wx.DefaultSize)) wx.EVT_BUTTON(self, ID_ZOOM_TO_FIT_BUTTON, self.ZoomToFit) tb.Realize() return tb def SetMode(self,event): for id in [ID_ZOOM_IN_BUTTON,ID_ZOOM_OUT_BUTTON,ID_MOVE_MODE_BUTTON]: self.ToolBar.ToggleTool(id,0) self.ToolBar.ToggleTool(event.GetId(),1) if event.GetId() == ID_ZOOM_IN_BUTTON: self.SetGUIMode("ZoomIn") elif event.GetId() == ID_ZOOM_OUT_BUTTON: self.SetGUIMode("ZoomOut") elif event.GetId() == ID_MOVE_MODE_BUTTON: self.SetGUIMode("Move") def SetGUIMode(self,Mode): if Mode in ["ZoomIn","ZoomOut","Move",None]: self.GUIMode = Mode else: raise "Not a valid Mode" def FlatEarthProjection(self,CenterPoint): return array((cos(pi*CenterPoint[1]/180),1),Float) def LeftButtonEvent(self,event): if self.EnclosingFrame: if event.Moving: position = self.PixelToWorld((event.GetX(),event.GetY())) self.EnclosingFrame.SetStatusText("%8.3f, %8.3f"%tuple(position)) if self.GUIMode: if self.GUIMode == "ZoomIn": if event.LeftDown(): self.StartRBBox = (event.GetX(),event.GetY()) self.PrevRBBox = None elif event.Dragging() and event.LeftIsDown() and self.StartRBBox: x0,y0 = self.StartRBBox x1,y1 = event.GetX(),event.GetY() w, h = abs(x1-x0),abs(y1-y0) w = max(w,int(h*self.AspectRatio)) h = int(w/self.AspectRatio) x_c, y_c = (x0+x1)/2 , (y0+y1)/2 dc = wx.ClientDC(self.DrawPanel) dc.BeginDrawing() dc.SetPen(wx.Pen('WHITE', 2,wx.SHORT_DASH)) dc.SetBrush(wx.TRANSPARENT_BRUSH) dc.SetLogicalFunction(wx.XOR) if self.PrevRBBox: dc.DrawRectangleXY(*self.PrevRBBox) dc.DrawRectangleXY(x_c-w/2,y_c-h/2,w,h) self.PrevRBBox = (x_c-w/2,y_c-h/2,w,h) dc.EndDrawing() elif event.LeftUp() and self.StartRBBox : self.PrevRBBox = None EndRBBox = (event.GetX(),event.GetY()) StartRBBox = self.StartRBBox # if mouse has moved less that ten pixels, don't use the box. if abs(StartRBBox[0] - EndRBBox[0]) > 10 and abs(StartRBBox[1] - EndRBBox[1]) > 10: EndRBBox = self.PixelToWorld(EndRBBox) StartRBBox = self.PixelToWorld(StartRBBox) BB = array(((min(EndRBBox[0],StartRBBox[0]), min(EndRBBox[1],StartRBBox[1])), (max(EndRBBox[0],StartRBBox[0]), max(EndRBBox[1],StartRBBox[1]))),Float) self.ZoomToBB(BB) else: Center = self.PixelToWorld(StartRBBox) self.Zoom(1.5,Center) self.StartRBBox = None if self.GUIMode == "ZoomOut": if event.LeftDown(): Center = self.PixelToWorld((event.GetX(),event.GetY())) self.Zoom(1/1.5,Center) elif self.GUIMode == "Move": if event.LeftDown(): self.StartMove = array((event.GetX(),event.GetY())) self.PrevMoveBox = None elif event.Dragging() and event.LeftIsDown() and self.StartMove: x_1,y_1 = event.GetX(),event.GetY() w, h = self.PanelSize x_tl, y_tl = x_1 - self.StartMove[0], y_1 - self.StartMove[1] dc = wx.ClientDC(self.DrawPanel) dc.BeginDrawing() dc.SetPen(wx.Pen('WHITE', 1,)) dc.SetBrush(wx.TRANSPARENT_BRUSH) dc.SetLogicalFunction(wx.XOR) if self.PrevMoveBox: dc.DrawRectangleXY(*self.PrevMoveBox) dc.DrawRectangleXY(x_tl,y_tl,w,h) self.PrevMoveBox = (x_tl,y_tl,w,h) dc.EndDrawing() elif event.LeftUp() and self.StartMove: self.PrevMoveBox = None StartMove = self.StartMove EndMove = array((event.GetX(),event.GetY())) if sum((StartMove-EndMove)**2) > 16: self.Move(StartMove-EndMove,'Pixel') self.StartMove = None def RightButtonEvent(self,event): if self.GUIMode: if self.GUIMode == "ZoomIn": Center = self.PixelToWorld((event.GetX(),event.GetY())) self.Zoom(1/1.5,Center) elif self.GUIMode == "ZoomOut": Center = self.PixelToWorld((event.GetX(),event.GetY())) self.Zoom(1.5,Center) else: event.Skip() event.Skip() def MakeNewBuffers(self): # Make new offscreen bitmap: self._Buffer = wx.EmptyBitmap(self.PanelSize[0],self.PanelSize[1]) if self.UseBackground: self._BackBuffer = wx.EmptyBitmap(self.PanelSize[0],self.PanelSize[1]) self._BackgroundDirty = 1 else: pass def OnSize(self,event): self.PanelSize = array(self.DrawPanel.GetClientSizeTuple(),Int32) try: self.AspectRatio = self.PanelSize[0]/self.PanelSize[1] except ZeroDivisionError: self.AspectRatio = 1.0 self.MakeNewBuffers() self.Draw() def OnPaint(self, event): #dc = wx.BufferedPaintDC(self.DrawPanel, self._Buffer) dc = wx.PaintDC(self.DrawPanel) dc.DrawBitmap(self._Buffer, (0,0)) def Draw(self): """ The Draw method gets pretty complicated because of all the buffers There is a main buffer set up to double buffer the screen, so you can get quick re-draws when the window gets uncovered. If self.UseBackground is set, and an object is set up with the "ForeGround" flag, then it gets drawn to the screen after blitting the background. This is done so that you can have a complicated background, but have something changing on the foreground, without having to wait for the background to get re-drawn. This can be used to support simple animation, for instance. """ if self.Debug: start = clock() ScreenDC = wx.ClientDC(self.DrawPanel) ViewPortWorld = ( self.PixelToWorld((0,0)), self.PixelToWorld(self.PanelSize) ) ViewPortBB = array( ( minimum.reduce(ViewPortWorld), maximum.reduce(ViewPortWorld) ) ) if self.UseBackground: dc = wx.MemoryDC() dc.SelectObject(self._BackBuffer) dc.SetBackground(self.DrawPanel.BackgroundBrush) if self._DrawList: if self._BackgroundDirty: dc.BeginDrawing() dc.Clear() i = 0 for Object in self._DrawList: if self.BBCheck(Object.BoundingBox,ViewPortBB): #print "object is in Bounding Box" i+=1 Object._Draw(dc,self.WorldToPixel,self.ScaleFunction) if i % self.NumBetweenBlits == 0: ScreenDC.Blit((0, 0), self.PanelSize, dc, (0, 0)) dc.EndDrawing() else: dc.Clear() self._BackgroundDirty = 0 dc.SelectObject(self._Buffer) dc.BeginDrawing() ##Draw Background on Main Buffer: dc.DrawBitmap(self._BackBuffer,0,0) #Draw the OnTop stuff i = 0 for Object in self._TopDrawList: i+=1 Object._Draw(dc,self.WorldToPixel,self.ScaleFunction) if i % self.NumBetweenBlits == 0: ScreenDC.Blit((0, 0), self.PanelSize, dc, (0, 0)) dc.EndDrawing() else: # not using a Background DC dc = wx.MemoryDC() dc.SelectObject(self._Buffer) dc.SetBackground(self.DrawPanel.BackgroundBrush) if self._DrawList: dc.BeginDrawing() dc.Clear() i = 0 for Object in self._DrawList: if self.BBCheck(Object.BoundingBox,ViewPortBB): #print "object is in Bounding Box" i+=1 Object._Draw(dc,self.WorldToPixel,self.ScaleFunction) if i % self.NumBetweenBlits == 0: ScreenDC.Blit((0, 0), self.PanelSize, dc, (0, 0)) dc.EndDrawing() else: dc.Clear() # now refresh the screen #ScreenDC.DrawBitmap(self._Buffer,0,0) #NOTE: uisng DrawBitmap didn't work right on MSW ScreenDC.Blit((0, 0), self.PanelSize, dc, (0, 0)) # If the canvas is in the middle of a zoom or move, the Rubber Band box needs to be re-drawn if self.PrevRBBox: ScreenDC.SetPen(wx.Pen('WHITE', 2,wx.SHORT_DASH)) ScreenDC.SetBrush(wx.TRANSPARENT_BRUSH) ScreenDC.SetLogicalFunction(wx.XOR) ScreenDC.DrawRectangleXY(*self.PrevRBBox) elif self.PrevMoveBox: ScreenDC.SetPen(wx.Pen('WHITE', 1,)) ScreenDC.SetBrush(wx.TRANSPARENT_BRUSH) ScreenDC.SetLogicalFunction(wx.XOR) ScreenDC.DrawRectangleXY(*self.PrevMoveBox) if self.Debug: print "Drawing took %f seconds of CPU time"%(clock()-start) def BBCheck(self, BB1, BB2): """ BBCheck(BB1, BB2) returns True is the Bounding boxes intesect, False otherwise """ if ( (BB1[1,0] > BB2[0,0]) and (BB1[0,0] < BB2[1,0]) and (BB1[1,1] > BB2[0,1]) and (BB1[0,1] < BB2[1,1]) ): return True else: return False def Move(self,shift,CoordType): """ move the image in the window. shift is an (x,y) tuple, specifying the amount to shift in each direction It can be in any of three coordinates: Panel, Pixel, World, specified by the CoordType parameter Panel coordinates means you want to shift the image by some fraction of the size of the displaed image Pixel coordinates means you want to shift the image by some number of pixels World coordinates meand you want to shift the image by an amount in Floating point world coordinates """ shift = array(shift,Float) if CoordType == 'Panel':# convert from panel coordinates shift = shift * array((-1,1),Float) *self.PanelSize/self.TransformVector elif CoordType == 'Pixel': # convert from pixel coordinates shift = shift/self.TransformVector elif CoordType == 'World': # No conversion pass else: raise 'CoordType must be either "Panel", "Pixel", or "World"' self.ViewPortCenter = self.ViewPortCenter + shift self.MapProjectionVector = self.ProjectionFun(self.ViewPortCenter) self.TransformVector = array((self.Scale,-self.Scale),Float)* self.MapProjectionVector self._BackgroundDirty = 1 self.Draw() def Zoom(self,factor,center = None): """ Zoom(factor, center) changes the amount of zoom of the image by factor. If factor is greater than one, the image gets larger. If factor is less than one, the image gets smaller. Center is a tuple of (x,y) coordinates of the center of the viewport, after zooming. If center is not given, the center will stay the same. """ self.Scale = self.Scale*factor if center: self.ViewPortCenter = array(center,Float) self.MapProjectionVector = self.ProjectionFun(self.ViewPortCenter) self.TransformVector = array((self.Scale,-self.Scale),Float)* self.MapProjectionVector self._BackgroundDirty = 1 self.Draw() def ZoomToFit(self,event): self.ZoomToBB() def ZoomToBB(self,NewBB = None,DrawFlag = 1): """ Zooms the image to the bounding box given, or to the bounding box of all the objects on the canvas, if none is given. """ if NewBB: BoundingBox = NewBB else: if self.BoundingBoxDirty: self._ResetBoundingBox() BoundingBox = self.BoundingBox if BoundingBox: self.ViewPortCenter = array(((BoundingBox[0,0]+BoundingBox[1,0])/2, (BoundingBox[0,1]+BoundingBox[1,1])/2 ),Float) self.MapProjectionVector = self.ProjectionFun(self.ViewPortCenter) # Compute the new Scale BoundingBox = BoundingBox * self.MapProjectionVector try: self.Scale = min((self.PanelSize[0] / (BoundingBox[1,0]-BoundingBox[0,0])), (self.PanelSize[1] / (BoundingBox[1,1]-BoundingBox[0,1])))*0.95 except ZeroDivisionError: # this will happen if the BB has zero width or height try: #width self.Scale = (self.PanelSize[0] / (BoundingBox[1,0]-BoundingBox[0,0]))*0.95 except ZeroDivisionError: try: # height self.Scale = (self.PanelSize[1] / (BoundingBox[1,1]-BoundingBox[0,1]))*0.95 except ZeroDivisionError: #zero size! (must be a single point) self.Scale = 1 self.TransformVector = array((self.Scale,-self.Scale),Float)* self.MapProjectionVector if DrawFlag: self._BackgroundDirty = 1 self.Draw() def RemoveObjects(self,Objects): for Object in Objects: self.RemoveObject(Object,ResetBB = 0) self.BoundingBoxDirty = 1 def RemoveObject(self,Object,ResetBB = 1): if Object.Foreground: self._TopDrawList.remove(Object) else: self._DrawList.remove(Object) self._BackgroundDirty = 1 if ResetBB: self.BoundingBoxDirty = 1 def Clear(self, ResetBB = True): self._DrawList = [] if self.UseBackground: self._TopDrawList = [] self._BackgroundDirty = 1 if ResetBB: self._ResetBoundingBox() def _AddBoundingBox(self,NewBB): if self.BoundingBox is None: self.BoundingBox = NewBB self.ZoomToBB(NewBB,DrawFlag = 0) else: self.BoundingBox = array(((min(self.BoundingBox[0,0],NewBB[0,0]), min(self.BoundingBox[0,1],NewBB[0,1])), (max(self.BoundingBox[1,0],NewBB[1,0]), max(self.BoundingBox[1,1],NewBB[1,1]))),Float) def _ResetBoundingBox(self): # NOTE: could you remove an item without recomputing the entire bounding box? self.BoundingBox = None if self._DrawList: self.BoundingBox = self._DrawList[0].BoundingBox for Object in self._DrawList[1:]: self._AddBoundingBox(Object.BoundingBox) if self.UseBackground: for Object in self._TopDrawList: self._AddBoundingBox(Object.BoundingBox) if self.BoundingBox is None: self.ViewPortCenter= array( (0,0), Float) self.TransformVector = array( (1,-1), Float) self.MapProjectionVector = array( (1,1), Float) self.Scale = 1 self.BoundingBoxDirty = 0 def PixelToWorld(self,Points): """ Converts coordinates from Pixel coordinates to world coordinates. Points is a tuple of (x,y) coordinates, or a list of such tuples, or a NX2 Numpy array of x,y coordinates. """ return (((array(Points,Float) - (self.PanelSize/2))/self.TransformVector) + self.ViewPortCenter) def WorldToPixel(self,Coordinates): """ This function will get passed to the drawing functions of the objects, to transform from world to pixel coordinates. Coordinates should be a NX2 array of (x,y) coordinates, or a 2-tuple, or sequence of 2-tuples. """ return (((array(Coordinates,Float) - self.ViewPortCenter)*self.TransformVector)+(self.PanelSize/2)).astype('i') def ScaleFunction(self,Lengths): """ This function will get passed to the drawing functions of the objects, to Change a length from world to pixel coordinates. Lengths should be a NX2 array of (x,y) coordinates, or a 2-tuple, or sequence of 2-tuples. """ return (array(Lengths,Float)*self.TransformVector).astype('i') ## This is a set of methods that add objects to the Canvas. It kind ## of seems like a lot of duplication, but I wanted to be able to ## instantiate the draw objects separatley form adding them, but ## also to be able to do add one oin one step. I'm open to better ## ideas... def AddRectangle(self,x,y,width,height, LineColor = "Black", LineStyle = "Solid", LineWidth = 1, FillColor = None, FillStyle = "Solid", Foreground = 0): Object = Rectangle(x,y,width,height,LineColor,LineStyle,LineWidth,FillColor,FillStyle,Foreground) self.AddObject(Object) return Object def AddEllipse(self,x,y,width,height, LineColor = "Black", LineStyle = "Solid", LineWidth = 1, FillColor = None, FillStyle = "Solid", Foreground = 0): Object = Ellipse(x,y,width,height,LineColor,LineStyle,LineWidth,FillColor,FillStyle,Foreground) self.AddObject(Object) return Object def AddCircle(self,x,y,Diameter, LineColor = "Black", LineStyle = "Solid", LineWidth = 1, FillColor = None, FillStyle = "Solid", Foreground = 0): Object = Circle(x,y,Diameter,LineColor,LineStyle,LineWidth,FillColor,FillStyle,Foreground) self.AddObject(Object) return Object def AddDot(self,x,y,Diameter, LineColor = "Black", LineStyle = "Solid", LineWidth = 1, FillColor = None, FillStyle = "Solid", Foreground = 0): Object = Dot(x,y,Diameter,LineColor,LineStyle,LineWidth,FillColor,FillStyle,Foreground) self.AddObject(Object) return Object def AddPolygon(self,Points, LineColor = "Black", LineStyle = "Solid", LineWidth = 1, FillColor = None, FillStyle = "Solid", Foreground = 0): Object = Polygon(Points,LineColor,LineStyle,LineWidth,FillColor,FillStyle,Foreground) self.AddObject(Object) return Object def AddLine(self,Points, LineColor = "Black", LineStyle = "Solid", LineWidth = 1, Foreground = 0): Object = Line(Points,LineColor,LineStyle,LineWidth,Foreground) self.AddObject(Object) return Object def AddLineSet(self,Points, LineColors = "Black", LineStyles = "Solid", LineWidths = 1, Foreground = 0): Object = LineSet(Points,LineColors,LineStyles,LineWidths,Foreground) self.AddObject(Object) return Object def AddPointSet(self,Points, Color = "Black", Diameter = 1, Foreground = 0): Object = PointSet(Points,Color,Diameter,Foreground) self.AddObject(Object) return Object def AddText(self,String,x,y, Size = 20, ForeGround = 'Black', BackGround = None, Family = 'Swiss', Style = 'Normal', Weight = 'Normal', Underline = 0, Position = 'tl', Foreground = 0): Object = Text(String,x,y,Size,ForeGround,BackGround,Family,Style,Weight,Underline,Position,Foreground) self.AddObject(Object) return Object def AddObject(self,obj): # put in a reference to the Canvas, so remove and other stuff can work obj._Canvas = self if obj.Foreground and self.UseBackground: self._TopDrawList.append(obj) else: self._DrawList.append(obj) self._backgrounddirty = 1 self._AddBoundingBox(obj.BoundingBox) return None