source: trunk/packages/vizservers/nanovis/HeightMap.cpp @ 1380

Last change on this file since 1380 was 1188, checked in by vrinside, 16 years ago
File size: 18.7 KB
Line 
1 
2#include <memory.h>
3#include <stdlib.h>
4#include <sys/time.h>
5#include <sys/types.h>
6#include <unistd.h>
7#include <fcntl.h>
8#include <stdlib.h>
9#include <GL/glew.h>
10#include <GL/gl.h>
11#include "Grid.h"
12#include "HeightMap.h"
13#include "ContourLineFilter.h"
14#include "TypeDefs.h"
15#include "Texture1D.h"
16#include "R2/R2FilePath.h"
17#include "RpField1D.h"
18#include "RenderContext.h"
19
20bool HeightMap::update_pending = false;
21double HeightMap::valueMin = 0.0;
22double HeightMap::valueMax = 1.0;
23
24#define TOPCONTOUR      0
25//#define TOPCONTOUR    1
26HeightMap::HeightMap() :
27    _vertexBufferObjectID(0),
28    _textureBufferObjectID(0),
29    _vertexCount(0),
30    _contour(0),
31    _topContour(0),
32    _colorMap(0),
33    _indexBuffer(0),
34    _indexCount(0),
35    _contourColor(1.0f, 0.0f, 0.0f),
36    _contourVisible(false),
37    _topContourVisible(true),
38    _visible(false),
39    _scale(1.0f, 1.0f, 1.0f),
40    _centerPoint(0.0f, 0.0f, 0.0f),
41    heights_(NULL)
42{
43    _shader = new NvShader();
44    _shader->loadFragmentProgram("heightcolor.cg", "main");
45    _tf = _shader->getNamedParameterFromFP("tf");
46}
47
48HeightMap::~HeightMap()
49{
50    reset();
51
52    if (_shader) {
53        delete _shader;
54    }
55
56    // TMP
57    //if (_colorMap) delete _colorMap;
58}
59
60void HeightMap::render(graphics::RenderContext* renderContext)
61{
62    if (renderContext->getCullMode() == graphics::RenderContext::NO_CULL) {
63        glDisable(GL_CULL_FACE);
64    } else {
65        glEnable(GL_CULL_FACE);
66        glCullFace((GLuint) renderContext->getCullMode());
67    }
68    glPolygonMode(GL_FRONT_AND_BACK, (GLuint) renderContext->getPolygonMode());
69    glShadeModel((GLuint) renderContext->getShadingModel());
70
71    glPushMatrix();
72
73#ifndef notdef
74    if (_scale.x != 0.0) {
75        glScalef(1 / _scale.x, 1 / _scale.y , 1 / _scale.z);
76    }
77#endif
78    glTranslatef(-_centerPoint.x, -_centerPoint.y, -_centerPoint.z);
79
80    if (_contour != NULL) {
81        glDepthRange (0.001, 1.0);
82    }
83       
84    glEnable(GL_DEPTH_TEST);
85
86    if (_vertexBufferObjectID) {
87        glColor3f(1.0f, 1.0f, 1.0f);
88        glShadeModel(GL_SMOOTH);
89        glEnable(GL_BLEND);
90        glEnableClientState(GL_VERTEX_ARRAY);
91        glDisableClientState(GL_COLOR_ARRAY);
92        glDisableClientState(GL_INDEX_ARRAY);
93        glDisableClientState(GL_NORMAL_ARRAY);
94       
95        if (_colorMap) {
96            // PUT vertex program here
97            //
98            //
99           
100            cgGLBindProgram(_shader->getFP());
101            cgGLEnableProfile(CG_PROFILE_FP30);
102           
103            cgGLSetTextureParameter(_tf, _colorMap->id);
104            cgGLEnableTextureParameter(_tf);
105           
106            glEnable(GL_TEXTURE_1D);
107            _colorMap->getTexture()->activate();
108           
109            glEnableClientState(GL_TEXTURE_COORD_ARRAY);
110        }
111       
112        glBindBuffer(GL_ARRAY_BUFFER, _vertexBufferObjectID);
113        glVertexPointer(3, GL_FLOAT, 12, 0);
114       
115        glBindBuffer(GL_ARRAY_BUFFER, _textureBufferObjectID);
116        ::glTexCoordPointer(3, GL_FLOAT, 12, 0);
117       
118#define _TRIANGLES_
119#ifdef _TRIANGLES_
120        glDrawElements(GL_TRIANGLES, _indexCount, GL_UNSIGNED_INT,
121                       _indexBuffer);
122#else                   
123        glDrawElements(GL_QUADS, _indexCount, GL_UNSIGNED_INT,
124                       _indexBuffer);
125#endif
126
127        glBindBuffer(GL_ARRAY_BUFFER, 0);
128       
129        glDisableClientState(GL_VERTEX_ARRAY);
130        if (_colorMap) {
131            _colorMap->getTexture()->deactivate();
132            glDisableClientState(GL_TEXTURE_COORD_ARRAY);
133           
134            cgGLDisableProfile(CG_PROFILE_FP30);
135        }
136    }
137   
138    glShadeModel(GL_FLAT);
139    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
140   
141    if (_contour != NULL) {
142        if (_contourVisible) {
143            glDisable(GL_BLEND);
144            glDisable(GL_TEXTURE_2D);
145            glColor4f(_contourColor.x, _contourColor.y, _contourColor.z, 1.0f);
146            glDepthRange (0.0, 0.999);
147            _contour->render();
148            glDepthRange (0.0, 1.0);
149        }
150
151#if TOPCONTOUR
152        if (_topContourVisible) {
153            glDisable(GL_BLEND);
154            glDisable(GL_TEXTURE_2D);
155            glColor4f(_contourColor.x, _contourColor.y, _contourColor.z, 1.0f);
156            //glDepthRange (0.0, 0.999);
157            _topContour->render();
158            //glDepthRange (0.0, 1.0);
159        }
160#endif
161    }
162    glPopMatrix();
163}
164
165void
166HeightMap::createIndexBuffer(int xCount, int zCount, float* heights)
167{
168    if (_indexBuffer != NULL) {
169        delete [] _indexBuffer;
170    }
171    _indexCount = (xCount - 1) * (zCount - 1) * 6;
172    _indexBuffer = new int[_indexCount];
173   
174    int i, j;
175    int boundaryWidth = xCount - 1;
176    int boundaryHeight = zCount - 1;
177    int* ptr = _indexBuffer;
178    int index1, index2, index3, index4;
179    bool index1Valid, index2Valid, index3Valid, index4Valid;
180    index1Valid = index2Valid = index3Valid = index4Valid = true;
181
182    if (heights) {
183        int ic = 0;
184        for (i = 0; i < boundaryHeight; ++i) {
185            for (j = 0; j < boundaryWidth; ++j) {
186                index1 = i * xCount +j;
187                if (isnan(heights[index1])) index1Valid = false;
188                index2 = (i + 1) * xCount + j;
189                if (isnan(heights[index2])) index2Valid = false;
190                index3 = (i + 1) * xCount + j + 1;
191                if (isnan(heights[index3])) index3Valid = false;
192                index4 = i * xCount + j + 1;
193                if (isnan(heights[index4])) index4Valid = false;
194           
195#ifdef _TRIANGLES_
196                if (index1Valid && index2Valid && index3Valid) {
197                    *ptr = index1; ++ptr;
198                    *ptr = index2; ++ptr;
199                    *ptr = index3; ++ptr;
200                    ++ic;
201                }
202                if (index1Valid && index3Valid && index4Valid) {
203                    *ptr = index1; ++ptr;
204                    *ptr = index3; ++ptr;
205                    *ptr = index4; ++ptr;
206                    ++ic;
207                }
208#else
209                if (index1Valid && index2Valid && index3Valid && index4Valid) {
210                    *ptr = index1; ++ptr;
211                    *ptr = index2; ++ptr;
212                    *ptr = index3; ++ptr;
213                    *ptr = index4; ++ptr;
214                    ++ic;
215                }
216#endif
217            }
218        }
219    } else {
220        for (i = 0; i < boundaryHeight; ++i) {
221            for (j = 0; j < boundaryWidth; ++j) {
222                *ptr = i * xCount + j; ++ptr;
223                *ptr = (i + 1) * xCount + j; ++ptr;
224                *ptr = (i + 1) * xCount + j + 1; ++ptr;
225                *ptr = i * xCount + j; ++ptr;
226                *ptr = (i + 1) * xCount + j + 1; ++ptr;
227                *ptr = i * xCount + j + 1; ++ptr;
228            }
229        }
230    }
231}
232
233void
234HeightMap::reset()
235{
236    if (_vertexBufferObjectID) {
237        glDeleteBuffers(1, &_vertexBufferObjectID);
238        _vertexBufferObjectID = 0;
239    }
240    if (_textureBufferObjectID) {
241        glDeleteBuffers(1, &_textureBufferObjectID);
242        _textureBufferObjectID = 0;
243    }
244    if (_contour != NULL) {
245        delete _contour;
246        _contour = NULL;
247    }
248}
249
250void
251HeightMap::setHeight(int xCount, int yCount, Vector3* heights)
252{
253    _vertexCount = xCount * yCount;
254    reset();
255   
256    heights_ = (float *)heights;
257    float min, max;
258    min = heights[0].y, max = heights[0].y;
259
260    int count = xCount * yCount;
261    for (int i = 0; i < count; ++i) {
262        if (min > heights[i].y) {
263            min = heights[i].y;
264        }
265        if (max < heights[i].y) {
266            max = heights[i].y;
267        }
268    }
269
270    _scale.x = 1.0f;
271    _scale.z = max - min;
272    _scale.y = 1.0f;
273
274    xAxis.SetRange(0.0, 1.0);
275    yAxis.SetRange(0.0, 1.0);
276    zAxis.SetRange(0.0, 1.0);
277    wAxis.SetRange(min, max);
278    update_pending = true;
279   
280    _centerPoint.set(_scale.x * 0.5, _scale.z * 0.5 + min, _scale.y * 0.5);
281
282    Vector3* texcoord = new Vector3[count];
283    for (int i = 0; i < count; ++i) {
284        texcoord[i].set(0, 0, heights[i].y);
285    }
286   
287    glGenBuffers(1, &_vertexBufferObjectID);
288    glBindBuffer(GL_ARRAY_BUFFER, _vertexBufferObjectID);
289    glBufferData(GL_ARRAY_BUFFER, _vertexCount * sizeof( Vector3 ), heights,
290        GL_STATIC_DRAW);
291    glGenBuffers(1, &_textureBufferObjectID);
292    glBindBuffer(GL_ARRAY_BUFFER, _textureBufferObjectID);
293    glBufferData(GL_ARRAY_BUFFER, _vertexCount * sizeof(float) * 3, texcoord,
294        GL_STATIC_DRAW);
295    glBindBuffer(GL_ARRAY_BUFFER, 0);
296   
297    delete [] texcoord;
298   
299   
300    ContourLineFilter lineFilter;
301    _contour = lineFilter.create(0.0f, 1.0f, 10, heights, xCount, yCount);
302
303#if TOPCONTOUR
304    ContourLineFilter topLineFilter;
305    topLineFilter.setHeightTop(true);
306    _topContour = topLineFilter.create(0.0f, 1.0f, 10, heights, xCount, yCount);
307#endif
308
309    //if (heightMap)
310    //{
311    //  VertexBuffer* vertexBuffer = new VertexBuffer(VertexBuffer::POSITION3, xCount * yCount, sizeof(Vector3) * xCount * yCount, heightMap, false);
312    this->createIndexBuffer(xCount, yCount, 0);
313    //}
314    //else
315    //{
316    //printf("ERROR - HeightMap::setHeight\n");
317    //}
318}
319
320void
321HeightMap::setHeight(float xMin, float yMin, float xMax, float yMax,
322                     int xNum, int yNum, float* heights)
323{
324    _vertexCount = xNum * yNum;
325    xNum_ = xNum, yNum_ = yNum;
326    heights_ = heights;
327    reset();
328   
329    // Get the min/max of the heights. */
330    float min, max;
331    min = max = heights[0];
332    for (int i = 0; i < _vertexCount; ++i) {
333        if (min > heights[i]) {
334            min = heights[i];
335        } else if (max < heights[i]) {
336            max = heights[i];
337        }
338    }
339#ifdef notdef
340    if (retainScale_) {
341        // Check the units of each axis.  If they are the same, we want to
342        // retain the surface's aspect ratio when transforming coordinates to
343        // the grid. Use the range of the longest axis when the units are the
344        // same. 
345        if (xAxis.units() != NULL) && (xAxis.units() == yAxis.units()) {
346        }
347        if (yAxis.units() != NULL) && (yAxis.units() == zAxis.units()) {
348        }
349    }
350#endif
351
352    wAxis.SetRange(min, max);
353    yAxis.SetRange(min, max);
354    xAxis.SetRange(xMin, xMax);
355    zAxis.SetRange(yMin, yMax);
356   
357   
358    min = 0.0, max = 1.0;
359    xMin = yMin = min = 0.0;
360    xMax = yMax = max = 1.0;
361    // Save the scales.
362    _scale.x = _scale.y = _scale.z = 1.0;
363
364    update_pending = true;
365
366    _centerPoint.set(0.5, 0.5, 0.5);
367   
368#ifndef notdef
369    Vector3* texcoord = new Vector3[_vertexCount];
370    for (int i = 0; i < _vertexCount; ++i) {
371        texcoord[i].set(0, 0, heights[i]);
372    }
373   
374    Vector3* heightMap = createHeightVertices(xMin, yMin, xMax, yMax, xNum, yNum, heights);
375   
376    glGenBuffers(1, &_vertexBufferObjectID);
377    glBindBuffer(GL_ARRAY_BUFFER, _vertexBufferObjectID);
378    glBufferData(GL_ARRAY_BUFFER, _vertexCount * sizeof( Vector3 ), heightMap,
379        GL_STATIC_DRAW);
380    glGenBuffers(1, &_textureBufferObjectID);
381    glBindBuffer(GL_ARRAY_BUFFER, _textureBufferObjectID);
382    glBufferData(GL_ARRAY_BUFFER, _vertexCount * sizeof(float) * 3, texcoord,
383        GL_STATIC_DRAW);
384    glBindBuffer(GL_ARRAY_BUFFER, 0);
385   
386    delete [] texcoord;
387   
388   
389    ContourLineFilter lineFilter;
390    //lineFilter.setColorMap(_colorMap);
391    _contour = lineFilter.create(0.0f, 1.0f, 10, heightMap, xNum, yNum);
392   
393#if TOPCONTOUR
394    ContourLineFilter topLineFilter;
395    topLineFilter.setHeightTop(true);
396    _topContour = topLineFilter.create(0.0f, 1.0f, 10, heightMap, xNum, yNum);
397#endif
398
399
400    //if (heightMap)
401    //{
402    //  VertexBuffer* vertexBuffer = new VertexBuffer(VertexBuffer::POSITION3, xNum * yNum,
403    // sizeof(Vector3) * xNum * yNum, heightMap, false);
404    if (_indexBuffer != NULL) {
405        free(_indexBuffer);
406        _indexBuffer = NULL;
407    }
408    this->createIndexBuffer(xNum, yNum, heights);
409    //}
410    //else
411    //{
412    //printf("ERROR - HeightMap::setHeight\n");
413    //}
414#endif
415}
416
417Vector3*
418HeightMap::createHeightVertices(float xMin, float yMin, float xMax,
419                                float yMax, int xNum, int yNum, float* height)
420{
421    Vector3* vertices = (Vector3*) malloc(sizeof(Vector3) * xNum * yNum);
422
423    Vector3* dstDataPtr = vertices;
424    float* srcDataPtr = height;
425   
426    for (int y = 0; y < yNum; ++y) {
427        float yCoord;
428
429        yCoord = yMin + ((yMax - yMin) * y) / (yNum - 1);
430        for (int x = 0; x < xNum; ++x) {
431            float xCoord;
432
433            xCoord = xMin + ((xMax - xMin) * x) / (xNum - 1);
434            dstDataPtr->set(xCoord, *srcDataPtr, yCoord);
435
436            ++dstDataPtr;
437            ++srcDataPtr;
438        }
439    }
440    return vertices;
441}
442
443void HeightMap::setColorMap(TransferFunction* colorMap)
444{
445    //if (colorMap) colorMap->addRef();
446    //if (_colorMap) _colorMap->unrefDelete();
447    _colorMap = colorMap;
448}
449
450// Maps the data coordinates of the surface into the grid's axes.
451void
452HeightMap::MapToGrid(Grid *gridPtr)
453{
454    int count = xNum_ * yNum_;
455
456    reset();
457
458    // The range of the grid's y-axis 0..1 represents the distance between the
459    // smallest and largest major ticks for all surfaces plotted.  Translate
460    // this surface's y-values (heights) into the grid's axis coordinates.
461
462    float yScale = 1.0 / (gridPtr->yAxis.max() - gridPtr->yAxis.min());
463    float *p, *q, *pend;
464    float *normHeights = new float[count];
465    for (p = heights_, pend = p + count, q = normHeights; p < pend; p++, q++) {
466        *q = (*p - gridPtr->yAxis.min()) * yScale;
467    }
468    Vector3 *t, *texcoord;
469    texcoord = new Vector3[count];
470    for (t = texcoord, p = normHeights, pend = p + count; p < pend; p++, t++) {
471        t->set(0, 0, *p);
472    }
473
474    // Normalize the mesh coordinates (x and z min/max) the range of the major
475    // ticks for the x and z grid axes as well.
476
477    float xScale, zScale;
478    float xMin, xMax, zMin, zMax;
479
480    xScale = 1.0 / (gridPtr->xAxis.max() - gridPtr->xAxis.min());
481    xMin = (xAxis.min() - gridPtr->xAxis.min()) * xScale;
482    xMax = (xAxis.max() - gridPtr->xAxis.min()) * xScale;
483    zScale = 1.0 / (gridPtr->zAxis.max() - gridPtr->zAxis.min());
484    zMin = (zAxis.min() - gridPtr->zAxis.min()) * zScale;
485    zMax = (zAxis.max() - gridPtr->zAxis.min()) * zScale;
486
487    Vector3* vertices;
488    vertices = createHeightVertices(xMin, zMin, xMax, zMax, xNum_, yNum_,
489        normHeights);
490   
491    glGenBuffers(1, &_vertexBufferObjectID);
492    glBindBuffer(GL_ARRAY_BUFFER, _vertexBufferObjectID);
493    glBufferData(GL_ARRAY_BUFFER, _vertexCount * sizeof(Vector3), vertices,
494        GL_STATIC_DRAW);
495    glGenBuffers(1, &_textureBufferObjectID);
496    glBindBuffer(GL_ARRAY_BUFFER, _textureBufferObjectID);
497    glBufferData(GL_ARRAY_BUFFER, _vertexCount * sizeof(float) * 3, texcoord,
498        GL_STATIC_DRAW);
499    glBindBuffer(GL_ARRAY_BUFFER, 0);
500    delete [] texcoord;
501
502    ContourLineFilter lineFilter;
503    //lineFilter.setColorMap(_colorMap);
504    _contour = lineFilter.create(0.0f, 1.0f, 10, vertices, xNum_, yNum_);
505   
506#if TOPCONTOUR
507    ContourLineFilter topLineFilter;
508    topLineFilter.setHeightTop(true);
509    _topContour = topLineFilter.create(0.0f, 1.0f, 10, vertices, xNum_, yNum_);
510#endif
511    this->createIndexBuffer(xNum_, yNum_, normHeights);
512    delete [] normHeights;
513}
514
515void HeightMap::render_topview(graphics::RenderContext* renderContext, int render_width, int render_height)
516{
517   glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
518    glPushAttrib(GL_VIEWPORT_BIT);
519    glViewport(0, 0, render_width, render_height);
520    glMatrixMode(GL_PROJECTION);
521    glPushMatrix();
522    glLoadIdentity();
523    //gluOrtho2D(0, render_width, 0, render_height);
524    glOrtho(-.5, .5, -.5, .5, -50, 50);
525    glMatrixMode(GL_MODELVIEW);
526    glPushMatrix();
527    glLoadIdentity();
528
529    glTranslatef(0.0, 0.0, -10.0);
530
531    // put camera rotation and traslation
532    //glScalef(1 / _scale.x, 1 / _scale.y , 1 / _scale.z);
533
534    if (renderContext->getCullMode() == graphics::RenderContext::NO_CULL) {
535        glDisable(GL_CULL_FACE);
536    } else {
537        glEnable(GL_CULL_FACE);
538        glCullFace((GLuint) renderContext->getCullMode());
539    }
540
541    glPolygonMode(GL_FRONT_AND_BACK, (GLuint) renderContext->getPolygonMode());
542    glShadeModel((GLuint) renderContext->getShadingModel());
543
544    glPushMatrix();
545
546    //glTranslatef(-_centerPoint.x, -_centerPoint.y, -_centerPoint.z);
547
548    //glScalef(0.01, 0.01, 0.01f);
549    glRotatef(90.0f, 1.0f, 0.0f, 0.0f);
550    glTranslatef(-_centerPoint.x, -_centerPoint.y, -_centerPoint.z);
551    if (_contour != NULL) {
552        glDepthRange (0.001, 1.0);
553    }
554       
555    glEnable(GL_DEPTH_TEST);
556
557    glEnable(GL_BLEND);
558    glEnable(GL_TEXTURE_2D);
559    if (_vertexBufferObjectID)
560    {
561        printf("TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT\n");
562        glColor3f(1.0f, 1.0f, 1.0f);
563        glShadeModel(GL_SMOOTH);
564        glEnable(GL_BLEND);
565        glEnableClientState(GL_VERTEX_ARRAY);
566        glDisableClientState(GL_COLOR_ARRAY);
567        glDisableClientState(GL_INDEX_ARRAY);
568        glDisableClientState(GL_NORMAL_ARRAY);
569       
570        if (_colorMap) {
571            cgGLBindProgram(_shader->getFP());
572            cgGLEnableProfile(CG_PROFILE_FP30);
573           
574            cgGLSetTextureParameter(_tf, _colorMap->id);
575            cgGLEnableTextureParameter(_tf);
576           
577            glEnable(GL_TEXTURE_1D);
578            _colorMap->getTexture()->activate();
579           
580            glEnableClientState(GL_TEXTURE_COORD_ARRAY);
581        }
582        else {
583            glDisableClientState(GL_TEXTURE_COORD_ARRAY);
584        }
585       
586        glBindBuffer(GL_ARRAY_BUFFER, _vertexBufferObjectID);
587        glVertexPointer(3, GL_FLOAT, 12, 0);
588       
589        glBindBuffer(GL_ARRAY_BUFFER, _textureBufferObjectID);
590        ::glTexCoordPointer(3, GL_FLOAT, 12, 0);
591       
592#define _TRIANGLES_
593#ifdef _TRIANGLES_
594        glDrawElements(GL_TRIANGLES, _indexCount, GL_UNSIGNED_INT,
595                       _indexBuffer);
596#else                   
597        glDrawElements(GL_QUADS, _indexCount, GL_UNSIGNED_INT,
598                       _indexBuffer);
599#endif
600
601        glBindBuffer(GL_ARRAY_BUFFER, 0);
602       
603        glDisableClientState(GL_VERTEX_ARRAY);
604        if (_colorMap) {
605            _colorMap->getTexture()->deactivate();
606            glDisableClientState(GL_TEXTURE_COORD_ARRAY);
607           
608            cgGLDisableProfile(CG_PROFILE_FP30);
609        }
610    }
611   
612    glShadeModel(GL_FLAT);
613    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
614   
615    if (_contour != NULL) {
616        if (_contourVisible) {
617            glDisable(GL_BLEND);
618            glDisable(GL_TEXTURE_2D);
619            glColor4f(_contourColor.x, _contourColor.y, _contourColor.z, 1.0f);
620            glDepthRange (0.0, 0.999);
621            _contour->render();
622            glDepthRange (0.0, 1.0);
623        }
624
625#if TOPCONTOUR
626        if (_topContourVisible) {
627            glDisable(GL_BLEND);
628            glDisable(GL_TEXTURE_2D);
629            glColor4f(_contourColor.x, _contourColor.y, _contourColor.z, 1.0f);
630            //glDepthRange (0.0, 0.999);
631            _topContour->render();
632            //glDepthRange (0.0, 1.0);
633        }
634#endif
635    }
636   
637    glPopMatrix();
638    glPopMatrix();
639
640    glMatrixMode(GL_PROJECTION);
641    glPushMatrix();
642    glMatrixMode(GL_MODELVIEW);
643
644    glPopAttrib();
645}
646
Note: See TracBrowser for help on using the repository browser.