/[thuban]/branches/WIP-pyshapelib-bramz/Thuban/UI/view.py
ViewVC logotype

Annotation of /branches/WIP-pyshapelib-bramz/Thuban/UI/view.py

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1344 - (hide annotations)
Tue Jul 1 16:11:26 2003 UTC (21 years, 8 months ago) by jonathan
Original Path: trunk/thuban/Thuban/UI/view.py
File MIME type: text/x-python
File size: 39082 byte(s)
Fixes RTbug #1974, 1971.
(MapCanvas.__init__): Subscribe to the idle time event. Set
        background color to white.
(MapCanvas.OnPaint): Set a flag indicating that we should
        render the map during idle time. If we already have a bitmap
        just draw it now.
(MapCanvas.OnIdle): New. Render the map only during idle time.
        This also fixes a problem with the busy cursor under gtk.

1 bh 404 # Copyright (c) 2001, 2002, 2003 by Intevation GmbH
2 bh 6 # Authors:
3     # Bernhard Herzog <[email protected]>
4 frank 910 # Frank Koormann <[email protected]>
5 bh 6 #
6     # This program is free software under the GPL (>=v2)
7     # Read the file COPYING coming with Thuban for details.
8    
9     """
10     Classes for display of a map and interaction with it
11     """
12    
13     __version__ = "$Revision$"
14    
15 frank 910 from Thuban import _
16    
17 jonathan 799 import sys
18 frank 910 import os.path
19 jonathan 799
20 bh 6 from math import hypot
21    
22 jonathan 1285 from wxPython.wx import wxWindow, \
23 bh 6 wxPaintDC, wxColour, wxClientDC, wxINVERT, wxTRANSPARENT_BRUSH, wxFont,\
24 jonathan 822 EVT_PAINT, EVT_LEFT_DOWN, EVT_LEFT_UP, EVT_MOTION, EVT_LEAVE_WINDOW, \
25 jonathan 1285 wxBITMAP_TYPE_XPM, wxCursor, wxImageFromBitmap, wxPlatform, \
26     wxBeginBusyCursor, wxEndBusyCursor
27 bh 6
28 jonathan 1285
29 frank 910 # Export related stuff
30     if wxPlatform == '__WXMSW__':
31     from wxPython.wx import wxMetaFileDC
32     from wxPython.wx import wxFileDialog, wxSAVE, wxOVERWRITE_PROMPT, wxID_OK
33 bh 6
34     from wxPython import wx
35    
36     from wxproj import point_in_polygon_shape, shape_centroid
37    
38     from Thuban.Model.messages import MAP_PROJECTION_CHANGED, \
39 jonathan 1221 LAYER_PROJECTION_CHANGED, \
40 jonathan 565 MAP_LAYERS_CHANGED, LAYER_CHANGED, LAYER_VISIBILITY_CHANGED
41 bh 6 from Thuban.Model.layer import SHAPETYPE_POLYGON, SHAPETYPE_ARC, \
42     SHAPETYPE_POINT
43     from Thuban.Model.label import ALIGN_CENTER, ALIGN_TOP, ALIGN_BOTTOM, \
44     ALIGN_LEFT, ALIGN_RIGHT
45 bh 122 from Thuban.Lib.connector import Publisher
46 jonathan 1344 from Thuban.Model.color import Color, Transparent
47 bh 6
48 jonathan 822 import resource
49    
50 bh 535 from selection import Selection
51 frank 910 from renderer import ScreenRenderer, ExportRenderer, PrinterRenderer
52 bh 6
53     import labeldialog
54    
55 frank 855 from messages import LAYER_SELECTED, SHAPES_SELECTED, VIEW_POSITION, \
56     SCALE_CHANGED
57 bh 6
58 jonathan 1344 #from common import ThubanBeginBusyCursor, ThubanEndBusyCursor
59 bh 6
60     #
61     # The tools
62     #
63    
64     class Tool:
65    
66     """
67     Base class for the interactive tools
68     """
69    
70     def __init__(self, view):
71     """Intitialize the tool. The view is the canvas displaying the map"""
72     self.view = view
73     self.start = self.current = None
74     self.dragging = 0
75     self.drawn = 0
76    
77     def Name(self):
78     """Return the tool's name"""
79     return ''
80    
81     def drag_start(self, x, y):
82     self.start = self.current = x, y
83     self.dragging = 1
84    
85     def drag_move(self, x, y):
86     self.current = x, y
87    
88     def drag_stop(self, x, y):
89     self.current = x, y
90     self.dragging = 0
91    
92     def Show(self, dc):
93     if not self.drawn:
94     self.draw(dc)
95     self.drawn = 1
96    
97     def Hide(self, dc):
98     if self.drawn:
99     self.draw(dc)
100     self.drawn = 0
101    
102     def draw(self, dc):
103     pass
104    
105     def MouseDown(self, event):
106     self.drag_start(event.m_x, event.m_y)
107    
108     def MouseMove(self, event):
109     if self.dragging:
110     self.drag_move(event.m_x, event.m_y)
111    
112     def MouseUp(self, event):
113     if self.dragging:
114     self.drag_move(event.m_x, event.m_y)
115    
116     def Cancel(self):
117     self.dragging = 0
118    
119    
120     class RectTool(Tool):
121    
122     """Base class for tools that draw rectangles while dragging"""
123    
124     def draw(self, dc):
125     sx, sy = self.start
126     cx, cy = self.current
127     dc.DrawRectangle(sx, sy, cx - sx, cy - sy)
128    
129     class ZoomInTool(RectTool):
130    
131     """The Zoom-In Tool"""
132    
133     def Name(self):
134     return "ZoomInTool"
135    
136     def proj_rect(self):
137     """return the rectangle given by start and current in projected
138     coordinates"""
139     sx, sy = self.start
140     cx, cy = self.current
141     left, top = self.view.win_to_proj(sx, sy)
142     right, bottom = self.view.win_to_proj(cx, cy)
143     return (min(left, right), min(top, bottom),
144     max(left, right), max(top, bottom))
145    
146     def MouseUp(self, event):
147     if self.dragging:
148     Tool.MouseUp(self, event)
149 bh 57 sx, sy = self.start
150     cx, cy = self.current
151 bh 288 if sx == cx or sy == cy:
152     # Just a mouse click or a degenerate rectangle. Simply
153     # zoom in by a factor of two
154     # FIXME: For a click this is the desired behavior but should we
155     # really do this for degenrate rectagles as well or
156     # should we ignore them?
157 bh 57 self.view.ZoomFactor(2, center = (cx, cy))
158     else:
159     # A drag. Zoom in to the rectangle
160     self.view.FitRectToWindow(self.proj_rect())
161 bh 6
162    
163     class ZoomOutTool(RectTool):
164    
165     """The Zoom-Out Tool"""
166 bh 239
167 bh 6 def Name(self):
168     return "ZoomOutTool"
169    
170     def MouseUp(self, event):
171     if self.dragging:
172     Tool.MouseUp(self, event)
173     sx, sy = self.start
174     cx, cy = self.current
175 bh 288 if sx == cx or sy == cy:
176     # Just a mouse click or a degenerate rectangle. Simply
177     # zoom out by a factor of two.
178     # FIXME: For a click this is the desired behavior but should we
179     # really do this for degenrate rectagles as well or
180     # should we ignore them?
181 bh 239 self.view.ZoomFactor(0.5, center = (cx, cy))
182 bh 57 else:
183     # A drag. Zoom out to the rectangle
184     self.view.ZoomOutToRect((min(sx, cx), min(sy, cy),
185     max(sx, cx), max(sy, cy)))
186 bh 6
187    
188     class PanTool(Tool):
189    
190     """The Pan Tool"""
191    
192     def Name(self):
193     return "PanTool"
194    
195     def MouseMove(self, event):
196     if self.dragging:
197     Tool.MouseMove(self, event)
198 bh 159 sx, sy = self.start
199 bh 6 x, y = self.current
200     width, height = self.view.GetSizeTuple()
201 bh 159
202     bitmapdc = wx.wxMemoryDC()
203     bitmapdc.SelectObject(self.view.bitmap)
204    
205 bh 6 dc = self.view.drag_dc
206 bh 159 dc.Blit(0, 0, width, height, bitmapdc, sx - x, sy - y)
207 bh 6
208     def MouseUp(self, event):
209     if self.dragging:
210     Tool.MouseUp(self, event)
211     sx, sy = self.start
212     cx, cy = self.current
213     self.view.Translate(cx - sx, cy - sy)
214 bh 246
215 bh 6 class IdentifyTool(Tool):
216    
217     """The "Identify" Tool"""
218 bh 246
219 bh 6 def Name(self):
220     return "IdentifyTool"
221    
222     def MouseUp(self, event):
223     self.view.SelectShapeAt(event.m_x, event.m_y)
224    
225    
226     class LabelTool(Tool):
227    
228     """The "Label" Tool"""
229    
230     def Name(self):
231     return "LabelTool"
232    
233     def MouseUp(self, event):
234     self.view.LabelShapeAt(event.m_x, event.m_y)
235    
236    
237     class MapPrintout(wx.wxPrintout):
238    
239     """
240     wxPrintout class for printing Thuban maps
241     """
242    
243 frank 910 def __init__(self, canvas, map, region, selected_layer, selected_shapes):
244 bh 6 wx.wxPrintout.__init__(self)
245 frank 910 self.canvas = canvas
246 bh 6 self.map = map
247 frank 910 self.region = region
248     self.selected_layer = selected_layer
249     self.selected_shapes = selected_shapes
250 bh 6
251     def GetPageInfo(self):
252     return (1, 1, 1, 1)
253    
254     def HasPage(self, pagenum):
255     return pagenum == 1
256    
257     def OnPrintPage(self, pagenum):
258     if pagenum == 1:
259     self.draw_on_dc(self.GetDC())
260    
261     def draw_on_dc(self, dc):
262     width, height = self.GetPageSizePixels()
263 frank 910 scale, offset, mapregion = OutputTransform(self.canvas.scale,
264     self.canvas.offset,
265     self.canvas.GetSizeTuple(),
266     self.GetPageSizePixels())
267 bh 6 resx, resy = self.GetPPIPrinter()
268 frank 910 renderer = PrinterRenderer(dc, scale, offset, resolution = resy)
269     x, y, width, height = self.region
270     canvas_scale = self.canvas.scale
271     renderer.RenderMap(self.map,
272     (0,0,
273     (width/canvas_scale)*scale,
274     (height/canvas_scale)*scale),
275     mapregion,
276     self.selected_layer, self.selected_shapes)
277 jan 1035 return True
278 bh 6
279 bh 122 class MapCanvas(wxWindow, Publisher):
280 bh 6
281     """A widget that displays a map and offers some interaction"""
282    
283 bh 535 # Some messages that can be subscribed/unsubscribed directly through
284 bh 789 # the MapCanvas come in fact from other objects. This is a dict
285     # mapping those messages to the names of the instance variables they
286     # actually come from. The delegation is implemented in the Subscribe
287     # and Unsubscribe methods
288 bh 535 delegated_messages = {LAYER_SELECTED: "selection",
289     SHAPES_SELECTED: "selection"}
290    
291     # Methods delegated to some instance variables. The delegation is
292     # implemented in the __getattr__ method.
293     delegated_methods = {"SelectLayer": "selection",
294     "SelectShapes": "selection",
295     "SelectedLayer": "selection",
296 jonathan 831 "HasSelectedLayer": "selection",
297 jonathan 883 "HasSelectedShapes": "selection",
298     "SelectedShapes": "selection"}
299 bh 535
300     def __init__(self, parent, winid):
301 bh 6 wxWindow.__init__(self, parent, winid)
302     self.SetBackgroundColour(wxColour(255, 255, 255))
303 bh 125
304     # the map displayed in this canvas. Set with SetMap()
305 bh 6 self.map = None
306 bh 125
307 jonathan 1221 # current map projection. should only differ from map.projection
308     # when the map's projection is changing and we need access to the
309     # old projection.
310     self.current_map_proj = None
311    
312 bh 125 # scale and offset describe the transformation from projected
313     # coordinates to window coordinates.
314 bh 6 self.scale = 1.0
315     self.offset = (0, 0)
316 bh 125
317     # whether the user is currently dragging the mouse, i.e. moving
318     # the mouse while pressing a mouse button
319 bh 6 self.dragging = 0
320 bh 125
321     # the currently active tool
322 bh 6 self.tool = None
323 bh 125
324     # The current mouse position of the last OnMotion event or None
325     # if the mouse is outside the window.
326     self.current_position = None
327    
328     # the bitmap serving as backing store
329     self.bitmap = None
330    
331 bh 535 # the selection
332     self.selection = Selection()
333     self.selection.Subscribe(SHAPES_SELECTED , self.shape_selected)
334 bh 125
335 bh 174 # keep track of which layers/shapes are selected to make sure we
336     # only redraw when necessary
337     self.last_selected_layer = None
338     self.last_selected_shape = None
339    
340 jonathan 1344 self.backgroundColor = wx.wxWHITE_BRUSH
341    
342 bh 125 # subscribe the WX events we're interested in
343 bh 6 EVT_PAINT(self, self.OnPaint)
344     EVT_LEFT_DOWN(self, self.OnLeftDown)
345     EVT_LEFT_UP(self, self.OnLeftUp)
346     EVT_MOTION(self, self.OnMotion)
347 bh 122 EVT_LEAVE_WINDOW(self, self.OnLeaveWindow)
348 bh 125 wx.EVT_SIZE(self, self.OnSize)
349 jonathan 1344 wx.EVT_IDLE(self, self.OnIdle)
350 bh 6
351 bh 122 def __del__(self):
352     wxWindow.__del__(self)
353     Publisher.__del__(self)
354    
355 bh 535 def Subscribe(self, channel, *args):
356     """Extend the inherited method to handle delegated messages.
357    
358     If channel is one of the delegated messages call the appropriate
359     object's Subscribe method. Otherwise just call the inherited
360     method.
361     """
362     if channel in self.delegated_messages:
363     object = getattr(self, self.delegated_messages[channel])
364     object.Subscribe(channel, *args)
365     else:
366     Publisher.Subscribe(self, channel, *args)
367    
368     def Unsubscribe(self, channel, *args):
369     """Extend the inherited method to handle delegated messages.
370    
371     If channel is one of the delegated messages call the appropriate
372     object's Unsubscribe method. Otherwise just call the inherited
373     method.
374     """
375     if channel in self.delegated_messages:
376     object = getattr(self, self.delegated_messages[channel])
377     object.Unsubscribe(channel, *args)
378     else:
379     Publisher.Unsubscribe(self, channel, *args)
380    
381     def __getattr__(self, attr):
382     if attr in self.delegated_methods:
383     return getattr(getattr(self, self.delegated_methods[attr]), attr)
384     raise AttributeError(attr)
385    
386 bh 6 def OnPaint(self, event):
387     dc = wxPaintDC(self)
388 jonathan 1285
389 jonathan 1344 if self.map is not None and self.map.HasLayers():
390     if self.bitmap in (None, -1):
391     # set the flag that we should redraw the
392     # bitmap in idle time
393     self.bitmap = -1
394     return
395 jonathan 799
396 jonathan 1344 # blit the bitmap to the screen
397     dc.BeginDrawing()
398     dc.DrawBitmap(self.bitmap, 0, 0)
399     dc.EndDrawing()
400     else:
401     # If we've got no map or if the map is empty, simply clear
402     # the screen.
403 jonathan 799
404 jonathan 1344 # XXX it's probably possible to get rid of this. The
405     # background color of the window is already white and the
406     # only thing we may have to do is to call self.Refresh()
407     # with a true argument in the right places.
408     dc.BeginDrawing()
409     dc.SetBackground(self.backgroundColor)
410     dc.Clear()
411     dc.EndDrawing()
412 bh 246
413 jonathan 1344 def OnIdle(self, event):
414     # render the screen if necessary
415 bh 125
416 jonathan 1344 if self.bitmap != -1:
417     return
418 bh 6
419 jonathan 1344 wxBeginBusyCursor()
420     try:
421     width, height = self.GetSizeTuple()
422    
423 bh 125 bitmap = wx.wxEmptyBitmap(width, height)
424     dc = wx.wxMemoryDC()
425     dc.SelectObject(bitmap)
426     dc.BeginDrawing()
427 bh 57
428 jonathan 1344 dc.SetBackground(self.backgroundColor)
429 jonathan 799 dc.Clear()
430 bh 6
431 bh 535 selected_layer = self.selection.SelectedLayer()
432     selected_shapes = self.selection.SelectedShapes()
433 bh 57
434 bh 125 # draw the map into the bitmap
435     renderer = ScreenRenderer(dc, self.scale, self.offset)
436 bh 149
437 bh 296 # Pass the entire bitmap as update region to the renderer.
438 bh 149 # We're redrawing the whole bitmap, after all.
439     renderer.RenderMap(self.map, (0, 0, width, height),
440 bh 535 selected_layer, selected_shapes)
441 bh 125
442     dc.EndDrawing()
443     dc.SelectObject(wx.wxNullBitmap)
444 jonathan 1344
445 bh 125 self.bitmap = bitmap
446 jonathan 1344 finally:
447     wxEndBusyCursor()
448     pass
449 bh 125
450 jonathan 1344 # This causes a paint event that then draws the bitmap
451     self.redraw()
452 bh 6
453 frank 910 def Export(self):
454 jonathan 967 if self.scale == 0:
455     return
456    
457 frank 910 if hasattr(self, "export_path"):
458     export_path = self.export_path
459     else:
460     export_path="."
461     dlg = wxFileDialog(self, _("Export Map"), export_path, "",
462     "Enhanced Metafile (*.wmf)|*.wmf",
463     wxSAVE|wxOVERWRITE_PROMPT)
464     if dlg.ShowModal() == wxID_OK:
465     self.export_path = os.path.dirname(dlg.GetPath())
466     dc = wxMetaFileDC(dlg.GetPath())
467    
468     scale, offset, mapregion = OutputTransform(self.scale,
469 frank 926 self.offset,
470 frank 910 self.GetSizeTuple(),
471     dc.GetSizeTuple())
472    
473     selected_layer = self.selection.SelectedLayer()
474     selected_shapes = self.selection.SelectedShapes()
475    
476     renderer = ExportRenderer(dc, scale, offset)
477    
478     # Pass the entire bitmap as update region to the renderer.
479     # We're redrawing the whole bitmap, after all.
480     width, height = self.GetSizeTuple()
481     renderer.RenderMap(self.map,
482     (0,0,
483     (width/self.scale)*scale,
484     (height/self.scale)*scale),
485     mapregion,
486     selected_layer, selected_shapes)
487     dc.EndDrawing()
488     dc.Close()
489     dlg.Destroy()
490    
491 bh 6 def Print(self):
492     printer = wx.wxPrinter()
493 frank 910 width, height = self.GetSizeTuple()
494     selected_layer = self.selection.SelectedLayer()
495     selected_shapes = self.selection.SelectedShapes()
496    
497     printout = MapPrintout(self, self.map, (0, 0, width, height),
498     selected_layer, selected_shapes)
499 jan 1035 printer.Print(self, printout, True)
500 bh 6 printout.Destroy()
501 bh 246
502 bh 6 def SetMap(self, map):
503 jonathan 565 redraw_channels = (MAP_LAYERS_CHANGED, LAYER_CHANGED,
504 bh 6 LAYER_VISIBILITY_CHANGED)
505     if self.map is not None:
506     for channel in redraw_channels:
507 bh 125 self.map.Unsubscribe(channel, self.full_redraw)
508 bh 6 self.map.Unsubscribe(MAP_PROJECTION_CHANGED,
509 jonathan 1221 self.map_projection_changed)
510     self.map.Unsubscribe(LAYER_PROJECTION_CHANGED,
511     self.layer_projection_changed)
512 bh 6 self.map = map
513 jonathan 1221 self.current_map_proj = self.map.GetProjection()
514 bh 535 self.selection.ClearSelection()
515 bh 6 if self.map is not None:
516     for channel in redraw_channels:
517 bh 125 self.map.Subscribe(channel, self.full_redraw)
518 jonathan 1221 self.map.Subscribe(MAP_PROJECTION_CHANGED, self.map_projection_changed)
519     self.map.Subscribe(LAYER_PROJECTION_CHANGED, self.layer_projection_changed)
520 bh 6 self.FitMapToWindow()
521 bh 57 # force a redraw. If map is not empty, it's already been called
522     # by FitMapToWindow but if map is empty it hasn't been called
523     # yet so we have to explicitly call it.
524 bh 125 self.full_redraw()
525 bh 6
526     def Map(self):
527 bh 295 """Return the map displayed by this canvas"""
528 bh 6 return self.map
529    
530     def redraw(self, *args):
531 jonathan 1344 self.Refresh(False)
532 bh 6
533 bh 125 def full_redraw(self, *args):
534     self.bitmap = None
535     self.redraw()
536    
537 jonathan 1221 def map_projection_changed(self, *args):
538    
539     proj = self.current_map_proj
540     self.current_map_proj = self.map.GetProjection()
541    
542     bbox = None
543    
544     if proj is not None and self.current_map_proj is not None:
545     width, height = self.GetSizeTuple()
546     llx, lly = self.win_to_proj(0, height)
547     urx, ury = self.win_to_proj(width, 0)
548     bbox = proj.Inverse(llx, lly) + proj.Inverse(urx, ury)
549     bbox = self.current_map_proj.ForwardBBox(bbox)
550    
551     if bbox is not None:
552     self.FitRectToWindow(bbox)
553     else:
554     self.FitMapToWindow()
555    
556 bh 125 self.full_redraw()
557 bh 6
558 jonathan 1221 def layer_projection_changed(self, *args):
559     self.full_redraw()
560    
561 bh 6 def set_view_transform(self, scale, offset):
562 bh 1111 # width/height of the projected bbox
563     llx, lly, urx, ury = bbox = self.map.ProjectedBoundingBox()
564     pwidth = float(urx - llx)
565     pheight = float(ury - lly)
566    
567     # width/height of the window
568     wwidth, wheight = self.GetSizeTuple()
569    
570     # The window's center in projected coordinates assuming the new
571     # scale/offset
572     pcenterx = (wwidth/2 - offset[0]) / scale
573     pcentery = (offset[1] - wheight/2) / scale
574    
575     # The window coordinates used when drawing the shapes must fit
576     # into 16bit signed integers.
577     max_len = max(pwidth, pheight)
578     if max_len:
579     max_scale = 32000.0 / max_len
580     else:
581     # FIXME: What to do in this case? The bbox is effectively
582     # empty so any scale should work.
583     max_scale = scale
584    
585     # The minimal scale is somewhat arbitrarily set to half that of
586     # the bbox fit into the window
587     scales = []
588     if pwidth:
589     scales.append(wwidth / pwidth)
590     if pheight:
591     scales.append(wheight / pheight)
592     if scales:
593     min_scale = 0.5 * min(scales)
594     else:
595     min_scale = scale
596    
597     if scale > max_scale:
598     scale = max_scale
599     elif scale < min_scale:
600     scale = min_scale
601    
602 bh 6 self.scale = scale
603 jonathan 940
604 bh 1111 # determine new offset to preserve the center
605     self.offset = (wwidth/2 - scale * pcenterx,
606     wheight/2 + scale * pcentery)
607 bh 125 self.full_redraw()
608 frank 855 self.issue(SCALE_CHANGED, scale)
609 bh 6
610     def proj_to_win(self, x, y):
611     """\
612     Return the point in window coords given by projected coordinates x y
613     """
614 jonathan 967 if self.scale == 0:
615     return (0, 0)
616    
617 bh 6 offx, offy = self.offset
618     return (self.scale * x + offx, -self.scale * y + offy)
619    
620     def win_to_proj(self, x, y):
621     """\
622     Return the point in projected coordinates given by window coords x y
623     """
624 jonathan 967 if self.scale == 0:
625     return (0, 0)
626    
627 bh 6 offx, offy = self.offset
628     return ((x - offx) / self.scale, (offy - y) / self.scale)
629    
630     def FitRectToWindow(self, rect):
631 bh 293 """Fit the rectangular region given by rect into the window.
632 bh 535
633 bh 293 Set scale so that rect (in projected coordinates) just fits into
634     the window and center it.
635     """
636 bh 6 width, height = self.GetSizeTuple()
637     llx, lly, urx, ury = rect
638 bh 45 if llx == urx or lly == ury:
639 jonathan 799 # zero width or zero height. Do Nothing
640 bh 45 return
641 bh 6 scalex = width / (urx - llx)
642     scaley = height / (ury - lly)
643     scale = min(scalex, scaley)
644     offx = 0.5 * (width - (urx + llx) * scale)
645     offy = 0.5 * (height + (ury + lly) * scale)
646     self.set_view_transform(scale, (offx, offy))
647    
648     def FitMapToWindow(self):
649 bh 293 """Fit the map to the window
650 bh 535
651 bh 293 Set the scale so that the map fits exactly into the window and
652     center it in the window.
653 bh 6 """
654 jonathan 1236 if self.map is not None:
655     bbox = self.map.ProjectedBoundingBox()
656     if bbox is not None:
657     self.FitRectToWindow(bbox)
658 bh 6
659 jonathan 822 def FitLayerToWindow(self, layer):
660     """Fit the given layer to the window.
661    
662     Set the scale so that the layer fits exactly into the window and
663     center it in the window.
664     """
665    
666     bbox = layer.LatLongBoundingBox()
667     if bbox is not None:
668     proj = self.map.GetProjection()
669     if proj is not None:
670     bbox = proj.ForwardBBox(bbox)
671    
672 jonathan 824 if bbox is not None:
673     self.FitRectToWindow(bbox)
674    
675 jonathan 831 def FitSelectedToWindow(self):
676     layer = self.selection.SelectedLayer()
677     shapes = self.selection.SelectedShapes()
678    
679     bbox = layer.ShapesBoundingBox(shapes)
680     if bbox is not None:
681     proj = self.map.GetProjection()
682     if proj is not None:
683     bbox = proj.ForwardBBox(bbox)
684    
685     if bbox is not None:
686 jonathan 1105 if len(shapes) == 1 and layer.ShapeType() == SHAPETYPE_POINT:
687     self.ZoomFactor(1, self.proj_to_win(bbox[0], bbox[1]))
688     else:
689     self.FitRectToWindow(bbox)
690 jonathan 831
691 bh 57 def ZoomFactor(self, factor, center = None):
692     """Multiply the zoom by factor and center on center.
693    
694     The optional parameter center is a point in window coordinates
695     that should be centered. If it is omitted, it defaults to the
696     center of the window
697     """
698 jonathan 967 if self.scale > 0:
699     width, height = self.GetSizeTuple()
700     scale = self.scale * factor
701     offx, offy = self.offset
702     if center is not None:
703     cx, cy = center
704     else:
705     cx = width / 2
706     cy = height / 2
707     offset = (factor * (offx - cx) + width / 2,
708     factor * (offy - cy) + height / 2)
709     self.set_view_transform(scale, offset)
710 bh 6
711     def ZoomOutToRect(self, rect):
712 bh 293 """Zoom out to fit the currently visible region into rect.
713 bh 6
714 bh 293 The rect parameter is given in window coordinates
715     """
716 bh 6 # determine the bbox of the displayed region in projected
717     # coordinates
718     width, height = self.GetSizeTuple()
719     llx, lly = self.win_to_proj(0, height - 1)
720     urx, ury = self.win_to_proj(width - 1, 0)
721    
722     sx, sy, ex, ey = rect
723     scalex = (ex - sx) / (urx - llx)
724     scaley = (ey - sy) / (ury - lly)
725     scale = min(scalex, scaley)
726    
727     offx = 0.5 * ((ex + sx) - (urx + llx) * scale)
728     offy = 0.5 * ((ey + sy) + (ury + lly) * scale)
729     self.set_view_transform(scale, (offx, offy))
730    
731     def Translate(self, dx, dy):
732 bh 295 """Move the map by dx, dy pixels"""
733 bh 6 offx, offy = self.offset
734     self.set_view_transform(self.scale, (offx + dx, offy + dy))
735    
736 bh 356 def SelectTool(self, tool):
737     """Make tool the active tool.
738    
739     The parameter should be an instance of Tool or None to indicate
740     that no tool is active.
741     """
742     self.tool = tool
743    
744 bh 6 def ZoomInTool(self):
745 bh 295 """Start the zoom in tool"""
746 bh 356 self.SelectTool(ZoomInTool(self))
747 bh 6
748     def ZoomOutTool(self):
749 bh 295 """Start the zoom out tool"""
750 bh 356 self.SelectTool(ZoomOutTool(self))
751 bh 6
752     def PanTool(self):
753 bh 295 """Start the pan tool"""
754 bh 356 self.SelectTool(PanTool(self))
755 jonathan 822 #img = resource.GetImageResource("pan", wxBITMAP_TYPE_XPM)
756     #bmp = resource.GetBitmapResource("pan", wxBITMAP_TYPE_XPM)
757     #print bmp
758     #img = wxImageFromBitmap(bmp)
759     #print img
760     #cur = wxCursor(img)
761     #print cur
762     #self.SetCursor(cur)
763 bh 6
764     def IdentifyTool(self):
765 bh 295 """Start the identify tool"""
766 bh 356 self.SelectTool(IdentifyTool(self))
767 bh 6
768     def LabelTool(self):
769 bh 295 """Start the label tool"""
770 bh 356 self.SelectTool(LabelTool(self))
771 bh 6
772     def CurrentTool(self):
773 bh 295 """Return the name of the current tool or None if no tool is active"""
774 bh 6 return self.tool and self.tool.Name() or None
775    
776 bh 122 def CurrentPosition(self):
777     """Return current position of the mouse in projected coordinates.
778    
779     The result is a 2-tuple of floats with the coordinates. If the
780     mouse is not in the window, the result is None.
781     """
782     if self.current_position is not None:
783     x, y = self.current_position
784     return self.win_to_proj(x, y)
785     else:
786     return None
787    
788     def set_current_position(self, event):
789     """Set the current position from event
790    
791     Should be called by all events that contain mouse positions
792     especially EVT_MOTION. The event paramete may be None to
793     indicate the the pointer left the window.
794     """
795     if event is not None:
796     self.current_position = (event.m_x, event.m_y)
797     else:
798     self.current_position = None
799     self.issue(VIEW_POSITION)
800    
801 bh 6 def OnLeftDown(self, event):
802 bh 122 self.set_current_position(event)
803 bh 6 if self.tool is not None:
804     self.drag_dc = wxClientDC(self)
805     self.drag_dc.SetLogicalFunction(wxINVERT)
806     self.drag_dc.SetBrush(wxTRANSPARENT_BRUSH)
807     self.CaptureMouse()
808     self.tool.MouseDown(event)
809     self.tool.Show(self.drag_dc)
810     self.dragging = 1
811 bh 246
812 bh 6 def OnLeftUp(self, event):
813 bh 122 self.set_current_position(event)
814 bh 6 if self.dragging:
815 bh 261 self.ReleaseMouse()
816 bh 404 try:
817     self.tool.Hide(self.drag_dc)
818     self.tool.MouseUp(event)
819     finally:
820     self.drag_dc = None
821     self.dragging = 0
822 bh 6
823     def OnMotion(self, event):
824 bh 122 self.set_current_position(event)
825 bh 6 if self.dragging:
826     self.tool.Hide(self.drag_dc)
827     self.tool.MouseMove(event)
828     self.tool.Show(self.drag_dc)
829    
830 bh 122 def OnLeaveWindow(self, event):
831     self.set_current_position(None)
832    
833 bh 125 def OnSize(self, event):
834     # the window's size has changed. We have to get a new bitmap. If
835     # we want to be clever we could try to get by without throwing
836     # everything away. E.g. when the window gets smaller, we could
837     # either keep the bitmap or create the new one from the old one.
838     # Even when the window becomes larger some parts of the bitmap
839     # could be reused.
840     self.full_redraw()
841 jonathan 799 pass
842 bh 125
843 bh 6 def shape_selected(self, layer, shape):
844 bh 535 """Receiver for the SHAPES_SELECTED messages. Redraw the map."""
845     # The selection object takes care that it only issues
846     # SHAPES_SELECTED messages when the set of selected shapes has
847     # actually changed, so we can do a full redraw unconditionally.
848     # FIXME: We should perhaps try to limit the redraw to the are
849     # actually covered by the shapes before and after the selection
850     # change.
851     self.full_redraw()
852 bh 6
853 bh 301 def unprojected_rect_around_point(self, x, y, dist):
854     """return a rect dist pixels around (x, y) in unprojected corrdinates
855 bh 159
856     The return value is a tuple (minx, miny, maxx, maxy) suitable a
857     parameter to a layer's ShapesInRegion method.
858     """
859     map_proj = self.map.projection
860     if map_proj is not None:
861     inverse = map_proj.Inverse
862     else:
863     inverse = None
864    
865     xs = []
866     ys = []
867     for dx, dy in ((-1, -1), (1, -1), (1, 1), (-1, 1)):
868 bh 301 px, py = self.win_to_proj(x + dist * dx, y + dist * dy)
869 bh 159 if inverse:
870     px, py = inverse(px, py)
871     xs.append(px)
872     ys.append(py)
873     return (min(xs), min(ys), max(xs), max(ys))
874    
875 bh 246 def find_shape_at(self, px, py, select_labels = 0, searched_layer = None):
876 bh 43 """Determine the shape at point px, py in window coords
877    
878     Return the shape and the corresponding layer as a tuple (layer,
879     shape).
880    
881     If the optional parameter select_labels is true (default false)
882     search through the labels. If a label is found return it's index
883     as the shape and None as the layer.
884    
885 bh 246 If the optional parameter searched_layer is given (or not None
886     which it defaults to), only search in that layer.
887 bh 43 """
888 bh 6 map_proj = self.map.projection
889     if map_proj is not None:
890     forward = map_proj.Forward
891     else:
892     forward = None
893    
894     scale = self.scale
895 jonathan 967
896     if scale == 0:
897     return None, None
898    
899 bh 6 offx, offy = self.offset
900    
901     if select_labels:
902     labels = self.map.LabelLayer().Labels()
903 bh 246
904 bh 6 if labels:
905     dc = wxClientDC(self)
906     font = wxFont(10, wx.wxSWISS, wx.wxNORMAL, wx.wxNORMAL)
907     dc.SetFont(font)
908 bh 60 for i in range(len(labels) - 1, -1, -1):
909 bh 6 label = labels[i]
910     x = label.x
911     y = label.y
912     text = label.text
913     if forward:
914     x, y = forward(x, y)
915     x = x * scale + offx
916     y = -y * scale + offy
917     width, height = dc.GetTextExtent(text)
918     if label.halign == ALIGN_LEFT:
919     # nothing to be done
920     pass
921     elif label.halign == ALIGN_RIGHT:
922     x = x - width
923     elif label.halign == ALIGN_CENTER:
924     x = x - width/2
925     if label.valign == ALIGN_TOP:
926     # nothing to be done
927     pass
928     elif label.valign == ALIGN_BOTTOM:
929     y = y - height
930     elif label.valign == ALIGN_CENTER:
931     y = y - height/2
932     if x <= px < x + width and y <= py <= y + height:
933     return None, i
934 bh 43
935 bh 246 if searched_layer:
936     layers = [searched_layer]
937 bh 43 else:
938     layers = self.map.Layers()
939    
940 bh 6 for layer_index in range(len(layers) - 1, -1, -1):
941     layer = layers[layer_index]
942    
943     # search only in visible layers
944 jonathan 1277 if not layer.Visible() or not layer.HasShapes():
945 bh 6 continue
946    
947 jonathan 433 filled = layer.GetClassification().GetDefaultFill() \
948 jonathan 1344 is not Transparent
949 jonathan 469 stroked = layer.GetClassification().GetDefaultLineColor() \
950 jonathan 1344 is not Transparent
951 bh 246
952 bh 6 layer_proj = layer.projection
953     if layer_proj is not None:
954     inverse = layer_proj.Inverse
955     else:
956     inverse = None
957 bh 246
958 bh 6 shapetype = layer.ShapeType()
959    
960     select_shape = -1
961 bh 159
962 bh 301 # Determine the ids of the shapes that overlap a tiny area
963     # around the point. For layers containing points we have to
964     # choose a larger size of the box we're testing agains so
965     # that we take the size of the markers into account
966     # FIXME: Once the markers are more flexible this part has to
967     # become more flexible too, of course
968     if shapetype == SHAPETYPE_POINT:
969     box = self.unprojected_rect_around_point(px, py, 5)
970     else:
971     box = self.unprojected_rect_around_point(px, py, 1)
972 bh 159 shape_ids = layer.ShapesInRegion(box)
973     shape_ids.reverse()
974    
975 bh 6 if shapetype == SHAPETYPE_POLYGON:
976 bh 159 for i in shape_ids:
977 bh 1219 shapefile = layer.ShapeStore().Shapefile().cobject()
978     result = point_in_polygon_shape(shapefile, i,
979 bh 6 filled, stroked,
980     map_proj, layer_proj,
981     scale, -scale, offx, offy,
982     px, py)
983     if result:
984     select_shape = i
985     break
986     elif shapetype == SHAPETYPE_ARC:
987 bh 159 for i in shape_ids:
988 bh 1219 shapefile = layer.ShapeStore().Shapefile().cobject()
989     result = point_in_polygon_shape(shapefile,
990 bh 6 i, 0, 1,
991     map_proj, layer_proj,
992     scale, -scale, offx, offy,
993     px, py)
994     if result < 0:
995     select_shape = i
996     break
997     elif shapetype == SHAPETYPE_POINT:
998 bh 159 for i in shape_ids:
999 bh 6 shape = layer.Shape(i)
1000     x, y = shape.Points()[0]
1001     if inverse:
1002     x, y = inverse(x, y)
1003     if forward:
1004     x, y = forward(x, y)
1005     x = x * scale + offx
1006     y = -y * scale + offy
1007     if hypot(px - x, py - y) < 5:
1008     select_shape = i
1009     break
1010    
1011     if select_shape >= 0:
1012     return layer, select_shape
1013     return None, None
1014    
1015 bh 246 def SelectShapeAt(self, x, y, layer = None):
1016     """\
1017     Select and return the shape and its layer at window position (x, y)
1018    
1019     If layer is given, only search in that layer. If no layer is
1020     given, search through all layers.
1021    
1022     Return a tuple (layer, shapeid). If no shape is found, return
1023     (None, None).
1024     """
1025     layer, shape = result = self.find_shape_at(x, y, searched_layer=layer)
1026 bh 43 # If layer is None, then shape will also be None. We don't want
1027     # to deselect the currently selected layer, so we simply select
1028     # the already selected layer again.
1029     if layer is None:
1030 bh 535 layer = self.selection.SelectedLayer()
1031     shapes = []
1032     else:
1033     shapes = [shape]
1034     self.selection.SelectShapes(layer, shapes)
1035 bh 246 return result
1036 bh 6
1037     def LabelShapeAt(self, x, y):
1038 bh 295 """Add or remove a label at window position x, y.
1039    
1040     If there's a label at the given position, remove it. Otherwise
1041     determine the shape at the position, run the label dialog and
1042     unless the user cancels the dialog, add a laber.
1043     """
1044 bh 6 ox = x; oy = y
1045     label_layer = self.map.LabelLayer()
1046     layer, shape_index = self.find_shape_at(x, y, select_labels = 1)
1047     if layer is None and shape_index is not None:
1048     # a label was selected
1049     label_layer.RemoveLabel(shape_index)
1050     elif layer is not None:
1051 bh 1219 text = labeldialog.run_label_dialog(self,
1052     layer.ShapeStore().Table(),
1053     shape_index)
1054 bh 6 if text:
1055     proj = self.map.projection
1056     if proj is not None:
1057     map_proj = proj
1058     else:
1059     map_proj = None
1060     proj = layer.projection
1061     if proj is not None:
1062     layer_proj = proj
1063     else:
1064     layer_proj = None
1065    
1066     shapetype = layer.ShapeType()
1067     if shapetype == SHAPETYPE_POLYGON:
1068 bh 1219 shapefile = layer.ShapeStore().Shapefile().cobject()
1069     x, y = shape_centroid(shapefile, shape_index,
1070 bh 6 map_proj, layer_proj, 1, 1, 0, 0)
1071     if map_proj is not None:
1072     x, y = map_proj.Inverse(x, y)
1073     else:
1074     shape = layer.Shape(shape_index)
1075     if shapetype == SHAPETYPE_POINT:
1076     x, y = shape.Points()[0]
1077     else:
1078     # assume SHAPETYPE_ARC
1079     points = shape.Points()
1080     x, y = points[len(points) / 2]
1081     if layer_proj is not None:
1082     x, y = layer_proj.Inverse(x, y)
1083     if shapetype == SHAPETYPE_POINT:
1084     halign = ALIGN_LEFT
1085     valign = ALIGN_CENTER
1086     elif shapetype == SHAPETYPE_POLYGON:
1087     halign = ALIGN_CENTER
1088     valign = ALIGN_CENTER
1089     elif shapetype == SHAPETYPE_ARC:
1090     halign = ALIGN_LEFT
1091     valign = ALIGN_CENTER
1092     label_layer.AddLabel(x, y, text,
1093     halign = halign, valign = valign)
1094 frank 910
1095     def OutputTransform(canvas_scale, canvas_offset, canvas_size, device_extend):
1096     """Calculate dimensions to transform canvas content to output device."""
1097     width, height = device_extend
1098    
1099     # Only 80 % of the with are available for the map
1100     width = width * 0.8
1101    
1102     # Define the distance of the map from DC border
1103     distance = 20
1104    
1105     if height < width:
1106     # landscape
1107     map_height = height - 2*distance
1108     map_width = map_height
1109     else:
1110     # portrait, recalibrate width (usually the legend width is too
1111     # small
1112     width = width * 0.9
1113     map_height = width - 2*distance
1114     map_width = map_height
1115    
1116     mapregion = (distance, distance,
1117     distance+map_width, distance+map_height)
1118    
1119     canvas_width, canvas_height = canvas_size
1120    
1121     scalex = map_width / (canvas_width/canvas_scale)
1122     scaley = map_height / (canvas_height/canvas_scale)
1123     scale = min(scalex, scaley)
1124     canvas_offx, canvas_offy = canvas_offset
1125     offx = scale*canvas_offx/canvas_scale
1126     offy = scale*canvas_offy/canvas_scale
1127    
1128     return scale, (offx, offy), mapregion

Properties

Name Value
svn:eol-style native
svn:keywords Author Date Id Revision

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26