/[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 1771 - (hide annotations)
Mon Oct 6 11:28:38 2003 UTC (21 years, 5 months ago) by bh
Original Path: trunk/thuban/Thuban/UI/viewport.py
File MIME type: text/x-python
File size: 32669 byte(s)
(ViewPort.GetTextExtent): Turn this method
into what would be a "pure virtual function" in C++: always raise
NotImplementedError. Mock implementations for test cases don't
belong into the real code

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 bh 1771 """Return the extent of the text
625 jonathan 1386
626 bh 1771 This method must be implemented by derived classes. The return
627     value must have the same format as that of the GetTextExtent of
628     the wx DC objects.
629     """
630     raise NotImplementedError
631    
632 jonathan 1386 def find_shape_at(self, px, py, select_labels = 0, searched_layer = None):
633     """Determine the shape at point px, py in window coords
634    
635     Return the shape and the corresponding layer as a tuple (layer,
636 bh 1589 shapeid).
637 jonathan 1386
638     If the optional parameter select_labels is true (default false)
639     search through the labels. If a label is found return it's index
640     as the shape and None as the layer.
641    
642     If the optional parameter searched_layer is given (or not None
643     which it defaults to), only search in that layer.
644     """
645 bh 1589
646     # First if the caller wants to select labels, search and return
647     # it if one is found. We must do this first because the labels
648     # are currently always drawn above all other layers.
649     if select_labels:
650     label = self._find_label_at(px, py)
651     if label is not None:
652     return None, label
653    
654     #
655     # Search the normal layers
656     #
657    
658     # Determine which layers to search. If the caller gave a
659     # specific layer, we only search that. Otherwise we have to
660     # search all visible vector layers in the map in reverse order.
661     if searched_layer:
662     layers = [searched_layer]
663     else:
664     layers = [layer for layer in self.map.Layers()
665     if layer.HasShapes() and layer.Visible()]
666     layers.reverse()
667    
668     # Search through the layers.
669     for layer in layers:
670     shape = self._find_shape_in_layer(layer, px, py)
671     if shape is not None:
672     return layer, shape
673     return None, None
674    
675     def _find_shape_in_layer(self, layer, px, py):
676     """Internal: Return the id of the shape at (px, py) in layer
677    
678     Return None if no shape is at those coordinates.
679     """
680    
681     # For convenience, bind some methods and values to local
682     # variables.
683 jonathan 1386 map_proj = self.map.projection
684     if map_proj is not None:
685     forward = map_proj.Forward
686     else:
687     forward = None
688    
689     scale = self.scale
690    
691     offx, offy = self.offset
692    
693 bh 1589 table = layer.ShapeStore().Table()
694     lc = layer.GetClassification()
695     field = layer.GetClassificationColumn()
696 jonathan 1386
697 bh 1589 # defaults to fall back on
698     filled = lc.GetDefaultFill() is not Transparent
699     stroked = lc.GetDefaultLineColor() is not Transparent
700 jonathan 1386
701 bh 1589 # Determine the ids of the shapes that overlap a tiny area
702     # around the point. For layers containing points we have to
703     # choose a larger size of the box we're testing against so
704     # that we take the size of the markers into account
705     # FIXME: Once the markers are more flexible this part has to
706     # become more flexible too, of course
707     if layer.ShapeType() == SHAPETYPE_POINT:
708     box = self.unprojected_rect_around_point(px, py, 5)
709 jonathan 1386 else:
710 bh 1589 box = self.unprojected_rect_around_point(px, py, 1)
711 jonathan 1386
712 bh 1589 hittester = self._get_hit_tester(layer)
713 bh 1593 for shape in layer.ShapesInRegion(box):
714 bh 1589 if field:
715 bh 1593 record = table.ReadRowAsDict(shape.ShapeID())
716 bh 1589 group = lc.FindGroup(record[field])
717     props = group.GetProperties()
718     filled = props.GetFill() is not Transparent
719     stroked = props.GetLineColor() is not Transparent
720 bh 1593 hit = hittester(layer, shape, filled, stroked, px, py)
721 bh 1589 if hit:
722 bh 1593 return shape.ShapeID()
723 bh 1589 return None
724 jonathan 1386
725 bh 1589 def _get_hit_tester(self, layer):
726     """Internal: Return a hit tester suitable for the layer
727 jonathan 1386
728 bh 1589 The return value is a callable that accepts a shape object and
729     some other parameters and and returns a boolean to indicate
730     whether that shape has been hit. The callable is called like
731     this:
732 jonathan 1386
733 bh 1589 callable(layer, shape, filled, stroked, x, y)
734     """
735     store = layer.ShapeStore()
736     shapetype = store.ShapeType()
737 jonathan 1386
738 bh 1589 if shapetype == SHAPETYPE_POINT:
739     return self._hit_point
740     elif shapetype == SHAPETYPE_ARC:
741     return self._hit_arc
742     elif shapetype == SHAPETYPE_POLYGON:
743     return self._hit_polygon
744     else:
745     raise ValueError("Unknown shapetype %r" % shapetype)
746 jonathan 1386
747 bh 1589 def projected_points(self, layer, points):
748     """Return the projected coordinates of the points taken from layer.
749 jonathan 1386
750 bh 1589 Transform all the points in the list of lists of coordinate
751     pairs in points.
752 jonathan 1386
753 bh 1589 The transformation applies the inverse of the layer's projection
754     if any, then the map's projection if any and finally applies
755     self.scale and self.offset.
756 jonathan 1386
757 bh 1589 The returned list has the same structure as the one returned the
758     shape's Points method.
759     """
760     proj = self.map.GetProjection()
761     if proj is not None:
762     forward = proj.Forward
763     else:
764     forward = None
765     proj = layer.GetProjection()
766     if proj is not None:
767     inverse = proj.Inverse
768     else:
769     inverse = None
770     result = []
771     scale = self.scale
772     offx, offy = self.offset
773     for part in points:
774     result.append([])
775     for x, y in part:
776     if inverse:
777     x, y = inverse(x, y)
778     if forward:
779     x, y = forward(x, y)
780     result[-1].append((x * scale + offx,
781     -y * scale + offy))
782     return result
783 jonathan 1386
784 bh 1589 def _hit_point(self, layer, shape, filled, stroked, px, py):
785     """Internal: return whether a click at (px,py) hits the point shape
786 jonathan 1386
787 bh 1589 The filled and stroked parameters determine whether the shape is
788     assumed to be filled or stroked respectively.
789     """
790     x, y = self.projected_points(layer, shape.Points())[0][0]
791     return hypot(px - x, py - y) < 5 and (filled or stroked)
792 jonathan 1386
793 bh 1589 def _hit_arc(self, layer, shape, filled, stroked, px, py):
794     """Internal: return whether a click at (px,py) hits the arc shape
795    
796     The filled and stroked parameters determine whether the shape is
797     assumed to be filled or stroked respectively.
798     """
799     if not stroked:
800     return 0
801     points = self.projected_points(layer, shape.Points())
802     return hittest.arc_hit(points, px, py)
803    
804     def _hit_polygon(self, layer, shape, filled, stroked, px, py):
805     """Internal: return whether a click at (px,py) hits the polygon shape
806    
807     The filled and stroked parameters determine whether the shape is
808     assumed to be filled or stroked respectively.
809     """
810     points = self.projected_points(layer, shape.Points())
811     hit = hittest.polygon_hit(points, px, py)
812     if filled:
813     return bool(hit)
814     return stroked and hit < 0
815    
816     def _find_label_at(self, px, py):
817     """Internal: Find the label at (px, py) and return its index
818    
819     Return None if no label is hit.
820     """
821     map_proj = self.map.projection
822     if map_proj is not None:
823     forward = map_proj.Forward
824     else:
825     forward = None
826     scale = self.scale
827     offx, offy = self.offset
828    
829     labels = self.map.LabelLayer().Labels()
830     if labels:
831     for i in range(len(labels) - 1, -1, -1):
832     label = labels[i]
833     x = label.x
834     y = label.y
835     text = label.text
836     if forward:
837     x, y = forward(x, y)
838     x = x * scale + offx
839     y = -y * scale + offy
840     width, height = self.GetTextExtent(text)
841     if label.halign == ALIGN_LEFT:
842     # nothing to be done
843     pass
844     elif label.halign == ALIGN_RIGHT:
845     x = x - width
846     elif label.halign == ALIGN_CENTER:
847     x = x - width/2
848     if label.valign == ALIGN_TOP:
849     # nothing to be done
850     pass
851     elif label.valign == ALIGN_BOTTOM:
852     y = y - height
853     elif label.valign == ALIGN_CENTER:
854     y = y - height/2
855     if x <= px < x + width and y <= py <= y + height:
856     return i
857     return None
858    
859 jonathan 1386 def SelectShapeAt(self, x, y, layer = None):
860     """\
861     Select and return the shape and its layer at window position (x, y)
862    
863     If layer is given, only search in that layer. If no layer is
864     given, search through all layers.
865    
866     Return a tuple (layer, shapeid). If no shape is found, return
867     (None, None).
868     """
869     layer, shape = result = self.find_shape_at(x, y, searched_layer=layer)
870     # If layer is None, then shape will also be None. We don't want
871     # to deselect the currently selected layer, so we simply select
872     # the already selected layer again.
873     if layer is None:
874     layer = self.selection.SelectedLayer()
875     shapes = []
876     else:
877     shapes = [shape]
878     self.selection.SelectShapes(layer, shapes)
879     return result
880    
881     def LabelShapeAt(self, x, y, text = None):
882     """Add or remove a label at window position x, y.
883    
884     If there's a label at the given position, remove it. Otherwise
885     determine the shape at the position and add a label.
886    
887     Return True is an action was performed, False otherwise.
888     """
889     label_layer = self.map.LabelLayer()
890     layer, shape_index = self.find_shape_at(x, y, select_labels = 1)
891     if layer is None and shape_index is not None:
892     # a label was selected
893     label_layer.RemoveLabel(shape_index)
894     return True
895     elif layer is not None and text:
896     proj = self.map.projection
897     if proj is not None:
898     map_proj = proj
899     else:
900     map_proj = None
901     proj = layer.projection
902     if proj is not None:
903     layer_proj = proj
904     else:
905     layer_proj = None
906    
907     shapetype = layer.ShapeType()
908     if shapetype == SHAPETYPE_POLYGON:
909     shapefile = layer.ShapeStore().Shapefile().cobject()
910     x, y = shape_centroid(shapefile, shape_index,
911     map_proj, layer_proj, 1, 1, 0, 0)
912     if map_proj is not None:
913     x, y = map_proj.Inverse(x, y)
914     else:
915     shape = layer.Shape(shape_index)
916     if shapetype == SHAPETYPE_POINT:
917 bh 1551 x, y = shape.Points()[0][0]
918 jonathan 1386 else:
919     # assume SHAPETYPE_ARC
920 bh 1551 points = shape.Points()[0]
921 jonathan 1386 x, y = points[len(points) / 2]
922     if layer_proj is not None:
923     x, y = layer_proj.Inverse(x, y)
924     if shapetype == SHAPETYPE_POINT:
925     halign = ALIGN_LEFT
926     valign = ALIGN_CENTER
927     elif shapetype == SHAPETYPE_POLYGON:
928     halign = ALIGN_CENTER
929     valign = ALIGN_CENTER
930     elif shapetype == SHAPETYPE_ARC:
931     halign = ALIGN_LEFT
932     valign = ALIGN_CENTER
933     label_layer.AddLabel(x, y, text,
934     halign = halign, valign = valign)
935     return True
936     return False
937    
938 bh 1454 def output_transform(canvas_scale, canvas_offset, canvas_size, device_extend):
939 jonathan 1386 """Calculate dimensions to transform canvas content to output device."""
940     width, height = device_extend
941    
942     # Only 80 % of the with are available for the map
943     width = width * 0.8
944    
945     # Define the distance of the map from DC border
946     distance = 20
947    
948     if height < width:
949     # landscape
950     map_height = height - 2*distance
951     map_width = map_height
952     else:
953     # portrait, recalibrate width (usually the legend width is too
954     # small
955     width = width * 0.9
956     map_height = width - 2*distance
957     map_width = map_height
958    
959     mapregion = (distance, distance,
960     distance+map_width, distance+map_height)
961    
962     canvas_width, canvas_height = canvas_size
963    
964     scalex = map_width / (canvas_width/canvas_scale)
965     scaley = map_height / (canvas_height/canvas_scale)
966     scale = min(scalex, scaley)
967     canvas_offx, canvas_offy = canvas_offset
968     offx = scale*canvas_offx/canvas_scale
969     offy = scale*canvas_offy/canvas_scale
970    
971     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