Currently job artifacts in CI/CD pipelines on LRZ GitLab never expire. Starting from Wed 26.1.2022 the default expiration time will be 30 days (GitLab default). Currently existing artifacts in already completed jobs will not be affected by the change. The latest artifacts for all jobs in the latest successful pipelines will be kept. More information: https://gitlab.lrz.de/help/user/admin_area/settings/continuous_integration.html#default-artifacts-expiration

datacontainerinspectorcanvas.cpp 22 KB
Newer Older
1
2
// ================================================================================================
// 
schultezub's avatar
schultezub committed
3
// This file is part of the CAMPVis Software Framework.
4
// 
5
// If not explicitly stated otherwise: Copyright (C) 2012-2014, all rights reserved,
schultezub's avatar
schultezub committed
6
//      Christian Schulte zu Berge <christian.szb@in.tum.de>
7
//      Chair for Computer Aided Medical Procedures
8
9
//      Technische Universitaet Muenchen
//      Boltzmannstr. 3, 85748 Garching b. Muenchen, Germany
10
// 
schultezub's avatar
schultezub committed
11
// For a full list of authors and contributors, please refer to the file "AUTHORS.txt".
12
// 
13
14
15
16
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 
// except in compliance with the License. You may obtain a copy of the License at
// 
// http://www.apache.org/licenses/LICENSE-2.0
17
// 
18
19
20
21
// Unless required by applicable law or agreed to in writing, software distributed under the 
// License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 
// either express or implied. See the License for the specific language governing permissions 
// and limitations under the License.
22
23
24
25
26
// 
// ================================================================================================

#include "datacontainerinspectorcanvas.h"

27
28
29
#include "cgt/assert.h"
#include "cgt/shadermanager.h"
#include "cgt/textureunit.h"
30

31
32
#include "core/datastructures/datacontainer.h"
#include "core/datastructures/datahandle.h"
33
#include "core/datastructures/renderdata.h"
34
#include "core/datastructures/imagerepresentationgl.h"
35
#include "core/datastructures/imagerepresentationlocal.h"
36
#include "core/datastructures/facegeometry.h"
37
#include "core/datastructures/geometrydatafactory.h"
38
39
#include "core/classification/tfgeometry1d.h"
#include "core/classification/geometry1dtransferfunction.h"
40

41
42
#include "datacontainerinspectorwidget.h"

43
#include "ext/cgt/navigation/trackball.h"
44

45

schultezub's avatar
schultezub committed
46
namespace campvis {
47
48

