/[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 1221 - (hide annotations)
Tue Jun 17 15:24: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: 39209 byte(s)
(MapCanvas.__init__): New instance variable
        current_map_proj to remember the current map projection so that
        when the projection changes we know what the previous projection
        was.
(MapCanvas.SetMap): Unsubscribe and subscribe to
        LAYER_PROJECTION_CHANGED events.
(MapCanvas.projection_changed): Split into two methods that respond
        to map and layer projection changes.
(MapCanvas.map_projection_changed): New. Takes the current view and
        projects it using the new projection. This does not cause the
        map to be redrawn at full extent.
(MapCanvas.layer_projection_changed): New. Cause a redraw which
        will draw each layer in its new projection.

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     from wxPython.wx import wxWindow,\
23     wxPaintDC, wxColour, wxClientDC, wxINVERT, wxTRANSPARENT_BRUSH, wxFont,\
24 jonathan 822 EVT_PAINT, EVT_LEFT_DOWN, EVT_LEFT_UP, EVT_MOTION, EVT_LEAVE_WINDOW, \
25     wxBITMAP_TYPE_XPM, wxBeginBusyCursor, wxEndBusyCursor, wxCursor, \
26 frank 910 wxImageFromBitmap, wxPlatform
27 bh 6
28 frank 910 # Export related stuff
29     if wxPlatform == '__WXMSW__':
30     from wxPython.wx import wxMetaFileDC
31     from wxPython.wx import wxFileDialog, wxSAVE, wxOVERWRITE_PROMPT, wxID_OK
32 bh 6
33     from wxPython import wx
34    
35     from wxproj import point_in_polygon_shape, shape_centroid
36    
37     from Thuban.Model.messages import MAP_PROJECTION_CHANGED, \
38 jonathan 1221 LAYER_PROJECTION_CHANGED, \
39 jonathan 565 MAP_LAYERS_CHANGED, LAYER_CHANGED, LAYER_VISIBILITY_CHANGED
40 bh 6 from Thuban.Model.layer import SHAPETYPE_POLYGON, SHAPETYPE_ARC, \
41     SHAPETYPE_POINT
42     from Thuban.Model.label import ALIGN_CENTER, ALIGN_TOP, ALIGN_BOTTOM, \
43     ALIGN_LEFT, ALIGN_RIGHT
44 bh 122 from Thuban.Lib.connector import Publisher
45 jonathan 433 from Thuban.Model.color import Color
46 bh 6
47 jonathan 822 import resource
48    
49 bh 535 from selection import Selection
50 frank 910 from renderer import ScreenRenderer, ExportRenderer, PrinterRenderer
51 bh 6
52     import labeldialog
53    
54 frank 855 from messages import LAYER_SELECTED, SHAPES_SELECTED, VIEW_POSITION, \
55     SCALE_CHANGED
56 bh 6
57    
58     #
59     # The tools
60     #
61    
62     class Tool:
63    
64     """
65     Base class for the interactive tools
66     """
67    
68     def __init__(self, view):
69     """Intitialize the tool. The view is the canvas displaying the map"""
70     self.view = view
71     self.start = self.current = None
72     self.dragging = 0
73     self.drawn = 0
74    
75     def Name(self):
76     """Return the tool's name"""
77     return ''
78    
79     def drag_start(self, x, y):
80     self.start = self.current = x, y
81     self.dragging = 1
82    
83     def drag_move(self, x, y):
84     self.current = x, y
85    
86     def drag_stop(self, x, y):
87     self.current = x, y
88     self.dragging = 0
89    
90     def Show(self, dc):
91     if not self.drawn:
92     self.draw(dc)
93     self.drawn = 1
94    
95     def Hide(self, dc):
96     if self.drawn:
97     self.draw(dc)
98     self.drawn = 0
99    
100     def draw(self, dc):
101     pass
102    
103     def MouseDown(self, event):
104     self.drag_start(event.m_x, event.m_y)
105    
106     def MouseMove(self, event):
107     if self.dragging:
108     self.drag_move(event.m_x, event.m_y)
109    
110     def MouseUp(self, event):
111     if self.dragging:
112     self.drag_move(event.m_x, event.m_y)
113    
114     def Cancel(self):
115     self.dragging = 0
116    
117    
118     class RectTool(Tool):
119    
120     """Base class for tools that draw rectangles while dragging"""
121    
122     def draw(self, dc):
123     sx, sy = self.start
124     cx, cy = self.current
125     dc.DrawRectangle(sx, sy, cx - sx, cy - sy)
126    
127     class ZoomInTool(RectTool):
128    
129     """The Zoom-In Tool"""
130    
131     def Name(self):
132     return "ZoomInTool"
133    
134     def proj_rect(self):
135     """return the rectangle given by start and current in projected
136     coordinates"""
137     sx, sy = self.start
138     cx, cy = self.current
139     left, top = self.view.win_to_proj(sx, sy)
140     right, bottom = self.view.win_to_proj(cx, cy)
141     return (min(left, right), min(top, bottom),
142     max(left, right), max(top, bottom))
143    
144     def MouseUp(self, event):
145     if self.dragging:
146     Tool.MouseUp(self, event)
147 bh 57 sx, sy = self.start
148     cx, cy = self.current
149 bh 288 if sx == cx or sy == cy:
150     # Just a mouse click or a degenerate rectangle. Simply
151     # zoom in by a factor of two
152     # FIXME: For a click this is the desired behavior but should we
153     # really do this for degenrate rectagles as well or
154     # should we ignore them?
155 bh 57 self.view.ZoomFactor(2, center = (cx, cy))
156     else:
157     # A drag. Zoom in to the rectangle
158     self.view.FitRectToWindow(self.proj_rect())
159 bh 6
160    
161     class ZoomOutTool(RectTool):
162    
163     """The Zoom-Out Tool"""
164 bh 239
165 bh 6 def Name(self):
166     return "ZoomOutTool"
167    
168     def MouseUp(self, event):
169     if self.dragging:
170     Tool.MouseUp(self, event)
171     sx, sy = self.start
172     cx, cy = self.current
173 bh 288 if sx == cx or sy == cy:
174     # Just a mouse click or a degenerate rectangle. Simply
175     # zoom out by a factor of two.
176     # FIXME: For a click this is the desired behavior but should we
177     # really do this for degenrate rectagles as well or
178     # should we ignore them?
179 bh 239 self.view.ZoomFactor(0.5, center = (cx, cy))
180 bh 57 else:
181     # A drag. Zoom out to the rectangle
182     self.view.ZoomOutToRect((min(sx, cx), min(sy, cy),
183     max(sx, cx), max(sy, cy)))
184 bh 6
185    
186     class PanTool(Tool):
187    
188     """The Pan Tool"""
189    
190     def Name(self):
191     return "PanTool"
192    
193     def MouseMove(self, event):
194     if self.dragging:
195     Tool.MouseMove(self, event)
196 bh 159 sx, sy = self.start
197 bh 6 x, y = self.current
198     width, height = self.view.GetSizeTuple()
199 bh 159
200     bitmapdc = wx.wxMemoryDC()
201     bitmapdc.SelectObject(self.view.bitmap)
202    
203 bh 6 dc = self.view.drag_dc
204 bh 159 dc.Blit(0, 0, width, height, bitmapdc, sx - x, sy - y)
205 bh 6
206     def MouseUp(self, event):
207     if self.dragging:
208     Tool.MouseUp(self, event)
209     sx, sy = self.start
210     cx, cy = self.current
211     self.view.Translate(cx - sx, cy - sy)
212 bh 246
213 bh 6 class IdentifyTool(Tool):
214    
215     """The "Identify" Tool"""
216 bh 246
217 bh 6 def Name(self):
218     return "IdentifyTool"
219    
220     def MouseUp(self, event):
221     self.view.SelectShapeAt(event.m_x, event.m_y)
222    
223    
224     class LabelTool(Tool):
225    
226     """The "Label" Tool"""
227    
228     def Name(self):
229     return "LabelTool"
230    
231     def MouseUp(self, event):
232     self.view.LabelShapeAt(event.m_x, event.m_y)
233    
234    
235     class MapPrintout(wx.wxPrintout):
236    
237     """
238     wxPrintout class for printing Thuban maps
239     """
240    
241 frank 910 def __init__(self, canvas, map, region, selected_layer, selected_shapes):
242 bh 6 wx.wxPrintout.__init__(self)
243 frank 910 self.canvas = canvas
244 bh 6 self.map = map
245 frank 910 self.region = region
246     self.selected_layer = selected_layer
247     self.selected_shapes = selected_shapes
248 bh 6
249     def GetPageInfo(self):
250     return (1, 1, 1, 1)
251    
252     def HasPage(self, pagenum):
253     return pagenum == 1
254    
255     def OnPrintPage(self, pagenum):
256     if pagenum == 1:
257     self.draw_on_dc(self.GetDC())
258    
259     def draw_on_dc(self, dc):
260     width, height = self.GetPageSizePixels()
261 frank 910 scale, offset, mapregion = OutputTransform(self.canvas.scale,
262     self.canvas.offset,
263     self.canvas.GetSizeTuple(),
264     self.GetPageSizePixels())
265 bh 6 resx, resy = self.GetPPIPrinter()
266 frank 910 renderer = PrinterRenderer(dc, scale, offset, resolution = resy)
267     x, y, width, height = self.region
268     canvas_scale = self.canvas.scale
269     renderer.RenderMap(self.map,
270     (0,0,
271     (width/canvas_scale)*scale,
272     (height/canvas_scale)*scale),
273     mapregion,
274     self.selected_layer, self.selected_shapes)
275 jan 1035 return True
276 bh 6
277 bh 246
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 940 wxBeginBusyCursor()
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     wxEndBusyCursor()
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     bbox = self.map.ProjectedBoundingBox()
657     if bbox is not None:
658     self.FitRectToWindow(bbox)
659    
660 jonathan 822 def FitLayerToWindow(self, layer):
661     """Fit the given layer to the window.
662    
663     Set the scale so that the layer fits exactly into the window and
664     center it in the window.
665     """
666    
667     bbox = layer.LatLongBoundingBox()
668     if bbox is not None:
669     proj = self.map.GetProjection()
670     if proj is not None:
671     bbox = proj.ForwardBBox(bbox)
672    
673 jonathan 824 if bbox is not None:
674     self.FitRectToWindow(bbox)
675    
676 jonathan 831 def FitSelectedToWindow(self):
677     layer = self.selection.SelectedLayer()
678     shapes = self.selection.SelectedShapes()
679    
680     bbox = layer.ShapesBoundingBox(shapes)
681     if bbox is not None:
682     proj = self.map.GetProjection()
683     if proj is not None:
684     bbox = proj.ForwardBBox(bbox)
685    
686     if bbox is not None:
687 jonathan 1105 if len(shapes) == 1 and layer.ShapeType() == SHAPETYPE_POINT:
688     self.ZoomFactor(1, self.proj_to_win(bbox[0], bbox[1]))
689     else:
690     self.FitRectToWindow(bbox)
691 jonathan 831
692 bh 57 def ZoomFactor(self, factor, center = None):
693     """Multiply the zoom by factor and center on center.
694    
695     The optional parameter center is a point in window coordinates
696     that should be centered. If it is omitted, it defaults to the
697     center of the window
698     """
699 jonathan 967 if self.scale > 0:
700     width, height = self.GetSizeTuple()
701     scale = self.scale * factor
702     offx, offy = self.offset
703     if center is not None:
704     cx, cy = center
705     else:
706     cx = width / 2
707     cy = height / 2
708     offset = (factor * (offx - cx) + width / 2,
709     factor * (offy - cy) + height / 2)
710     self.set_view_transform(scale, offset)
711 bh 6
712     def ZoomOutToRect(self, rect):
713 bh 293 """Zoom out to fit the currently visible region into rect.
714 bh 6
715 bh 293 The rect parameter is given in window coordinates
716     """
717 bh 6 # determine the bbox of the displayed region in projected
718     # coordinates
719     width, height = self.GetSizeTuple()
720     llx, lly = self.win_to_proj(0, height - 1)
721     urx, ury = self.win_to_proj(width - 1, 0)
722    
723     sx, sy, ex, ey = rect
724     scalex = (ex - sx) / (urx - llx)
725     scaley = (ey - sy) / (ury - lly)
726     scale = min(scalex, scaley)
727    
728     offx = 0.5 * ((ex + sx) - (urx + llx) * scale)
729     offy = 0.5 * ((ey + sy) + (ury + lly) * scale)
730     self.set_view_transform(scale, (offx, offy))
731    
732     def Translate(self, dx, dy):
733 bh 295 """Move the map by dx, dy pixels"""
734 bh 6 offx, offy = self.offset
735     self.set_view_transform(self.scale, (offx + dx, offy + dy))
736    
737 bh 356 def SelectTool(self, tool):
738     """Make tool the active tool.
739    
740     The parameter should be an instance of Tool or None to indicate
741     that no tool is active.
742     """
743     self.tool = tool
744    
745 bh 6 def ZoomInTool(self):
746 bh 295 """Start the zoom in tool"""
747 bh 356 self.SelectTool(ZoomInTool(self))
748 bh 6
749     def ZoomOutTool(self):
750 bh 295 """Start the zoom out tool"""
751 bh 356 self.SelectTool(ZoomOutTool(self))
752 bh 6
753     def PanTool(self):
754 bh 295 """Start the pan tool"""
755 bh 356 self.SelectTool(PanTool(self))
756 jonathan 822 #img = resource.GetImageResource("pan", wxBITMAP_TYPE_XPM)
757     #bmp = resource.GetBitmapResource("pan", wxBITMAP_TYPE_XPM)
758     #print bmp
759     #img = wxImageFromBitmap(bmp)
760     #print img
761     #cur = wxCursor(img)
762     #print cur
763     #self.SetCursor(cur)
764 bh 6
765     def IdentifyTool(self):
766 bh 295 """Start the identify tool"""
767 bh 356 self.SelectTool(IdentifyTool(self))
768 bh 6
769     def LabelTool(self):
770 bh 295 """Start the label tool"""
771 bh 356 self.SelectTool(LabelTool(self))
772 bh 6
773     def CurrentTool(self):
774 bh 295 """Return the name of the current tool or None if no tool is active"""
775 bh 6 return self.tool and self.tool.Name() or None
776    
777 bh 122 def CurrentPosition(self):
778     """Return current position of the mouse in projected coordinates.
779    
780     The result is a 2-tuple of floats with the coordinates. If the
781     mouse is not in the window, the result is None.
782     """
783     if self.current_position is not None:
784     x, y = self.current_position
785     return self.win_to_proj(x, y)
786     else:
787     return None
788    
789     def set_current_position(self, event):
790     """Set the current position from event
791    
792     Should be called by all events that contain mouse positions
793     especially EVT_MOTION. The event paramete may be None to
794     indicate the the pointer left the window.
795     """
796     if event is not None:
797     self.current_position = (event.m_x, event.m_y)
798     else:
799     self.current_position = None
800     self.issue(VIEW_POSITION)
801    
802 bh 6 def OnLeftDown(self, event):
803 bh 122 self.set_current_position(event)
804 bh 6 if self.tool is not None:
805     self.drag_dc = wxClientDC(self)
806     self.drag_dc.SetLogicalFunction(wxINVERT)
807     self.drag_dc.SetBrush(wxTRANSPARENT_BRUSH)
808     self.CaptureMouse()
809     self.tool.MouseDown(event)
810     self.tool.Show(self.drag_dc)
811     self.dragging = 1
812 bh 246
813 bh 6 def OnLeftUp(self, event):
814 bh 122 self.set_current_position(event)
815 bh 6 if self.dragging:
816 bh 261 self.ReleaseMouse()
817 bh 404 try:
818     self.tool.Hide(self.drag_dc)
819     self.tool.MouseUp(event)
820     finally:
821     self.drag_dc = None
822     self.dragging = 0
823 bh 6
824     def OnMotion(self, event):
825 bh 122 self.set_current_position(event)
826 bh 6 if self.dragging:
827     self.tool.Hide(self.drag_dc)
828     self.tool.MouseMove(event)
829     self.tool.Show(self.drag_dc)
830    
831 bh 122 def OnLeaveWindow(self, event):
832     self.set_current_position(None)
833    
834 bh 125 def OnSize(self, event):
835     # the window's size has changed. We have to get a new bitmap. If
836     # we want to be clever we could try to get by without throwing
837     # everything away. E.g. when the window gets smaller, we could
838     # either keep the bitmap or create the new one from the old one.
839     # Even when the window becomes larger some parts of the bitmap
840     # could be reused.
841     self.full_redraw()
842 jonathan 799 pass
843 bh 125
844 bh 6 def shape_selected(self, layer, shape):
845 bh 535 """Receiver for the SHAPES_SELECTED messages. Redraw the map."""
846     # The selection object takes care that it only issues
847     # SHAPES_SELECTED messages when the set of selected shapes has
848     # actually changed, so we can do a full redraw unconditionally.
849     # FIXME: We should perhaps try to limit the redraw to the are
850     # actually covered by the shapes before and after the selection
851     # change.
852     self.full_redraw()
853 bh 6
854 bh 301 def unprojected_rect_around_point(self, x, y, dist):
855     """return a rect dist pixels around (x, y) in unprojected corrdinates
856 bh 159
857     The return value is a tuple (minx, miny, maxx, maxy) suitable a
858     parameter to a layer's ShapesInRegion method.
859     """
860     map_proj = self.map.projection
861     if map_proj is not None:
862     inverse = map_proj.Inverse
863     else:
864     inverse = None
865    
866     xs = []
867     ys = []
868     for dx, dy in ((-1, -1), (1, -1), (1, 1), (-1, 1)):
869 bh 301 px, py = self.win_to_proj(x + dist * dx, y + dist * dy)
870 bh 159 if inverse:
871     px, py = inverse(px, py)
872     xs.append(px)
873     ys.append(py)
874     return (min(xs), min(ys), max(xs), max(ys))
875    
876 bh 246 def find_shape_at(self, px, py, select_labels = 0, searched_layer = None):
877 bh 43 """Determine the shape at point px, py in window coords
878    
879     Return the shape and the corresponding layer as a tuple (layer,
880     shape).
881    
882     If the optional parameter select_labels is true (default false)
883     search through the labels. If a label is found return it's index
884     as the shape and None as the layer.
885    
886 bh 246 If the optional parameter searched_layer is given (or not None
887     which it defaults to), only search in that layer.
888 bh 43 """
889 bh 6 map_proj = self.map.projection
890     if map_proj is not None:
891     forward = map_proj.Forward
892     else:
893     forward = None
894    
895     scale = self.scale
896 jonathan 967
897     if scale == 0:
898     return None, None
899    
900 bh 6 offx, offy = self.offset
901    
902     if select_labels:
903     labels = self.map.LabelLayer().Labels()
904 bh 246
905 bh 6 if labels:
906     dc = wxClientDC(self)
907     font = wxFont(10, wx.wxSWISS, wx.wxNORMAL, wx.wxNORMAL)
908     dc.SetFont(font)
909 bh 60 for i in range(len(labels) - 1, -1, -1):
910 bh 6 label = labels[i]
911     x = label.x
912     y = label.y
913     text = label.text
914     if forward:
915     x, y = forward(x, y)
916     x = x * scale + offx
917     y = -y * scale + offy
918     width, height = dc.GetTextExtent(text)
919     if label.halign == ALIGN_LEFT:
920     # nothing to be done
921     pass
922     elif label.halign == ALIGN_RIGHT:
923     x = x - width
924     elif label.halign == ALIGN_CENTER:
925     x = x - width/2
926     if label.valign == ALIGN_TOP:
927     # nothing to be done
928     pass
929     elif label.valign == ALIGN_BOTTOM:
930     y = y - height
931     elif label.valign == ALIGN_CENTER:
932     y = y - height/2
933     if x <= px < x + width and y <= py <= y + height:
934     return None, i
935 bh 43
936 bh 246 if searched_layer:
937     layers = [searched_layer]
938 bh 43 else:
939     layers = self.map.Layers()
940    
941 bh 6 for layer_index in range(len(layers) - 1, -1, -1):
942     layer = layers[layer_index]
943    
944     # search only in visible layers
945     if not layer.Visible():
946     continue
947    
948 jonathan 433 filled = layer.GetClassification().GetDefaultFill() \
949 jonathan 610 is not Color.Transparent
950 jonathan 469 stroked = layer.GetClassification().GetDefaultLineColor() \
951 jonathan 610 is not Color.Transparent
952 bh 246
953 bh 6 layer_proj = layer.projection
954     if layer_proj is not None:
955     inverse = layer_proj.Inverse
956     else:
957     inverse = None
958 bh 246
959 bh 6 shapetype = layer.ShapeType()
960    
961     select_shape = -1
962 bh 159
963 bh 301 # Determine the ids of the shapes that overlap a tiny area
964     # around the point. For layers containing points we have to
965     # choose a larger size of the box we're testing agains so
966     # that we take the size of the markers into account
967     # FIXME: Once the markers are more flexible this part has to
968     # become more flexible too, of course
969     if shapetype == SHAPETYPE_POINT:
970     box = self.unprojected_rect_around_point(px, py, 5)
971     else:
972     box = self.unprojected_rect_around_point(px, py, 1)
973 bh 159 shape_ids = layer.ShapesInRegion(box)
974     shape_ids.reverse()
975    
976 bh 6 if shapetype == SHAPETYPE_POLYGON:
977 bh 159 for i in shape_ids:
978 bh 1219 shapefile = layer.ShapeStore().Shapefile().cobject()
979     result = point_in_polygon_shape(shapefile, i,
980 bh 6 filled, stroked,
981     map_proj, layer_proj,
982     scale, -scale, offx, offy,
983     px, py)
984     if result:
985     select_shape = i
986     break
987     elif shapetype == SHAPETYPE_ARC:
988 bh 159 for i in shape_ids:
989 bh 1219 shapefile = layer.ShapeStore().Shapefile().cobject()
990     result = point_in_polygon_shape(shapefile,
991 bh 6 i, 0, 1,
992     map_proj, layer_proj,
993     scale, -scale, offx, offy,
994     px, py)
995     if result < 0:
996     select_shape = i
997     break
998     elif shapetype == SHAPETYPE_POINT:
999 bh 159 for i in shape_ids:
1000 bh 6 shape = layer.Shape(i)
1001     x, y = shape.Points()[0]
1002     if inverse:
1003     x, y = inverse(x, y)
1004     if forward:
1005     x, y = forward(x, y)
1006     x = x * scale + offx
1007     y = -y * scale + offy
1008     if hypot(px - x, py - y) < 5:
1009     select_shape = i
1010     break
1011    
1012     if select_shape >= 0:
1013     return layer, select_shape
1014     return None, None
1015    
1016 bh 246 def SelectShapeAt(self, x, y, layer = None):
1017     """\
1018     Select and return the shape and its layer at window position (x, y)
1019    
1020     If layer is given, only search in that layer. If no layer is
1021     given, search through all layers.
1022    
1023     Return a tuple (layer, shapeid). If no shape is found, return
1024     (None, None).
1025     """
1026     layer, shape = result = self.find_shape_at(x, y, searched_layer=layer)
1027 bh 43 # If layer is None, then shape will also be None. We don't want
1028     # to deselect the currently selected layer, so we simply select
1029     # the already selected layer again.
1030     if layer is None:
1031 bh 535 layer = self.selection.SelectedLayer()
1032     shapes = []
1033     else:
1034     shapes = [shape]
1035     self.selection.SelectShapes(layer, shapes)
1036 bh 246 return result
1037 bh 6
1038     def LabelShapeAt(self, x, y):
1039 bh 295 """Add or remove a label at window position x, y.
1040    
1041     If there's a label at the given position, remove it. Otherwise
1042     determine the shape at the position, run the label dialog and
1043     unless the user cancels the dialog, add a laber.
1044     """
1045 bh 6 ox = x; oy = y
1046     label_layer = self.map.LabelLayer()
1047     layer, shape_index = self.find_shape_at(x, y, select_labels = 1)
1048     if layer is None and shape_index is not None:
1049     # a label was selected
1050     label_layer.RemoveLabel(shape_index)
1051     elif layer is not None:
1052 bh 1219 text = labeldialog.run_label_dialog(self,
1053     layer.ShapeStore().Table(),
1054     shape_index)
1055 bh 6 if text:
1056     proj = self.map.projection
1057     if proj is not None:
1058     map_proj = proj
1059     else:
1060     map_proj = None
1061     proj = layer.projection
1062     if proj is not None:
1063     layer_proj = proj
1064     else:
1065     layer_proj = None
1066    
1067     shapetype = layer.ShapeType()
1068     if shapetype == SHAPETYPE_POLYGON:
1069 bh 1219 shapefile = layer.ShapeStore().Shapefile().cobject()
1070     x, y = shape_centroid(shapefile, shape_index,
1071 bh 6 map_proj, layer_proj, 1, 1, 0, 0)
1072     if map_proj is not None:
1073     x, y = map_proj.Inverse(x, y)
1074     else:
1075     shape = layer.Shape(shape_index)
1076     if shapetype == SHAPETYPE_POINT:
1077     x, y = shape.Points()[0]
1078     else:
1079     # assume SHAPETYPE_ARC
1080     points = shape.Points()
1081     x, y = points[len(points) / 2]
1082     if layer_proj is not None:
1083     x, y = layer_proj.Inverse(x, y)
1084     if shapetype == SHAPETYPE_POINT:
1085     halign = ALIGN_LEFT
1086     valign = ALIGN_CENTER
1087     elif shapetype == SHAPETYPE_POLYGON:
1088     halign = ALIGN_CENTER
1089     valign = ALIGN_CENTER
1090     elif shapetype == SHAPETYPE_ARC:
1091     halign = ALIGN_LEFT
1092     valign = ALIGN_CENTER
1093     label_layer.AddLabel(x, y, text,
1094     halign = halign, valign = valign)
1095 frank 910
1096     def OutputTransform(canvas_scale, canvas_offset, canvas_size, device_extend):
1097     """Calculate dimensions to transform canvas content to output device."""
1098     width, height = device_extend
1099    
1100     # Only 80 % of the with are available for the map
1101     width = width * 0.8
1102    
1103     # Define the distance of the map from DC border
1104     distance = 20
1105    
1106     if height < width:
1107     # landscape
1108     map_height = height - 2*distance
1109     map_width = map_height
1110     else:
1111     # portrait, recalibrate width (usually the legend width is too
1112     # small
1113     width = width * 0.9
1114     map_height = width - 2*distance
1115     map_width = map_height
1116    
1117     mapregion = (distance, distance,
1118     distance+map_width, distance+map_height)
1119    
1120     canvas_width, canvas_height = canvas_size
1121    
1122     scalex = map_width / (canvas_width/canvas_scale)
1123     scaley = map_height / (canvas_height/canvas_scale)
1124     scale = min(scalex, scaley)
1125     canvas_offx, canvas_offy = canvas_offset
1126     offx = scale*canvas_offx/canvas_scale
1127     offy = scale*canvas_offy/canvas_scale
1128    
1129     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