/[thuban]/branches/WIP-pyshapelib-bramz/Thuban/UI/view.py
ViewVC logotype

Contents of /branches/WIP-pyshapelib-bramz/Thuban/UI/view.py

Parent Directory Parent Directory | Revision Log Revision Log


Revision 78 - (show annotations)
Mon Feb 4 19:37:16 2002 UTC (23 years, 1 month ago) by bh
Original Path: trunk/thuban/Thuban/UI/view.py
File MIME type: text/x-python
File size: 21534 byte(s)
	* Thuban/UI/view.py (MapCanvas.SelectShapeAt): Search through all
	layers by default. Easier to use than the previous default of only
	searching through the select layer which meant that if no layer
	was selected, you couldn't select a shape.

1 # Copyright (c) 2001, 2002 by Intevation GmbH
2 # Authors:
3 # Bernhard Herzog <[email protected]>
4 #
5 # This program is free software under the GPL (>=v2)
6 # Read the file COPYING coming with Thuban for details.
7
8 """
9 Classes for display of a map and interaction with it
10 """
11
12 __version__ = "$Revision$"
13
14 from math import hypot
15
16 from wxPython.wx import wxWindow,\
17 wxPaintDC, wxColour, wxClientDC, wxINVERT, wxTRANSPARENT_BRUSH, wxFont,\
18 EVT_PAINT, EVT_LEFT_DOWN, EVT_LEFT_UP, EVT_MOTION
19
20
21 from wxPython import wx
22
23 from wxproj import point_in_polygon_shape, shape_centroid
24
25
26 from Thuban.Model.messages import MAP_PROJECTION_CHANGED, \
27 LAYERS_CHANGED, LAYER_LEGEND_CHANGED, LAYER_VISIBILITY_CHANGED
28 from Thuban.Model.layer import SHAPETYPE_POLYGON, SHAPETYPE_ARC, \
29 SHAPETYPE_POINT
30 from Thuban.Model.label import ALIGN_CENTER, ALIGN_TOP, ALIGN_BOTTOM, \
31 ALIGN_LEFT, ALIGN_RIGHT
32
33
34 from renderer import ScreenRenderer, PrinterRender
35
36 import labeldialog
37
38 from messages import SELECTED_SHAPE
39
40
41 #
42 # The tools
43 #
44
45 class Tool:
46
47 """
48 Base class for the interactive tools
49 """
50
51 def __init__(self, view):
52 """Intitialize the tool. The view is the canvas displaying the map"""
53 self.view = view
54 self.start = self.current = None
55 self.dragging = 0
56 self.drawn = 0
57
58 def Name(self):
59 """Return the tool's name"""
60 return ''
61
62 def drag_start(self, x, y):
63 self.start = self.current = x, y
64 self.dragging = 1
65
66 def drag_move(self, x, y):
67 self.current = x, y
68
69 def drag_stop(self, x, y):
70 self.current = x, y
71 self.dragging = 0
72
73 def Show(self, dc):
74 if not self.drawn:
75 self.draw(dc)
76 self.drawn = 1
77
78 def Hide(self, dc):
79 if self.drawn:
80 self.draw(dc)
81 self.drawn = 0
82
83 def draw(self, dc):
84 pass
85
86 def MouseDown(self, event):
87 self.drag_start(event.m_x, event.m_y)
88
89 def MouseMove(self, event):
90 if self.dragging:
91 self.drag_move(event.m_x, event.m_y)
92
93 def MouseUp(self, event):
94 if self.dragging:
95 self.drag_move(event.m_x, event.m_y)
96
97 def Cancel(self):
98 self.dragging = 0
99
100
101 class RectTool(Tool):
102
103 """Base class for tools that draw rectangles while dragging"""
104
105 def draw(self, dc):
106 sx, sy = self.start
107 cx, cy = self.current
108 dc.DrawRectangle(sx, sy, cx - sx, cy - sy)
109
110 class ZoomInTool(RectTool):
111
112 """The Zoom-In Tool"""
113
114 def Name(self):
115 return "ZoomInTool"
116
117 def proj_rect(self):
118 """return the rectangle given by start and current in projected
119 coordinates"""
120 sx, sy = self.start
121 cx, cy = self.current
122 left, top = self.view.win_to_proj(sx, sy)
123 right, bottom = self.view.win_to_proj(cx, cy)
124 return (min(left, right), min(top, bottom),
125 max(left, right), max(top, bottom))
126
127 def MouseUp(self, event):
128 if self.dragging:
129 Tool.MouseUp(self, event)
130 sx, sy = self.start
131 cx, cy = self.current
132 if sx == cx and sy == cy:
133 # Just a mouse click. Simply zoom in by a factor of two
134 self.view.ZoomFactor(2, center = (cx, cy))
135 else:
136 # A drag. Zoom in to the rectangle
137 self.view.FitRectToWindow(self.proj_rect())
138
139
140 class ZoomOutTool(RectTool):
141
142 """The Zoom-Out Tool"""
143
144 def Name(self):
145 return "ZoomOutTool"
146
147 def MouseUp(self, event):
148 if self.dragging:
149 Tool.MouseUp(self, event)
150 sx, sy = self.start
151 cx, cy = self.current
152 if sx == cx and sy == cy:
153 # Just a mouse click. Simply zoom out by a factor of two
154 self.view.ZoomFactor(0.5, center = (cy, cy))
155 else:
156 # A drag. Zoom out to the rectangle
157 self.view.ZoomOutToRect((min(sx, cx), min(sy, cy),
158 max(sx, cx), max(sy, cy)))
159
160
161 class PanTool(Tool):
162
163 """The Pan Tool"""
164
165 def Name(self):
166 return "PanTool"
167
168 def MouseMove(self, event):
169 if self.dragging:
170 x0, y0 = self.current
171 Tool.MouseMove(self, event)
172 x, y = self.current
173 width, height = self.view.GetSizeTuple()
174 dc = self.view.drag_dc
175 dc.Blit(0, 0, width, height, dc, x0 - x, y0 - y)
176
177 def MouseUp(self, event):
178 if self.dragging:
179 Tool.MouseUp(self, event)
180 sx, sy = self.start
181 cx, cy = self.current
182 self.view.Translate(cx - sx, cy - sy)
183
184 class IdentifyTool(Tool):
185
186 """The "Identify" Tool"""
187
188 def Name(self):
189 return "IdentifyTool"
190
191 def MouseUp(self, event):
192 self.view.SelectShapeAt(event.m_x, event.m_y)
193
194
195 class LabelTool(Tool):
196
197 """The "Label" Tool"""
198
199 def Name(self):
200 return "LabelTool"
201
202 def MouseUp(self, event):
203 self.view.LabelShapeAt(event.m_x, event.m_y)
204
205
206
207
208 class MapPrintout(wx.wxPrintout):
209
210 """
211 wxPrintout class for printing Thuban maps
212 """
213
214 def __init__(self, map):
215 wx.wxPrintout.__init__(self)
216 self.map = map
217
218 def GetPageInfo(self):
219 return (1, 1, 1, 1)
220
221 def HasPage(self, pagenum):
222 return pagenum == 1
223
224 def OnPrintPage(self, pagenum):
225 if pagenum == 1:
226 self.draw_on_dc(self.GetDC())
227
228 def draw_on_dc(self, dc):
229 width, height = self.GetPageSizePixels()
230 llx, lly, urx, ury = self.map.ProjectedBoundingBox()
231 scalex = width / (urx - llx)
232 scaley = height / (ury - lly)
233 scale = min(scalex, scaley)
234 offx = 0.5 * (width - (urx + llx) * scale)
235 offy = 0.5 * (height + (ury + lly) * scale)
236
237 resx, resy = self.GetPPIPrinter()
238 renderer = PrinterRender(dc, scale, (offx, offy), resolution = resx)
239 renderer.RenderMap(self.map)
240 return wx.true
241
242
243 class MapCanvas(wxWindow):
244
245 """A widget that displays a map and offers some interaction"""
246
247 def __init__(self, parent, winid, interactor):
248 wxWindow.__init__(self, parent, winid)
249 self.SetBackgroundColour(wxColour(255, 255, 255))
250 self.map = None
251 self.scale = 1.0
252 self.offset = (0, 0)
253 self.dragging = 0
254 self.tool = None
255 self.redraw_on_idle = 0
256 EVT_PAINT(self, self.OnPaint)
257 EVT_LEFT_DOWN(self, self.OnLeftDown)
258 EVT_LEFT_UP(self, self.OnLeftUp)
259 EVT_MOTION(self, self.OnMotion)
260 wx.EVT_IDLE(self, self.OnIdle)
261 self.interactor = interactor
262 self.interactor.Subscribe(SELECTED_SHAPE, self.shape_selected)
263
264 def OnPaint(self, event):
265 dc = wxPaintDC(self)
266 if self.map is not None and self.map.HasLayers():
267 # We have a non-empty map. Redraw it in idle time
268 self.redraw_on_idle = 1
269 else:
270 # If we've got no map or if the map is empty, simply clear
271 # the screen.
272
273 # XXX it's probably possible to get rid of this. The
274 # background color of the window is already white and the
275 # only thing we may have to do is to call self.Refresh()
276 # with a true argument in the right places.
277 dc.BeginDrawing()
278 dc.Clear()
279 dc.EndDrawing()
280
281 def do_redraw(self):
282 # This should only be called if we have a non-empty map. We draw
283 # it into a memory DC and then blit it to the screen.
284 width, height = self.GetSizeTuple()
285 bitmap = wx.wxEmptyBitmap(width, height)
286 dc = wx.wxMemoryDC()
287 dc.SelectObject(bitmap)
288 dc.BeginDrawing()
289
290 # clear the background
291 dc.SetBrush(wx.wxWHITE_BRUSH)
292 dc.SetPen(wx.wxTRANSPARENT_PEN)
293 dc.DrawRectangle(0, 0, width, height)
294
295 if 1: #self.interactor.selected_map is self.map:
296 selected_layer = self.interactor.selected_layer
297 selected_shape = self.interactor.selected_shape
298 else:
299 selected_layer = None
300 selected_shape = None
301
302 # draw the map into the bitmap
303 renderer = ScreenRenderer(dc, self.scale, self.offset)
304 renderer.RenderMap(self.map, selected_layer, selected_shape)
305
306 dc.EndDrawing()
307
308 # blit the bitmap to the screen
309 clientdc = wxClientDC(self)
310 clientdc.BeginDrawing()
311 clientdc.Blit(0, 0, width, height, dc, 0, 0)
312 clientdc.EndDrawing()
313
314 def Print(self):
315 printer = wx.wxPrinter()
316 printout = MapPrintout(self.map)
317 printer.Print(self, printout, wx.true)
318 printout.Destroy()
319
320 def SetMap(self, map):
321 redraw_channels = (LAYERS_CHANGED, LAYER_LEGEND_CHANGED,
322 LAYER_VISIBILITY_CHANGED)
323 if self.map is not None:
324 for channel in redraw_channels:
325 self.map.Unsubscribe(channel, self.redraw)
326 self.map.Unsubscribe(MAP_PROJECTION_CHANGED,
327 self.projection_changed)
328 self.map = map
329 if self.map is not None:
330 for channel in redraw_channels:
331 self.map.Subscribe(channel, self.redraw)
332 self.map.Subscribe(MAP_PROJECTION_CHANGED, self.projection_changed)
333 self.FitMapToWindow()
334 # force a redraw. If map is not empty, it's already been called
335 # by FitMapToWindow but if map is empty it hasn't been called
336 # yet so we have to explicitly call it.
337 self.redraw()
338
339 def Map(self):
340 return self.map
341
342 def redraw(self, *args):
343 self.Refresh(0)
344
345 def projection_changed(self, *args):
346 self.FitMapToWindow()
347 self.redraw()
348
349 def set_view_transform(self, scale, offset):
350 self.scale = scale
351 self.offset = offset
352 self.redraw()
353
354 def proj_to_win(self, x, y):
355 """\
356 Return the point in window coords given by projected coordinates x y
357 """
358 offx, offy = self.offset
359 return (self.scale * x + offx, -self.scale * y + offy)
360
361 def win_to_proj(self, x, y):
362 """\
363 Return the point in projected coordinates given by window coords x y
364 """
365 offx, offy = self.offset
366 return ((x - offx) / self.scale, (offy - y) / self.scale)
367
368 def FitRectToWindow(self, rect):
369 width, height = self.GetSizeTuple()
370 llx, lly, urx, ury = rect
371 if llx == urx or lly == ury:
372 # zero with or zero height. Do Nothing
373 return
374 scalex = width / (urx - llx)
375 scaley = height / (ury - lly)
376 scale = min(scalex, scaley)
377 offx = 0.5 * (width - (urx + llx) * scale)
378 offy = 0.5 * (height + (ury + lly) * scale)
379 self.set_view_transform(scale, (offx, offy))
380
381 def FitMapToWindow(self):
382 """\
383 Set the scale and offset so that the map is centered in the
384 window
385 """
386 bbox = self.map.ProjectedBoundingBox()
387 if bbox is not None:
388 self.FitRectToWindow(bbox)
389
390 def ZoomFactor(self, factor, center = None):
391 """Multiply the zoom by factor and center on center.
392
393 The optional parameter center is a point in window coordinates
394 that should be centered. If it is omitted, it defaults to the
395 center of the window
396 """
397 width, height = self.GetSizeTuple()
398 scale = self.scale * factor
399 offx, offy = self.offset
400 if center is not None:
401 cx, cy = center
402 else:
403 cx = width / 2
404 cy = height / 2
405 offset = (factor * (offx - cx) + width / 2,
406 factor * (offy - cy) + height / 2)
407 self.set_view_transform(scale, offset)
408
409 def ZoomOutToRect(self, rect):
410 # rect is given in window coordinates
411
412 # determine the bbox of the displayed region in projected
413 # coordinates
414 width, height = self.GetSizeTuple()
415 llx, lly = self.win_to_proj(0, height - 1)
416 urx, ury = self.win_to_proj(width - 1, 0)
417
418 sx, sy, ex, ey = rect
419 scalex = (ex - sx) / (urx - llx)
420 scaley = (ey - sy) / (ury - lly)
421 scale = min(scalex, scaley)
422
423 offx = 0.5 * ((ex + sx) - (urx + llx) * scale)
424 offy = 0.5 * ((ey + sy) + (ury + lly) * scale)
425 self.set_view_transform(scale, (offx, offy))
426
427 def Translate(self, dx, dy):
428 offx, offy = self.offset
429 self.set_view_transform(self.scale, (offx + dx, offy + dy))
430
431 def ZoomInTool(self):
432 self.tool = ZoomInTool(self)
433
434 def ZoomOutTool(self):
435 self.tool = ZoomOutTool(self)
436
437 def PanTool(self):
438 self.tool = PanTool(self)
439
440 def IdentifyTool(self):
441 self.tool = IdentifyTool(self)
442
443 def LabelTool(self):
444 self.tool = LabelTool(self)
445
446 def CurrentTool(self):
447 return self.tool and self.tool.Name() or None
448
449 def OnLeftDown(self, event):
450 if self.tool is not None:
451 self.drag_dc = wxClientDC(self)
452 self.drag_dc.SetLogicalFunction(wxINVERT)
453 self.drag_dc.SetBrush(wxTRANSPARENT_BRUSH)
454 self.CaptureMouse()
455 self.tool.MouseDown(event)
456 self.tool.Show(self.drag_dc)
457 self.dragging = 1
458
459 def OnLeftUp(self, event):
460 self.ReleaseMouse()
461 if self.dragging:
462 self.tool.Hide(self.drag_dc)
463 self.tool.MouseUp(event)
464 self.drag_dc = None
465 self.dragging = 0
466
467 def OnMotion(self, event):
468 if self.dragging:
469 self.tool.Hide(self.drag_dc)
470 self.tool.MouseMove(event)
471 self.tool.Show(self.drag_dc)
472
473 def OnIdle(self, event):
474 if self.redraw_on_idle:
475 self.do_redraw()
476 self.redraw_on_idle = 0
477
478 def shape_selected(self, layer, shape):
479 self.redraw()
480
481 def find_shape_at(self, px, py, select_labels = 0, selected_layer = 1):
482 """Determine the shape at point px, py in window coords
483
484 Return the shape and the corresponding layer as a tuple (layer,
485 shape).
486
487 If the optional parameter select_labels is true (default false)
488 search through the labels. If a label is found return it's index
489 as the shape and None as the layer.
490
491 If the optional parameter selected_layer is true (default), only
492 search in the currently selected layer.
493 """
494 map_proj = self.map.projection
495 if map_proj is not None:
496 forward = map_proj.Forward
497 else:
498 forward = None
499
500 scale = self.scale
501 offx, offy = self.offset
502
503 if select_labels:
504 labels = self.map.LabelLayer().Labels()
505
506 if labels:
507 dc = wxClientDC(self)
508 font = wxFont(10, wx.wxSWISS, wx.wxNORMAL, wx.wxNORMAL)
509 dc.SetFont(font)
510 for i in range(len(labels) - 1, -1, -1):
511 label = labels[i]
512 x = label.x
513 y = label.y
514 text = label.text
515 if forward:
516 x, y = forward(x, y)
517 x = x * scale + offx
518 y = -y * scale + offy
519 width, height = dc.GetTextExtent(text)
520 if label.halign == ALIGN_LEFT:
521 # nothing to be done
522 pass
523 elif label.halign == ALIGN_RIGHT:
524 x = x - width
525 elif label.halign == ALIGN_CENTER:
526 x = x - width/2
527 if label.valign == ALIGN_TOP:
528 # nothing to be done
529 pass
530 elif label.valign == ALIGN_BOTTOM:
531 y = y - height
532 elif label.valign == ALIGN_CENTER:
533 y = y - height/2
534 if x <= px < x + width and y <= py <= y + height:
535 return None, i
536
537 if selected_layer:
538 layer = self.interactor.SelectedLayer()
539 if layer is not None:
540 layers = [layer]
541 else:
542 # no layer selected. Use an empty list to effectively
543 # ignore all layers.
544 layers = []
545 else:
546 layers = self.map.Layers()
547
548 for layer_index in range(len(layers) - 1, -1, -1):
549 layer = layers[layer_index]
550
551 # search only in visible layers
552 if not layer.Visible():
553 continue
554
555 filled = layer.fill is not None
556 stroked = layer.stroke is not None
557
558 layer_proj = layer.projection
559 if layer_proj is not None:
560 inverse = layer_proj.Inverse
561 else:
562 inverse = None
563
564 shapetype = layer.ShapeType()
565
566 select_shape = -1
567 if shapetype == SHAPETYPE_POLYGON:
568 for i in range(layer.NumShapes() - 1, -1, -1):
569 result = point_in_polygon_shape(layer.shapefile.cobject(),
570 i,
571 filled, stroked,
572 map_proj, layer_proj,
573 scale, -scale, offx, offy,
574 px, py)
575 if result:
576 select_shape = i
577 break
578 elif shapetype == SHAPETYPE_ARC:
579 for i in range(layer.NumShapes() - 1, -1, -1):
580 result = point_in_polygon_shape(layer.shapefile.cobject(),
581 i, 0, 1,
582 map_proj, layer_proj,
583 scale, -scale, offx, offy,
584 px, py)
585 if result < 0:
586 select_shape = i
587 break
588 elif shapetype == SHAPETYPE_POINT:
589 for i in range(layer.NumShapes() - 1, -1, -1):
590 shape = layer.Shape(i)
591 x, y = shape.Points()[0]
592 if inverse:
593 x, y = inverse(x, y)
594 if forward:
595 x, y = forward(x, y)
596 x = x * scale + offx
597 y = -y * scale + offy
598 if hypot(px - x, py - y) < 5:
599 select_shape = i
600 break
601
602 if select_shape >= 0:
603 return layer, select_shape
604 return None, None
605
606 def SelectShapeAt(self, x, y):
607 layer, shape = self.find_shape_at(x, y, selected_layer = 0)
608 # If layer is None, then shape will also be None. We don't want
609 # to deselect the currently selected layer, so we simply select
610 # the already selected layer again.
611 if layer is None:
612 layer = self.interactor.SelectedLayer()
613 self.interactor.SelectLayerAndShape(layer, shape)
614
615 def LabelShapeAt(self, x, y):
616 ox = x; oy = y
617 label_layer = self.map.LabelLayer()
618 layer, shape_index = self.find_shape_at(x, y, select_labels = 1)
619 if layer is None and shape_index is not None:
620 # a label was selected
621 label_layer.RemoveLabel(shape_index)
622 elif layer is not None:
623 text = labeldialog.run_label_dialog(self, layer.table, shape_index)
624 if text:
625 proj = self.map.projection
626 if proj is not None:
627 map_proj = proj
628 else:
629 map_proj = None
630 proj = layer.projection
631 if proj is not None:
632 layer_proj = proj
633 else:
634 layer_proj = None
635
636 shapetype = layer.ShapeType()
637 if shapetype == SHAPETYPE_POLYGON:
638 x, y = shape_centroid(layer.shapefile.cobject(),
639 shape_index,
640 map_proj, layer_proj, 1, 1, 0, 0)
641 if map_proj is not None:
642 x, y = map_proj.Inverse(x, y)
643 else:
644 shape = layer.Shape(shape_index)
645 if shapetype == SHAPETYPE_POINT:
646 x, y = shape.Points()[0]
647 else:
648 # assume SHAPETYPE_ARC
649 points = shape.Points()
650 x, y = points[len(points) / 2]
651 if layer_proj is not None:
652 x, y = layer_proj.Inverse(x, y)
653 if shapetype == SHAPETYPE_POINT:
654 halign = ALIGN_LEFT
655 valign = ALIGN_CENTER
656 elif shapetype == SHAPETYPE_POLYGON:
657 halign = ALIGN_CENTER
658 valign = ALIGN_CENTER
659 elif shapetype == SHAPETYPE_ARC:
660 halign = ALIGN_LEFT
661 valign = ALIGN_CENTER
662 label_layer.AddLabel(x, y, text,
663 halign = halign, valign = valign)

Properties

Name Value
svn:eol-style native
svn:keywords Author Date Id Revision

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26