/[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 940 - (show annotations)
Tue May 20 15:25:33 2003 UTC (21 years, 9 months ago) by jonathan
Original Path: trunk/thuban/Thuban/UI/view.py
File MIME type: text/x-python
File size: 35965 byte(s)
(MapCanvas.set_view_transform): Try to limit
        how small the scale can get. This still needs more testing.

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