/[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 1111 - (show annotations)
Fri May 30 09:54:48 2003 UTC (21 years, 9 months ago) by bh
Original Path: trunk/thuban/Thuban/UI/view.py
File MIME type: text/x-python
File size: 37875 byte(s)
(MapCanvas.set_view_transform): Limit the
maximum and minimum scale factors.

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