/[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 1277 - (show annotations)
Fri Jun 20 17:46:45 2003 UTC (21 years, 8 months ago) by jonathan
Original Path: trunk/thuban/Thuban/UI/view.py
File MIME type: text/x-python
File size: 39325 byte(s)
Use Thuban[Begin|End]BusyCursor()
        instead of a direct call to wx[Begin|End]CusyCursor().
(MapCanvas.find_shape_at): Check if the current search layer
        support shapes, otherwise go on to the next layer.

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

Properties

Name Value
svn:eol-style native
svn:keywords Author Date Id Revision

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26