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

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

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1105 - (show annotations)
Fri May 30 06:30:15 2003 UTC (21 years, 9 months ago) by jonathan
Original Path: trunk/thuban/Thuban/UI/view.py
File MIME type: text/x-python
File size: 36423 byte(s)
(MapCanvas.OnPaint): Wrap code
        with try/finally. Fixes RTBug #1904.
(MapCanvas.FitSelectedToWindow): If a single point is selected
        simply center it on the display. Fixes RTBug #1849.

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