/[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 1035 - (show annotations)
Mon May 26 17:03:08 2003 UTC (21 years, 9 months ago) by jan
Original Path: trunk/thuban/Thuban/UI/view.py
File MIME type: text/x-python
File size: 36144 byte(s)
Replace the true/false of wxWindows by True/False of Python 2.2.1.

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
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
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 self.FitRectToWindow(bbox)
616
617 def ZoomFactor(self, factor, center = None):
618 """Multiply the zoom by factor and center on center.
619
620 The optional parameter center is a point in window coordinates
621 that should be centered. If it is omitted, it defaults to the
622 center of the window
623 """
624 if self.scale > 0:
625 width, height = self.GetSizeTuple()
626 scale = self.scale * factor
627 offx, offy = self.offset
628 if center is not None:
629 cx, cy = center
630 else:
631 cx = width / 2
632 cy = height / 2
633 offset = (factor * (offx - cx) + width / 2,
634 factor * (offy - cy) + height / 2)
635 self.set_view_transform(scale, offset)
636
637 def ZoomOutToRect(self, rect):
638 """Zoom out to fit the currently visible region into rect.
639
640 The rect parameter is given in window coordinates
641 """
642 # determine the bbox of the displayed region in projected
643 # coordinates
644 width, height = self.GetSizeTuple()
645 llx, lly = self.win_to_proj(0, height - 1)
646 urx, ury = self.win_to_proj(width - 1, 0)
647
648 sx, sy, ex, ey = rect
649 scalex = (ex - sx) / (urx - llx)
650 scaley = (ey - sy) / (ury - lly)
651 scale = min(scalex, scaley)
652
653 offx = 0.5 * ((ex + sx) - (urx + llx) * scale)
654 offy = 0.5 * ((ey + sy) + (ury + lly) * scale)
655 self.set_view_transform(scale, (offx, offy))
656
657 def Translate(self, dx, dy):
658 """Move the map by dx, dy pixels"""
659 offx, offy = self.offset
660 self.set_view_transform(self.scale, (offx + dx, offy + dy))
661
662 def SelectTool(self, tool):
663 """Make tool the active tool.
664
665 The parameter should be an instance of Tool or None to indicate
666 that no tool is active.
667 """
668 self.tool = tool
669
670 def ZoomInTool(self):
671 """Start the zoom in tool"""
672 self.SelectTool(ZoomInTool(self))
673
674 def ZoomOutTool(self):
675 """Start the zoom out tool"""
676 self.SelectTool(ZoomOutTool(self))
677
678 def PanTool(self):
679 """Start the pan tool"""
680 self.SelectTool(PanTool(self))
681 #img = resource.GetImageResource("pan", wxBITMAP_TYPE_XPM)
682 #bmp = resource.GetBitmapResource("pan", wxBITMAP_TYPE_XPM)
683 #print bmp
684 #img = wxImageFromBitmap(bmp)
685 #print img
686 #cur = wxCursor(img)
687 #print cur
688 #self.SetCursor(cur)
689
690 def IdentifyTool(self):
691 """Start the identify tool"""
692 self.SelectTool(IdentifyTool(self))
693
694 def LabelTool(self):
695 """Start the label tool"""
696 self.SelectTool(LabelTool(self))
697
698 def CurrentTool(self):
699 """Return the name of the current tool or None if no tool is active"""
700 return self.tool and self.tool.Name() or None
701
702 def CurrentPosition(self):
703 """Return current position of the mouse in projected coordinates.
704
705 The result is a 2-tuple of floats with the coordinates. If the
706 mouse is not in the window, the result is None.
707 """
708 if self.current_position is not None:
709 x, y = self.current_position
710 return self.win_to_proj(x, y)
711 else:
712 return None
713
714 def set_current_position(self, event):
715 """Set the current position from event
716
717 Should be called by all events that contain mouse positions
718 especially EVT_MOTION. The event paramete may be None to
719 indicate the the pointer left the window.
720 """
721 if event is not None:
722 self.current_position = (event.m_x, event.m_y)
723 else:
724 self.current_position = None
725 self.issue(VIEW_POSITION)
726
727 def OnLeftDown(self, event):
728 self.set_current_position(event)
729 if self.tool is not None:
730 self.drag_dc = wxClientDC(self)
731 self.drag_dc.SetLogicalFunction(wxINVERT)
732 self.drag_dc.SetBrush(wxTRANSPARENT_BRUSH)
733 self.CaptureMouse()
734 self.tool.MouseDown(event)
735 self.tool.Show(self.drag_dc)
736 self.dragging = 1
737
738 def OnLeftUp(self, event):
739 self.set_current_position(event)
740 if self.dragging:
741 self.ReleaseMouse()
742 try:
743 self.tool.Hide(self.drag_dc)
744 self.tool.MouseUp(event)
745 finally:
746 self.drag_dc = None
747 self.dragging = 0
748
749 def OnMotion(self, event):
750 self.set_current_position(event)
751 if self.dragging:
752 self.tool.Hide(self.drag_dc)
753 self.tool.MouseMove(event)
754 self.tool.Show(self.drag_dc)
755
756 def OnLeaveWindow(self, event):
757 self.set_current_position(None)
758
759 def OnSize(self, event):
760 # the window's size has changed. We have to get a new bitmap. If
761 # we want to be clever we could try to get by without throwing
762 # everything away. E.g. when the window gets smaller, we could
763 # either keep the bitmap or create the new one from the old one.
764 # Even when the window becomes larger some parts of the bitmap
765 # could be reused.
766 self.full_redraw()
767 pass
768
769 def shape_selected(self, layer, shape):
770 """Receiver for the SHAPES_SELECTED messages. Redraw the map."""
771 # The selection object takes care that it only issues
772 # SHAPES_SELECTED messages when the set of selected shapes has
773 # actually changed, so we can do a full redraw unconditionally.
774 # FIXME: We should perhaps try to limit the redraw to the are
775 # actually covered by the shapes before and after the selection
776 # change.
777 self.full_redraw()
778
779 def unprojected_rect_around_point(self, x, y, dist):
780 """return a rect dist pixels around (x, y) in unprojected corrdinates
781
782 The return value is a tuple (minx, miny, maxx, maxy) suitable a
783 parameter to a layer's ShapesInRegion method.
784 """
785 map_proj = self.map.projection
786 if map_proj is not None:
787 inverse = map_proj.Inverse
788 else:
789 inverse = None
790
791 xs = []
792 ys = []
793 for dx, dy in ((-1, -1), (1, -1), (1, 1), (-1, 1)):
794 px, py = self.win_to_proj(x + dist * dx, y + dist * dy)
795 if inverse:
796 px, py = inverse(px, py)
797 xs.append(px)
798 ys.append(py)
799 return (min(xs), min(ys), max(xs), max(ys))
800
801 def find_shape_at(self, px, py, select_labels = 0, searched_layer = None):
802 """Determine the shape at point px, py in window coords
803
804 Return the shape and the corresponding layer as a tuple (layer,
805 shape).
806
807 If the optional parameter select_labels is true (default false)
808 search through the labels. If a label is found return it's index
809 as the shape and None as the layer.
810
811 If the optional parameter searched_layer is given (or not None
812 which it defaults to), only search in that layer.
813 """
814 map_proj = self.map.projection
815 if map_proj is not None:
816 forward = map_proj.Forward
817 else:
818 forward = None
819
820 scale = self.scale
821
822 if scale == 0:
823 return None, None
824
825 offx, offy = self.offset
826
827 if select_labels:
828 labels = self.map.LabelLayer().Labels()
829
830 if labels:
831 dc = wxClientDC(self)
832 font = wxFont(10, wx.wxSWISS, wx.wxNORMAL, wx.wxNORMAL)
833 dc.SetFont(font)
834 for i in range(len(labels) - 1, -1, -1):
835 label = labels[i]
836 x = label.x
837 y = label.y
838 text = label.text
839 if forward:
840 x, y = forward(x, y)
841 x = x * scale + offx
842 y = -y * scale + offy
843 width, height = dc.GetTextExtent(text)
844 if label.halign == ALIGN_LEFT:
845 # nothing to be done
846 pass
847 elif label.halign == ALIGN_RIGHT:
848 x = x - width
849 elif label.halign == ALIGN_CENTER:
850 x = x - width/2
851 if label.valign == ALIGN_TOP:
852 # nothing to be done
853 pass
854 elif label.valign == ALIGN_BOTTOM:
855 y = y - height
856 elif label.valign == ALIGN_CENTER:
857 y = y - height/2
858 if x <= px < x + width and y <= py <= y + height:
859 return None, i
860
861 if searched_layer:
862 layers = [searched_layer]
863 else:
864 layers = self.map.Layers()
865
866 for layer_index in range(len(layers) - 1, -1, -1):
867 layer = layers[layer_index]
868
869 # search only in visible layers
870 if not layer.Visible():
871 continue
872
873 filled = layer.GetClassification().GetDefaultFill() \
874 is not Color.Transparent
875 stroked = layer.GetClassification().GetDefaultLineColor() \
876 is not Color.Transparent
877
878 layer_proj = layer.projection
879 if layer_proj is not None:
880 inverse = layer_proj.Inverse
881 else:
882 inverse = None
883
884 shapetype = layer.ShapeType()
885
886 select_shape = -1
887
888 # Determine the ids of the shapes that overlap a tiny area
889 # around the point. For layers containing points we have to
890 # choose a larger size of the box we're testing agains so
891 # that we take the size of the markers into account
892 # FIXME: Once the markers are more flexible this part has to
893 # become more flexible too, of course
894 if shapetype == SHAPETYPE_POINT:
895 box = self.unprojected_rect_around_point(px, py, 5)
896 else:
897 box = self.unprojected_rect_around_point(px, py, 1)
898 shape_ids = layer.ShapesInRegion(box)
899 shape_ids.reverse()
900
901 if shapetype == SHAPETYPE_POLYGON:
902 for i in shape_ids:
903 result = point_in_polygon_shape(layer.shapefile.cobject(),
904 i,
905 filled, stroked,
906 map_proj, layer_proj,
907 scale, -scale, offx, offy,
908 px, py)
909 if result:
910 select_shape = i
911 break
912 elif shapetype == SHAPETYPE_ARC:
913 for i in shape_ids:
914 result = point_in_polygon_shape(layer.shapefile.cobject(),
915 i, 0, 1,
916 map_proj, layer_proj,
917 scale, -scale, offx, offy,
918 px, py)
919 if result < 0:
920 select_shape = i
921 break
922 elif shapetype == SHAPETYPE_POINT:
923 for i in shape_ids:
924 shape = layer.Shape(i)
925 x, y = shape.Points()[0]
926 if inverse:
927 x, y = inverse(x, y)
928 if forward:
929 x, y = forward(x, y)
930 x = x * scale + offx
931 y = -y * scale + offy
932 if hypot(px - x, py - y) < 5:
933 select_shape = i
934 break
935
936 if select_shape >= 0:
937 return layer, select_shape
938 return None, None
939
940 def SelectShapeAt(self, x, y, layer = None):
941 """\
942 Select and return the shape and its layer at window position (x, y)
943
944 If layer is given, only search in that layer. If no layer is
945 given, search through all layers.
946
947 Return a tuple (layer, shapeid). If no shape is found, return
948 (None, None).
949 """
950 layer, shape = result = self.find_shape_at(x, y, searched_layer=layer)
951 # If layer is None, then shape will also be None. We don't want
952 # to deselect the currently selected layer, so we simply select
953 # the already selected layer again.
954 if layer is None:
955 layer = self.selection.SelectedLayer()
956 shapes = []
957 else:
958 shapes = [shape]
959 self.selection.SelectShapes(layer, shapes)
960 return result
961
962 def LabelShapeAt(self, x, y):
963 """Add or remove a label at window position x, y.
964
965 If there's a label at the given position, remove it. Otherwise
966 determine the shape at the position, run the label dialog and
967 unless the user cancels the dialog, add a laber.
968 """
969 ox = x; oy = y
970 label_layer = self.map.LabelLayer()
971 layer, shape_index = self.find_shape_at(x, y, select_labels = 1)
972 if layer is None and shape_index is not None:
973 # a label was selected
974 label_layer.RemoveLabel(shape_index)
975 elif layer is not None:
976 text = labeldialog.run_label_dialog(self, layer.table, shape_index)
977 if text:
978 proj = self.map.projection
979 if proj is not None:
980 map_proj = proj
981 else:
982 map_proj = None
983 proj = layer.projection
984 if proj is not None:
985 layer_proj = proj
986 else:
987 layer_proj = None
988
989 shapetype = layer.ShapeType()
990 if shapetype == SHAPETYPE_POLYGON:
991 x, y = shape_centroid(layer.shapefile.cobject(),
992 shape_index,
993 map_proj, layer_proj, 1, 1, 0, 0)
994 if map_proj is not None:
995 x, y = map_proj.Inverse(x, y)
996 else:
997 shape = layer.Shape(shape_index)
998 if shapetype == SHAPETYPE_POINT:
999 x, y = shape.Points()[0]
1000 else:
1001 # assume SHAPETYPE_ARC
1002 points = shape.Points()
1003 x, y = points[len(points) / 2]
1004 if layer_proj is not None:
1005 x, y = layer_proj.Inverse(x, y)
1006 if shapetype == SHAPETYPE_POINT:
1007 halign = ALIGN_LEFT
1008 valign = ALIGN_CENTER
1009 elif shapetype == SHAPETYPE_POLYGON:
1010 halign = ALIGN_CENTER
1011 valign = ALIGN_CENTER
1012 elif shapetype == SHAPETYPE_ARC:
1013 halign = ALIGN_LEFT
1014 valign = ALIGN_CENTER
1015 label_layer.AddLabel(x, y, text,
1016 halign = halign, valign = valign)
1017
1018 def OutputTransform(canvas_scale, canvas_offset, canvas_size, device_extend):
1019 """Calculate dimensions to transform canvas content to output device."""
1020 width, height = device_extend
1021
1022 # Only 80 % of the with are available for the map
1023 width = width * 0.8
1024
1025 # Define the distance of the map from DC border
1026 distance = 20
1027
1028 if height < width:
1029 # landscape
1030 map_height = height - 2*distance
1031 map_width = map_height
1032 else:
1033 # portrait, recalibrate width (usually the legend width is too
1034 # small
1035 width = width * 0.9
1036 map_height = width - 2*distance
1037 map_width = map_height
1038
1039 mapregion = (distance, distance,
1040 distance+map_width, distance+map_height)
1041
1042 canvas_width, canvas_height = canvas_size
1043
1044 scalex = map_width / (canvas_width/canvas_scale)
1045 scaley = map_height / (canvas_height/canvas_scale)
1046 scale = min(scalex, scaley)
1047 canvas_offx, canvas_offy = canvas_offset
1048 offx = scale*canvas_offx/canvas_scale
1049 offy = scale*canvas_offy/canvas_scale
1050
1051 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