/[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 1589 - (hide annotations)
Fri Aug 15 12:49:08 2003 UTC (21 years, 6 months ago) by bh
Original Path: trunk/thuban/Thuban/UI/viewport.py
File MIME type: text/x-python
File size: 33123 byte(s)
* Thuban/UI/viewport.py (ViewPort.find_shape_at)
(ViewPort._find_shape_in_layer, ViewPort._find_shape_in_layer)
(ViewPort._get_hit_tester, ViewPort.projected_points)
(ViewPort._hit_point, ViewPort._hit_arc, ViewPort._hit_polygon)
(ViewPort._find_label_at): Split the find_shape_at method into
several new methods and use the functions in the hit-test module.

* Thuban/UI/hittest.py: New module with Python-level hit-testing
functions

* test/test_hittest.py: New. Test for the new hittest module

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 1456 LAYER_PROJECTION_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     from Thuban.Lib.connector import Publisher
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     class ViewPort(Publisher):
206    
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     self.map = None
260     self.selection.Destroy()
261     self.tool = None
262     #self.selection.Unsubscribe(SHAPES_SELECTED, self.shape_selected)
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     Publisher.Subscribe(self, channel, *args)
276    
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     Publisher.Unsubscribe(self, channel, *args)
289    
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     if self.map is not None:
297     self.map.Unsubscribe(MAP_PROJECTION_CHANGED,
298     self.map_projection_changed)
299     self.map.Unsubscribe(LAYER_PROJECTION_CHANGED,
300     self.layer_projection_changed)
301 bh 1464 changed = self.map is not map
302 jonathan 1386 self.map = map
303     self.selection.ClearSelection()
304     if self.map is not None:
305 bh 1464 self.map.Subscribe(MAP_PROJECTION_CHANGED,
306     self.map_projection_changed)
307     self.map.Subscribe(LAYER_PROJECTION_CHANGED,
308     self.layer_projection_changed)
309 jonathan 1386 self.FitMapToWindow()
310 bh 1464 self.issue(MAP_REPLACED)
311 jonathan 1386
312     def Map(self):
313     """Return the map displayed by this canvas"""
314     return self.map
315    
316     def map_projection_changed(self, map, old_proj):
317    
318     proj = self.map.GetProjection()
319    
320     bbox = None
321    
322     if old_proj is not None and proj is not None:
323     width, height = self.GetPortSizeTuple()
324     llx, lly = self.win_to_proj(0, height)
325     urx, ury = self.win_to_proj(width, 0)
326     bbox = old_proj.Inverse(llx, lly) + old_proj.Inverse(urx, ury)
327     bbox = proj.ForwardBBox(bbox)
328    
329     if bbox is not None:
330     self.FitRectToWindow(bbox)
331     else:
332     self.FitMapToWindow()
333    
334     def layer_projection_changed(self, *args):
335     pass
336    
337 jonathan 1468 def calc_min_max_scales(self, scale = None):
338    
339     if scale is None:
340     scale = self.scale
341    
342 jonathan 1386 llx, lly, urx, ury = bbox = self.map.ProjectedBoundingBox()
343     pwidth = float(urx - llx)
344     pheight = float(ury - lly)
345    
346     # width/height of the window
347     wwidth, wheight = self.GetPortSizeTuple()
348    
349     # The window coordinates used when drawing the shapes must fit
350     # into 16bit signed integers.
351     max_len = max(pwidth, pheight)
352     if max_len:
353     max_scale = 32767.0 / max_len
354     else:
355     # FIXME: What to do in this case? The bbox is effectively
356     # empty so any scale should work.
357     max_scale = scale
358    
359     # The minimal scale is somewhat arbitrarily set to half that of
360     # the bbox fit into the window
361     scales = []
362     if pwidth:
363     scales.append(wwidth / pwidth)
364     if pheight:
365     scales.append(wheight / pheight)
366     if scales:
367     min_scale = 0.5 * min(scales)
368     else:
369     min_scale = scale
370    
371 jonathan 1468 return min_scale, max_scale
372    
373     def set_view_transform(self, scale, offset):
374     # width/height of the window
375     wwidth, wheight = self.GetPortSizeTuple()
376    
377     # The window's center in projected coordinates assuming the new
378     # scale/offset
379     pcenterx = (wwidth/2 - offset[0]) / scale
380     pcentery = (offset[1] - wheight/2) / scale
381    
382     min_scale, max_scale = self.calc_min_max_scales(scale)
383    
384 jonathan 1386 if scale > max_scale:
385     scale = max_scale
386     elif scale < min_scale:
387     scale = min_scale
388    
389     self.scale = scale
390    
391     # determine new offset to preserve the center
392     self.offset = (wwidth/2 - scale * pcenterx,
393     wheight/2 + scale * pcentery)
394     self.issue(SCALE_CHANGED, scale)
395    
396     def GetPortSizeTuple(self):
397     return self.size
398    
399     def proj_to_win(self, x, y):
400     """\
401     Return the point in window coords given by projected coordinates x y
402     """
403     offx, offy = self.offset
404     return (self.scale * x + offx, -self.scale * y + offy)
405    
406     def win_to_proj(self, x, y):
407     """\
408     Return the point in projected coordinates given by window coords x y
409     """
410     offx, offy = self.offset
411     return ((x - offx) / self.scale, (offy - y) / self.scale)
412    
413     def FitRectToWindow(self, rect):
414     """Fit the rectangular region given by rect into the window.
415    
416     Set scale so that rect (in projected coordinates) just fits into
417     the window and center it.
418     """
419     width, height = self.GetPortSizeTuple()
420     llx, lly, urx, ury = rect
421     if llx == urx or lly == ury:
422     # zero width or zero height. Do Nothing
423     return
424     scalex = width / (urx - llx)
425     scaley = height / (ury - lly)
426     scale = min(scalex, scaley)
427     offx = 0.5 * (width - (urx + llx) * scale)
428     offy = 0.5 * (height + (ury + lly) * scale)
429     self.set_view_transform(scale, (offx, offy))
430    
431     def FitMapToWindow(self):
432     """Fit the map to the window
433    
434     Set the scale so that the map fits exactly into the window and
435     center it in the window.
436     """
437     if self.map is not None:
438     bbox = self.map.ProjectedBoundingBox()
439     if bbox is not None:
440     self.FitRectToWindow(bbox)
441    
442     def FitLayerToWindow(self, layer):
443     """Fit the given layer to the window.
444    
445     Set the scale so that the layer fits exactly into the window and
446     center it in the window.
447     """
448    
449     bbox = layer.LatLongBoundingBox()
450     if bbox is not None:
451     proj = self.map.GetProjection()
452     if proj is not None:
453     bbox = proj.ForwardBBox(bbox)
454    
455     if bbox is not None:
456     self.FitRectToWindow(bbox)
457    
458     def FitSelectedToWindow(self):
459     layer = self.selection.SelectedLayer()
460     shapes = self.selection.SelectedShapes()
461    
462     bbox = layer.ShapesBoundingBox(shapes)
463     if bbox is not None:
464     proj = self.map.GetProjection()
465     if proj is not None:
466     bbox = proj.ForwardBBox(bbox)
467    
468     if bbox is not None:
469     if len(shapes) == 1 and layer.ShapeType() == SHAPETYPE_POINT:
470 jonathan 1468 self.ZoomFactor(self.calc_min_max_scales()[1] / self.scale,
471     self.proj_to_win(bbox[0], bbox[1]))
472 jonathan 1386 else:
473     self.FitRectToWindow(bbox)
474    
475     def ZoomFactor(self, factor, center = None):
476     """Multiply the zoom by factor and center on center.
477    
478     The optional parameter center is a point in window coordinates
479     that should be centered. If it is omitted, it defaults to the
480     center of the window
481     """
482     width, height = self.GetPortSizeTuple()
483     scale = self.scale * factor
484     offx, offy = self.offset
485     if center is not None:
486     cx, cy = center
487     else:
488     cx = width / 2
489     cy = height / 2
490     offset = (factor * (offx - cx) + width / 2,
491     factor * (offy - cy) + height / 2)
492     self.set_view_transform(scale, offset)
493    
494     def ZoomOutToRect(self, rect):
495     """Zoom out to fit the currently visible region into rect.
496    
497     The rect parameter is given in window coordinates
498     """
499     # determine the bbox of the displayed region in projected
500     # coordinates
501     width, height = self.GetPortSizeTuple()
502     llx, lly = self.win_to_proj(0, height - 1)
503     urx, ury = self.win_to_proj(width - 1, 0)
504    
505     sx, sy, ex, ey = rect
506     scalex = (ex - sx) / (urx - llx)
507     scaley = (ey - sy) / (ury - lly)
508     scale = min(scalex, scaley)
509    
510     offx = 0.5 * ((ex + sx) - (urx + llx) * scale)
511     offy = 0.5 * ((ey + sy) + (ury + lly) * scale)
512     self.set_view_transform(scale, (offx, offy))
513    
514     def Translate(self, dx, dy):
515     """Move the map by dx, dy pixels"""
516     offx, offy = self.offset
517     self.set_view_transform(self.scale, (offx + dx, offy + dy))
518    
519     def SelectTool(self, tool):
520     """Make tool the active tool.
521    
522     The parameter should be an instance of Tool or None to indicate
523     that no tool is active.
524     """
525     self.tool = tool
526    
527     def ZoomInTool(self):
528     """Start the zoom in tool"""
529     self.SelectTool(ZoomInTool(self))
530    
531     def ZoomOutTool(self):
532     """Start the zoom out tool"""
533     self.SelectTool(ZoomOutTool(self))
534    
535     def PanTool(self):
536     """Start the pan tool"""
537     self.SelectTool(PanTool(self))
538    
539     def IdentifyTool(self):
540     """Start the identify tool"""
541     self.SelectTool(IdentifyTool(self))
542    
543     def LabelTool(self):
544     """Start the label tool"""
545     self.SelectTool(LabelTool(self))
546    
547     def CurrentTool(self):
548     """Return the name of the current tool or None if no tool is active"""
549     return self.tool and self.tool.Name() or None
550    
551     def CurrentPosition(self):
552     """Return current position of the mouse in projected coordinates.
553    
554     The result is a 2-tuple of floats with the coordinates. If the
555     mouse is not in the window, the result is None.
556     """
557     if self.current_position is not None:
558     x, y = self.current_position
559     return self.win_to_proj(x, y)
560     else:
561     return None
562    
563     def set_current_position(self, event):
564     """Set the current position from event
565    
566     Should be called by all events that contain mouse positions
567     especially EVT_MOTION. The event parameter may be None to
568     indicate the the pointer left the window.
569     """
570     if event is not None:
571     self.current_position = (event.m_x, event.m_y)
572     else:
573     self.current_position = None
574     self.issue(VIEW_POSITION)
575    
576     def MouseLeftDown(self, event):
577     self.set_current_position(event)
578     if self.tool is not None:
579     self.tool.MouseDown(event)
580    
581     def MouseLeftUp(self, event):
582     self.set_current_position(event)
583     if self.tool is not None:
584     self.tool.MouseUp(event)
585    
586     def MouseMove(self, event):
587     self.set_current_position(event)
588     if self.tool is not None:
589     self.tool.MouseMove(event)
590    
591     def shape_selected(self, layer, shape):
592     """Receiver for the SHAPES_SELECTED messages. Redraw the map."""
593     # The selection object takes care that it only issues
594     # SHAPES_SELECTED messages when the set of selected shapes has
595     # actually changed, so we can do a full redraw unconditionally.
596     # FIXME: We should perhaps try to limit the redraw to the are
597     # actually covered by the shapes before and after the selection
598     # change.
599     pass
600    
601     def unprojected_rect_around_point(self, x, y, dist):
602     """Return a rect dist pixels around (x, y) in unprojected coordinates
603    
604     The return value is a tuple (minx, miny, maxx, maxy) suitable a
605     parameter to a layer's ShapesInRegion method.
606     """
607     map_proj = self.map.projection
608     if map_proj is not None:
609     inverse = map_proj.Inverse
610     else:
611     inverse = None
612    
613     xs = []
614     ys = []
615     for dx, dy in ((-1, -1), (1, -1), (1, 1), (-1, 1)):
616     px, py = self.win_to_proj(x + dist * dx, y + dist * dy)
617     if inverse:
618     px, py = inverse(px, py)
619     xs.append(px)
620     ys.append(py)
621     return (min(xs), min(ys), max(xs), max(ys))
622    
623     def GetTextExtent(self, text):
624     # arbitrary numbers, really just so the tests pass
625     return 40, 20
626    
627     def find_shape_at(self, px, py, select_labels = 0, searched_layer = None):
628     """Determine the shape at point px, py in window coords
629    
630     Return the shape and the corresponding layer as a tuple (layer,
631 bh 1589 shapeid).
632 jonathan 1386
633     If the optional parameter select_labels is true (default false)
634     search through the labels. If a label is found return it's index
635     as the shape and None as the layer.
636    
637     If the optional parameter searched_layer is given (or not None
638     which it defaults to), only search in that layer.
639     """
640 bh 1589
641     # First if the caller wants to select labels, search and return
642     # it if one is found. We must do this first because the labels
643     # are currently always drawn above all other layers.
644     if select_labels:
645     label = self._find_label_at(px, py)
646     if label is not None:
647     return None, label
648    
649     #
650     # Search the normal layers
651     #
652    
653     # Determine which layers to search. If the caller gave a
654     # specific layer, we only search that. Otherwise we have to
655     # search all visible vector layers in the map in reverse order.
656     if searched_layer:
657     layers = [searched_layer]
658     else:
659     layers = [layer for layer in self.map.Layers()
660     if layer.HasShapes() and layer.Visible()]
661     layers.reverse()
662    
663     # Search through the layers.
664     for layer in layers:
665     shape = self._find_shape_in_layer(layer, px, py)
666     if shape is not None:
667     return layer, shape
668     return None, None
669    
670     def _find_shape_in_layer(self, layer, px, py):
671     """Internal: Return the id of the shape at (px, py) in layer
672    
673     Return None if no shape is at those coordinates.
674     """
675    
676     # For convenience, bind some methods and values to local
677     # variables.
678 jonathan 1386 map_proj = self.map.projection
679     if map_proj is not None:
680     forward = map_proj.Forward
681     else:
682     forward = None
683    
684     scale = self.scale
685    
686     offx, offy = self.offset
687    
688 bh 1589 table = layer.ShapeStore().Table()
689     lc = layer.GetClassification()
690     field = layer.GetClassificationColumn()
691 jonathan 1386
692 bh 1589 # defaults to fall back on
693     filled = lc.GetDefaultFill() is not Transparent
694     stroked = lc.GetDefaultLineColor() is not Transparent
695 jonathan 1386
696 bh 1589 # Determine the ids of the shapes that overlap a tiny area
697     # around the point. For layers containing points we have to
698     # choose a larger size of the box we're testing against so
699     # that we take the size of the markers into account
700     # FIXME: Once the markers are more flexible this part has to
701     # become more flexible too, of course
702     if layer.ShapeType() == SHAPETYPE_POINT:
703     box = self.unprojected_rect_around_point(px, py, 5)
704 jonathan 1386 else:
705 bh 1589 box = self.unprojected_rect_around_point(px, py, 1)
706 jonathan 1386
707 bh 1589 shape_ids = layer.ShapesInRegion(box)
708     shape_ids.reverse()
709 jonathan 1386
710 bh 1589 hittester = self._get_hit_tester(layer)
711     for i in shape_ids:
712     # shapefile = layer.ShapeStore().Shapefile().cobject()
713     if field:
714     record = table.ReadRowAsDict(i)
715     group = lc.FindGroup(record[field])
716     props = group.GetProperties()
717     filled = props.GetFill() is not Transparent
718     stroked = props.GetLineColor() is not Transparent
719     hit = hittester(layer, layer.Shape(i), filled, stroked, px, py)
720     if hit:
721     return i
722     return None
723 jonathan 1386
724 bh 1589 def _get_hit_tester(self, layer):
725     """Internal: Return a hit tester suitable for the layer
726 jonathan 1386
727 bh 1589 The return value is a callable that accepts a shape object and
728     some other parameters and and returns a boolean to indicate
729     whether that shape has been hit. The callable is called like
730     this:
731 jonathan 1386
732 bh 1589 callable(layer, shape, filled, stroked, x, y)
733     """
734     store = layer.ShapeStore()
735     shapetype = store.ShapeType()
736 jonathan 1386
737 bh 1589 if shapetype == SHAPETYPE_POINT:
738     return self._hit_point
739     elif shapetype == SHAPETYPE_ARC:
740     return self._hit_arc
741     elif shapetype == SHAPETYPE_POLYGON:
742     return self._hit_polygon
743     else:
744     raise ValueError("Unknown shapetype %r" % shapetype)
745     # if store.RawShapeFormat() == RAW_SHAPEFILE:
746     # # Special fast versions for shapefiles
747     # if shapetype == SHAPETYPE_POINT:
748     # # Actually this particular tester is not a special one,
749     # # but the generic one :)
750     # return self._hit_point
751     # elif shapetype == SHAPETYPE_ARC:
752     # return self._hit_arc_shapefile
753     # elif shapetype == SHAPETYPE_POLYGON:
754     # return self._hit_polygon_shapefile
755     # else:
756 jonathan 1386
757 bh 1589 def projected_points(self, layer, points):
758     """Return the projected coordinates of the points taken from layer.
759 jonathan 1386
760 bh 1589 Transform all the points in the list of lists of coordinate
761     pairs in points.
762 jonathan 1386
763 bh 1589 The transformation applies the inverse of the layer's projection
764     if any, then the map's projection if any and finally applies
765     self.scale and self.offset.
766 jonathan 1386
767 bh 1589 The returned list has the same structure as the one returned the
768     shape's Points method.
769     """
770     proj = self.map.GetProjection()
771     if proj is not None:
772     forward = proj.Forward
773     else:
774     forward = None
775     proj = layer.GetProjection()
776     if proj is not None:
777     inverse = proj.Inverse
778     else:
779     inverse = None
780     result = []
781     scale = self.scale
782     offx, offy = self.offset
783     for part in points:
784     result.append([])
785     for x, y in part:
786     if inverse:
787     x, y = inverse(x, y)
788     if forward:
789     x, y = forward(x, y)
790     result[-1].append((x * scale + offx,
791     -y * scale + offy))
792     return result
793 jonathan 1386
794 bh 1589 def _hit_point(self, layer, shape, filled, stroked, px, py):
795     """Internal: return whether a click at (px,py) hits the point shape
796 jonathan 1386
797 bh 1589 The filled and stroked parameters determine whether the shape is
798     assumed to be filled or stroked respectively.
799     """
800     x, y = self.projected_points(layer, shape.Points())[0][0]
801     return hypot(px - x, py - y) < 5 and (filled or stroked)
802 jonathan 1386
803 bh 1589 def _hit_arc(self, layer, shape, filled, stroked, px, py):
804     """Internal: return whether a click at (px,py) hits the arc shape
805    
806     The filled and stroked parameters determine whether the shape is
807     assumed to be filled or stroked respectively.
808     """
809     if not stroked:
810     return 0
811     points = self.projected_points(layer, shape.Points())
812     return hittest.arc_hit(points, px, py)
813    
814     def _hit_polygon(self, layer, shape, filled, stroked, px, py):
815     """Internal: return whether a click at (px,py) hits the polygon shape
816    
817     The filled and stroked parameters determine whether the shape is
818     assumed to be filled or stroked respectively.
819     """
820     points = self.projected_points(layer, shape.Points())
821     hit = hittest.polygon_hit(points, px, py)
822     if filled:
823     return bool(hit)
824     return stroked and hit < 0
825    
826     def _find_label_at(self, px, py):
827     """Internal: Find the label at (px, py) and return its index
828    
829     Return None if no label is hit.
830     """
831     map_proj = self.map.projection
832     if map_proj is not None:
833     forward = map_proj.Forward
834     else:
835     forward = None
836     scale = self.scale
837     offx, offy = self.offset
838    
839     labels = self.map.LabelLayer().Labels()
840     if labels:
841     for i in range(len(labels) - 1, -1, -1):
842     label = labels[i]
843     x = label.x
844     y = label.y
845     text = label.text
846     if forward:
847     x, y = forward(x, y)
848     x = x * scale + offx
849     y = -y * scale + offy
850     width, height = self.GetTextExtent(text)
851     if label.halign == ALIGN_LEFT:
852     # nothing to be done
853     pass
854     elif label.halign == ALIGN_RIGHT:
855     x = x - width
856     elif label.halign == ALIGN_CENTER:
857     x = x - width/2
858     if label.valign == ALIGN_TOP:
859     # nothing to be done
860     pass
861     elif label.valign == ALIGN_BOTTOM:
862     y = y - height
863     elif label.valign == ALIGN_CENTER:
864     y = y - height/2
865     if x <= px < x + width and y <= py <= y + height:
866     return i
867     return None
868    
869 jonathan 1386 def SelectShapeAt(self, x, y, layer = None):
870     """\
871     Select and return the shape and its layer at window position (x, y)
872    
873     If layer is given, only search in that layer. If no layer is
874     given, search through all layers.
875    
876     Return a tuple (layer, shapeid). If no shape is found, return
877     (None, None).
878     """
879     layer, shape = result = self.find_shape_at(x, y, searched_layer=layer)
880     # If layer is None, then shape will also be None. We don't want
881     # to deselect the currently selected layer, so we simply select
882     # the already selected layer again.
883     if layer is None:
884     layer = self.selection.SelectedLayer()
885     shapes = []
886     else:
887     shapes = [shape]
888     self.selection.SelectShapes(layer, shapes)
889     return result
890    
891     def LabelShapeAt(self, x, y, text = None):
892     """Add or remove a label at window position x, y.
893    
894     If there's a label at the given position, remove it. Otherwise
895     determine the shape at the position and add a label.
896    
897     Return True is an action was performed, False otherwise.
898     """
899     label_layer = self.map.LabelLayer()
900     layer, shape_index = self.find_shape_at(x, y, select_labels = 1)
901     if layer is None and shape_index is not None:
902     # a label was selected
903     label_layer.RemoveLabel(shape_index)
904     return True
905     elif layer is not None and text:
906     proj = self.map.projection
907     if proj is not None:
908     map_proj = proj
909     else:
910     map_proj = None
911     proj = layer.projection
912     if proj is not None:
913     layer_proj = proj
914     else:
915     layer_proj = None
916    
917     shapetype = layer.ShapeType()
918     if shapetype == SHAPETYPE_POLYGON:
919     shapefile = layer.ShapeStore().Shapefile().cobject()
920     x, y = shape_centroid(shapefile, shape_index,
921     map_proj, layer_proj, 1, 1, 0, 0)
922     if map_proj is not None:
923     x, y = map_proj.Inverse(x, y)
924     else:
925     shape = layer.Shape(shape_index)
926     if shapetype == SHAPETYPE_POINT:
927 bh 1551 x, y = shape.Points()[0][0]
928 jonathan 1386 else:
929     # assume SHAPETYPE_ARC
930 bh 1551 points = shape.Points()[0]
931 jonathan 1386 x, y = points[len(points) / 2]
932     if layer_proj is not None:
933     x, y = layer_proj.Inverse(x, y)
934     if shapetype == SHAPETYPE_POINT:
935     halign = ALIGN_LEFT
936     valign = ALIGN_CENTER
937     elif shapetype == SHAPETYPE_POLYGON:
938     halign = ALIGN_CENTER
939     valign = ALIGN_CENTER
940     elif shapetype == SHAPETYPE_ARC:
941     halign = ALIGN_LEFT
942     valign = ALIGN_CENTER
943     label_layer.AddLabel(x, y, text,
944     halign = halign, valign = valign)
945     return True
946     return False
947    
948 bh 1454 def output_transform(canvas_scale, canvas_offset, canvas_size, device_extend):
949 jonathan 1386 """Calculate dimensions to transform canvas content to output device."""
950     width, height = device_extend
951    
952     # Only 80 % of the with are available for the map
953     width = width * 0.8
954    
955     # Define the distance of the map from DC border
956     distance = 20
957    
958     if height < width:
959     # landscape
960     map_height = height - 2*distance
961     map_width = map_height
962     else:
963     # portrait, recalibrate width (usually the legend width is too
964     # small
965     width = width * 0.9
966     map_height = width - 2*distance
967     map_width = map_height
968    
969     mapregion = (distance, distance,
970     distance+map_width, distance+map_height)
971    
972     canvas_width, canvas_height = canvas_size
973    
974     scalex = map_width / (canvas_width/canvas_scale)
975     scaley = map_height / (canvas_height/canvas_scale)
976     scale = min(scalex, scaley)
977     canvas_offx, canvas_offy = canvas_offset
978     offx = scale*canvas_offx/canvas_scale
979     offy = scale*canvas_offy/canvas_scale
980    
981     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