/[thuban]/branches/WIP-pyshapelib-bramz/Thuban/UI/viewport.py
ViewVC logotype

Annotation of /branches/WIP-pyshapelib-bramz/Thuban/UI/viewport.py

Parent Directory Parent Directory | Revision Log Revision Log


Revision 2544 - (hide annotations)
Mon Jan 24 11:19:53 2005 UTC (20 years, 1 month ago) by bh
Original Path: trunk/thuban/Thuban/UI/viewport.py
File MIME type: text/x-python
File size: 35159 byte(s)
Rework the status bar updates a bit to make sure the message about
the projections is produced at the right times.

* Thuban/UI/mainwindow.py (MainWindow.update_status_bar_messages):
New class variable with messages that may require a status bar
update.
(MainWindow.view_position_changed)
(MainWindow.update_status_bar): Rename from view_position_changed
to update_status_bar.  It's meaning has changed now that it may
also generate messages about problems with projection settings.
(MainWindow.__init__): Use the new update_status_bar_messages
class variable to subscribe update_status_bar
(MainWindow.set_position_text): Update doc-string.  This method
has to be renamed at some point.  See doc-string and comments.
(MainWindow.OnClose): Unsubscribe update_status_bar from all
messages in update_status_bar_messages

* Thuban/UI/viewport.py (ViewPort.forwarded_map_messages): New
class attribute.  map messages to be forwarded by the viewport.
(ViewPort._subscribe_map, ViewPort._unsubscribe_map): (un)subscribe
the messages in forwarded_map_messages

