/[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 1985 - (hide annotations)
Thu Nov 27 16:04:42 2003 UTC (21 years, 3 months ago) by bh
Original Path: trunk/thuban/Thuban/UI/viewport.py
File MIME type: text/x-python
File size: 33334 byte(s)
(ViewPort.map_projection_changed): Use
InverseBBox to unproject bboxes

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