    DataContainerInspectorCanvas::DataContainerInspectorCanvas(QWidget* parent /*= 0*/) 
49
        : cgt::QtThreadedCanvas("DataContainer Inspector", cgt::ivec2(640, 480), cgt::GLCanvas::RGBA_BUFFER, parent, true)
50
        , p_currentSlice("CurrentSlice", "Slice", -1, -1, -1)
51
        , p_transferFunction("TransferFunction", "Transfer Function", new Geometry1DTransferFunction(256, cgt::vec2(0.f, 1.f)))
52
53
54
55
        , p_renderRChannel("RenderRChannel", "Render Red Channel", true)
        , p_renderGChannel("RenderGChannel", "Render Green Channel", true)
        , p_renderBChannel("RenderBChannel", "Render Blue Channel", true)
        , p_renderAChannel("RenderAChannel", "Render Alpha Channel", true)
56
57
58
59
60
        , p_geometryRendererProperties("GeometryRendererProperties", "GeometryRenderer Properties")
        , _texturesDirty(true)
        , _dataContainer(nullptr)
        , _paintShader(nullptr)
        , _quad(nullptr)
61
62
        , _numTiles(0, 0)
        , _quadSize(0, 0)
63
        , _localDataContainer("Local DataContainer for DataContainerInspectorCanvas")
64
        , p_viewportSize("ViewportSize", "Viewport Size", cgt::ivec2(200), cgt::ivec2(0, 0), cgt::ivec2(10000))
65
        , _tcp(&p_viewportSize)
66
        , _geometryRenderer(&p_viewportSize)
67
    {
68
        static_cast<Geometry1DTransferFunction*>(p_transferFunction.getTF())->addGeometry(TFGeometry1D::createQuad(cgt::vec2(0.f, 1.f), cgt::col4(0, 0, 0, 255), cgt::col4(255, 255, 255, 255)));
69

70
71
        GLCtxtMgr.registerContextAndInitGlew(this, "DataContainerInspector");
        GLCtxtMgr.releaseContext(this, false);
72
        
73
74
75
76
77
78
        addProperty(p_currentSlice);
        addProperty(p_transferFunction);
        addProperty(p_renderRChannel);
        addProperty(p_renderGChannel);
        addProperty(p_renderBChannel);
        addProperty(p_renderAChannel);
79
80
81
82
83
84

        p_geometryRendererProperties.addPropertyCollection(_geometryRenderer);
        _geometryRenderer.p_geometryID.setVisible(false);
        _geometryRenderer.p_textureID.setVisible(false);
        _geometryRenderer.p_renderTargetID.setVisible(false);
        _geometryRenderer.p_lightId.setVisible(false);
85
        _geometryRenderer.p_camera.setVisible(false);
86
87
88
89
90
91
92
93
94
95
        _geometryRenderer.p_coloringMode.setVisible(false);
        _geometryRenderer.p_pointSize.setVisible(false);
        _geometryRenderer.p_lineWidth.setVisible(false);
        _geometryRenderer.p_showWireframe.setVisible(false);
        _geometryRenderer.p_wireframeColor.setVisible(false);
        _geometryRenderer.p_renderMode.selectByOption(GL_POLYGON);
        _geometryRenderer.p_enableShading.s_changed.connect(this, &DataContainerInspectorCanvas::onGeometryRendererPropertyChanged);
        _geometryRenderer.p_renderMode.s_changed.connect(this, &DataContainerInspectorCanvas::onGeometryRendererPropertyChanged);
        _geometryRenderer.p_solidColor.s_changed.connect(this, &DataContainerInspectorCanvas::onGeometryRendererPropertyChanged);
        addProperty(p_geometryRendererProperties);
96
97
98
99
100
101
102
103

        p_geometryRendererProperties.setVisible(false);
        p_currentSlice.setVisible(false);
        p_transferFunction.setVisible(false);
        p_renderRChannel.setVisible(false);
        p_renderGChannel.setVisible(false);
        p_renderBChannel.setVisible(false);
        p_renderAChannel.setVisible(false);
104
105
106
107
108
109
    }

    DataContainerInspectorCanvas::~DataContainerInspectorCanvas() {

    }

110
    void DataContainerInspectorCanvas::init() {
schultezub's avatar
schultezub committed
111
112
        initAllProperties();

113
        _paintShader = ShdrMgr.load("core/glsl/passthrough.vert", "application/glsl/datacontainerinspector.frag", "");
114
115
        _paintShader->setAttributeLocation(0, "in_Position");
        _paintShader->setAttributeLocation(1, "in_TexCoords");
116
117
        createQuad();

118
        // set this as painter to get notified when window size changes
119
        setPainter(this, false);
120
        getEventHandler()->addEventListenerToFront(this);
121

122
        _geometryRenderer.init();
123
124
125
    }

    void DataContainerInspectorCanvas::deinit() {
schultezub's avatar
schultezub committed
126
127
        deinitAllProperties();

128
129
130
131
        if (_dataContainer != 0) {
            _dataContainer->s_dataAdded.disconnect(this);
        }

132
        _geometryRenderer.deinit();
mostajab's avatar
mostajab committed
133

134
135
        _handles.clear();
        _localDataContainer.clear();
mostajab's avatar
mostajab committed
136
        _textures.clear();
137
        ShdrMgr.dispose(_paintShader);
138
        delete _quad;
139
140

        GLCtxtMgr.removeContext(this);
141
142
143
144
145
146
147
148
    }

    QSize DataContainerInspectorCanvas::sizeHint() const {
        return QSize(640, 480);
    }

