/[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 1593 - (hide annotations)
Fri Aug 15 14:10:27 2003 UTC (21 years, 6 months ago) by bh
Original Path: trunk/thuban/Thuban/UI/viewport.py
File MIME type: text/x-python
File size: 32492 byte(s)
Change the way shapes are returned by a shape store. The
ShapesInRegion method returns an iterator over actual shape
objects instead of a list of shape ids.

* Thuban/Model/data.py (ShapefileShape.ShapeID): New. Return shape
id.
(ShapefileStore.ShapesInRegion): Return an iterator over the
shapes which yields shape objects instead of returning a list of
shape ids
(ShapefileStore.AllShapes): New. Return an iterator over all
shapes in the shape store
(DerivedShapeStore.AllShapes): New. Like in ShapefileStore

* Thuban/Model/layer.py (Layer.ShapesInRegion): Update
doc-string.

* Thuban/UI/baserenderer.py
(BaseRenderer.layer_ids, BaseRenderer.layer_shapes): Rename to
layer_shapes and make it return an iterator containg shapes
instead of a list of ids.
(BaseRenderer.draw_shape_layer): Update doc-string; Adapt to
layer_shapes() change

* Thuban/UI/renderer.py (ScreenRenderer.layer_ids)
(ScreenRenderer.layer_shapes): Rename as in BaseRenderer

* Thuban/UI/viewport.py (ViewPort._find_shape_in_layer): Adapt to
changes in the ShapesInRegion return value.
(ViewPort._get_hit_tester): Remove commented out code

* test/mockgeo.py (SimpleShapeStore.ShapesInRegion): Adapt to the
new return value.
(SimpleShapeStore.AllShapes): New. Implement this method too.

* test/test_layer.py (TestLayer.test_arc_layer)
(TestLayer.test_polygon_layer, TestLayer.test_point_layer)
(TestLayer.test_point_layer_with_projection)
(TestLayer.test_derived_store): Adapt to changes in the
ShapesInRegion return value.

* test/test_shapefilestore.py
(TestShapefileStoreArc.test_shapes_in_region)
(TestShapefileStorePolygon.test_shapes_in_region)
(TestShapefileStorePoint.test_shapes_in_region): Adapt to changes
in the ShapesInRegion return value.
(TestShapefileStorePoint.test_all_shapes)
(TestShapefileStoreArc.test_shape_shapeid): New tests for the new
methods

* test/test_derivedshapestore.py
(TestDerivedShapeStore.test_shapes_in_region): Adapt to changes in
the ShapesInRegion return value.
(TestDerivedShapeStore.test_all_shapes)
(TestDerivedShapeStore.test_shape_shapeid): New tests for the new
methods

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