/[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 1221 - (show annotations)
Tue Jun 17 15:24:45 2003 UTC (21 years, 8 months ago) by jonathan
Original Path: trunk/thuban/Thuban/UI/view.py
File MIME type: text/x-python
File size: 39209 byte(s)
(MapCanvas.__init__): New instance variable
        current_map_proj to remember the current map projection so that
        when the projection changes we know what the previous projection
        was.
(MapCanvas.SetMap): Unsubscribe and subscribe to
        LAYER_PROJECTION_CHANGED events.
(MapCanvas.projection_changed): Split into two methods that respond
        to map and layer projection changes.
(MapCanvas.map_projection_changed): New. Takes the current view and
        projects it using the new projection. This does not cause the
        map to be redrawn at full extent.
(MapCanvas.layer_projection_changed): New. Cause a redraw which
        will draw each layer in its new projection.

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