source: geovis/trunk/Renderer.h @ 6488

Last change on this file since 6488 was 6488, checked in by ldelgass, 8 years ago

Add verticalDatum override option to addElevationLayer, prep for adding to
protocol.

File size: 21.2 KB
Line 
1/* -*- mode: c++; c-basic-offset: 4; indent-tabs-mode: nil -*- */
2/*
3 * Copyright (C) 2004-2015  HUBzero Foundation, LLC
4 *
5 * Author: Leif Delgass <ldelgass@purdue.edu>
6 */
7
8#ifndef GEOVIS_RENDERER_H
9#define GEOVIS_RENDERER_H
10
11#include <sys/time.h> // For struct timeval
12#include <ctime> // For time_t
13#include <string>
14#include <vector>
15#include <tr1/unordered_map>
16#include <typeinfo>
17
18#include <osg/ref_ptr>
19#include <osg/Node>
20#include <osg/Image>
21#include <osg/TransferFunction>
22#include <osgText/String>
23#include <osgViewer/Viewer>
24#include <osgGA/StateSetManipulator>
25#include <osgUtil/IncrementalCompileOperation>
26
27#include <osgEarth/Version>
28#include <osgEarth/StringUtils>
29#include <osgEarth/Map>
30#include <osgEarth/Viewpoint>
31#include <osgEarth/ImageLayer>
32#include <osgEarth/ElevationLayer>
33#include <osgEarth/ModelLayer>
34#include <osgEarth/TileSource>
35#include <osgEarth/ModelSource>
36#include <osgEarth/GeoData>
37#include <osgEarthAnnotation/AnnotationNode>
38#include <osgEarthAnnotation/FeatureNode>
39#include <osgEarthUtil/Sky>
40#include <osgEarthUtil/RTTPicker>
41#include <osgEarthUtil/EarthManipulator>
42#include <osgEarthUtil/MouseCoordsTool>
43#include <osgEarthUtil/Controls>
44#include <osgEarthUtil/Formatter>
45#include <osgEarthUtil/MGRSFormatter>
46#include <osgEarthUtil/AutoClipPlaneHandler>
47#include <osgEarthUtil/VerticalScale>
48
49#define USE_RTT_PICKER
50
51#include "Types.h"
52#include "Trace.h"
53#include "MouseCoordsTool.h"
54#include "ScaleBar.h"
55#include "Placard.h"
56
57// Controls if TGA format is sent to client
58//#define RENDER_TARGA
59#define TARGA_BYTES_PER_PIXEL 3
60
61namespace GeoVis {
62
63class ScreenCaptureCallback : public osg::Camera::DrawCallback
64{
65public:
66    ScreenCaptureCallback(osg::Texture2D *texture = NULL) :
67        osg::Camera::DrawCallback(),
68        _texture(texture)
69    {
70        _image = new osg::Image;
71    }
72
73    virtual void operator()(osg::RenderInfo &renderInfo) const
74    {
75        FRAME("Enter ScreenCaptureCallback");
76        int width, height;
77        if (renderInfo.getCurrentCamera() == NULL) {
78            ERROR("No camera");
79            return;
80        }
81        if (renderInfo.getCurrentCamera()->getViewport() == NULL) {
82            ERROR("No viewport");
83            return;
84        }
85        width = (int)renderInfo.getCurrentCamera()->getViewport()->width();
86        height = (int)renderInfo.getCurrentCamera()->getViewport()->height();
87        FRAME("readPixels: %d x %d", width, height);
88#if 0 //USE_OFFSCREEN_FRAMEBUFFER
89        _image = _texture->getImage();
90#else
91#ifdef RENDER_TARGA
92        _image->readPixels(0, 0, width, height,
93                           GL_BGR, GL_UNSIGNED_BYTE);
94#else
95        _image->readPixels(0, 0, width, height,
96                           GL_RGB, GL_UNSIGNED_BYTE);
97#endif
98#endif
99    }
100
101    osg::Image *getImage()
102    {
103        return _image.get();
104    }
105
106    osg::Texture2D *getTexture()
107    {
108        return _texture.get();
109    }
110
111private:
112    osg::ref_ptr<osg::Texture2D> _texture;
113    osg::ref_ptr<osg::Image> _image;
114};
115
116/**
117 * \brief GIS Renderer
118 */
119class Renderer
120{
121public:
122    typedef std::string ColorMapId;
123    typedef std::string ViewpointId;
124
125    enum GraticuleType {
126        GRATICULE_UTM,
127        GRATICULE_MGRS,
128        GRATICULE_GEODETIC,
129        GRATICULE_SHADER
130    };
131
132    enum CoordinateDisplayType {
133        COORDS_LATLONG_DECIMAL_DEGREES,
134        COORDS_LATLONG_DEGREES_DECIMAL_MINUTES,
135        COORDS_LATLONG_DEGREES_MINUTES_SECONDS,
136        COORDS_MGRS
137    };
138
139    enum SelectMode {
140        SELECT_OFF,
141        SELECT_ON
142    };
143
144    Renderer();
145    virtual ~Renderer();
146
147    void setResourcePath(const std::string& path)
148    {
149        TRACE("Set resource path to %s", path.c_str());
150        _resourcePath = path;
151    }
152
153    void setCacheBaseDirectory(const std::string& path)
154    {
155        TRACE("Set cache base dir to %s", path.c_str());
156        _cacheBaseDir = path;
157    }
158
159    std::string getCacheDirectory() const
160    {
161        return _cacheDir;
162    }
163
164    void setupCache();
165
166    std::string getIconFile(const char *name) const;
167
168    std::string getBaseImage() const;
169
170    std::string getPinIcon() const;
171
172    void setAttribution(const std::string& attrib);
173
174    // Colormaps
175
176    void addColorMap(const ColorMapId& id, osg::TransferFunction1D *xfer);
177
178    void deleteColorMap(const ColorMapId& id);
179
180    void setColorMapNumberOfTableEntries(const ColorMapId& id, int numEntries);
181
182    bool renderColorMap(const ColorMapId& id, int width, int height,
183                        osg::Image *imgData,
184                        bool opaque, float bgColor[3],
185                        bool bgr = false,
186                        int bytesPerPixel = 3) const;
187
188    void getColorMapRange(const ColorMapId& id, float *min, float *max) const;
189
190    std::string getColorMapFilePath(const ColorMapId& id) const;
191
192    void saveCLRFile(const std::string& path, osg::TransferFunction1D *xfer);
193
194    // Scene
195
196    void loadEarthFile(const char *path);
197
198    void resetMap(osgEarth::MapOptions::CoordinateSystemType type,
199                  const osg::Vec4f& bgColor = osg::Vec4f(1,1,1,1),
200                  const char *profile = NULL,
201                  double bounds[4] = NULL);
202
203    void clearMap();
204
205    // Map options
206
207    void setCoordinateReadout(bool state,
208                              CoordinateDisplayType type = COORDS_LATLONG_DECIMAL_DEGREES,
209                              int precision = -1);
210
211    void setReadout(int mouseX, int mouseY);
212
213    void clearReadout();
214
215    void setScaleBar(bool state);
216
217    void setScaleBarUnits(ScaleBarUnits units);
218
219    void setGraticule(bool enable, GraticuleType type = GRATICULE_SHADER);
220
221    void setViewerLightType(osg::View::LightingMode mode);
222
223    void setLighting(bool state);
224
225    void setTerrainColor(const osg::Vec4f& color);
226
227    void setTerrainEdges(bool state) {}
228
229    void setTerrainLineColor(const osg::Vec4f& color) {}
230
231    void setTerrainLineWidth(float width) {}
232
233    void setTerrainLighting(bool state);
234
235    void setTerrainVerticalScale(double scale);
236
237    void setTerrainWireframe(bool state);
238
239    void setEphemerisTime(time_t utcTime);
240
241    void setEphemerisTime(int year, int month, int day, double hours);
242
243    void setSkyAmbient(float ambientLevel);
244
245    // Image raster layers
246
247    int getNumImageLayers() const
248    {
249        return (_map.valid() ? _map->getNumImageLayers() : 0);
250    }
251
252    void getImageLayerNames(std::vector<std::string>& names)
253    {
254        if (_map.valid()) {
255            osgEarth::ImageLayerVector layerVector;
256            _map->getImageLayers(layerVector);
257            osgEarth::ImageLayerVector::const_iterator itr;
258            for (itr = layerVector.begin(); itr != layerVector.end(); ++itr) {
259                //osgEarth::UID uid = (*itr)->getUID();
260                names.push_back((*itr)->getName());
261            }
262        }
263    }
264
265    bool addImageLayer(const char *name,
266                       osgEarth::TileSourceOptions& opts,
267                       unsigned int pos = UINT_MAX,
268                       bool enableCache = true,
269                       bool coverage = false,
270                       bool makeShared = false,
271                       bool visible = true,
272                       unsigned int minLOD = 0,
273                       unsigned int maxLOD = 23);
274
275    void removeImageLayer(const char *name);
276
277    void moveImageLayer(const char *name, unsigned int pos);
278
279    bool getImageLayerExtent(const char *name, osgEarth::GeoExtent &ext);
280
281    void setImageLayerVisibleRange(const char *name, float min, float max);
282
283    void setImageLayerLODRange(const char *name, int min, int max);
284
285    void setImageLayerOpacity(const char *name, double opacity);
286
287    void setImageLayerVisibility(const char *name, bool state);
288
289    void addColorFilter(const char *name, const char *shader);
290
291    void removeColorFilter(const char *name, int idx = -1);
292
293    // Elevation raster layers
294
295    int getNumElevationLayers() const
296    {
297        return (_map.valid() ? _map->getNumElevationLayers() : 0);
298    }
299
300    void getElevationLayerNames(std::vector<std::string>& names)
301    {
302        if (_map.valid()) {
303            osgEarth::ElevationLayerVector layerVector;
304            _map->getElevationLayers(layerVector);
305            osgEarth::ElevationLayerVector::const_iterator itr;
306            for (itr = layerVector.begin(); itr != layerVector.end(); ++itr) {
307                //osgEarth::UID uid = (*itr)->getUID();
308                names.push_back((*itr)->getName());
309            }
310        }
311    }
312
313    void addElevationLayer(const char *name,
314                           osgEarth::TileSourceOptions& opts,
315                           unsigned int pos = UINT_MAX,
316                           bool enableCache = true,
317                           bool visible = true,
318                           const char *verticalDatum = NULL,
319                           unsigned int minLOD = 0,
320                           unsigned int maxLOD = 23);
321
322    void removeElevationLayer(const char *name);
323
324    void moveElevationLayer(const char *name, unsigned int pos);
325
326    bool getElevationLayerExtent(const char *name, osgEarth::GeoExtent &ext);
327
328    void setElevationLayerVisibleRange(const char *name, float min, float max);
329
330    void setElevationLayerVisibility(const char *name, bool state);
331
332    // Model layers
333
334    int getNumModelLayers() const
335    {
336        return (_map.valid() ? _map->getNumModelLayers() : 0);
337    }
338
339    void getModelLayerNames(std::vector<std::string>& names)
340    {
341        if (_map.valid()) {
342            osgEarth::ModelLayerVector layerVector;
343            _map->getModelLayers(layerVector);
344            osgEarth::ModelLayerVector::const_iterator itr;
345            for (itr = layerVector.begin(); itr != layerVector.end(); ++itr) {
346                //osgEarth::UID uid = (*itr)->getUID();
347                names.push_back((*itr)->getName());
348            }
349        }
350    }
351
352    void addModelLayer(const char *name,
353                       osgEarth::ModelSourceOptions& opts,
354                       unsigned int pos = UINT_MAX,
355                       bool enableCache = true,
356                       bool lighting = true,
357                       bool visible = true);
358
359    void removeModelLayer(const char *name);
360
361    void moveModelLayer(const char *name, unsigned int pos);
362
363    bool getModelLayerExtent(const char *name, osgEarth::GeoExtent &ext);
364
365    //void setModelLayerVisibleRange(const char *name, float min, float max);
366
367    void setModelLayerOpacity(const char *name, double opacity);
368
369    void setModelLayerVisibility(const char *name, bool state);
370
371    // Render window
372
373    void setWindowSize(int width, int height);
374
375    int getWindowWidth() const
376    {
377        return _windowWidth;
378    }
379
380    int getWindowHeight() const
381    {
382        return _windowHeight;
383    }
384
385    // Camera
386
387    void saveNamedViewpoint(const char *name);
388
389    bool restoreNamedViewpoint(const char *name, double durationSecs);
390
391    bool removeNamedViewpoint(const char *name);
392
393    osgEarth::Viewpoint getViewpoint();
394
395    void setViewpoint(const osgEarth::Viewpoint& v, double durationSecs = 0.0);
396
397    double getMaxDistanceFromExtent(const osgEarth::GeoExtent& extent);
398
399    void setViewpointFromExtent(const osgEarth::GeoExtent& ext,
400                                double durationSecs = 0.0);
401
402    void setViewpointFromRect(double xmin, double ymin,
403                              double xmax, double ymax,
404                              const osgEarth::SpatialReference *srs = NULL,
405                              double durationSecs = 0.0);
406
407    void resetCamera(bool resetOrientation = true);
408
409    void setCameraOrientation(const double quat[4], bool absolute = true);
410
411    void panCamera(double x, double y);
412
413    void rotateCamera(double x, double y);
414
415    void zoomCamera(double z);
416
417    void setCameraDistance(double dist);
418
419    // Keyboard events
420
421    void keyPress(int key);
422
423    void keyRelease(int key);
424
425    // Mouse events
426
427    void setThrowingEnabled(bool state);
428
429    void mouseClick(int button, double x, double y);
430
431    void mouseDoubleClick(int button, double x, double y);
432
433    void mouseDrag(int button, double x, double y);
434
435    void mouseRelease(int button, double x, double y);
436
437    void mouseMotion(double x, double y);
438
439    void mouseScroll(int direction);
440
441    void pickPending(bool value)
442    { _pickPending = value; }
443
444    // Rendering an image
445
446    void setBackgroundColor(float color[3]);
447
448    void eventuallyRender();
449
450    bool render();
451
452    osg::Image *getRenderedFrame();
453
454    bool mapMouseCoords(float mouseX, float mouseY,
455                        osgEarth::GeoPoint &pt, bool invertY = true);
456
457    double computeMapScale();
458
459    const osgEarth::SpatialReference *getMapSRS()
460    {
461        if (_mapNode.valid()) {
462            return _mapNode->getMapSRS();
463        } else {
464            return NULL;
465        }
466    }
467
468    void addPlaceNode(double latitude, double longitude, char *labelText);
469
470    void hoverPlaceNode(int x, int y, bool invertY = true);
471
472    void deletePlaceNode(int x, int y, bool invertY = true);
473
474    bool getMousePoint(double *x, double *y, double *z)
475    {
476        return (_coordsCallback.valid() && _coordsCallback->report(x, y, z));
477    }
478
479    bool mouseToLatLong(int mouseX, int mouseY,
480                        double *latitude, double *longitude);
481
482    bool getWorldCoords(const osgEarth::GeoPoint& mapPt, osg::Vec3d *world);
483
484    bool worldToScreen(const osg::Vec3d& world, osg::Vec3d *screen,
485                       bool invertY = true);
486
487    bool worldToScreen(std::vector<osg::Vec3d>& coords, bool invertY = true);
488
489    void setMaximumFrameRateInHertz(double rate)
490    {
491        if (rate > 60.0)
492            rate = 60.0;
493        if (rate < 0.25)
494            rate = 0.25;
495        _minFrameTime = 1.0/rate;
496        if (_viewer.valid()) {
497            osgUtil::IncrementalCompileOperation *op =
498                _viewer->getDatabasePager()->getIncrementalCompileOperation();
499            if (op != NULL) {
500                TRACE("Setting DB Pager target frame rate to %g", rate);
501                op->setTargetFrameRate(rate);
502            }
503        }
504        TRACE("Frame rate target: %.2f Hz", (float)getMaximumFrameRateInHertz());
505        TRACE("Frame time target: %.2f msec", _minFrameTime * 1000.0f);
506    }
507
508    void setMaximumBitrate(double bitsPerSecond)
509    {
510        TRACE("Setting max bitrate to %g", bitsPerSecond);
511        unsigned long bitsPerFrame = (_windowWidth * _windowHeight * 3 + 16) * 8;
512        double fps = bitsPerSecond / ((double)bitsPerFrame);
513        setMaximumFrameRateInHertz(fps);
514        TRACE("Bandwidth target: %.2f Mbps", (float)(getMaximumBitrate()/1.0e6));
515    }
516
517    double getMaximumFrameRateInHertz()
518    {
519        return (1.0/_minFrameTime);
520    }
521
522    double getMaximumBitrate()
523    {
524        unsigned long bitsPerFrame = (_windowWidth * _windowHeight * 3 + 16) * 8;
525        return ((double)bitsPerFrame * getMaximumFrameRateInHertz());
526    }
527
528    void markFrameStart();
529
530    void markFrameEnd();
531
532    void setIdleTimeout(long timeout)
533    {
534        if (timeout != -1L &&
535            (double)timeout < _minFrameTime) {
536            ERROR("Timeout must be more than %g sec", _minFrameTime);
537            return;
538        }
539        TRACE("Setting idle timeout to %ld sec", timeout);
540        _idleTimeout = timeout;
541    }
542
543    long getIdleTimeout()
544    {
545        return _idleTimeout;
546    }
547
548    void getTimeout(struct timeval *tv);
549
550    void mapNodeUpdate();
551
552    std::string getCanonicalPath(const std::string& url) const;
553
554    void writeScene(const std::string& file);
555
556    void enablePlacard(const char *layerName, bool state);
557
558    void setPlacardConfig(const Placard& placardConf, const char *layerName);
559
560    void setSelectMode(SelectMode mode);
561
562    void selectFeatures(std::vector<unsigned long>& featureIDs,
563                        const char *layerName, bool clear = true);
564
565    void deselectFeatures(std::vector<unsigned long>& featureIDs,
566                          const char *layerName);
567
568    void addPlacard(const osgEarth::GeoPoint& location,
569                    osgEarth::Features::Feature *feature,
570                    const char *layerName);
571
572    void clearSelection();
573
574    void addRhumbBox(double latMin, double latMax,
575                     double longMin, double longMax);
576
577    void initBoxSelection(int x, int y);
578    void updateBoxSelection(int x, int y);
579    void getBoxSelection(double *latMin, double *latMax,
580                         double *longMin, double *longMax,
581                         const osgEarth::SpatialReference *outSRS = NULL);
582    void clearBoxSelection();
583
584    bool select(osgEarth::Annotation::AnnotationNode *node)
585    {
586        if (_selected.find(node) == _selected.end()) {
587            _selected.insert(node);
588            return true;
589        } else {
590            return false;
591        }
592    }
593
594    std::set<osgEarth::Annotation::AnnotationNode*>& getHovered()
595    { return _hovered; }
596    std::set<osgEarth::Annotation::AnnotationNode*>& getSelected()
597    { return _selected; }
598    osg::Group *getSceneRoot()
599    { return _sceneRoot.get(); }
600    osgEarth::MapNode *getMapNode()
601    { return _mapNode.get(); }
602    osg::Group *getAnnotations()
603    {
604        initAnnotations();
605        return _annotations.get();
606    }
607    osg::Group *getPlaceNodes()
608    {
609        initAnnotations();
610        return _placeNodes.get();
611    }
612    Placard getPlacardConfig(const char *layerName)
613    {
614        Placard ret;
615        PlacardHashmap::iterator itr = _placardConfigs.find(layerName);
616        if (itr != _placardConfigs.end()) {
617            ret = itr->second;
618        }
619        return ret;
620    }
621
622private:
623    typedef std::tr1::unordered_map<ColorMapId, osg::ref_ptr<osg::TransferFunction1D> > ColorMapHashmap;
624    typedef std::tr1::unordered_map<ViewpointId, osgEarth::Viewpoint> ViewpointHashmap;
625    typedef std::tr1::unordered_map<std::string, Placard> PlacardHashmap;
626    typedef std::tr1::unordered_map<std::string, std::set<unsigned long> > FeatureSelectionHashmap;
627
628    void initAnnotations();
629
630    void clearSelectionAnnotationNodes();
631
632    void initViewer();
633
634    void finalizeViewer();
635   
636    void initControls();
637
638    void updateDebugLabel();
639
640    void initEarthManipulator();
641
642    void initMouseCoordsTool(CoordinateDisplayType type = COORDS_LATLONG_DECIMAL_DEGREES,
643                             int precision = -1);
644
645    osgEarth::Util::MGRSFormatter::Precision getMGRSPrecision(int precisionInMeters);
646
647    void initColorMaps();
648
649    void initCamera();
650
651    bool isPagerIdle();
652
653    bool checkNeedToDoFrame();
654
655    osgGA::EventQueue *getEventQueue();
656
657    bool _needsRedraw;
658    int _windowWidth, _windowHeight;
659    double _mapScale;
660    float _bgColor[3];
661
662    long _idleTimeout;
663    double _minFrameTime;
664    double _lastFrameTime;
665    double _renderTime;
666    osg::Timer_t _startFrameTime;
667    osg::Timer_t _renderStartTime;
668    osg::Timer_t _renderStopTime;
669
670    ColorMapHashmap _colorMaps;
671
672    std::string _resourcePath;
673    std::string _cacheBaseDir;
674    std::string _cacheDir;
675    std::string _baseURI;
676    std::string _attribution;
677
678    osg::ref_ptr<osg::Group> _sceneRoot;
679    osg::ref_ptr<osg::Group> _graticule;
680    osg::ref_ptr<osg::Group> _annotations;
681    PlacardHashmap _placardConfigs;
682    double _anchorLat, _anchorLong;
683    SelectMode _selectMode;
684    osg::ref_ptr<osgEarth::Annotation::FeatureNode> _selectionBox;
685#ifdef USE_RTT_PICKER
686    osg::ref_ptr<osgEarth::Util::RTTPicker> _picker;
687#endif
688    bool _pickPending;
689    osg::ref_ptr<osg::Group> _placeNodes;
690    FeatureSelectionHashmap _selectedFeatures;
691    std::set<osgEarth::Annotation::AnnotationNode *> _hovered;
692    std::set<osgEarth::Annotation::AnnotationNode *> _selected;
693    osg::ref_ptr<osgEarth::MapNode> _mapNode;
694    osg::ref_ptr<osgEarth::Map> _map;
695    osg::ref_ptr<osgEarth::Util::SkyNode> _skyNode;
696    osg::ref_ptr<osgViewer::Viewer> _viewer;
697    osg::ref_ptr<ScreenCaptureCallback> _captureCallback;
698    osg::ref_ptr<osgEarth::Util::AutoClipPlaneCullCallback> _clipPlaneCullCallback;
699    osg::ref_ptr<osgEarth::Util::Controls::HBox> _coordsBox;
700    osg::ref_ptr<MouseCoordsTool> _mouseCoordsTool;
701    osg::ref_ptr<MouseCoordsCallback> _coordsCallback;
702    osg::ref_ptr<osgEarth::Util::Controls::HBox> _debugBox;
703    osg::ref_ptr<osgEarth::Util::Controls::LabelControl> _debugLabel;
704    osg::ref_ptr<osgEarth::Util::Controls::HBox> _copyrightScaleBox;
705    osg::ref_ptr<osgEarth::Util::Controls::LabelControl> _copyrightLabel;
706    osg::ref_ptr<osgEarth::Util::Controls::LabelControl> _scaleLabel;
707    osg::ref_ptr<osgEarth::Util::Controls::Frame> _scaleBar;
708    ScaleBarUnits _scaleBarUnits;
709    osg::ref_ptr<osgEarth::Util::EarthManipulator> _manipulator;
710    osg::ref_ptr<osgGA::StateSetManipulator> _stateManip;
711    osg::ref_ptr<osgEarth::Util::VerticalScale> _verticalScale;
712    ViewpointHashmap _viewpoints;
713};
714
715class MapNodeCallback : public osg::NodeCallback
716{
717public:
718    MapNodeCallback(Renderer *renderer) :
719        _renderer(renderer)
720    {}
721
722    virtual void operator()(osg::Node* node, osg::NodeVisitor* nv)
723    {
724        _renderer->mapNodeUpdate();
725        traverse(node, nv);
726    }
727private:
728    Renderer *_renderer;
729};
730
731class SelectPlaceNodesVisitor : public osg::NodeVisitor
732{
733public:
734    SelectPlaceNodesVisitor(Renderer *renderer,
735                            double latMin, double latMax,
736                            double longMin, double longMax) :
737        osg::NodeVisitor(osg::NodeVisitor::TRAVERSE_ALL_CHILDREN),
738        _renderer(renderer),
739        _latMin(latMin), _latMax(latMax),
740        _longMin(longMin), _longMax(longMax)
741    {}
742
743    virtual void apply(osg::Node& node);
744
745private:
746    Renderer *_renderer;
747    double _latMin, _latMax, _longMin, _longMax;
748};
749
750}
751
752#endif
Note: See TracBrowser for help on using the repository browser.