1 bh 2544 # Copyright (c) 2003-2005 by Intevation GmbH
2 jonathan 1386 # Authors:
3 jan 2396 # Bernhard Herzog <[email protected]> (2003, 2004)
4     # Jonathan Coles <[email protected]> (2003)
5     # Jan-Oliver Wagner <[email protected]> (2004)
6 jonathan 1386 #
7     # This program is free software under the GPL (>=v2)
8     # Read the file COPYING coming with Thuban for details.
9    
10     """
11     Classes for display of a map and interaction with it
12     """
13    
14     __version__ = "$Revision$"
15 jan 2396 # $Source$
16     # $Id$
17 jonathan 1386
18     from math import hypot
19    
20     from wxproj import point_in_polygon_shape, shape_centroid
21    
22     from Thuban.Model.messages import MAP_PROJECTION_CHANGED, \
23 bh 2544 LAYER_PROJECTION_CHANGED, TITLE_CHANGED, \
24     MAP_LAYERS_ADDED, MAP_LAYERS_REMOVED
25 bh 1589 from Thuban.Model.data import SHAPETYPE_POLYGON, SHAPETYPE_ARC, \
26     SHAPETYPE_POINT, RAW_SHAPEFILE
27 jonathan 1386 from Thuban.Model.label import ALIGN_CENTER, ALIGN_TOP, ALIGN_BOTTOM, \
28     ALIGN_LEFT, ALIGN_RIGHT
29 bh 1780 from Thuban.Lib.connector import Publisher, Conduit
30 bh 1456 from Thuban.Model.color import Transparent
31 jonathan 1386
32     from selection import Selection
33    
34     from messages import LAYER_SELECTED, SHAPES_SELECTED, VIEW_POSITION, \
35 bh 2544 SCALE_CHANGED, MAP_REPLACED
36 jonathan 1386
37 bh 1589 import hittest
38    
39 jonathan 1386 #
40     # The tools
41     #
42    
43     class Tool:
44    
45     """
46     Base class for the interactive tools
47     """
48    
49     def __init__(self, view):
50     """Intitialize the tool. The view is the canvas displaying the map"""
51     self.view = view
52     self.start = self.current = None
53     self.dragging = 0
54     self.drawn = 0
55    
56     def __del__(self):
57     del self.view
58    
59     def Name(self):
60     """Return the tool's name"""
61     return ''
62    
63     def drag_start(self, x, y):
64     self.start = self.current = x, y
65     self.dragging = 1
66    
67     def drag_move(self, x, y):
68     self.current = x, y
69    
70     def drag_stop(self, x, y):
71     self.current = x, y
72     self.dragging = 0
73    
74     def Show(self, dc):
75     if not self.drawn:
76     self.draw(dc)
77     self.drawn = 1
78    
79     def Hide(self, dc):
80     if self.drawn:
81     self.draw(dc)
82     self.drawn = 0
83    
84     def draw(self, dc):
85     pass
86    
87     def MouseDown(self, event):
88     self.drag_start(event.m_x, event.m_y)
89    
90     def MouseMove(self, event):
91     if self.dragging:
92     self.drag_move(event.m_x, event.m_y)
93    
94     def MouseUp(self, event):
95     if self.dragging:
96 jonathan 1405 self.drag_stop(event.m_x, event.m_y)
97 jonathan 1386
98     def Cancel(self):
99     self.dragging = 0
100    
101    
102     class RectTool(Tool):
103    
104     """Base class for tools that draw rectangles while dragging"""
105    
106     def draw(self, dc):
107     sx, sy = self.start
108     cx, cy = self.current
109     dc.DrawRectangle(sx, sy, cx - sx, cy - sy)
110    
111     class ZoomInTool(RectTool):
112    
113     """The Zoom-In Tool"""
114    
115     def Name(self):
116     return "ZoomInTool"
117    
118     def proj_rect(self):
119     """return the rectangle given by start and current in projected
120     coordinates"""
121     sx, sy = self.start
122     cx, cy = self.current
123     left, top = self.view.win_to_proj(sx, sy)
124     right, bottom = self.view.win_to_proj(cx, cy)
125     return (min(left, right), min(top, bottom),
126     max(left, right), max(top, bottom))
127    
128     def MouseUp(self, event):
129     if self.dragging:
130     Tool.MouseUp(self, event)
131     sx, sy = self.start
132     cx, cy = self.current
133     if sx == cx or sy == cy:
134     # Just a mouse click or a degenerate rectangle. Simply
135     # zoom in by a factor of two
136     # FIXME: For a click this is the desired behavior but should we
137     # really do this for degenrate rectagles as well or
138     # should we ignore them?
139     self.view.ZoomFactor(2, center = (cx, cy))
140     else:
141     # A drag. Zoom in to the rectangle
142     self.view.FitRectToWindow(self.proj_rect())
143    
144    
145     class ZoomOutTool(RectTool):
146    
147     """The Zoom-Out Tool"""
148    
149     def Name(self):
150     return "ZoomOutTool"
151    
152     def MouseUp(self, event):
153     if self.dragging:
154     Tool.MouseUp(self, event)
155     sx, sy = self.start
156     cx, cy = self.current
157     if sx == cx or sy == cy:
158     # Just a mouse click or a degenerate rectangle. Simply
159     # zoom out by a factor of two.
160     # FIXME: For a click this is the desired behavior but should we
161     # really do this for degenrate rectagles as well or
162     # should we ignore them?
163     self.view.ZoomFactor(0.5, center = (cx, cy))
164     else:
165     # A drag. Zoom out to the rectangle
166     self.view.ZoomOutToRect((min(sx, cx), min(sy, cy),
167     max(sx, cx), max(sy, cy)))
168    
169     class PanTool(Tool):
170    
171     """The Pan Tool"""
172    
173     def Name(self):
174     return "PanTool"
175    
176     def MouseMove(self, event):
177     if self.dragging:
178     Tool.MouseMove(self, event)
179    
180     def MouseUp(self, event):
181     if self.dragging:
182     Tool.MouseUp(self, event)
183     sx, sy = self.start
184     cx, cy = self.current
185     self.view.Translate(cx - sx, cy - sy)
186    
187     class IdentifyTool(Tool):
188    
189     """The "Identify" Tool"""
190    
191     def Name(self):
192     return "IdentifyTool"
193    
194     def MouseUp(self, event):
195     self.view.SelectShapeAt(event.m_x, event.m_y)
196    
197    
198     class LabelTool(Tool):
199    
200     """The "Label" Tool"""
201    
202     def Name(self):
203     return "LabelTool"
204    
205     def MouseUp(self, event):
206     self.view.LabelShapeAt(event.m_x, event.m_y)
207    
208    
209 bh 1780 class ViewPort(Conduit):
210 jonathan 1386
211     """An abstract view of the main window"""
212    
213     # Some messages that can be subscribed/unsubscribed directly through
214     # the MapCanvas come in fact from other objects. This is a dict
215     # mapping those messages to the names of the instance variables they
216     # actually come from. The delegation is implemented in the Subscribe
217     # and Unsubscribe methods
218     delegated_messages = {LAYER_SELECTED: "selection",
219     SHAPES_SELECTED: "selection"}
220    
221     # Methods delegated to some instance variables. The delegation is
222     # implemented in the __getattr__ method.
223     delegated_methods = {"SelectLayer": "selection",
224     "SelectShapes": "selection",
225     "SelectedLayer": "selection",
226     "HasSelectedLayer": "selection",
227     "HasSelectedShapes": "selection",
228     "SelectedShapes": "selection"}
229    
230 bh 2544 # Some messages are forwarded from the currently shown map. This is
231     # simply a list of the channels to forward. The _subscribe_map and
232     # _unsubscribe_map methods use this to handle the forwarding.
233     forwarded_map_messages = (LAYER_PROJECTION_CHANGED, TITLE_CHANGED,
234     MAP_PROJECTION_CHANGED,
235     MAP_LAYERS_ADDED, MAP_LAYERS_REMOVED)
236    
237 jonathan 1386 def __init__(self, size = (400, 300)):
238    
239     self.size = size
240    
241     # the map displayed in this canvas. Set with SetMap()
242     self.map = None
243    
244     # scale and offset describe the transformation from projected
245     # coordinates to window coordinates.
246     self.scale = 1.0
247     self.offset = (0, 0)
248    
249     # whether the user is currently dragging the mouse, i.e. moving
250     # the mouse while pressing a mouse button
251     self.dragging = 0
252    
253     # the currently active tool
254     self.tool = None
255    
256     # The current mouse position of the last OnMotion event or None
257     # if the mouse is outside the window.
258     self.current_position = None
259    
260     # the selection
261     self.selection = Selection()
262     self.selection.Subscribe(SHAPES_SELECTED, self.shape_selected)
263    
264     # keep track of which layers/shapes are selected to make sure we
265     # only redraw when necessary
266     self.last_selected_layer = None
267     self.last_selected_shape = None
268    
269     def Destroy(self):
270 bh 1780 self._unsubscribe_map(self.map)
271 jonathan 1386 self.map = None
272     self.selection.Destroy()
273     self.tool = None
274    
275     def Subscribe(self, channel, *args):
276     """Extend the inherited method to handle delegated messages.
277    
278     If channel is one of the delegated messages call the appropriate
279     object's Subscribe method. Otherwise just call the inherited
280     method.
281     """
282     if channel in self.delegated_messages:
283     object = getattr(self, self.delegated_messages[channel])
284     object.Subscribe(channel, *args)
285     else:
286 bh 1780 Conduit.Subscribe(self, channel, *args)
287 jonathan 1386
288     def Unsubscribe(self, channel, *args):
289     """Extend the inherited method to handle delegated messages.
290    
291     If channel is one of the delegated messages call the appropriate
292     object's Unsubscribe method. Otherwise just call the inherited
293     method.
294     """
295     if channel in self.delegated_messages:
296     object = getattr(self, self.delegated_messages[channel])
297     object.Unsubscribe(channel, *args)
298     else:
299 bh 1780 Conduit.Unsubscribe(self, channel, *args)
300 jonathan 1386
301     def __getattr__(self, attr):
302     if attr in self.delegated_methods:
303     return getattr(getattr(self, self.delegated_methods[attr]), attr)
304     raise AttributeError(attr)
305    
306     def SetMap(self, map):
307 bh 1780 self._unsubscribe_map(self.map)
308 bh 1464 changed = self.map is not map
309 jonathan 1386 self.map = map
310     self.selection.ClearSelection()
311 bh 1780 self._subscribe_map(self.map)
312 jonathan 1386 self.FitMapToWindow()
313 bh 1464 self.issue(MAP_REPLACED)
314 jonathan 1386
315 bh 1780 def _subscribe_map(self, map):
316     """Internal: Subscribe to some of the map's messages"""
317     if map is not None:
318     map.Subscribe(LAYER_PROJECTION_CHANGED,
319     self.layer_projection_changed)
320     map.Subscribe(MAP_PROJECTION_CHANGED,
321     self.map_projection_changed)
322 bh 2544 for channel in self.forwarded_map_messages:
323     self.subscribe_forwarding(channel, map)
324 bh 1780
325     def _unsubscribe_map(self, map):
326     """
327     Internal: Unsubscribe from the messages subscribed to in _subscribe_map
328     """
329     if map is not None:
330 bh 2544 for channel in self.forwarded_map_messages:
331     self.unsubscribe_forwarding(channel, map)
332 bh 1780 map.Unsubscribe(MAP_PROJECTION_CHANGED,
333     self.map_projection_changed)
334     map.Unsubscribe(LAYER_PROJECTION_CHANGED,
335     self.layer_projection_changed)
336    
337 jonathan 1386 def Map(self):
338 bh 1780 """Return the map displayed by this canvas or None if no map is shown
339     """
340 jonathan 1386 return self.map
341    
342     def map_projection_changed(self, map, old_proj):
343 bh 2289 """Subscribed to the map's MAP_PROJECTION_CHANGED message
344    
345     If the projection changes, the region shown is probably not
346     meaningful anymore in the new projection. Therefore this method
347     tries to keep the same region visible as before.
348     """
349 jonathan 1386 proj = self.map.GetProjection()
350    
351     bbox = None
352    
353 bh 2289 if old_proj is not None and proj is not None and self.map.HasLayers():
354 jonathan 1386 width, height = self.GetPortSizeTuple()
355     llx, lly = self.win_to_proj(0, height)
356     urx, ury = self.win_to_proj(width, 0)
357 bh 1985 bbox = old_proj.InverseBBox((llx, lly, urx, ury))
358 jonathan 1386 bbox = proj.ForwardBBox(bbox)
359    
360     if bbox is not None:
361     self.FitRectToWindow(bbox)
362     else:
363     self.FitMapToWindow()
364    
365     def layer_projection_changed(self, *args):
366 bh 1780 """Subscribed to the LAYER_PROJECTION_CHANGED messages
367 jonathan 1386
368 bh 1780 This base-class implementation does nothing currently, but it
369     can be extended in derived classes to e.g. redraw the window.
370     """
371    
372 jonathan 1468 def calc_min_max_scales(self, scale = None):
373     if scale is None:
374     scale = self.scale
375    
376 jonathan 1386 llx, lly, urx, ury = bbox = self.map.ProjectedBoundingBox()
377     pwidth = float(urx - llx)
378     pheight = float(ury - lly)
379    
380     # width/height of the window
381     wwidth, wheight = self.GetPortSizeTuple()
382    
383     # The window coordinates used when drawing the shapes must fit
384     # into 16bit signed integers.
385     max_len = max(pwidth, pheight)
386     if max_len:
387     max_scale = 32767.0 / max_len
388     else:
389     # FIXME: What to do in this case? The bbox is effectively
390     # empty so any scale should work.
391     max_scale = scale
392    
393     # The minimal scale is somewhat arbitrarily set to half that of
394     # the bbox fit into the window
395     scales = []
396     if pwidth:
397     scales.append(wwidth / pwidth)
398     if pheight:
399     scales.append(wheight / pheight)
400     if scales:
401     min_scale = 0.5 * min(scales)
402     else:
403     min_scale = scale
404    
405 jonathan 1468 return min_scale, max_scale
406    
407     def set_view_transform(self, scale, offset):
408     # width/height of the window
409     wwidth, wheight = self.GetPortSizeTuple()
410    
411     # The window's center in projected coordinates assuming the new
412     # scale/offset
413     pcenterx = (wwidth/2 - offset[0]) / scale
414     pcentery = (offset[1] - wheight/2) / scale
415    
416     min_scale, max_scale = self.calc_min_max_scales(scale)
417    
418 jonathan 1386 if scale > max_scale:
419     scale = max_scale
420     elif scale < min_scale:
421     scale = min_scale
422    
423     self.scale = scale
424    
425     # determine new offset to preserve the center
426     self.offset = (wwidth/2 - scale * pcenterx,
427     wheight/2 + scale * pcentery)
428     self.issue(SCALE_CHANGED, scale)
429    
430     def GetPortSizeTuple(self):
431     return self.size
432    
433     def proj_to_win(self, x, y):
434     """\
435     Return the point in window coords given by projected coordinates x y
436     """
437     offx, offy = self.offset
438     return (self.scale * x + offx, -self.scale * y + offy)
439    
440     def win_to_proj(self, x, y):
441     """\
442     Return the point in projected coordinates given by window coords x y
443     """
444     offx, offy = self.offset
445     return ((x - offx) / self.scale, (offy - y) / self.scale)
446    
447 bh 2297 def VisibleExtent(self):
448     """Return the extent of the visible region in projected coordinates
449    
450     The return values is a tuple (llx, lly, urx, ury) describing the
451     region.
452     """
453     width, height = self.GetPortSizeTuple()
454     llx, lly = self.win_to_proj(0, height)
455     urx, ury = self.win_to_proj(width, 0)
456     return (llx, lly, urx, ury)
457    
458 jonathan 1386 def FitRectToWindow(self, rect):
459     """Fit the rectangular region given by rect into the window.
460    
461     Set scale so that rect (in projected coordinates) just fits into
462     the window and center it.
463     """
464     width, height = self.GetPortSizeTuple()
465     llx, lly, urx, ury = rect
466     if llx == urx or lly == ury:
467     # zero width or zero height. Do Nothing
468     return
469     scalex = width / (urx - llx)
470     scaley = height / (ury - lly)
471     scale = min(scalex, scaley)
472     offx = 0.5 * (width - (urx + llx) * scale)
473     offy = 0.5 * (height + (ury + lly) * scale)
474     self.set_view_transform(scale, (offx, offy))
475    
476     def FitMapToWindow(self):
477     """Fit the map to the window
478    
479     Set the scale so that the map fits exactly into the window and
480     center it in the window.
481     """
482     if self.map is not None:
483     bbox = self.map.ProjectedBoundingBox()
484     if bbox is not None:
485     self.FitRectToWindow(bbox)
486    
487     def FitLayerToWindow(self, layer):
488     """Fit the given layer to the window.
489    
490     Set the scale so that the layer fits exactly into the window and
491     center it in the window.
492     """
493    
494     bbox = layer.LatLongBoundingBox()
495     if bbox is not None:
496     proj = self.map.GetProjection()
497     if proj is not None:
498     bbox = proj.ForwardBBox(bbox)
499    
500     if bbox is not None:
501     self.FitRectToWindow(bbox)
502    
503     def FitSelectedToWindow(self):
504     layer = self.selection.SelectedLayer()
505     shapes = self.selection.SelectedShapes()
506    
507     bbox = layer.ShapesBoundingBox(shapes)
508     if bbox is not None:
509     proj = self.map.GetProjection()
510     if proj is not None:
511     bbox = proj.ForwardBBox(bbox)
512    
513     if bbox is not None:
514     if len(shapes) == 1 and layer.ShapeType() == SHAPETYPE_POINT:
515 jonathan 1468 self.ZoomFactor(self.calc_min_max_scales()[1] / self.scale,
516     self.proj_to_win(bbox[0], bbox[1]))
517 jonathan 1386 else:
518     self.FitRectToWindow(bbox)
519    
520     def ZoomFactor(self, factor, center = None):
521     """Multiply the zoom by factor and center on center.
522    
523     The optional parameter center is a point in window coordinates
524     that should be centered. If it is omitted, it defaults to the
525     center of the window
526     """
527     width, height = self.GetPortSizeTuple()
528     scale = self.scale * factor
529     offx, offy = self.offset
530     if center is not None:
531     cx, cy = center
532     else:
533     cx = width / 2
534     cy = height / 2
535     offset = (factor * (offx - cx) + width / 2,
536     factor * (offy - cy) + height / 2)
537     self.set_view_transform(scale, offset)
538    
539     def ZoomOutToRect(self, rect):
540     """Zoom out to fit the currently visible region into rect.
541    
542     The rect parameter is given in window coordinates
543     """
544     # determine the bbox of the displayed region in projected
545     # coordinates
546     width, height = self.GetPortSizeTuple()
547     llx, lly = self.win_to_proj(0, height - 1)
548     urx, ury = self.win_to_proj(width - 1, 0)
549    
550     sx, sy, ex, ey = rect
551     scalex = (ex - sx) / (urx - llx)
552     scaley = (ey - sy) / (ury - lly)
553     scale = min(scalex, scaley)
554    
555     offx = 0.5 * ((ex + sx) - (urx + llx) * scale)
556     offy = 0.5 * ((ey + sy) + (ury + lly) * scale)
557     self.set_view_transform(scale, (offx, offy))
558    
559     def Translate(self, dx, dy):
560     """Move the map by dx, dy pixels"""
561     offx, offy = self.offset
562     self.set_view_transform(self.scale, (offx + dx, offy + dy))
563    
564     def SelectTool(self, tool):
565     """Make tool the active tool.
566    
567     The parameter should be an instance of Tool or None to indicate
568     that no tool is active.
569     """
570     self.tool = tool
571    
572     def ZoomInTool(self):
573     """Start the zoom in tool"""
574     self.SelectTool(ZoomInTool(self))
575    
576     def ZoomOutTool(self):
577     """Start the zoom out tool"""
578     self.SelectTool(ZoomOutTool(self))
579    
580     def PanTool(self):
581     """Start the pan tool"""
582     self.SelectTool(PanTool(self))
583    
584     def IdentifyTool(self):
585     """Start the identify tool"""
586     self.SelectTool(IdentifyTool(self))
587    
588     def LabelTool(self):
589     """Start the label tool"""
590     self.SelectTool(LabelTool(self))
591    
592     def CurrentTool(self):
593     """Return the name of the current tool or None if no tool is active"""
594     return self.tool and self.tool.Name() or None
595    
596     def CurrentPosition(self):
597     """Return current position of the mouse in projected coordinates.
598    
599     The result is a 2-tuple of floats with the coordinates. If the
600     mouse is not in the window, the result is None.
601     """
602     if self.current_position is not None:
603     x, y = self.current_position
604     return self.win_to_proj(x, y)
605     else:
606     return None
607    
608     def set_current_position(self, event):
609     """Set the current position from event
610    
611     Should be called by all events that contain mouse positions
612     especially EVT_MOTION. The event parameter may be None to
613     indicate the the pointer left the window.
614     """
615     if event is not None:
616     self.current_position = (event.m_x, event.m_y)
617     else:
618     self.current_position = None
619     self.issue(VIEW_POSITION)
620    
621     def MouseLeftDown(self, event):
622     self.set_current_position(event)
623     if self.tool is not None:
624     self.tool.MouseDown(event)
625    
626     def MouseLeftUp(self, event):
627     self.set_current_position(event)
628     if self.tool is not None:
629     self.tool.MouseUp(event)
630    
631     def MouseMove(self, event):
632     self.set_current_position(event)
633     if self.tool is not None:
634     self.tool.MouseMove(event)
635    
636     def shape_selected(self, layer, shape):
637     """Receiver for the SHAPES_SELECTED messages. Redraw the map."""
638     # The selection object takes care that it only issues
639     # SHAPES_SELECTED messages when the set of selected shapes has
640     # actually changed, so we can do a full redraw unconditionally.
641     # FIXME: We should perhaps try to limit the redraw to the are
642     # actually covered by the shapes before and after the selection
643     # change.
644     pass
645    
646     def unprojected_rect_around_point(self, x, y, dist):
647     """Return a rect dist pixels around (x, y) in unprojected coordinates
648    
649     The return value is a tuple (minx, miny, maxx, maxy) suitable a
650     parameter to a layer's ShapesInRegion method.
651     """
652     map_proj = self.map.projection
653     if map_proj is not None:
654     inverse = map_proj.Inverse
655     else:
656     inverse = None
657    
658     xs = []
659     ys = []
660     for dx, dy in ((-1, -1), (1, -1), (1, 1), (-1, 1)):
661     px, py = self.win_to_proj(x + dist * dx, y + dist * dy)
662     if inverse:
663     px, py = inverse(px, py)
664     xs.append(px)
665     ys.append(py)
666     return (min(xs), min(ys), max(xs), max(ys))
667    
668     def GetTextExtent(self, text):
669 bh 1771 """Return the extent of the text
670 jonathan 1386
671 bh 1771 This method must be implemented by derived classes. The return
672     value must have the same format as that of the GetTextExtent of
673     the wx DC objects.
674     """
675     raise NotImplementedError
676    
677 jonathan 1386 def find_shape_at(self, px, py, select_labels = 0, searched_layer = None):
678     """Determine the shape at point px, py in window coords
679    
680     Return the shape and the corresponding layer as a tuple (layer,
681 bh 1589 shapeid).
682 jonathan 1386
683     If the optional parameter select_labels is true (default false)
684     search through the labels. If a label is found return it's index
685     as the shape and None as the layer.
686    
687     If the optional parameter searched_layer is given (or not None
688     which it defaults to), only search in that layer.
689     """
690 bh 1589
691     # First if the caller wants to select labels, search and return
692     # it if one is found. We must do this first because the labels
693     # are currently always drawn above all other layers.
694     if select_labels:
695     label = self._find_label_at(px, py)
696     if label is not None:
697     return None, label
698    
699     #
700     # Search the normal layers
701     #
702    
703     # Determine which layers to search. If the caller gave a
704     # specific layer, we only search that. Otherwise we have to
705     # search all visible vector layers in the map in reverse order.
706     if searched_layer:
707     layers = [searched_layer]
708     else:
709     layers = [layer for layer in self.map.Layers()
710     if layer.HasShapes() and layer.Visible()]
711     layers.reverse()
712    
713     # Search through the layers.
714     for layer in layers:
715     shape = self._find_shape_in_layer(layer, px, py)
716     if shape is not None:
717     return layer, shape
718     return None, None
719    
720     def _find_shape_in_layer(self, layer, px, py):
721     """Internal: Return the id of the shape at (px, py) in layer
722    
723     Return None if no shape is at those coordinates.
724     """
725    
726     # For convenience, bind some methods and values to local
727     # variables.
728 jonathan 1386 map_proj = self.map.projection
729     if map_proj is not None:
730     forward = map_proj.Forward
731     else:
732     forward = None
733    
734     scale = self.scale
735    
736     offx, offy = self.offset
737    
738 bh 1589 table = layer.ShapeStore().Table()
739     lc = layer.GetClassification()
740     field = layer.GetClassificationColumn()
741 jonathan 1386
742 bh 1589 # defaults to fall back on
743     filled = lc.GetDefaultFill() is not Transparent
744     stroked = lc.GetDefaultLineColor() is not Transparent
745 jonathan 1386
746 bh 1589 # Determine the ids of the shapes that overlap a tiny area
747     # around the point. For layers containing points we have to
748     # choose a larger size of the box we're testing against so
749     # that we take the size of the markers into account
750 jan 2396 # The size of the box is determined by the largest symbol
751     # of the corresponding layer.
752     maxsize = 1
753 bh 1589 if layer.ShapeType() == SHAPETYPE_POINT:
754 jan 2396 for group in layer.GetClassification():
755     props = group.GetProperties()
756     if props.GetSize() > maxsize:
757     maxsize = props.GetSize()
758     box = self.unprojected_rect_around_point(px, py, maxsize)
759 jonathan 1386
760 jan 2396 # determine the function that does the hit test based on the layer
761 bh 1589 hittester = self._get_hit_tester(layer)
762 jan 2396
763 bh 1593 for shape in layer.ShapesInRegion(box):
764 bh 1589 if field:
765 bh 1593 record = table.ReadRowAsDict(shape.ShapeID())
766 bh 1589 group = lc.FindGroup(record[field])
767     props = group.GetProperties()
768     filled = props.GetFill() is not Transparent
769     stroked = props.GetLineColor() is not Transparent
770 jan 2396
771     if layer.ShapeType() == SHAPETYPE_POINT:
772     hit = hittester(layer, shape, filled, stroked,
773     px, py, size = props.GetSize())
774     else:
775     hit = hittester(layer, shape, filled, stroked, px, py)
776    
777 bh 1589 if hit:
778 bh 1593 return shape.ShapeID()
779 bh 1589 return None
780 jonathan 1386
781 bh 1589 def _get_hit_tester(self, layer):
782     """Internal: Return a hit tester suitable for the layer
783 jonathan 1386
784 bh 1589 The return value is a callable that accepts a shape object and
785     some other parameters and and returns a boolean to indicate
786     whether that shape has been hit. The callable is called like
787     this:
788 jonathan 1386
789 bh 1589 callable(layer, shape, filled, stroked, x, y)
790     """
791     store = layer.ShapeStore()
792     shapetype = store.ShapeType()
793 jonathan 1386
794 bh 1589 if shapetype == SHAPETYPE_POINT:
795     return self._hit_point
796     elif shapetype == SHAPETYPE_ARC:
797     return self._hit_arc
798     elif shapetype == SHAPETYPE_POLYGON:
799     return self._hit_polygon
800     else:
801     raise ValueError("Unknown shapetype %r" % shapetype)
802 jonathan 1386
803 bh 1589 def projected_points(self, layer, points):
804     """Return the projected coordinates of the points taken from layer.
805 jonathan 1386
806 bh 1589 Transform all the points in the list of lists of coordinate
807     pairs in points.
808 jonathan 1386
809 bh 1589 The transformation applies the inverse of the layer's projection
810     if any, then the map's projection if any and finally applies
811     self.scale and self.offset.
812 jonathan 1386
813 bh 1589 The returned list has the same structure as the one returned the
814     shape's Points method.
815     """
816     proj = self.map.GetProjection()
817     if proj is not None:
818     forward = proj.Forward
819     else:
820     forward = None
821     proj = layer.GetProjection()
822     if proj is not None:
823     inverse = proj.Inverse
824     else:
825     inverse = None
826     result = []
827     scale = self.scale
828     offx, offy = self.offset
829     for part in points:
830     result.append([])
831     for x, y in part:
832     if inverse:
833     x, y = inverse(x, y)
834     if forward:
835     x, y = forward(x, y)
836     result[-1].append((x * scale + offx,
837     -y * scale + offy))
838     return result
839 jonathan 1386
840 jan 2396 def _hit_point(self, layer, shape, filled, stroked, px, py, size = 5):
841 bh 1589 """Internal: return whether a click at (px,py) hits the point shape
842 jonathan 1386
843 bh 1589 The filled and stroked parameters determine whether the shape is
844     assumed to be filled or stroked respectively.
845 jan 2396
846     size -- defines the size of the point symbol. For the hitting
847     test it is assumed the symbol is a circle of this size
848     (radius).
849 bh 1589 """
850     x, y = self.projected_points(layer, shape.Points())[0][0]
851 jan 2396 return hypot(px - x, py - y) < size and (filled or stroked)
852 jonathan 1386
853 bh 1589 def _hit_arc(self, layer, shape, filled, stroked, px, py):
854     """Internal: return whether a click at (px,py) hits the arc shape
855    
856     The filled and stroked parameters determine whether the shape is
857     assumed to be filled or stroked respectively.
858     """
859     if not stroked:
860     return 0
861     points = self.projected_points(layer, shape.Points())
862     return hittest.arc_hit(points, px, py)
863    
864     def _hit_polygon(self, layer, shape, filled, stroked, px, py):
865     """Internal: return whether a click at (px,py) hits the polygon shape
866    
867     The filled and stroked parameters determine whether the shape is
868     assumed to be filled or stroked respectively.
869     """
870     points = self.projected_points(layer, shape.Points())
871     hit = hittest.polygon_hit(points, px, py)
872     if filled:
873     return bool(hit)
874     return stroked and hit < 0
875    
876     def _find_label_at(self, px, py):
877     """Internal: Find the label at (px, py) and return its index
878    
879     Return None if no label is hit.
880     """
881     map_proj = self.map.projection
882     if map_proj is not None:
883     forward = map_proj.Forward
884     else:
885     forward = None
886     scale = self.scale
887     offx, offy = self.offset
888    
889     labels = self.map.LabelLayer().Labels()
890     if labels:
891     for i in range(len(labels) - 1, -1, -1):
892     label = labels[i]
893     x = label.x
894     y = label.y
895     text = label.text
896     if forward:
897     x, y = forward(x, y)
898     x = x * scale + offx
899     y = -y * scale + offy
900     width, height = self.GetTextExtent(text)
901     if label.halign == ALIGN_LEFT:
902     # nothing to be done
903     pass
904     elif label.halign == ALIGN_RIGHT:
905     x = x - width
906     elif label.halign == ALIGN_CENTER:
907     x = x - width/2
908     if label.valign == ALIGN_TOP:
909     # nothing to be done
910     pass
911     elif label.valign == ALIGN_BOTTOM:
912     y = y - height
913     elif label.valign == ALIGN_CENTER:
914     y = y - height/2
915     if x <= px < x + width and y <= py <= y + height:
916     return i
917     return None
918    
919 jonathan 1386 def SelectShapeAt(self, x, y, layer = None):
920     """\
921     Select and return the shape and its layer at window position (x, y)
922    
923     If layer is given, only search in that layer. If no layer is
924     given, search through all layers.
925    
926     Return a tuple (layer, shapeid). If no shape is found, return
927     (None, None).
928     """
929     layer, shape = result = self.find_shape_at(x, y, searched_layer=layer)
930     # If layer is None, then shape will also be None. We don't want
931     # to deselect the currently selected layer, so we simply select
932     # the already selected layer again.
933     if layer is None:
934     layer = self.selection.SelectedLayer()
935     shapes = []
936     else:
937     shapes = [shape]
938     self.selection.SelectShapes(layer, shapes)
939     return result
940    
941     def LabelShapeAt(self, x, y, text = None):
942     """Add or remove a label at window position x, y.
943    
944     If there's a label at the given position, remove it. Otherwise
945     determine the shape at the position and add a label.
946    
947     Return True is an action was performed, False otherwise.
948     """
949     label_layer = self.map.LabelLayer()
950     layer, shape_index = self.find_shape_at(x, y, select_labels = 1)
951     if layer is None and shape_index is not None:
952     # a label was selected
953     label_layer.RemoveLabel(shape_index)
954     return True
955     elif layer is not None and text:
956     proj = self.map.projection
957     if proj is not None:
958     map_proj = proj
959     else:
960     map_proj = None
961     proj = layer.projection
962     if proj is not None:
963     layer_proj = proj
964     else:
965     layer_proj = None
966    
967     shapetype = layer.ShapeType()
968     if shapetype == SHAPETYPE_POLYGON:
969     shapefile = layer.ShapeStore().Shapefile().cobject()
970     x, y = shape_centroid(shapefile, shape_index,
971     map_proj, layer_proj, 1, 1, 0, 0)
972     if map_proj is not None:
973     x, y = map_proj.Inverse(x, y)
974     else:
975     shape = layer.Shape(shape_index)
976     if shapetype == SHAPETYPE_POINT:
977 bh 1551 x, y = shape.Points()[0][0]
978 jonathan 1386 else:
979     # assume SHAPETYPE_ARC
980 bh 1551 points = shape.Points()[0]
981 jonathan 1386 x, y = points[len(points) / 2]
982     if layer_proj is not None:
983     x, y = layer_proj.Inverse(x, y)
984     if shapetype == SHAPETYPE_POINT:
985     halign = ALIGN_LEFT
986     valign = ALIGN_CENTER
987     elif shapetype == SHAPETYPE_POLYGON:
988     halign = ALIGN_CENTER
989     valign = ALIGN_CENTER
990     elif shapetype == SHAPETYPE_ARC:
991     halign = ALIGN_LEFT
992     valign = ALIGN_CENTER
993     label_layer.AddLabel(x, y, text,
994     halign = halign, valign = valign)
995     return True
996     return False
997    
998 bh 1454 def output_transform(canvas_scale, canvas_offset, canvas_size, device_extend):
999 jonathan 1386 """Calculate dimensions to transform canvas content to output device."""
1000     width, height = device_extend
1001    
1002     # Only 80 % of the with are available for the map
1003     width = width * 0.8
1004    
1005     # Define the distance of the map from DC border
1006     distance = 20
1007    
1008     if height < width:
1009     # landscape
1010     map_height = height - 2*distance
1011     map_width = map_height
1012     else:
1013     # portrait, recalibrate width (usually the legend width is too
1014     # small
1015     width = width * 0.9
1016     map_height = width - 2*distance
1017     map_width = map_height
1018    
1019     mapregion = (distance, distance,
1020     distance+map_width, distance+map_height)
1021    
1022     canvas_width, canvas_height = canvas_size
1023    
1024     scalex = map_width / (canvas_width/canvas_scale)
1025     scaley = map_height / (canvas_height/canvas_scale)
1026     scale = min(scalex, scaley)
1027     canvas_offx, canvas_offy = canvas_offset
1028     offx = scale*canvas_offx/canvas_scale
1029     offy = scale*canvas_offy/canvas_scale
1030    
1031     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