/[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 2297 - (hide annotations)
Thu Jul 22 13:07:52 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: 33958 byte(s)
* Thuban/UI/viewport.py (ViewPort.VisibleExtent): New.  Return the
visible extent of the map in projected coordinates

* test/test_viewport.py (SimpleViewPortTest.test_default_size)
(SimpleViewPortTest.test_init_with_size): Add some VisibleExtent()
tests.
(SimpleViewPortTest.test_visible_extent): New. The real test for
VisibleExtent()

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