]> Creatis software - cpPlugins.git/blob - lib/cpExtensions/Visualization/ImageInteractorStyle.cxx
...
[cpPlugins.git] / lib / cpExtensions / Visualization / ImageInteractorStyle.cxx
1 #include <cpExtensions/Visualization/ImageInteractorStyle.h>
2
3 #include <cmath>
4 #include <ctime>
5
6 #include <vtkAnnotatedCubeActor.h>
7 #include <vtkAxesActor.h>
8 #include <vtkCallbackCommand.h>
9 #include <vtkCamera.h>
10 #include <vtkCellArray.h>
11 #include <vtkCommand.h>
12 #include <vtkMatrix4x4.h>
13 #include <vtkPropAssembly.h>
14 #include <vtkProperty.h>
15 #include <vtkRendererCollection.h>
16 #include <vtkRenderWindow.h>
17 #include <vtkRenderWindowInteractor.h>
18
19 #include <cpExtensions/Visualization/ImageSliceActors.h>
20 #include <cpExtensions/Visualization/MPRActors.h>
21
22 // -------------------------------------------------------------------------
23 const int cpExtensions::Visualization::
24 ImageInteractorStyle::CursorEvent = vtkCommand::UserEvent + 1;
25 const int cpExtensions::Visualization::
26 ImageInteractorStyle::RadiusEvent = vtkCommand::UserEvent + 2;
27 const int cpExtensions::Visualization::
28 ImageInteractorStyle::DoubleClickEvent = vtkCommand::UserEvent + 3;
29
30 // -------------------------------------------------------------------------
31 cpExtensions::Visualization::ImageInteractorStyle::
32 Self* cpExtensions::Visualization::ImageInteractorStyle::
33 New( )
34 {
35   return( new Self( ) );
36 }
37
38 // -------------------------------------------------------------------------
39 void cpExtensions::Visualization::ImageInteractorStyle::
40 Configure( ImageSliceActors* slice_actors, MPRActors* mpr_actors )
41 {
42   this->m_SliceActors = slice_actors;
43   this->m_MPRActors = mpr_actors;
44   this->SetModeToNavigation( );
45   this->PropPicker->AddPickList( slice_actors->GetImageActor( ) );
46   this->Modified( );
47 }
48
49 // -------------------------------------------------------------------------
50 void cpExtensions::Visualization::ImageInteractorStyle::
51 AssociateInteractor( vtkRenderWindowInteractor* interactor )
52 {
53   if( interactor != NULL )
54   {
55     this->AssociatedInteractors.push_back( interactor );
56     this->Modified( );
57
58   } // fi
59 }
60
61 // -------------------------------------------------------------------------
62 void cpExtensions::Visualization::ImageInteractorStyle::
63 SetModeToNavigation( )
64 {
65   this->Mode = Self::NavigationMode;
66 }
67
68 // -------------------------------------------------------------------------
69 void cpExtensions::Visualization::ImageInteractorStyle::
70 SetModeToDeformation( )
71 {
72   this->Mode = Self::DeformationMode;
73 }
74
75 // -------------------------------------------------------------------------
76 void cpExtensions::Visualization::ImageInteractorStyle::
77 SetInteractor( vtkRenderWindowInteractor* interactor, const int& axis )
78 {
79   this->Superclass::SetInteractor( interactor );
80   this->OrientationWidget->SetInteractor( interactor );
81   interactor->SetInteractorStyle( this );
82   if( interactor == NULL )
83     return;
84
85   // Get camera, avoiding segfaults
86   vtkRenderer* ren =
87     interactor->GetRenderWindow( )->GetRenderers( )->GetFirstRenderer( );
88   if( ren == NULL )
89     return;
90   vtkCamera* cam = ren->GetActiveCamera( );
91   if( cam == NULL )
92     return;
93
94   // Parallel projections are better when displaying 2D images
95   cam->ParallelProjectionOn( );
96   cam->SetFocalPoint( double( 0 ), double( 0 ), double( 0 ) );
97   if( axis == 0 )
98   {
99     cam->SetPosition( double( 1 ), double( 0 ), double( 0 ) );
100     cam->SetViewUp  ( double( 0 ), double( 1 ), double( 0 ) );
101   }
102   else if( axis == 1 )
103   {
104     cam->SetPosition( double( 0 ), double( 1 ), double(  0 ) );
105     cam->SetViewUp  ( double( 0 ), double( 0 ), double( -1 ) );
106   }
107   else // if( axis == 2 )
108   {
109     cam->SetPosition( double( 0 ), double( 0 ), double( 1 ) );
110     cam->SetViewUp  ( double( 0 ), double( 1 ), double( 0 ) );
111
112   } // fi
113   ren->ResetCamera( );
114
115   // Enable 2D orientation widget
116   this->OrientationWidget->SetEnabled( 1 );
117   this->OrientationWidget->InteractiveOff( );
118 }
119
120 // -------------------------------------------------------------------------
121 void cpExtensions::Visualization::ImageInteractorStyle::
122 OnMouseMove( )
123 {
124   if( this->m_MPRActors == NULL )
125     return;
126
127   if( this->CursorMoving )
128   {
129     bool picked = this->_PickPosition( this->Cursor );
130     if( picked )
131     {
132       for( int i = 0; i < 3; ++i )
133         if( this->m_SliceActors->GetAxis( ) != i )
134           this->m_MPRActors->SetSlice( i, this->Cursor[ i ] );
135       this->InvokeEvent( Self::CursorEvent, this->Cursor );
136       this->Interactor->Render( );
137       this->_RenderAssociateInteractors( );
138
139     } // fi
140   }
141   else if( this->RadiusMoving )
142   {
143     bool picked = this->_PickPosition( this->Radius );
144     if( picked )
145     {
146       this->InvokeEvent( Self::RadiusEvent, this->Radius );
147       this->_UpdateRadius( );
148
149     } // fi
150   }
151   else
152   {
153     switch( this->State )
154     {
155     case VTKIS_WINDOW_LEVEL:
156       this->WindowLevel( );
157       break;
158     case VTKIS_DOLLY:
159       this->Dolly( );
160       break;
161     case VTKIS_PAN:
162       this->Pan( );
163       break;
164     } // hctiws
165
166   } // fi
167 }
168
169 // -------------------------------------------------------------------------
170 void cpExtensions::Visualization::ImageInteractorStyle::
171 OnLeftButtonDown( )
172 {
173   static double pnt[ 3 ];
174   static int pos[ 2 ];
175   this->Interactor->GetEventPosition( pos );
176   this->FindPokedRenderer( pos[ 0 ], pos[ 1 ] );
177   if( this->CurrentRenderer == NULL )
178     return;
179   this->GrabFocus( this->EventCallbackCommand );
180
181   // TODO: check this code
182   // Manage double-click
183   static const long epsilon_time = 800;
184   static long last_click_time = -( epsilon_time << 1 );
185   long click_time = static_cast< long >( std::clock( ) );
186   if( ( click_time - last_click_time ) < epsilon_time )
187   {
188     last_click_time = -( epsilon_time << 1 );
189     if( this->_PickPosition( pnt ) )
190       this->InvokeEvent( Self::DoubleClickEvent, pnt );
191   }
192   else
193   {
194     last_click_time = click_time;
195     if( this->Interactor->GetControlKey( ) )
196       this->StartCursorMoving( );
197
198   } // fi
199 }
200
201 // -------------------------------------------------------------------------
202 void cpExtensions::Visualization::ImageInteractorStyle::
203 OnLeftButtonUp( )
204 {
205   if( this->CursorMoving )
206   {
207     this->EndCursorMoving( );
208     if( this->Interactor )
209       this->ReleaseFocus( );
210
211   } // fi
212 }
213
214 // -------------------------------------------------------------------------
215 void cpExtensions::Visualization::ImageInteractorStyle::
216 OnMiddleButtonDown( )
217 {
218   int x = this->Interactor->GetEventPosition( )[ 0 ];
219   int y = this->Interactor->GetEventPosition( )[ 1 ];
220
221   this->FindPokedRenderer( x, y );
222   if( this->CurrentRenderer == NULL )
223     return;
224   this->GrabFocus( this->EventCallbackCommand );
225
226   if( this->Interactor->GetAltKey( ) )
227   {
228   }
229   else if( this->Interactor->GetControlKey( ) )
230   {
231     this->StartRadiusMoving( );
232   }
233   else if( this->Interactor->GetShiftKey( ) )
234   {
235   }
236   else
237     this->StartPan( );
238 }
239
240 // -------------------------------------------------------------------------
241 void cpExtensions::Visualization::ImageInteractorStyle::
242 OnMiddleButtonUp( )
243 {
244   if( this->RadiusMoving )
245   {
246     this->EndRadiusMoving( );
247     if( this->Interactor )
248       this->ReleaseFocus( );
249   }
250   else
251   {
252     switch( this->State )
253     {
254     case VTKIS_PAN:
255       this->EndPan( );
256       break;
257     } // hctiws
258
259   } // fi
260 }
261
262 // -------------------------------------------------------------------------
263 void cpExtensions::Visualization::ImageInteractorStyle::
264 OnRightButtonDown( )
265 {
266   int x = this->Interactor->GetEventPosition( )[ 0 ];
267   int y = this->Interactor->GetEventPosition( )[ 1 ];
268
269   this->FindPokedRenderer( x, y );
270   if( this->CurrentRenderer == NULL )
271     return;
272   this->GrabFocus( this->EventCallbackCommand );
273
274   if( this->Interactor->GetControlKey( ) )
275   {
276     this->WindowLevelStartPosition[ 0 ] = x;
277     this->WindowLevelStartPosition[ 1 ] = y;
278     this->StartWindowLevel( );
279   }
280   else
281   {
282     this->StartDolly( );
283
284   } // fi
285 }
286
287 // -------------------------------------------------------------------------
288 void cpExtensions::Visualization::ImageInteractorStyle::
289 OnRightButtonUp( )
290 {
291   switch( this->State )
292   {
293   case VTKIS_WINDOW_LEVEL:
294   {
295     this->EndWindowLevel( );
296     if( this->Interactor )
297       this->ReleaseFocus( );
298   }
299   break;
300   case VTKIS_DOLLY:
301     this->EndDolly( );
302     break;
303   } // hctiws
304 }
305
306 // -------------------------------------------------------------------------
307 void cpExtensions::Visualization::ImageInteractorStyle::
308 OnMouseWheelForward( )
309 {
310   if( this->m_SliceActors == NULL || this->Interactor == NULL )
311     return;
312   int off = 1;
313   if( this->Interactor->GetShiftKey( ) == 1 )
314     off *= 10;
315   int s = this->m_SliceActors->GetSliceNumber( ) + off;
316   int maxs = this->m_SliceActors->GetSliceNumberMaxValue( );
317   this->m_SliceActors->SetSliceNumber( ( s < maxs )? s: maxs );
318   this->m_MPRActors->SetSlice(
319     this->m_SliceActors->GetAxis( ),
320     this->m_SliceActors->GetSliceNumber( )
321     );
322   this->Interactor->Render( );
323   this->_RenderAssociateInteractors( );
324 }
325
326 // -------------------------------------------------------------------------
327 void cpExtensions::Visualization::ImageInteractorStyle::
328 OnMouseWheelBackward( )
329 {
330   if( this->m_SliceActors == NULL || this->Interactor == NULL )
331     return;
332   int off = 1;
333   if( this->Interactor->GetShiftKey( ) == 1 )
334     off *= 10;
335   int s = this->m_SliceActors->GetSliceNumber( ) - off;
336   int mins = this->m_SliceActors->GetSliceNumberMinValue( );
337   this->m_SliceActors->SetSliceNumber( ( mins < s )? s: mins );
338   this->m_MPRActors->SetSlice(
339     this->m_SliceActors->GetAxis( ),
340     this->m_SliceActors->GetSliceNumber( )
341     );
342   this->Interactor->Render( );
343   this->_RenderAssociateInteractors( );
344 }
345
346 // -------------------------------------------------------------------------
347 void cpExtensions::Visualization::ImageInteractorStyle::
348 OnChar( )
349 {
350   switch( this->Interactor->GetKeyCode( ) )
351   {
352   case 'r': case 'R':
353   {
354     vtkRenderer* ren =
355       this->Interactor->GetRenderWindow( )->
356       GetRenderers( )->GetFirstRenderer( );
357     if( ren != NULL )
358       ren->ResetCamera( );
359     this->Interactor->Render( );
360   }
361   break;
362   case 'w': case 'W': case 'l': case 'L':
363   {
364     if( this->m_MPRActors != NULL )
365     {
366       this->m_MPRActors->ResetWindowLevel( );
367       this->Interactor->Render( );
368       this->_RenderAssociateInteractors( );
369
370     } // fi
371   }
372   break;
373   } // hctiws
374 }
375
376 // -------------------------------------------------------------------------
377 void cpExtensions::Visualization::ImageInteractorStyle::
378 WindowLevel( )
379 {
380   if( this->Mode == Self::NavigationMode )
381   {
382     if( this->Interactor == NULL )
383       return;
384     vtkRenderer* ren =
385       this->Interactor->GetRenderWindow( )->
386       GetRenderers( )->GetFirstRenderer( );
387     if( ren == NULL )
388       return;
389
390     // Compute scales
391     this->WindowLevelCurrentPosition[ 0 ] =
392       this->Interactor->GetEventPosition( )[ 0 ];
393     this->WindowLevelCurrentPosition[ 1 ] =
394       this->Interactor->GetEventPosition( )[ 1 ];
395     int* size = ren->GetSize( );
396     double sw = double(
397       this->WindowLevelCurrentPosition[ 0 ] -
398       this->WindowLevelStartPosition[ 0 ]
399       ) / double( size[ 0 ] );
400     double sl = (
401       this->WindowLevelStartPosition[ 1 ] -
402       this->WindowLevelCurrentPosition[ 1 ]
403       ) / double( size[ 1 ] );
404
405     double w = this->WindowLevelInitial[ 0 ] * ( double( 1 ) + sw );
406     double l = this->WindowLevelInitial[ 1 ] * ( double( 1 ) + sl );
407     double minw = this->m_MPRActors->GetMinWindow( );
408     double maxw = this->m_MPRActors->GetMaxWindow( );
409     double minl = this->m_MPRActors->GetMinLevel( );
410     double maxl = this->m_MPRActors->GetMaxLevel( );
411
412     if( w < minw ) w = minw;
413     if( maxw < w ) w = maxw;
414     if( l < minl ) l = minl;
415     if( maxl < l ) l = maxl;
416
417     this->m_MPRActors->SetWindowLevel( w, l );
418     this->Interactor->Render( );
419     this->_RenderAssociateInteractors( );
420   }
421   else if( this->Mode == Self::DeformationMode )
422   {
423     // TODO
424
425   } // fi
426 }
427
428 // -------------------------------------------------------------------------
429 void cpExtensions::Visualization::ImageInteractorStyle::
430 StartWindowLevel( )
431 {
432   if( this->State != VTKIS_NONE )
433     return;
434   if( this->Mode == Self::NavigationMode )
435   {
436     this->StartState( VTKIS_WINDOW_LEVEL );
437
438     this->WindowLevelInitial[ 0 ] = this->m_MPRActors->GetWindow( );
439     this->WindowLevelInitial[ 1 ] = this->m_MPRActors->GetLevel( );
440   }
441   else if( this->Mode == Self::DeformationMode )
442   {
443     // TODO
444
445   } // fi
446 }
447
448 // -------------------------------------------------------------------------
449 void cpExtensions::Visualization::ImageInteractorStyle::
450 EndWindowLevel( )
451 {
452   if( this->Mode == Self::NavigationMode )
453   {
454     if( this->State != VTKIS_WINDOW_LEVEL )
455       return;
456     this->StopState( );
457   }
458   else
459   {
460     // TODO
461
462   } // fi
463 }
464
465 // -------------------------------------------------------------------------
466 void cpExtensions::Visualization::ImageInteractorStyle::
467 StartCursorMoving( )
468 {
469   if( this->CursorMoving )
470     return;
471   this->_PickPosition( this->Cursor );
472   this->CursorMoving = true;
473 }
474
475 // -------------------------------------------------------------------------
476 void cpExtensions::Visualization::ImageInteractorStyle::
477 EndCursorMoving( )
478 {
479   if( !( this->CursorMoving ) )
480     return;
481   this->CursorMoving = false;
482 }
483
484 // -------------------------------------------------------------------------
485 void cpExtensions::Visualization::ImageInteractorStyle::
486 StartRadiusMoving( )
487 {
488   if( this->RadiusMoving )
489     return;
490   this->_PickPosition( this->Radius );
491   this->RadiusMoving = true;
492   this->_UpdateRadius( );
493 }
494
495 // -------------------------------------------------------------------------
496 void cpExtensions::Visualization::ImageInteractorStyle::
497 EndRadiusMoving( )
498 {
499   if( !( this->RadiusMoving ) )
500     return;
501   this->RadiusMoving = false;
502   this->_UpdateRadius( );
503   this->InvokeEvent( Self::RadiusEvent, NULL );
504 }
505
506 // -------------------------------------------------------------------------
507 cpExtensions::Visualization::ImageInteractorStyle::
508 ImageInteractorStyle( )
509   : Superclass( ),
510     Mode( Self::NavigationMode ),
511     m_SliceActors( NULL ),
512     m_MPRActors( NULL ),
513     CursorMoving( false ),
514     RadiusMoving( false )
515 {
516   // Orientation marks
517   vtkSmartPointer< vtkAnnotatedCubeActor > cube =
518     vtkSmartPointer< vtkAnnotatedCubeActor >::New( );
519   cube->GetCubeProperty( )->SetColor( 0.9, 0.7, 0.2 );
520   cube->GetTextEdgesProperty( )->SetLineWidth( 1 );
521   cube->GetTextEdgesProperty( )->SetDiffuse( 0 );
522   cube->GetTextEdgesProperty( )->SetAmbient( 1 );
523   cube->GetTextEdgesProperty( )->SetColor( 0.18, 0.28, 0.23 );
524   cube->GetXPlusFaceProperty( )->SetColor( 1, 0, 0 );
525   cube->GetXPlusFaceProperty( )->SetInterpolationToFlat( );
526   cube->GetXMinusFaceProperty( )->SetColor( 1, 0, 0 );
527   cube->GetXMinusFaceProperty( )->SetInterpolationToFlat( );
528   cube->GetYPlusFaceProperty( )->SetColor( 0, 1, 0 );
529   cube->GetYPlusFaceProperty( )->SetInterpolationToFlat( );
530   cube->GetYMinusFaceProperty( )->SetColor( 0, 1, 0 );
531   cube->GetYMinusFaceProperty( )->SetInterpolationToFlat( );
532   cube->GetZPlusFaceProperty( )->SetColor( 0, 0, 1 );
533   cube->GetZPlusFaceProperty( )->SetInterpolationToFlat( );
534   cube->GetZMinusFaceProperty( )->SetColor( 0, 0, 1 );
535   cube->GetZMinusFaceProperty( )->SetInterpolationToFlat( );
536
537   vtkSmartPointer< vtkAxesActor > axes =
538     vtkSmartPointer< vtkAxesActor >::New( );
539   axes->AxisLabelsOff( );
540   axes->SetShaftTypeToCylinder( );
541   axes->SetTotalLength( 2, 2, 2 );
542
543   vtkSmartPointer< vtkPropAssembly > actors =
544     vtkSmartPointer< vtkPropAssembly >::New( );
545   actors->AddPart( cube );
546   actors->AddPart( axes );
547
548   this->OrientationWidget =
549     vtkSmartPointer< vtkOrientationMarkerWidget >::New( );
550   this->OrientationWidget->SetOutlineColor( 0.93, 0.57, 0.13 );
551   this->OrientationWidget->SetOrientationMarker( actors );
552   this->OrientationWidget->SetViewport( 0.0, 0.0, 0.2, 0.2 );
553
554   // Circle
555   unsigned long circle_samples = 1000;
556   this->Circle = vtkSmartPointer< vtkPolyData >::New( );
557
558   vtkSmartPointer< vtkPoints > circle_points =
559     vtkSmartPointer< vtkPoints >::New( );
560   vtkSmartPointer< vtkCellArray > circle_lines =
561     vtkSmartPointer< vtkCellArray >::New( );
562   for( unsigned long s = 0; s < circle_samples; ++s )
563   {
564     double t = double( 6.2832 ) * double( s ) / double( circle_samples );
565     circle_points->InsertNextPoint(
566       std::cos( t ), std::sin( t ), double( 0 )
567       );
568
569     circle_lines->InsertNextCell( 2 );
570     circle_lines->InsertCellPoint( s );
571     circle_lines->InsertCellPoint( ( s + 1 ) % circle_samples );
572
573   } // rof
574   this->Circle->SetPoints( circle_points );
575   this->Circle->SetLines( circle_lines );
576
577   this->CircleMapper = vtkSmartPointer< vtkPolyDataMapper >::New( );
578   this->CircleMapper->SetInputData( this->Circle );
579   this->CircleActor = vtkSmartPointer< vtkActor >::New( );
580   this->CircleActor->SetMapper( this->CircleMapper );
581   this->CircleActor->GetProperty( )->SetColor( 1, 0, 1 );
582   this->CircleActor->GetProperty( )->SetLineWidth( 2 );
583
584   this->PropPicker = vtkSmartPointer< vtkPropPicker >::New( );
585   this->PropPicker->PickFromListOn( );
586 }
587
588 // -------------------------------------------------------------------------
589 cpExtensions::Visualization::ImageInteractorStyle::
590 ~ImageInteractorStyle( )
591 {
592 }
593
594 // -------------------------------------------------------------------------
595 void cpExtensions::Visualization::ImageInteractorStyle::
596 _RenderAssociateInteractors( )
597 {
598   std::vector< vtkRenderWindowInteractor* >::iterator rIt =
599     this->AssociatedInteractors.begin( );
600   for( ; rIt != this->AssociatedInteractors.end( ); ++rIt )
601     ( *rIt )->Render( );
602 }
603
604 // -------------------------------------------------------------------------
605 bool cpExtensions::Visualization::ImageInteractorStyle::
606 _PickPosition( double pos[ 3 ] )
607 {
608   if( this->m_SliceActors == NULL )
609     return( false );
610
611   double x = double( this->Interactor->GetEventPosition( )[ 0 ] );
612   double y = double( this->Interactor->GetEventPosition( )[ 1 ] );
613   this->FindPokedRenderer( x, y );
614   int success =
615     this->PropPicker->Pick( x, y, double( 0 ), this->CurrentRenderer );
616   if( success == 0 )
617     return( false );
618   this->PropPicker->GetPickPosition( pos );
619
620   int axis = this->m_SliceActors->GetAxis( );
621   double* bounds = this->m_SliceActors->GetDisplayBounds( );
622   pos[ axis ] = bounds[ axis << 1 ];
623
624   return( true );
625 }
626
627 // -------------------------------------------------------------------------
628 void cpExtensions::Visualization::ImageInteractorStyle::
629 _UpdateCursor( )
630 {
631   std::cout << "upcur" << std::endl;
632 }
633
634 // -------------------------------------------------------------------------
635 void cpExtensions::Visualization::ImageInteractorStyle::
636 _UpdateRadius( )
637 {
638   vtkRenderer* ren =
639     this->Interactor->GetRenderWindow( )->
640     GetRenderers( )->GetFirstRenderer( );
641   if( ren == NULL )
642     return;
643   vtkCamera* cam = ren->GetActiveCamera( );
644   if( cam == NULL )
645     return;
646
647   if( this->RadiusMoving )
648   {
649     double x = this->Cursor[ 0 ] - this->Radius[ 0 ];
650     double y = this->Cursor[ 1 ] - this->Radius[ 1 ];
651     double z = this->Cursor[ 2 ] - this->Radius[ 2 ];
652     double r = std::sqrt( ( x * x ) + ( y * y ) + ( z * z ) );
653
654     vtkMatrix4x4* cam_matrix = cam->GetModelViewTransformMatrix( );
655     vtkSmartPointer< vtkMatrix4x4 > circle_matrix =
656       this->CircleActor->GetUserMatrix( );
657     if( circle_matrix.GetPointer( ) == NULL )
658     {
659       circle_matrix = vtkSmartPointer< vtkMatrix4x4 >::New( );
660       this->CircleActor->SetUserMatrix( circle_matrix );
661
662     } // fi
663     for( int i = 0; i < 4; ++i )
664     {
665       for( int j = 0; j < 4; ++j )
666       {
667         double v = cam_matrix->GetElement( i, j );
668         if( i < 3 && j == 3 )
669           v = this->Cursor[ i ];
670         if( i < 3 && j < 3 )
671           v *= r;
672         circle_matrix->SetElement( i, j, v );
673
674       } // rof
675
676     } // rof
677     this->CircleActor->Modified( );
678     ren->AddActor( this->CircleActor );
679   }
680   else
681     ren->RemoveActor( this->CircleActor );
682
683   this->Interactor->Render( );
684 }
685
686 // eof - $RCSfile$