root/OpenSceneGraph/trunk/examples/osgkdtree/osgkdtree.cpp @ 8415

Revision 8415, 20.1 kB (checked in by robert, 7 years ago)

Introduce a lower overhead data structure for leaves.

Line 
1/* OpenSceneGraph example, osgintersection.
2*
3*  Permission is hereby granted, free of charge, to any person obtaining a copy
4*  of this software and associated documentation files (the "Software"), to deal
5*  in the Software without restriction, including without limitation the rights
6*  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7*  copies of the Software, and to permit persons to whom the Software is
8*  furnished to do so, subject to the following conditions:
9*
10*  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
11*  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
12*  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
13*  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
14*  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
15*  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
16*  THE SOFTWARE.
17*/
18
19 
20#include <osgDB/ReadFile>
21
22#define _GLIBCXX_DEBUG
23
24#include <osg/ArgumentParser>
25#include <osg/ApplicationUsage>
26#include <osg/Timer>
27#include <osg/CoordinateSystemNode>
28#include <osg/Notify>
29#include <osg/io_utils>
30#include <osg/Geometry>
31
32
33#include <osgUtil/IntersectionVisitor>
34#include <osgUtil/LineSegmentIntersector>
35#include <osgUtil/UpdateVisitor>
36
37#include <osgSim/LineOfSight>
38#include <osgSim/HeightAboveTerrain>
39#include <osgSim/ElevationSlice>
40
41#include <iostream>
42
43
44namespace osg
45{
46
47typedef int value_type;
48typedef std::vector< value_type >   Indices;
49
50//#define LEAF_OBJECT
51//#define VERBOSE_OUTPUT
52
53#ifdef LEAF_OBJECT
54class KDNode
55{
56    public:
57   
58        KDNode():
59            first(0),
60            second(0) {}
61   
62        KDNode(const KDNode& rhs):
63            first(rhs.first),
64            second(rhs.second) {}
65
66        KDNode& operator = (const KDNode& rhs)
67        {
68            first = rhs.first;
69            second = rhs.second;
70            return *this;
71        }
72
73        value_type first;
74        value_type second;
75};
76
77class KDLeaf : public osg::Referenced
78{
79    public:
80   
81        KDLeaf() {}
82
83        Indices _vertexIndices;       
84   
85    protected:
86   
87        virtual ~KDLeaf() {}
88};
89
90#else
91    typedef std::pair< value_type, value_type> KDNode;
92    typedef std::pair< value_type, value_type> KDLeaf;
93#endif
94
95
96class KDTree : public osg::Shape
97{
98    public:
99   
100   
101        KDTree() {}
102       
103        KDTree(const KDTree& rhs, const CopyOp& copyop=CopyOp::SHALLOW_COPY):
104            Shape(rhs,copyop) {}
105
106        META_Shape(osg, KDTree)
107       
108   
109        typedef std::vector< unsigned int > AxisStack;
110        typedef std::vector< KDNode >       KDNodeList;
111
112#ifdef LEAF_OBJECT
113        typedef std::vector< osg::ref_ptr<KDLeaf> > KDLeafList;
114
115        /// note, leafNum is negative to distinguish from nodeNum
116        int addLeaf(KDLeaf* leaf) { int num = _kdLeaves.size(); _kdLeaves.push_back(leaf); return -(num+1); }
117
118        int replaceLeaf(int leafNum, KDLeaf* leaf)
119        {
120            int num = -leafNum-1;
121           
122            if (num>_kdLeaves.size()-1)
123            {
124                osg::notify(osg::NOTICE)<<"Warning: replaceChild("<<leafNum<<", leaf), num = "<<num<<" _kdLeaves.size()="<<_kdLeaves.size()<<std::endl;
125                return leafNum;
126            }
127           
128            _kdLeaves[num] = leaf; return leafNum;
129        }
130
131        /// note, leafNum is negative to distinguish from nodeNum
132        KDLeaf* getLeaf(int leafNum)
133        {
134            int num = -leafNum-1;
135            if (num<0 || num>_kdLeaves.size()-1)
136            {
137                osg::notify(osg::NOTICE)<<"Warning: getLeaf("<<leafNum<<", num = "<<num<<") _kdLeaves.size()="<<_kdLeaves.size()<<std::endl;
138                return 0;
139            }
140
141            return _kdLeaves[num].get();
142        }
143
144#else
145        typedef std::vector< KDLeaf > KDLeafList;
146
147        /// note, leafNum is negative to distinguish from nodeNum
148        int addLeaf(const KDLeaf& leaf) { int num = _kdLeaves.size(); _kdLeaves.push_back(leaf); return -(num+1); }
149
150        int replaceLeaf(int leafNum, const KDLeaf& leaf)
151        {
152            int num = -leafNum-1;
153           
154            if (num>_kdLeaves.size()-1)
155            {
156                osg::notify(osg::NOTICE)<<"Warning: replaceChild("<<leafNum<<", leaf), num = "<<num<<" _kdLeaves.size()="<<_kdLeaves.size()<<std::endl;
157                return leafNum;
158            }
159           
160            _kdLeaves[num] = leaf; return leafNum;
161        }
162
163        /// note, leafNum is negative to distinguish from nodeNum
164        KDLeaf& getLeaf(int leafNum)
165        {
166            int num = -leafNum-1;
167            if (num<0 || num>_kdLeaves.size()-1)
168            {
169                osg::notify(osg::NOTICE)<<"Warning: getLeaf("<<leafNum<<", num = "<<num<<") _kdLeaves.size()="<<_kdLeaves.size()<<std::endl;
170            }
171
172            return _kdLeaves[num];
173        }
174#endif       
175       
176       
177        int addNode(const KDNode& node)
178        {
179            int num = _kdNodes.size();
180            _kdNodes.push_back(node);
181            return num;
182        }
183
184        /// note, nodeNum is positive to distinguish from leftNum
185        KDNode& getNode(int nodeNum)
186        {
187            if (nodeNum<0 || nodeNum>_kdNodes.size()-1)
188            {
189                osg::notify(osg::NOTICE)<<"Warning: getNode("<<nodeNum<<") _kdNodes.size()="<<_kdNodes.size()<<std::endl;
190            }
191            return _kdNodes[nodeNum];
192        }
193
194
195        osg::observer_ptr<osg::Geometry> _geometry;
196
197        osg::BoundingBox                _bb;
198
199        AxisStack                       _axisStack;
200        KDNodeList                      _kdNodes;
201        KDLeafList                      _kdLeaves;
202        Indices                         _vertexIndices;
203};
204
205class KDTreeTraverser
206{
207    public:
208   
209   
210        std::ostream& output(unsigned int level)
211        {
212            for(unsigned int i=0; i<level; ++i)
213            {
214                osg::notify(osg::NOTICE)<<"  ";
215            }
216            return osg::notify(osg::NOTICE);
217        }
218   
219
220        void traverse(KDTree& tree, KDLeaf& leaf, unsigned int level)
221        {
222            output(level)<<"leaf("<<level<<") { ";
223
224#ifdef LEAF_OBJECT
225            for(unsigned int i=0; i<leaf._vertexIndices.size(); ++i)
226            {
227                if (i==0) osg::notify(osg::NOTICE)<<leaf._vertexIndices[i];
228                else osg::notify(osg::NOTICE)<<", "<<leaf._vertexIndices[i];               
229            }
230#else
231            unsigned int end = leaf.first+leaf.second;
232            for(unsigned int i=leaf.first; i<end; ++i)
233            {
234                if (i==leaf.first) osg::notify(osg::NOTICE)<<tree._vertexIndices[i];
235                else osg::notify(osg::NOTICE)<<", "<<tree._vertexIndices[i];               
236            }
237#endif
238            osg::notify(osg::NOTICE)<<"}"<<std::endl;;
239           
240        }
241
242        void traverse(KDTree& tree, value_type nodeIndex, unsigned int level)
243        {
244            output(level)<<"traverse("<<nodeIndex<<", "<< level<<") { "<<std::endl;
245           
246            if (nodeIndex>=0)
247            {       
248                KDNode& node = tree._kdNodes[nodeIndex];
249                if (node.first) traverse(tree,node.first,level+1);
250                else output(level+1)<<"empty left child()"<<std::endl;
251               
252                if (node.second) traverse(tree,node.second,level+1);
253                else output(level+1)<<"empty right child()"<<std::endl;
254            }
255            else 
256            {
257                value_type leafIndex = -nodeIndex-1;
258#ifdef LEAF_OBJECT
259                KDLeaf& leaf = *(tree._kdLeaves[leafIndex]);
260#else
261                KDLeaf& leaf = tree._kdLeaves[leafIndex];
262#endif
263                traverse(tree, leaf, level);
264            }
265
266            output(level)<<"}"<<std::endl;;
267        }
268
269
270        void traverse(KDTree& tree)
271        {
272            osg::notify(osg::NOTICE)<<"traverse(tree)"<<std::endl;
273            if (!tree._kdNodes.empty())
274            {
275                traverse(tree,0,0);
276            }
277            else if (!tree._kdLeaves.empty())
278            {
279#ifdef LEAF_OBJECT
280                traverse(tree, *tree._kdLeaves.front(), 0);
281#else
282                traverse(tree, tree._kdLeaves.front(), 0);
283#endif
284            }
285        }
286   
287};
288
289
290class KDTreeBuilder : public osg::NodeVisitor
291{
292    public:
293   
294        KDTreeBuilder():
295            osg::NodeVisitor(osg::NodeVisitor::TRAVERSE_ALL_CHILDREN),
296            _maxNumLevels(24),
297            _targetNumVerticesPerLeaf(8),
298            _numVerticesProcessed(0)
299        {           
300        }
301   
302   
303        void apply(osg::Geode& geode)
304        {
305            for(unsigned int i=0; i<geode.getNumDrawables(); ++i)
306            {
307                osg::Geometry* geom = geode.getDrawable(i)->asGeometry();
308                if (geom)
309                {
310                    geom->setShape(createKDTree(geom));
311                }   
312            }
313        }
314   
315        KDTree* createKDTree(osg::Geometry* geometry);
316       
317        void computeDivisions(KDTree& kdTree);
318       
319        int divide(KDTree& kdTree, osg::BoundingBox& bb, int nodeIndex, unsigned int level);
320
321        unsigned int _maxNumLevels;
322        unsigned int _targetNumVerticesPerLeaf;
323       
324        unsigned int _numVerticesProcessed;   
325
326};
327
328
329KDTree* KDTreeBuilder::createKDTree(osg::Geometry* geometry)
330{
331#ifdef VERBOSE_OUTPUT   
332    osg::notify(osg::NOTICE)<<"osg::KDTreeBuilder::createKDTree()"<<std::endl;
333#endif
334
335    osg::Vec3Array* vertices = dynamic_cast<osg::Vec3Array*>(geometry->getVertexArray());
336    if (!vertices) return 0;
337
338    osg::ref_ptr<KDTree> kdTree = new KDTree;
339    kdTree->_geometry = geometry;
340    kdTree->_bb = kdTree->_geometry->getBound();
341   
342   
343    unsigned int estimatedSize = (unsigned int)(float(vertices->size())/float(_targetNumVerticesPerLeaf)*1.5);
344
345#ifdef VERBOSE_OUTPUT   
346    osg::notify(osg::NOTICE)<<"kdTree->_kdNodes.reserve()="<<estimatedSize<<std::endl<<std::endl;
347#endif
348
349    kdTree->_kdNodes.reserve(estimatedSize);
350    kdTree->_kdLeaves.reserve(estimatedSize);
351   
352    computeDivisions(*kdTree);
353
354
355    _numVerticesProcessed += vertices->size();
356
357#ifdef LEAF_OBJECT
358    // create initial leaf list   
359    KDLeaf* leaf = new KDLeaf;
360    leaf->_vertexIndices.reserve(vertices->size());
361    for(unsigned int i=0; i<vertices->size(); ++i)
362    {
363        leaf->_vertexIndices.push_back(i);
364    }
365#else
366
367    kdTree->_vertexIndices.reserve(vertices->size());
368    for(unsigned int i=0; i<vertices->size(); ++i)
369    {
370        kdTree->_vertexIndices.push_back(i);
371    }
372    KDLeaf leaf(0, kdTree->_vertexIndices.size());
373
374#endif
375
376    int leafNum = kdTree->addLeaf(leaf);
377    osg::BoundingBox bb = kdTree->_bb;
378    int nodeNum = divide(*kdTree, bb, leafNum, 0);
379
380
381
382   
383#ifdef VERBOSE_OUTPUT   
384    osg::notify(osg::NOTICE)<<"Root nodeNum="<<nodeNum<<std::endl;
385#endif
386   
387#ifdef VERBOSE_OUTPUT   
388   
389    KDTreeTraverser traverser;
390    traverser.traverse(*kdTree);
391   
392    osg::notify(osg::NOTICE)<<"Final kdTree->_kdNodes.size()="<<kdTree->_kdNodes.size()<<std::endl;
393    osg::notify(osg::NOTICE)<<"Final kdTree->_kdLeaves.size()="<<kdTree->_kdLeaves.size()<<std::endl;
394
395    osg::notify(osg::NOTICE)<<"osg::KDTreeBuilder::createKDTree() completed"<<std::endl<<std::endl;
396#endif
397
398    return kdTree.release();
399}   
400
401void KDTreeBuilder::computeDivisions(KDTree& kdTree)
402{
403
404
405    osg::Vec3 dimensions(kdTree._bb.xMax()-kdTree._bb.xMin(),
406                         kdTree._bb.yMax()-kdTree._bb.yMin(),
407                         kdTree._bb.zMax()-kdTree._bb.zMin());
408
409#ifdef VERBOSE_OUTPUT   
410    osg::notify(osg::NOTICE)<<"computeDivisions("<<_maxNumLevels<<") "<<dimensions<< " { "<<std::endl;
411#endif
412
413    kdTree._axisStack.reserve(_maxNumLevels);
414 
415    int level = 0;
416    for(unsigned int level=0; level<_maxNumLevels; ++level)
417    {
418        int axis = 0;
419        if (dimensions[0]>=dimensions[1])
420        {
421            if (dimensions[0]>=dimensions[2]) axis = 0;
422            else axis = 2;
423        }
424        else if (dimensions[1]>=dimensions[2]) axis = 1;
425        else axis = 2;
426
427        kdTree._axisStack.push_back(axis);
428        dimensions[axis] /= 2.0f;
429
430#ifdef VERBOSE_OUTPUT   
431        osg::notify(osg::NOTICE)<<"  "<<level<<", "<<dimensions<<", "<<axis<<std::endl;
432#endif
433    }
434
435#ifdef VERBOSE_OUTPUT   
436    osg::notify(osg::NOTICE)<<"}"<<std::endl;
437#endif
438}
439
440int KDTreeBuilder::divide(KDTree& kdTree, osg::BoundingBox& bb, int nodeIndex, unsigned int level)
441{
442    if (kdTree._axisStack.size()<=level) return nodeIndex;
443
444
445    int axis = kdTree._axisStack[level];
446
447#ifdef VERBOSE_OUTPUT   
448    //osg::notify(osg::NOTICE)<<"divide("<<nodeIndex<<", "<<level<< "), axis="<<axis<<std::endl;
449#endif
450
451    if (nodeIndex>=0)
452    {
453#ifdef VERBOSE_OUTPUT   
454        osg::notify(osg::NOTICE)<<"  divide node"<<std::endl;
455#endif
456        KDNode& node = kdTree.getNode(nodeIndex);
457        return nodeIndex;
458    }
459    else
460    {   
461
462#ifdef LEAF_OBJECT
463        if (kdTree.getLeaf(nodeIndex)->_vertexIndices.size()<=_targetNumVerticesPerLeaf) return nodeIndex;
464#else
465        if (kdTree.getLeaf(nodeIndex).second<=_targetNumVerticesPerLeaf) return nodeIndex;
466#endif   
467        //osg::notify(osg::NOTICE)<<"  divide leaf"<<std::endl;
468       
469        int nodeNum = kdTree.addNode(KDNode());
470
471        float original_min = bb._min[axis];
472        float original_max = bb._max[axis];
473
474        float mid = (original_min+original_max)*0.5f;
475
476#ifdef LEAF_OBJECT
477        {
478            osg::ref_ptr<KDLeaf> leaf = kdTree.getLeaf(nodeIndex);
479
480            // create new node, and add two leaves to it.
481            osg::ref_ptr<KDLeaf> leftLeaf = new KDLeaf;
482            osg::ref_ptr<KDLeaf> rightLeaf = new KDLeaf;
483
484
485            osg::Vec3Array* vertices = dynamic_cast<osg::Vec3Array*>(kdTree._geometry->getVertexArray());
486
487            //osg::notify(osg::NOTICE)<<"  divide leaf->_vertexIndices.size()="<<leaf->_vertexIndices.size()<<std::endl;
488
489            unsigned int estimatedSize = leaf->_vertexIndices.size();
490            leftLeaf->_vertexIndices.reserve(estimatedSize);
491            rightLeaf->_vertexIndices.reserve(estimatedSize);
492
493            for(unsigned int i=0; i<leaf->_vertexIndices.size(); ++i)
494            {
495                unsigned int vi = leaf->_vertexIndices[i];
496                osg::Vec3& v = (*vertices)[vi];
497                if (v[axis] <= mid) leftLeaf->_vertexIndices.push_back(vi);
498                else rightLeaf->_vertexIndices.push_back(vi);
499            }
500
501            if (leftLeaf->_vertexIndices.empty())
502            {
503                //osg::notify(osg::NOTICE)<<"LeftLeaf empty"<<std::endl;
504                kdTree.getNode(nodeNum).first = 0;
505                kdTree.getNode(nodeNum).second = kdTree.replaceLeaf(nodeIndex, rightLeaf.get());
506            }
507            else if (rightLeaf->_vertexIndices.empty())
508            {
509                //osg::notify(osg::NOTICE)<<"RightLeaf empty"<<std::endl;
510                kdTree.getNode(nodeNum).first = kdTree.replaceLeaf(nodeIndex, leftLeaf.get());
511                kdTree.getNode(nodeNum).second = 0;
512            }
513            else
514            {
515                kdTree.getNode(nodeNum).first = kdTree.replaceLeaf(nodeIndex, leftLeaf.get());
516                kdTree.getNode(nodeNum).second = kdTree.addLeaf(rightLeaf.get());
517            }
518        }
519#else
520        {
521            KDLeaf& leaf = kdTree.getLeaf(nodeIndex);
522
523            osg::Vec3Array* vertices = dynamic_cast<osg::Vec3Array*>(kdTree._geometry->getVertexArray());
524
525            //osg::notify(osg::NOTICE)<<"  divide leaf->_vertexIndices.size()="<<leaf->_vertexIndices.size()<<std::endl;
526
527            unsigned int estimatedSize = leaf.second;
528
529            int end = leaf.first+leaf.second-1;
530            int left = leaf.first;
531            int right = leaf.first+leaf.second-1;
532           
533            while(left<right)
534            {
535                while(left<right && ((*vertices)[kdTree._vertexIndices[left]])[axis]<=mid) { ++left; }
536               
537                while(left<right && ((*vertices)[kdTree._vertexIndices[right]])[axis]>mid) { --right; }
538
539                if (left<right)
540                {
541                    std::swap(kdTree._vertexIndices[left], kdTree._vertexIndices[right]);
542                    ++left;
543                    --right;
544                }
545            }
546           
547            if (left==right)
548            {
549                if (((*vertices)[kdTree._vertexIndices[left]])[axis]<=mid) ++left;
550                else --right;
551            }
552           
553            KDLeaf leftLeaf(leaf.first, (right-leaf.first)+1);
554            KDLeaf rightLeaf(left, (end-left)+1);
555
556#if 0           
557            osg::notify(osg::NOTICE)<<"In  leaf.first     ="<<leaf.first     <<" leaf.second     ="<<leaf.second<<std::endl;
558            osg::notify(osg::NOTICE)<<"    leftLeaf.first ="<<leftLeaf.first <<" leftLeaf.second ="<<leftLeaf.second<<std::endl;
559            osg::notify(osg::NOTICE)<<"    rightLeaf.first="<<rightLeaf.first<<" rightLeaf.second="<<rightLeaf.second<<std::endl;
560            osg::notify(osg::NOTICE)<<"    left="<<left<<" right="<<right<<std::endl;
561
562            if (leaf.second != (leftLeaf.second +rightLeaf.second))
563            {
564                osg::notify(osg::NOTICE)<<"*** Error in size, leaf.second="<<leaf.second
565                                        <<", leftLeaf.second="<<leftLeaf.second
566                                        <<", rightLeaf.second="<<rightLeaf.second<<std::endl;
567            }
568            else
569            {
570                osg::notify(osg::NOTICE)<<"Size OK, leaf.second="<<leaf.second
571                                        <<", leftLeaf.second="<<leftLeaf.second
572                                        <<", rightLeaf.second="<<rightLeaf.second<<std::endl;
573            }
574#endif
575            if (leftLeaf.second<=0)
576            {
577                //osg::notify(osg::NOTICE)<<"LeftLeaf empty"<<std::endl;
578                kdTree.getNode(nodeNum).first = 0;
579                kdTree.getNode(nodeNum).second = kdTree.replaceLeaf(nodeIndex, rightLeaf);
580            }
581            else if (rightLeaf.second<=0)
582            {
583                //osg::notify(osg::NOTICE)<<"RightLeaf empty"<<std::endl;
584                kdTree.getNode(nodeNum).first = kdTree.replaceLeaf(nodeIndex, leftLeaf);
585                kdTree.getNode(nodeNum).second = 0;
586            }
587            else
588            {
589                kdTree.getNode(nodeNum).first = kdTree.replaceLeaf(nodeIndex, leftLeaf);
590                kdTree.getNode(nodeNum).second = kdTree.addLeaf(rightLeaf);
591            }
592        }
593#endif
594       
595        int originalLeftChildIndex = kdTree.getNode(nodeNum).first;
596        int originalRightChildIndex = kdTree.getNode(nodeNum).second;
597
598
599        float restore = bb._max[axis];
600        bb._max[axis] = mid;
601
602        //osg::notify(osg::NOTICE)<<"  divide leftLeaf "<<kdTree.getNode(nodeNum).first<<std::endl;
603        int leftChildIndex = divide(kdTree, bb, originalLeftChildIndex, level+1);
604
605        bb._max[axis] = restore;
606       
607        restore = bb._min[axis];
608        bb._min[axis] = mid;
609
610        //osg::notify(osg::NOTICE)<<"  divide rightLeaf "<<kdTree.getNode(nodeNum).second<<std::endl;
611        int rightChildIndex = divide(kdTree, bb, originalRightChildIndex, level+1);
612       
613        bb._min[axis] = restore;
614       
615        kdTree.getNode(nodeNum).first = leftChildIndex;
616        kdTree.getNode(nodeNum).second = rightChildIndex;
617       
618        return nodeNum;       
619    }
620
621
622}
623
624}
625
626int main(int argc, char **argv)
627{
628    // use an ArgumentParser object to manage the program arguments.
629    osg::ArgumentParser arguments(&argc,argv);
630   
631    osg::ref_ptr<osg::Node> scene = osgDB::readNodeFiles(arguments);
632   
633    if (!scene)
634    {
635        std::cout<<"No model loaded, please specify a valid model on the command line."<<std::endl;
636        return 0;
637    }
638
639
640    osgUtil::UpdateVisitor updateVisitor;
641    updateVisitor.setFrameStamp(new osg::FrameStamp);
642    scene->accept(updateVisitor);
643    scene->getBound();
644
645
646    osg::Timer_t start = osg::Timer::instance()->tick();
647   
648    osg::KDTreeBuilder builder;
649    scene->accept(builder);
650   
651    osg::Timer_t end = osg::Timer::instance()->tick();
652    double time = osg::Timer::instance()->delta_s(start,end);
653    osg::notify(osg::NOTICE)<<"Time to build "<<time*1000.0<<"ms "<<builder._numVerticesProcessed<<std::endl;
654    osg::notify(osg::NOTICE)<<"build speed "<<(double(builder._numVerticesProcessed)/time)/1000000.0<<"M vertices per second"<<std::endl;
655   
656   
657    return 0;
658}
Note: See TracBrowser for help on using the browser.