/[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 1344 - (show annotations)
Tue Jul 1 16:11:26 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: 39082 byte(s)
Fixes RTbug #1974, 1971.
(MapCanvas.__init__): Subscribe to the idle time event. Set
        background color to white.
(MapCanvas.OnPaint): Set a flag indicating that we should
        render the map during idle time. If we already have a bitmap
        just draw it now.
(MapCanvas.OnIdle): New. Render the map only during idle time.
        This also fixes a problem with the busy cursor under gtk.

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