    void DataContainerInspectorCanvas::paint() {
        tbb::mutex::scoped_lock lock(_localMutex);
149
        if (_texturesDirty) {
150
            updateTextures();
151
152
153
154
155
156
157
158
        }
        if (_geometriesDirty) {
            // update geometry renderings if necessary
            for (auto it = _geometryNames.begin(); it != _geometryNames.end(); ++it) {
                renderGeometryIntoTexture(it->first, it->second);
            }
            _geometriesDirty = false;
        }
159

160
161
162
163
        if (_textures.empty()) {
            return;
        }

164
        glPushAttrib(GL_ALL_ATTRIB_BITS);
165
        glViewport(0, 0, size_.x, size_.y);
166
167
168
169
170
        glClearColor(0.7f, 0.7f, 0.7f, 1.f);
        glClear(GL_COLOR_BUFFER_BIT);
        LGL_ERROR;

        // update layout dimensions
171
172
        _numTiles.x = ceil(sqrt(static_cast<float>(_textures.size())));
        _numTiles.y = ceil(static_cast<float>(_textures.size()) / _numTiles.x);
173
        _quadSize = size_ / _numTiles;
174
175
176

        _paintShader->activate();

177
        cgt::mat4 projection = cgt::mat4::createOrtho(0, size_.x, 0, size_.y, -1, 1);
178
179
        _paintShader->setUniform("_projectionMatrix", projection);

180
        cgt::TextureUnit tfUnit, unit2d, unit3d;
181
        p_transferFunction.getTF()->bind(_paintShader, tfUnit);
182
183
        _paintShader->setUniform("_texture2d", unit2d.getUnitNumber());
        _paintShader->setUniform("_texture3d", unit3d.getUnitNumber());
184

185
186
187
188
189
        _paintShader->setUniform("_renderRChannel", p_renderRChannel.getValue());
        _paintShader->setUniform("_renderGChannel", p_renderGChannel.getValue());
        _paintShader->setUniform("_renderBChannel", p_renderBChannel.getValue());
        _paintShader->setUniform("_renderAChannel", p_renderAChannel.getValue());

190
191
192
193
194
195
        for (int y = 0; y < _numTiles.y; ++y) {
            for (int x = 0; x < _numTiles.x; ++x) {
                int index = (_numTiles.x * y) + x;
                if (index >= static_cast<int>(_textures.size()))
                    break;

196
                // gather image
197
                cgtAssert(dynamic_cast<const ImageData*>(_textures[index].getData()), "Found sth. else than ImageData in render texture vector. This should not happen!");
198
199
200
201
                const ImageData* id = static_cast<const ImageData*>(_textures[index].getData());

                // compute transformation matrices
                float renderTargetRatio = static_cast<float>(_quadSize.x) / static_cast<float>(_quadSize.y);
202
203
                float sliceRatio = (static_cast<float>(id->getSize().x) * std::abs(id->getMappingInformation().getVoxelSize().x))
                                 / (static_cast<float>(id->getSize().y) * std::abs(id->getMappingInformation().getVoxelSize().y));
204
                float ratioRatio = sliceRatio / renderTargetRatio;
205
                cgt::mat4 viewMatrix = (ratioRatio > 1) ? cgt::mat4::createScale(cgt::vec3(1.f, 1.f / ratioRatio, 1.f)) : cgt::mat4::createScale(cgt::vec3(ratioRatio, 1.f, 1.f));
206

207
208
                cgt::mat4 scaleMatrix = cgt::mat4::createScale(cgt::vec3(_quadSize, 1.f));
                cgt::mat4 translation = cgt::mat4::createTranslation(cgt::vec3(_quadSize.x * x, _quadSize.y * y, 0.f));
209

210
211
212
                _paintShader->setUniform("_modelMatrix", translation * scaleMatrix * viewMatrix);

                // render texture
213
                paintTexture(id->getRepresentation<ImageRepresentationGL>()->getTexture(), unit2d, unit3d);
214
215
216
217
218
219
220
221
            }
        }

        _paintShader->deactivate();
        LGL_ERROR;
        glPopAttrib();
    }

222
223
    void DataContainerInspectorCanvas::paintTexture(const cgt::Texture* texture, const cgt::TextureUnit& unit2d, const cgt::TextureUnit& unit3d) {
        cgtAssert(texture != nullptr, "Texture to paint is 0. This should not happen!");
224
225
        if (texture == nullptr)
            return;
226
227

        _paintShader->setIgnoreUniformLocationError(true);
228
        if (texture->getType() == GL_TEXTURE_2D) {
229
230
231
            unit2d.activate();
            texture->bind();
            _paintShader->setUniform("_is3d", false);
232
            _paintShader->setUniform("_isDepthTexture", texture->isDepthTexture());
233
234
            _paintShader->setUniform("_2dTextureParams._size", cgt::vec2(texture->getDimensions().xy()));
            _paintShader->setUniform("_2dTextureParams._sizeRCP", cgt::vec2(1.f) / cgt::vec2(texture->getDimensions().xy()));
235
            _paintShader->setUniform("_2dTextureParams._numChannels", static_cast<int>(texture->getNumChannels()));
236
        }
237
        else if (texture->getType() == GL_TEXTURE_3D) {
238
239
240
            unit3d.activate();
            texture->bind();
            _paintShader->setUniform("_is3d", true);
241
            _paintShader->setUniform("_sliceNumber", p_currentSlice.getValue());
242
243
            _paintShader->setUniform("_3dTextureParams._size", cgt::vec3(texture->getDimensions()));
            _paintShader->setUniform("_3dTextureParams._sizeRCP", cgt::vec3(1.f) / cgt::vec3(texture->getDimensions()));
244
            _paintShader->setUniform("_3dTextureParams._numChannels", static_cast<int>(texture->getNumChannels()));
245
        }
246
247
        _paintShader->setIgnoreUniformLocationError(false);

248
        _quad->render(GL_TRIANGLE_FAN);
249
250
251
        LGL_ERROR;
    }

252
    void DataContainerInspectorCanvas::invalidate() {
Christian Schulte zu Berge's avatar
Christian Schulte zu Berge committed
253
        // only if inited
254
        if (_quad != 0 && _paintShader != 0) {
255
256
257
258
259
260
            // avoid recursive paints.
            if (! cgt::GlContextManager::getRef().checkWhetherThisThreadHasAcquiredOpenGlContext()) {
                // TODO: check, whether this should be done in an extra thread
                cgt::GLContextScopedLock lock(this);
                paint();
            }
261
        }
262
263
    }

264
    void DataContainerInspectorCanvas::createQuad() {
265
        delete _quad;
266
        _quad = 0;
267
        _quad = GeometryDataFactory::createQuad(cgt::vec3(0.f), cgt::vec3(1.f), cgt::vec3(0.f, 1.f, 0.f), cgt::vec3(1.f, 0.f, 0.f));
268
269
    }

270
271
272
273
    void DataContainerInspectorCanvas::repaint() {
        invalidate();
    }

274
    void DataContainerInspectorCanvas::sizeChanged(const cgt::ivec2& size) {
275
276
277
        invalidate();
    }

278
    void DataContainerInspectorCanvas::mouseMoveEvent(cgt::MouseEvent* e)
279
    {
280
        if (e->modifiers() & cgt::Event::CTRL) {
281
            int texIndx = (e->y() / _quadSize.y) * _numTiles.x + (e->x() / _quadSize.x);
282
            if (texIndx < 0 || texIndx >= static_cast<int>(_textures.size()))
283
284
                return;

285
            const ImageData* id = static_cast<const ImageData*>(_textures[texIndx].getData());
286
            const cgt::Texture* tex = id->getRepresentation<ImageRepresentationGL>()->getTexture();
287
            const ImageRepresentationLocal* localRep = id->getRepresentation<ImageRepresentationLocal>();
288
            cgt::svec2 imageSize = id->getSize().xy();
289

290
            cgt::vec2 lookupTexelFloat = cgt::vec2((e->coord() % _quadSize) * cgt::ivec2(imageSize)) / cgt::vec2(_quadSize);
291
292
293

            // compute transformation matrices
            float renderTargetRatio = static_cast<float>(_quadSize.x) / static_cast<float>(_quadSize.y);
294
295
            float sliceRatio = (static_cast<float>(id->getSize().x) * std::abs(id->getMappingInformation().getVoxelSize().x))
                / (static_cast<float>(id->getSize().y) * std::abs(id->getMappingInformation().getVoxelSize().y));
296
297
            float ratioRatio = sliceRatio / renderTargetRatio;

298
            lookupTexelFloat /= (ratioRatio > 1) ? cgt::vec2(1.f, 1.f / ratioRatio) : cgt::vec2(ratioRatio, 1.f);
299
            
300
            cgt::svec3 lookupTexel(lookupTexelFloat.x, imageSize.y - lookupTexelFloat.y, 0);
301
            if (lookupTexel.x >= 0 && lookupTexel.y >= 0 && lookupTexel.x < imageSize.x && lookupTexel.y < imageSize.y) {
302
303
                if (tex->isDepthTexture()) {
                    emit s_depthChanged(localRep->getElementNormalized(lookupTexel, 0));
304
                }
305
306
307
                else {
                    if (tex->getDimensions().z != 1) {
                        if (p_currentSlice.getValue() >= 0 && p_currentSlice.getValue() < tex->getDimensions().z) {
308
309
310
311
312
313
                            lookupTexel.z = static_cast<size_t>(p_currentSlice.getValue());
                            cgt::vec4 texel(0.f);
                            for (size_t i = 0; i < id->getNumChannels(); ++i) {
                                texel[i] = localRep->getElementNormalized(lookupTexel, i);
                            }
                            emit s_colorChanged(texel);
314
315
316
                        }
                    }
                    else if (tex->getDimensions().y != 1) {
317
318
319
320
321
                        cgt::vec4 texel(0.f);
                        for (size_t i = 0; i < id->getNumChannels(); ++i) {
                            texel[i] = localRep->getElementNormalized(lookupTexel, i);
                        }
                        emit s_colorChanged(texel);
322
                    }
323
                }
324
325
326
327
            }
        }
        else {
            e->ignore();
328
        }
329
330
    }

331
332
    void DataContainerInspectorCanvas::onEvent(cgt::Event* e) {
        cgt::EventListener::onEvent(e);
333
        
334
335
336
        if (!e->isAccepted()) {
            _tcp.onEvent(e);
            _tcp.process(_localDataContainer);
337
338
            e->accept();
            _geometriesDirty = true;
339
340
341
342
            invalidate();
        }
    }

343
344
345
346
347
348
349
350
351
352
353
354
355
    void DataContainerInspectorCanvas::onDataContainerChanged(const QString& key, QtDataHandle dh) {
        {
            tbb::mutex::scoped_lock lock(_localMutex);

            // check whether DataHandle is already existing
            std::map<QString, QtDataHandle>::iterator lb = _handles.lower_bound(key);
            if (lb == _handles.end() || lb->first != key) {
                // not existant -> do nothing
            }
            else {
                // existant -> replace
                lb->second = QtDataHandle(dh);
                // update _textures array
356
                _texturesDirty = true;
357
358
            }
        }
359
360
361

        if (_texturesDirty)
            invalidate();
362
363
364
365
366
367
368
369
370
    }

