/[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 2289 - (hide annotations)
Fri Jul 16 19:47:29 2004 UTC (20 years, 7 months ago) by bh
Original Path: trunk/thuban/Thuban/UI/viewport.py
File MIME type: text/x-python
File size: 33573 byte(s)
* test/test_viewport.py
(ViewPortTest.test_changing_map_projection): Check that changing
the projection of an empty map shown in a viewport doesn't lead to
exceptions in the viewport's handler for the
MAP_PROJECTION_CHANGED messages

* Thuban/UI/viewport.py (ViewPort.map_projection_changed): Only
try to keep the same region visible when the map actually contains
something

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