/[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 926 - (show annotations)
Mon May 19 12:09:01 2003 UTC (21 years, 9 months ago) by frank
Original Path: trunk/thuban/Thuban/UI/view.py
File MIME type: text/x-python
File size: 35852 byte(s)
Bugfix

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
38 from Thuban.Model.messages import MAP_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 wx.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 # scale and offset describe the transformation from projected
307 # coordinates to window coordinates.
308 self.scale = 1.0
309 self.offset = (0, 0)
310
311 # whether the user is currently dragging the mouse, i.e. moving
312 # the mouse while pressing a mouse button
313 self.dragging = 0
314
315 # the currently active tool
316 self.tool = None
317
318 # The current mouse position of the last OnMotion event or None
319 # if the mouse is outside the window.
320 self.current_position = None
321
322 # the bitmap serving as backing store
323 self.bitmap = None
324
325 # the selection
326 self.selection = Selection()
327 self.selection.Subscribe(SHAPES_SELECTED , self.shape_selected)
328
329 # keep track of which layers/shapes are selected to make sure we
330 # only redraw when necessary
331 self.last_selected_layer = None
332 self.last_selected_shape = None
333
334 # subscribe the WX events we're interested in
335 EVT_PAINT(self, self.OnPaint)
336 EVT_LEFT_DOWN(self, self.OnLeftDown)
337 EVT_LEFT_UP(self, self.OnLeftUp)
338 EVT_MOTION(self, self.OnMotion)
339 EVT_LEAVE_WINDOW(self, self.OnLeaveWindow)
340 wx.EVT_SIZE(self, self.OnSize)
341
342 def __del__(self):
343 wxWindow.__del__(self)
344 Publisher.__del__(self)
345
346 def Subscribe(self, channel, *args):
347 """Extend the inherited method to handle delegated messages.
348
349 If channel is one of the delegated messages call the appropriate
350 object's Subscribe method. Otherwise just call the inherited
351 method.
352 """
353 if channel in self.delegated_messages:
354 object = getattr(self, self.delegated_messages[channel])
355 object.Subscribe(channel, *args)
356 else:
357 Publisher.Subscribe(self, channel, *args)
358
359 def Unsubscribe(self, channel, *args):
360 """Extend the inherited method to handle delegated messages.
361
362 If channel is one of the delegated messages call the appropriate
363 object's Unsubscribe method. Otherwise just call the inherited
364 method.
365 """
366 if channel in self.delegated_messages:
367 object = getattr(self, self.delegated_messages[channel])
368 object.Unsubscribe(channel, *args)
369 else:
370 Publisher.Unsubscribe(self, channel, *args)
371
372 def __getattr__(self, attr):
373 if attr in self.delegated_methods:
374 return getattr(getattr(self, self.delegated_methods[attr]), attr)
375 raise AttributeError(attr)
376
377 def OnPaint(self, event):
378 dc = wxPaintDC(self)
379 clear = self.map is None or not self.map.HasLayers()
380
381 #wxBeginBusyCursor()
382
383 if not clear:
384 try:
385 self.do_redraw()
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
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 hasattr(self, "export_path"):
451 export_path = self.export_path
452 else:
453 export_path="."
454 dlg = wxFileDialog(self, _("Export Map"), export_path, "",
455 "Enhanced Metafile (*.wmf)|*.wmf",
456 wxSAVE|wxOVERWRITE_PROMPT)
457 if dlg.ShowModal() == wxID_OK:
458 self.export_path = os.path.dirname(dlg.GetPath())
459 dc = wxMetaFileDC(dlg.GetPath())
460
461 scale, offset, mapregion = OutputTransform(self.scale,
462 self.offset,
463 self.GetSizeTuple(),
464 dc.GetSizeTuple())
465
466 selected_layer = self.selection.SelectedLayer()
467 selected_shapes = self.selection.SelectedShapes()
468
469 renderer = ExportRenderer(dc, scale, offset)
470
471 # Pass the entire bitmap as update region to the renderer.
472 # We're redrawing the whole bitmap, after all.
473 width, height = self.GetSizeTuple()
474 renderer.RenderMap(self.map,
475 (0,0,
476 (width/self.scale)*scale,
477 (height/self.scale)*scale),
478 mapregion,
479 selected_layer, selected_shapes)
480 dc.EndDrawing()
481 dc.Close()
482 dlg.Destroy()
483
484 def Print(self):
485 printer = wx.wxPrinter()
486 width, height = self.GetSizeTuple()
487 selected_layer = self.selection.SelectedLayer()
488 selected_shapes = self.selection.SelectedShapes()
489
490 printout = MapPrintout(self, self.map, (0, 0, width, height),
491 selected_layer, selected_shapes)
492 printer.Print(self, printout, wx.true)
493 printout.Destroy()
494
495 def SetMap(self, map):
496 redraw_channels = (MAP_LAYERS_CHANGED, LAYER_CHANGED,
497 LAYER_VISIBILITY_CHANGED)
498 if self.map is not None:
499 for channel in redraw_channels:
500 self.map.Unsubscribe(channel, self.full_redraw)
501 self.map.Unsubscribe(MAP_PROJECTION_CHANGED,
502 self.projection_changed)
503 self.map = map
504 self.selection.ClearSelection()
505 if self.map is not None:
506 for channel in redraw_channels:
507 self.map.Subscribe(channel, self.full_redraw)
508 self.map.Subscribe(MAP_PROJECTION_CHANGED, self.projection_changed)
509 self.FitMapToWindow()
510 # force a redraw. If map is not empty, it's already been called
511 # by FitMapToWindow but if map is empty it hasn't been called
512 # yet so we have to explicitly call it.
513 self.full_redraw()
514
515 def Map(self):
516 """Return the map displayed by this canvas"""
517 return self.map
518
519 def redraw(self, *args):
520 self.Refresh(0)
521
522 def full_redraw(self, *args):
523 self.bitmap = None
524 self.redraw()
525
526 def projection_changed(self, *args):
527 self.FitMapToWindow()
528 self.full_redraw()
529
530 def set_view_transform(self, scale, offset):
531 self.scale = scale
532 self.offset = offset
533 self.full_redraw()
534 self.issue(SCALE_CHANGED, scale)
535
536 def proj_to_win(self, x, y):
537 """\
538 Return the point in window coords given by projected coordinates x y
539 """
540 offx, offy = self.offset
541 return (self.scale * x + offx, -self.scale * y + offy)
542
543 def win_to_proj(self, x, y):
544 """\
545 Return the point in projected coordinates given by window coords x y
546 """
547 offx, offy = self.offset
548 return ((x - offx) / self.scale, (offy - y) / self.scale)
549
550 def FitRectToWindow(self, rect):
551 """Fit the rectangular region given by rect into the window.
552
553 Set scale so that rect (in projected coordinates) just fits into
554 the window and center it.
555 """
556 width, height = self.GetSizeTuple()
557 llx, lly, urx, ury = rect
558 if llx == urx or lly == ury:
559 # zero width or zero height. Do Nothing
560 return
561 scalex = width / (urx - llx)
562 scaley = height / (ury - lly)
563 scale = min(scalex, scaley)
564 offx = 0.5 * (width - (urx + llx) * scale)
565 offy = 0.5 * (height + (ury + lly) * scale)
566 self.set_view_transform(scale, (offx, offy))
567
568 def FitMapToWindow(self):
569 """Fit the map to the window
570
571 Set the scale so that the map fits exactly into the window and
572 center it in the window.
573 """
574 bbox = self.map.ProjectedBoundingBox()
575 if bbox is not None:
576 self.FitRectToWindow(bbox)
577
578 def FitLayerToWindow(self, layer):
579 """Fit the given layer to the window.
580
581 Set the scale so that the layer fits exactly into the window and
582 center it in the window.
583 """
584
585 bbox = layer.LatLongBoundingBox()
586 if bbox is not None:
587 proj = self.map.GetProjection()
588 if proj is not None:
589 bbox = proj.ForwardBBox(bbox)
590
591 if bbox is not None:
592 self.FitRectToWindow(bbox)
593
594 def FitSelectedToWindow(self):
595 layer = self.selection.SelectedLayer()
596 shapes = self.selection.SelectedShapes()
597
598 bbox = layer.ShapesBoundingBox(shapes)
599 if bbox is not None:
600 proj = self.map.GetProjection()
601 if proj is not None:
602 bbox = proj.ForwardBBox(bbox)
603
604 if bbox is not None:
605 self.FitRectToWindow(bbox)
606
607 def ZoomFactor(self, factor, center = None):
608 """Multiply the zoom by factor and center on center.
609
610 The optional parameter center is a point in window coordinates
611 that should be centered. If it is omitted, it defaults to the
612 center of the window
613 """
614 width, height = self.GetSizeTuple()
615 scale = self.scale * factor
616 offx, offy = self.offset
617 if center is not None:
618 cx, cy = center
619 else:
620 cx = width / 2
621 cy = height / 2
622 offset = (factor * (offx - cx) + width / 2,
623 factor * (offy - cy) + height / 2)
624 self.set_view_transform(scale, offset)
625
626 def ZoomOutToRect(self, rect):
627 """Zoom out to fit the currently visible region into rect.
628
629 The rect parameter is given in window coordinates
630 """
631 # determine the bbox of the displayed region in projected
632 # coordinates
633 width, height = self.GetSizeTuple()
634 llx, lly = self.win_to_proj(0, height - 1)
635 urx, ury = self.win_to_proj(width - 1, 0)
636
637 sx, sy, ex, ey = rect
638 scalex = (ex - sx) / (urx - llx)
639 scaley = (ey - sy) / (ury - lly)
640 scale = min(scalex, scaley)
641
642 offx = 0.5 * ((ex + sx) - (urx + llx) * scale)
643 offy = 0.5 * ((ey + sy) + (ury + lly) * scale)
644 self.set_view_transform(scale, (offx, offy))
645
646 def Translate(self, dx, dy):
647 """Move the map by dx, dy pixels"""
648 offx, offy = self.offset
649 self.set_view_transform(self.scale, (offx + dx, offy + dy))
650
651 def SelectTool(self, tool):
652 """Make tool the active tool.
653
654 The parameter should be an instance of Tool or None to indicate
655 that no tool is active.
656 """
657 self.tool = tool
658
659 def ZoomInTool(self):
660 """Start the zoom in tool"""
661 self.SelectTool(ZoomInTool(self))
662
663 def ZoomOutTool(self):
664 """Start the zoom out tool"""
665 self.SelectTool(ZoomOutTool(self))
666
667 def PanTool(self):
668 """Start the pan tool"""
669 self.SelectTool(PanTool(self))
670 #img = resource.GetImageResource("pan", wxBITMAP_TYPE_XPM)
671 #bmp = resource.GetBitmapResource("pan", wxBITMAP_TYPE_XPM)
672 #print bmp
673 #img = wxImageFromBitmap(bmp)
674 #print img
675 #cur = wxCursor(img)
676 #print cur
677 #self.SetCursor(cur)
678
679 def IdentifyTool(self):
680 """Start the identify tool"""
681 self.SelectTool(IdentifyTool(self))
682
683 def LabelTool(self):
684 """Start the label tool"""
685 self.SelectTool(LabelTool(self))
686
687 def CurrentTool(self):
688 """Return the name of the current tool or None if no tool is active"""
689 return self.tool and self.tool.Name() or None
690
691 def CurrentPosition(self):
692 """Return current position of the mouse in projected coordinates.
693
694 The result is a 2-tuple of floats with the coordinates. If the
695 mouse is not in the window, the result is None.
696 """
697 if self.current_position is not None:
698 x, y = self.current_position
699 return self.win_to_proj(x, y)
700 else:
701 return None
702
703 def set_current_position(self, event):
704 """Set the current position from event
705
706 Should be called by all events that contain mouse positions
707 especially EVT_MOTION. The event paramete may be None to
708 indicate the the pointer left the window.
709 """
710 if event is not None:
711 self.current_position = (event.m_x, event.m_y)
712 else:
713 self.current_position = None
714 self.issue(VIEW_POSITION)
715
716 def OnLeftDown(self, event):
717 self.set_current_position(event)
718 if self.tool is not None:
719 self.drag_dc = wxClientDC(self)
720 self.drag_dc.SetLogicalFunction(wxINVERT)
721 self.drag_dc.SetBrush(wxTRANSPARENT_BRUSH)
722 self.CaptureMouse()
723 self.tool.MouseDown(event)
724 self.tool.Show(self.drag_dc)
725 self.dragging = 1
726
727 def OnLeftUp(self, event):
728 self.set_current_position(event)
729 if self.dragging:
730 self.ReleaseMouse()
731 try:
732 self.tool.Hide(self.drag_dc)
733 self.tool.MouseUp(event)
734 finally:
735 self.drag_dc = None
736 self.dragging = 0
737
738 def OnMotion(self, event):
739 self.set_current_position(event)
740 if self.dragging:
741 self.tool.Hide(self.drag_dc)
742 self.tool.MouseMove(event)
743 self.tool.Show(self.drag_dc)
744
745 def OnLeaveWindow(self, event):
746 self.set_current_position(None)
747
748 def OnSize(self, event):
749 # the window's size has changed. We have to get a new bitmap. If
750 # we want to be clever we could try to get by without throwing
751 # everything away. E.g. when the window gets smaller, we could
752 # either keep the bitmap or create the new one from the old one.
753 # Even when the window becomes larger some parts of the bitmap
754 # could be reused.
755 self.full_redraw()
756 pass
757
758 def shape_selected(self, layer, shape):
759 """Receiver for the SHAPES_SELECTED messages. Redraw the map."""
760 # The selection object takes care that it only issues
761 # SHAPES_SELECTED messages when the set of selected shapes has
762 # actually changed, so we can do a full redraw unconditionally.
763 # FIXME: We should perhaps try to limit the redraw to the are
764 # actually covered by the shapes before and after the selection
765 # change.
766 self.full_redraw()
767
768 def unprojected_rect_around_point(self, x, y, dist):
769 """return a rect dist pixels around (x, y) in unprojected corrdinates
770
771 The return value is a tuple (minx, miny, maxx, maxy) suitable a
772 parameter to a layer's ShapesInRegion method.
773 """
774 map_proj = self.map.projection
775 if map_proj is not None:
776 inverse = map_proj.Inverse
777 else:
778 inverse = None
779
780 xs = []
781 ys = []
782 for dx, dy in ((-1, -1), (1, -1), (1, 1), (-1, 1)):
783 px, py = self.win_to_proj(x + dist * dx, y + dist * dy)
784 if inverse:
785 px, py = inverse(px, py)
786 xs.append(px)
787 ys.append(py)
788 return (min(xs), min(ys), max(xs), max(ys))
789
790 def find_shape_at(self, px, py, select_labels = 0, searched_layer = None):
791 """Determine the shape at point px, py in window coords
792
793 Return the shape and the corresponding layer as a tuple (layer,
794 shape).
795
796 If the optional parameter select_labels is true (default false)
797 search through the labels. If a label is found return it's index
798 as the shape and None as the layer.
799
800 If the optional parameter searched_layer is given (or not None
801 which it defaults to), only search in that layer.
802 """
803 map_proj = self.map.projection
804 if map_proj is not None:
805 forward = map_proj.Forward
806 else:
807 forward = None
808
809 scale = self.scale
810 offx, offy = self.offset
811
812 if select_labels:
813 labels = self.map.LabelLayer().Labels()
814
815 if labels:
816 dc = wxClientDC(self)
817 font = wxFont(10, wx.wxSWISS, wx.wxNORMAL, wx.wxNORMAL)
818 dc.SetFont(font)
819 for i in range(len(labels) - 1, -1, -1):
820 label = labels[i]
821 x = label.x
822 y = label.y
823 text = label.text
824 if forward:
825 x, y = forward(x, y)
826 x = x * scale + offx
827 y = -y * scale + offy
828 width, height = dc.GetTextExtent(text)
829 if label.halign == ALIGN_LEFT:
830 # nothing to be done
831 pass
832 elif label.halign == ALIGN_RIGHT:
833 x = x - width
834 elif label.halign == ALIGN_CENTER:
835 x = x - width/2
836 if label.valign == ALIGN_TOP:
837 # nothing to be done
838 pass
839 elif label.valign == ALIGN_BOTTOM:
840 y = y - height
841 elif label.valign == ALIGN_CENTER:
842 y = y - height/2
843 if x <= px < x + width and y <= py <= y + height:
844 return None, i
845
846 if searched_layer:
847 layers = [searched_layer]
848 else:
849 layers = self.map.Layers()
850
851 for layer_index in range(len(layers) - 1, -1, -1):
852 layer = layers[layer_index]
853
854 # search only in visible layers
855 if not layer.Visible():
856 continue
857
858 filled = layer.GetClassification().GetDefaultFill() \
859 is not Color.Transparent
860 stroked = layer.GetClassification().GetDefaultLineColor() \
861 is not Color.Transparent
862
863 layer_proj = layer.projection
864 if layer_proj is not None:
865 inverse = layer_proj.Inverse
866 else:
867 inverse = None
868
869 shapetype = layer.ShapeType()
870
871 select_shape = -1
872
873 # Determine the ids of the shapes that overlap a tiny area
874 # around the point. For layers containing points we have to
875 # choose a larger size of the box we're testing agains so
876 # that we take the size of the markers into account
877 # FIXME: Once the markers are more flexible this part has to
878 # become more flexible too, of course
879 if shapetype == SHAPETYPE_POINT:
880 box = self.unprojected_rect_around_point(px, py, 5)
881 else:
882 box = self.unprojected_rect_around_point(px, py, 1)
883 shape_ids = layer.ShapesInRegion(box)
884 shape_ids.reverse()
885
886 if shapetype == SHAPETYPE_POLYGON:
887 for i in shape_ids:
888 result = point_in_polygon_shape(layer.shapefile.cobject(),
889 i,
890 filled, stroked,
891 map_proj, layer_proj,
892 scale, -scale, offx, offy,
893 px, py)
894 if result:
895 select_shape = i
896 break
897 elif shapetype == SHAPETYPE_ARC:
898 for i in shape_ids:
899 result = point_in_polygon_shape(layer.shapefile.cobject(),
900 i, 0, 1,
901 map_proj, layer_proj,
902 scale, -scale, offx, offy,
903 px, py)
904 if result < 0:
905 select_shape = i
906 break
907 elif shapetype == SHAPETYPE_POINT:
908 for i in shape_ids:
909 shape = layer.Shape(i)
910 x, y = shape.Points()[0]
911 if inverse:
912 x, y = inverse(x, y)
913 if forward:
914 x, y = forward(x, y)
915 x = x * scale + offx
916 y = -y * scale + offy
917 if hypot(px - x, py - y) < 5:
918 select_shape = i
919 break
920
921 if select_shape >= 0:
922 return layer, select_shape
923 return None, None
924
925 def SelectShapeAt(self, x, y, layer = None):
926 """\
927 Select and return the shape and its layer at window position (x, y)
928
929 If layer is given, only search in that layer. If no layer is
930 given, search through all layers.
931
932 Return a tuple (layer, shapeid). If no shape is found, return
933 (None, None).
934 """
935 layer, shape = result = self.find_shape_at(x, y, searched_layer=layer)
936 # If layer is None, then shape will also be None. We don't want
937 # to deselect the currently selected layer, so we simply select
938 # the already selected layer again.
939 if layer is None:
940 layer = self.selection.SelectedLayer()
941 shapes = []
942 else:
943 shapes = [shape]
944 self.selection.SelectShapes(layer, shapes)
945 return result
946
947 def LabelShapeAt(self, x, y):
948 """Add or remove a label at window position x, y.
949
950 If there's a label at the given position, remove it. Otherwise
951 determine the shape at the position, run the label dialog and
952 unless the user cancels the dialog, add a laber.
953 """
954 ox = x; oy = y
955 label_layer = self.map.LabelLayer()
956 layer, shape_index = self.find_shape_at(x, y, select_labels = 1)
957 if layer is None and shape_index is not None:
958 # a label was selected
959 label_layer.RemoveLabel(shape_index)
960 elif layer is not None:
961 text = labeldialog.run_label_dialog(self, layer.table, shape_index)
962 if text:
963 proj = self.map.projection
964 if proj is not None:
965 map_proj = proj
966 else:
967 map_proj = None
968 proj = layer.projection
969 if proj is not None:
970 layer_proj = proj
971 else:
972 layer_proj = None
973
974 shapetype = layer.ShapeType()
975 if shapetype == SHAPETYPE_POLYGON:
976 x, y = shape_centroid(layer.shapefile.cobject(),
977 shape_index,
978 map_proj, layer_proj, 1, 1, 0, 0)
979 if map_proj is not None:
980 x, y = map_proj.Inverse(x, y)
981 else:
982 shape = layer.Shape(shape_index)
983 if shapetype == SHAPETYPE_POINT:
984 x, y = shape.Points()[0]
985 else:
986 # assume SHAPETYPE_ARC
987 points = shape.Points()
988 x, y = points[len(points) / 2]
989 if layer_proj is not None:
990 x, y = layer_proj.Inverse(x, y)
991 if shapetype == SHAPETYPE_POINT:
992 halign = ALIGN_LEFT
993 valign = ALIGN_CENTER
994 elif shapetype == SHAPETYPE_POLYGON:
995 halign = ALIGN_CENTER
996 valign = ALIGN_CENTER
997 elif shapetype == SHAPETYPE_ARC:
998 halign = ALIGN_LEFT
999 valign = ALIGN_CENTER
1000 label_layer.AddLabel(x, y, text,
1001 halign = halign, valign = valign)
1002
1003 def OutputTransform(canvas_scale, canvas_offset, canvas_size, device_extend):
1004 """Calculate dimensions to transform canvas content to output device."""
1005 width, height = device_extend
1006
1007 # Only 80 % of the with are available for the map
1008 width = width * 0.8
1009
1010 # Define the distance of the map from DC border
1011 distance = 20
1012
1013 if height < width:
1014 # landscape
1015 map_height = height - 2*distance
1016 map_width = map_height
1017 else:
1018 # portrait, recalibrate width (usually the legend width is too
1019 # small
1020 width = width * 0.9
1021 map_height = width - 2*distance
1022 map_width = map_height
1023
1024 mapregion = (distance, distance,
1025 distance+map_width, distance+map_height)
1026
1027 canvas_width, canvas_height = canvas_size
1028
1029 scalex = map_width / (canvas_width/canvas_scale)
1030 scaley = map_height / (canvas_height/canvas_scale)
1031 scale = min(scalex, scaley)
1032 canvas_offx, canvas_offy = canvas_offset
1033 offx = scale*canvas_offx/canvas_scale
1034 offy = scale*canvas_offy/canvas_scale
1035
1036 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