/[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 1277 - (hide annotations)
Fri Jun 20 17:46:45 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: 39325 byte(s)
Use Thuban[Begin|End]BusyCursor()
        instead of a direct call to wx[Begin|End]CusyCursor().
(MapCanvas.find_shape_at): Check if the current search layer
        support shapes, otherwise go on to the next layer.

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