/[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 1271 - (show annotations)
Fri Jun 20 16:43:04 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: 39281 byte(s)
(MapCanvas.OnPaint): Call wxYield after
        turning on the busy cursor to allow the system to change the
        cursor before we begin painting. This fixes a problem that
        was occuring only under GTK. Fixes RTbug #1840.

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