/[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 2396 - (hide annotations)
Thu Nov 18 20:17:43 2004 UTC (20 years, 3 months ago) by jan
Original Path: trunk/thuban/Thuban/UI/viewport.py
File MIME type: text/x-python
File size: 34617 byte(s)
(ViewPort._hit_point): Added optional parameter
size' defaulting to the previously fixed value 5.
Extended doc-string.
(Viewport._find_shape_in_layer): Resolved FIXME regarding flexibility
for symbols.
Now the size of the largest point symbol is determined to find out
about whether the point has been hit.
This fixes the problem that only clicks inside a fixed distance of
5 where found.

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