/[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 1285 - (show annotations)
Mon Jun 23 10:30:53 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: 39104 byte(s)
(MapCanvas.OnPaint): Call wxBeginBusyCursor()
        directly to avoid the wxSafeYield() call which generates an
        OnPaint event causing infinite recursion. Don't try to catch
        exception anymore. This was for before there were limits on map
        scaling.

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