/[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 2700 - (hide annotations)
Mon Sep 18 14:27:02 2006 UTC (18 years, 5 months ago) by dpinte
Original Path: trunk/thuban/Thuban/UI/viewport.py
File MIME type: text/x-python
File size: 35633 byte(s)
2006-09-18 Didrik Pinte <dpinte@itae.be>
    
        * wxPython 2.6 update : wx 2.4 syntax has been updated to 2.6


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