    void DataContainerInspectorCanvas::setDataHandles(const std::vector< std::pair<QString, QtDataHandle> >& handles) {
        {
            tbb::mutex::scoped_lock lock(_localMutex);
            _handles.clear();
            for (std::vector< std::pair<QString, QtDataHandle> >::const_iterator it = handles.begin(); it != handles.end(); ++it)
                _handles.insert(*it);

371
372
373
374
375
376
377
378
379
380
381
382
383
384
            _localDataContainer.clear();
            _geometryNames.clear();

            // use LightSourceProvider processor to create lighting information.
            // This is needed to be done once after the local DataContainer got cleared.
            LightSourceProvider lsp;
            lsp.init();
            lsp.invalidate(AbstractProcessor::INVALID_RESULT);
            lsp.process(_localDataContainer);
            lsp.deinit();

            // reset trackball
            resetTrackball();

385
            _texturesDirty = true;
386
387
388
389
390
391
        }

        invalidate();
    }

    void DataContainerInspectorCanvas::updateTextures() {
392
        /// Calculate the maximum slices of the textures and fill the textures array
393
        int maxSlices = 1;
394
        _textures.clear();
395
        p_viewportSize.setValue(cgt::ivec2(width(), height()));
396

397
398
        for (std::map<QString, QtDataHandle>::iterator it = _handles.begin(); it != _handles.end(); ++it) {
            if (const ImageData* img = dynamic_cast<const ImageData*>(it->second.getData())) {
399
                if (const ImageRepresentationGL* imgGL = img->getRepresentation<ImageRepresentationGL>()) {
400
                    _textures.push_back(it->second);
401
                    maxSlices = std::max(maxSlices, imgGL->getTexture()->getDimensions().z);
402
403
                }
            }
404
405
406
            else if (const RenderData* rd = dynamic_cast<const RenderData*>(it->second.getData())) {
                for (size_t i = 0; i < rd->getNumColorTextures(); ++i) {
                    const ImageRepresentationGL* imgGL = rd->getColorTexture(i)->getRepresentation<ImageRepresentationGL>();
407
                    if (imgGL) {
408
                        _textures.push_back(rd->getColorDataHandle(i));
409
                    }
410
411
412
                }
                if (rd->hasDepthTexture()) {
                    const ImageRepresentationGL* imgGL = rd->getDepthTexture()->getRepresentation<ImageRepresentationGL>();
413
                    if (imgGL) {
414
                        _textures.push_back(rd->getDepthDataHandle());
415
                    }
416
417
418

                }
            }
419
420
            else if (const GeometryData* gd = dynamic_cast<const GeometryData*>(it->second.getData())) {
                std::string name = it->first.toStdString();
421
422

                // copy geometry over to local 
423
                _localDataContainer.addDataHandle(name + ".geometry", it->second);
424
425
426
427
428
429

                // render
                renderGeometryIntoTexture(name);

                // store name
                _geometryNames.push_back(std::make_pair(name, static_cast<int>(_textures.size()) - 1));
430
            }
431
        }
432
433
434

        if (maxSlices == 1)
            maxSlices = -1;
435
        p_currentSlice.setMaxValue(maxSlices - 1);
436
        _texturesDirty = false;
437
        _geometriesDirty = false;
438
439
    }

440

441
    void DataContainerInspectorCanvas::onPropertyChanged(const AbstractProperty* prop) {
442
        // ignore properties of the geometry renderer
443
444
        if (prop != &p_geometryRendererProperties)
            invalidate();
445
446
    }

447
    void DataContainerInspectorCanvas::onGeometryRendererPropertyChanged(const AbstractProperty* prop) {
448
        _geometriesDirty = true;
449
        invalidate();
450
    }
451

452
453
    void DataContainerInspectorCanvas::renderGeometryIntoTexture(const std::string& name, int textureIndex) {
        // setup GeometryRenderer
454
455
456
        _geometryRenderer.p_geometryID.setValue(name + ".geometry");
        _geometryRenderer.p_renderTargetID.setValue(name + ".rendered");
        _geometryRenderer.validate(AbstractProcessor::INVALID_PROPERTIES);
457
        _geometryRenderer.invalidate(AbstractProcessor::INVALID_RESULT);
458
        _geometryRenderer.process(_localDataContainer);
459
460
461
462
463
464
465
466
467
468
469
470
471
472

        // grab render result texture from local DataContainer and push into texture vector.
        ScopedTypedData<RenderData> rd(_localDataContainer, name + ".rendered");
        if (rd != nullptr && rd->getNumColorTextures() > 0) {
            auto rep = rd->getColorTexture(0)->getRepresentation<ImageRepresentationGL>();
            if (rep != nullptr) {
                if (textureIndex < 0 || textureIndex >= static_cast<int>(_textures.size())) {
                    _textures.push_back(rd->getColorDataHandle(0));
                }
                else {
                    _textures[textureIndex] = rd->getColorDataHandle(0);
                }
            }
            else {
473
                cgtAssert(false, "The rendered geometry does not have an OpenGL representation. Something went terribly wrong.");
474
475
476
            }
        }
        else {
477
            cgtAssert(false, "The rendered geometry does exist. Something went wrong.");
478
479
        }
    }
480
    
481
482
    void DataContainerInspectorCanvas::resetTrackball() {
        // check whether we have to render geometries
483
        cgt::Bounds unionBounds;
484
485
486
487
488
489
490
491
        for (std::map<QString, QtDataHandle>::iterator it = _handles.begin(); it != _handles.end(); ++it) {
            if (const GeometryData* gd = dynamic_cast<const GeometryData*>(it->second.getData())) {
                unionBounds.addVolume(gd->getWorldBounds());
            }
        }

        // if so, create a new trackball
        if (unionBounds.isDefined()) {
492
            _tcp.reinitializeCamera(unionBounds);
493
        }
494
495
496

        _tcp.invalidate(AbstractProcessor::INVALID_RESULT);
        _tcp.process(_localDataContainer);
497
    }
498

Sebastian Pölsterl's avatar
Sebastian Pölsterl committed
499
}