Beispiel #1
0
ModelLayer*
Map::getModelLayerByUID( UID layerUID ) const
{
    Threading::ScopedReadLock( const_cast<Map*>(this)->_mapDataMutex );
    for( ModelLayerVector::const_iterator i = _modelLayers.begin(); i != _modelLayers.end(); ++i )
        if ( i->get()->getUID() == layerUID )
            return i->get();
    return 0L;
}
Beispiel #2
0
ModelLayer*
Map::getModelLayerByName( const std::string& name ) const
{
    Threading::ScopedReadLock( const_cast<Map*>(this)->_mapDataMutex );
    for( ModelLayerVector::const_iterator i = _modelLayers.begin(); i != _modelLayers.end(); ++i )
        if ( i->get()->getName() == name )
            return i->get();
    return 0L;
}
Config
EarthFileSerializer2::serialize( MapNode* input ) const
{
    Config mapConf("map");
    mapConf.set("version", "2");

    if ( !input || !input->getMap() )
        return mapConf;

    Map* map = input->getMap();
    MapFrame mapf( map, Map::ENTIRE_MODEL );

    // the map and node options:
    Config optionsConf = map->getInitialMapOptions().getConfig();
    optionsConf.merge( input->getMapNodeOptions().getConfig() );
    mapConf.add( "options", optionsConf );

    // the layers
    for( ImageLayerVector::const_iterator i = mapf.imageLayers().begin(); i != mapf.imageLayers().end(); ++i )
    {
        ImageLayer* layer = i->get();
        //Config layerConf = layer->getInitialOptions().getConfig();
        Config layerConf = layer->getImageLayerOptions().getConfig();
        layerConf.set("name", layer->getName());
        layerConf.set("driver", layer->getInitialOptions().driver()->getDriver());        
        mapConf.add( "image", layerConf );
    }

    for( ElevationLayerVector::const_iterator i = mapf.elevationLayers().begin(); i != mapf.elevationLayers().end(); ++i )
    {
        ElevationLayer* layer = i->get();
        //Config layerConf = layer->getInitialOptions().getConfig();
        Config layerConf = layer->getElevationLayerOptions().getConfig();
        layerConf.set("name", layer->getName());
        layerConf.set("driver", layer->getInitialOptions().driver()->getDriver());        
        mapConf.add( "elevation", layerConf );
    }

    for( ModelLayerVector::const_iterator i = mapf.modelLayers().begin(); i != mapf.modelLayers().end(); ++i )
    {
        ModelLayer* layer = i->get();
        Config layerConf = layer->getModelLayerOptions().getConfig();
        layerConf.set("name", layer->getName());
        layerConf.set("driver", layer->getModelLayerOptions().driver()->getDriver());
        mapConf.add( "model", layerConf );
    }

    Config ext = input->externalConfig();
    if ( !ext.empty() )
    {
        ext.key() = "external";
        mapConf.add( ext );
    }

    return mapConf;
}
Beispiel #4
0
Revision
Map::getModelLayers( ModelLayerVector& out_list ) const
{
    out_list.reserve( _modelLayers.size() );

    Threading::ScopedReadLock lock( const_cast<Map*>(this)->_mapDataMutex );
    for( ModelLayerVector::const_iterator i = _modelLayers.begin(); i != _modelLayers.end(); ++i )
        out_list.push_back( i->get() );

    return _dataModelRevision;
}
Beispiel #5
0
void
ElevationQuery::gatherPatchLayers()
{
    // cache a vector of terrain patch models.
    _patchLayers.clear();
    for(ModelLayerVector::const_iterator i = _mapf.modelLayers().begin();
        i != _mapf.modelLayers().end();
        ++i)
    {
        if ( i->get()->isTerrainPatch() )
            _patchLayers.push_back( i->get() );
    }
}
Beispiel #6
0
void
MapNode::init()
{
    // Take a reference to this object so that it doesn't get inadvertently
    // deleting during startup. It is possible that during startup, a driver
    // will load that will take a reference to the MapNode (like in a
    // ModelSource node operation) and we don't want that deleting the MapNode
    // out from under us. 
    // This is paired by an unref_nodelete() at the end of this method.
    this->ref();

    // Protect the MapNode from the Optimizer
    setDataVariance(osg::Object::DYNAMIC);

    // initialize 0Ls
    _terrainEngine          = 0L;
    _terrainEngineContainer = 0L;
    _overlayDecorator       = 0L;

    setName( "osgEarth::MapNode" );

    // Since we have global uniforms in the stateset, mark it dynamic so it is immune to
    // multi-threaded overlap
    // TODO: do we need this anymore? there are no more global uniforms in here.. gw
    getOrCreateStateSet()->setDataVariance(osg::Object::DYNAMIC);

    _modelLayerCallback = new MapModelLayerCallback(this);

    _maskLayerNode = 0L;
    _lastNumBlacklistedFilenames = 0;

    // Set the global proxy settings
    // TODO: this should probably happen elsewhere, like in the registry?
    if ( _mapNodeOptions.proxySettings().isSet() )
    {
        HTTPClient::setProxySettings( _mapNodeOptions.proxySettings().get() );
    }

    // establish global driver options. These are OSG reader-writer options that
    // will make their way to any read* calls down the pipe
    const osgDB::Options* global_options = _map->getGlobalOptions();

    osg::ref_ptr<osgDB::Options> local_options = global_options ? 
        Registry::instance()->cloneOrCreateOptions( global_options ) :
        NULL;

    if ( local_options.valid() )
    {
        OE_INFO << LC
            << "Options string = " 
            << (local_options.valid()? local_options->getOptionString() : "<empty>")
            << std::endl;
    }

    // TODO: not sure why we call this here
    _map->setGlobalOptions( local_options.get() );

    // load and attach the terrain engine, but don't initialize it until we need it
    const TerrainOptions& terrainOptions = _mapNodeOptions.getTerrainOptions();

    _terrainEngine = TerrainEngineNodeFactory::create( _map.get(), terrainOptions );
    _terrainEngineInitialized = false;

    // the engine needs a container so we can set lighting state on the container and
    // not on the terrain engine itself. Setting the dynamic variance will prevent
    // an optimizer from collapsing the empty group node.
    _terrainEngineContainer = new osg::Group();
    _terrainEngineContainer->setDataVariance( osg::Object::DYNAMIC );
    this->addChild( _terrainEngineContainer );

    // initialize terrain-level lighting:
    if ( terrainOptions.enableLighting().isSet() )
    {
        _terrainEngineContainer->getOrCreateStateSet()->setMode( 
            GL_LIGHTING, 
            terrainOptions.enableLighting().value() ? 1 : 0 );
    }

    if ( _terrainEngine )
    {
        // inform the terrain engine of the map information now so that it can properly
        // initialize it's CoordinateSystemNode. This is necessary in order to support
        // manipulators and to set up the texture compositor prior to frame-loop 
        // initialization.
        _terrainEngine->preInitialize( _map.get(), terrainOptions );
        _terrainEngineContainer->addChild( _terrainEngine );
    }
    else
    {
        OE_WARN << "FAILED to create a terrain engine for this map" << std::endl;
    }

    // make a group for the model layers.
    // NOTE: for now, we are going to nullify any shader programs that occur above the model
    // group, since it does not YET support shader composition. Programs defined INSIDE a
    // model layer will still work OK though.
    _models = new osg::Group();
    _models->setName( "osgEarth::MapNode.modelsGroup" );
    addChild( _models.get() );

    // make a group for overlay model layers:
    _overlayModels = new ObserverGroup(); //osg::Group();
    _overlayModels->setName( "osgEarth::MapNode.overlayModelsGroup" );

    // a decorator for overlay models:
    _overlayDecorator = new OverlayDecorator();
    _overlayDecorator->setOverlayGraphTraversalMask( terrainOptions.secondaryTraversalMask().value() );

    if ( _mapNodeOptions.overlayBlending().isSet() )
        _overlayDecorator->setOverlayBlending( *_mapNodeOptions.overlayBlending() );
    if ( _mapNodeOptions.overlayTextureSize().isSet() )
        _overlayDecorator->setTextureSize( *_mapNodeOptions.overlayTextureSize() );
    if ( _mapNodeOptions.overlayMipMapping().isSet() )
        _overlayDecorator->setMipMapping( *_mapNodeOptions.overlayMipMapping() );

    addTerrainDecorator( _overlayDecorator );

    // install any pre-existing model layers:
    ModelLayerVector modelLayers;
    _map->getModelLayers( modelLayers );
    int modelLayerIndex = 0;
    for( ModelLayerVector::const_iterator k = modelLayers.begin(); k != modelLayers.end(); k++, modelLayerIndex++ )
    {
        onModelLayerAdded( k->get(), modelLayerIndex );
    }

    _mapCallback = new MapNodeMapCallbackProxy(this);
    // install a layer callback for processing further map actions:
    _map->addMapCallback( _mapCallback.get()  );

    osg::StateSet* ss = getOrCreateStateSet();

    if ( _mapNodeOptions.enableLighting().isSet() )
    {
        ss->setMode( 
            GL_LIGHTING, 
            _mapNodeOptions.enableLighting().value() ? 1 : 0 );
    }

    dirtyBound();

    // Install top-level shader programs:
    if ( Registry::capabilities().supportsGLSL() )
    {
        VirtualProgram* vp = new VirtualProgram();
        vp->setName( "MapNode" );
        vp->installDefaultColoringAndLightingShaders();
        ss->setAttributeAndModes( vp, osg::StateAttribute::ON );
    }

    // register for event traversals so we can deal with blacklisted filenames
    ADJUST_EVENT_TRAV_COUNT( this, 1 );

    // remove the temporary reference.
    this->unref_nodelete();
}
Beispiel #7
0
Config
EarthFileSerializer2::serialize(const MapNode* input, const std::string& referrer) const
{
    Config mapConf("map");
    mapConf.set("version", "2");

    if ( !input || !input->getMap() )
        return mapConf; 

    const Map* map = input->getMap();
    MapFrame mapf( map, Map::ENTIRE_MODEL );

    // the map and node options:
    Config optionsConf = map->getInitialMapOptions().getConfig();
    optionsConf.merge( input->getMapNodeOptions().getConfig() );
    mapConf.add( "options", optionsConf );

    // the layers
    for( ImageLayerVector::const_iterator i = mapf.imageLayers().begin(); i != mapf.imageLayers().end(); ++i )
    {
        ImageLayer* layer = i->get();
        //Config layerConf = layer->getInitialOptions().getConfig();
        Config layerConf = layer->getImageLayerOptions().getConfig();
        layerConf.set("name", layer->getName());
        layerConf.set("driver", layer->getInitialOptions().driver()->getDriver());        
        mapConf.add( "image", layerConf );
    }

    for( ElevationLayerVector::const_iterator i = mapf.elevationLayers().begin(); i != mapf.elevationLayers().end(); ++i )
    {
        ElevationLayer* layer = i->get();
        //Config layerConf = layer->getInitialOptions().getConfig();
        Config layerConf = layer->getElevationLayerOptions().getConfig();
        layerConf.set("name", layer->getName());
        layerConf.set("driver", layer->getInitialOptions().driver()->getDriver());        
        mapConf.add( "elevation", layerConf );
    }

    for( ModelLayerVector::const_iterator i = mapf.modelLayers().begin(); i != mapf.modelLayers().end(); ++i )
    {
        ModelLayer* layer = i->get();
        Config layerConf = layer->getModelLayerOptions().getConfig();
        layerConf.set("name", layer->getName());
        layerConf.set("driver", layer->getModelLayerOptions().driver()->getDriver());
        mapConf.add( "model", layerConf );
    }

    Config ext = input->externalConfig();
    if ( !ext.empty() )
    {
        ext.key() = "extensions";
        mapConf.add( ext );
    }

#if 1 // removed until it can be debugged.
    // Re-write pathnames in the Config so they are relative to the new referrer.
    if ( _rewritePaths && !referrer.empty() )
    {
        RewritePaths rewritePaths( referrer );
        rewritePaths.setRewriteAbsolutePaths( _rewriteAbsolutePaths );
        rewritePaths.apply( mapConf );
    }
#endif

    return mapConf;
}
Beispiel #8
0
void
MapNode::init()
{
    // Take a reference to this object so that it doesn't get inadvertently
    // deleting during startup. It is possible that during startup, a driver
    // will load that will take a reference to the MapNode (like in a
    // ModelSource node operation) and we don't want that deleting the MapNode
    // out from under us. 
    // This is paired by an unref_nodelete() at the end of this method.
    this->ref();

    // Protect the MapNode from the Optimizer
    setDataVariance(osg::Object::DYNAMIC);

    // Protect the MapNode from the ShaderGenerator
    ShaderGenerator::setIgnoreHint(this, true);

    // initialize 0Ls
    _terrainEngine          = 0L;
    _terrainEngineContainer = 0L;
    _overlayDecorator       = 0L;

    setName( "osgEarth::MapNode" );

    _maskLayerNode = 0L;
    _lastNumBlacklistedFilenames = 0;

    // Set the global proxy settings
    // TODO: this should probably happen elsewhere, like in the registry?
    if ( _mapNodeOptions.proxySettings().isSet() )
    {
        HTTPClient::setProxySettings( _mapNodeOptions.proxySettings().get() );
    }

    // establish global driver options. These are OSG reader-writer options that
    // will make their way to any read* calls down the pipe
    const osgDB::Options* global_options = _map->getGlobalOptions();

    osg::ref_ptr<osgDB::Options> local_options = global_options ? 
        Registry::instance()->cloneOrCreateOptions( global_options ) :
        NULL;

    if ( local_options.valid() )
    {
        OE_INFO << LC
            << "Options string = " 
            << (local_options.valid()? local_options->getOptionString() : "<empty>")
            << std::endl;
    }

    // TODO: not sure why we call this here
    _map->setGlobalOptions( local_options.get() );

    // load and attach the terrain engine, but don't initialize it until we need it
    const TerrainOptions& terrainOptions = _mapNodeOptions.getTerrainOptions();

    _terrainEngine = TerrainEngineNodeFactory::create( _map.get(), terrainOptions );
    _terrainEngineInitialized = false;

    // the engine needs a container so we can set lighting state on the container and
    // not on the terrain engine itself. Setting the dynamic variance will prevent
    // an optimizer from collapsing the empty group node.
    _terrainEngineContainer = new osg::Group();
    _terrainEngineContainer->setDataVariance( osg::Object::DYNAMIC );
    this->addChild( _terrainEngineContainer );

    // initialize terrain-level lighting:
    if ( terrainOptions.enableLighting().isSet() )
    {
        _terrainEngineContainer->getOrCreateStateSet()->addUniform(
            Registry::shaderFactory()->createUniformForGLMode(GL_LIGHTING, *terrainOptions.enableLighting()) );

        _terrainEngineContainer->getOrCreateStateSet()->setMode( 
            GL_LIGHTING, 
            terrainOptions.enableLighting().value() ? 1 : 0 );
    }

    if ( _terrainEngine )
    {
        // inform the terrain engine of the map information now so that it can properly
        // initialize it's CoordinateSystemNode. This is necessary in order to support
        // manipulators and to set up the texture compositor prior to frame-loop 
        // initialization.
        _terrainEngine->preInitialize( _map.get(), terrainOptions );
        _terrainEngineContainer->addChild( _terrainEngine );
    }
    else
    {
        OE_WARN << "FAILED to create a terrain engine for this map" << std::endl;
    }

    // make a group for the model layers.
    // NOTE: for now, we are going to nullify any shader programs that occur above the model
    // group, since it does not YET support shader composition. Programs defined INSIDE a
    // model layer will still work OK though.
    _models = new osg::Group();
    _models->setName( "osgEarth::MapNode.modelsGroup" );
    _models->getOrCreateStateSet()->setRenderBinDetails(2, "RenderBin");
    addChild( _models.get() );

    // a decorator for overlay models:
    _overlayDecorator = new OverlayDecorator();

    // install the Draping technique for overlays:
    {
        DrapingTechnique* draping = new DrapingTechnique();

        const char* envOverlayTextureSize = ::getenv("OSGEARTH_OVERLAY_TEXTURE_SIZE");

        if ( _mapNodeOptions.overlayBlending().isSet() )
            draping->setOverlayBlending( *_mapNodeOptions.overlayBlending() );
        if ( envOverlayTextureSize )
            draping->setTextureSize( as<int>(envOverlayTextureSize, 1024) );
        else if ( _mapNodeOptions.overlayTextureSize().isSet() )
            draping->setTextureSize( *_mapNodeOptions.overlayTextureSize() );
        if ( _mapNodeOptions.overlayMipMapping().isSet() )
            draping->setMipMapping( *_mapNodeOptions.overlayMipMapping() );
        if ( _mapNodeOptions.overlayAttachStencil().isSet() )
            draping->setAttachStencil( *_mapNodeOptions.overlayAttachStencil() );
        if ( _mapNodeOptions.overlayResolutionRatio().isSet() )
            draping->setResolutionRatio( *_mapNodeOptions.overlayResolutionRatio() );

        _overlayDecorator->addTechnique( draping );
    }

    // install the Clamping technique for overlays:
    {
        _overlayDecorator->addTechnique( new ClampingTechnique() );
    }


    addTerrainDecorator( _overlayDecorator );

    // install any pre-existing model layers:
    ModelLayerVector modelLayers;
    _map->getModelLayers( modelLayers );
    int modelLayerIndex = 0;
    for( ModelLayerVector::const_iterator k = modelLayers.begin(); k != modelLayers.end(); k++, modelLayerIndex++ )
    {
        onModelLayerAdded( k->get(), modelLayerIndex );
    }

    _mapCallback = new MapNodeMapCallbackProxy(this);
    // install a layer callback for processing further map actions:
    _map->addMapCallback( _mapCallback.get()  );

    osg::StateSet* stateset = getOrCreateStateSet();

    if ( _mapNodeOptions.enableLighting().isSet() )
    {
        stateset->addUniform(Registry::shaderFactory()->createUniformForGLMode(
            GL_LIGHTING, 
            _mapNodeOptions.enableLighting().value() ? 1 : 0));

        stateset->setMode( 
            GL_LIGHTING, 
            _mapNodeOptions.enableLighting().value() ? 1 : 0);
    }

    // Add in some global uniforms
    stateset->addUniform( new osg::Uniform("oe_isGeocentric", _map->isGeocentric()) );
    if ( _map->isGeocentric() )
    {
        OE_INFO << LC << "Adding ellipsoid uniforms.\n";

        // for a geocentric map, use an ellipsoid unit-frame transform and its inverse:
        osg::Vec3d ellipFrameInverse(
            _map->getSRS()->getEllipsoid()->getRadiusEquator(),
            _map->getSRS()->getEllipsoid()->getRadiusEquator(),
            _map->getSRS()->getEllipsoid()->getRadiusPolar());
        stateset->addUniform( new osg::Uniform("oe_ellipsoidFrameInverse", osg::Vec3f(ellipFrameInverse)) );

        osg::Vec3d ellipFrame = osg::componentDivide(osg::Vec3d(1.0,1.0,1.0), ellipFrameInverse);
        stateset->addUniform( new osg::Uniform("oe_ellipsoidFrame", osg::Vec3f(ellipFrame)) );
    }

    // install the default rendermode uniform:
    stateset->addUniform( new osg::Uniform("oe_isPickCamera", false) );

    dirtyBound();

    // register for event traversals so we can deal with blacklisted filenames
    ADJUST_EVENT_TRAV_COUNT( this, 1 );

    // remove the temporary reference.
    this->unref_nodelete();
}
Beispiel #9
0
void
MapNode::init()
{
    // Protect the MapNode from the Optimizer
    setDataVariance(osg::Object::DYNAMIC);

    setName( "osgEarth::MapNode" );

    // Since we have global uniforms in the stateset, mark it dynamic so it is immune to
    // multi-threaded overlap
    // TODO: do we need this anymore? there are no more global uniforms in here.. gw
    getOrCreateStateSet()->setDataVariance(osg::Object::DYNAMIC);

    _modelLayerCallback = new MapModelLayerCallback(this);

    _maskLayerNode = 0L;
    _lastNumBlacklistedFilenames = 0;

    // Set the global proxy settings
    // TODO: this should probably happen elsewhere, like in the registry?
    if ( _mapNodeOptions.proxySettings().isSet() )
    {
        HTTPClient::setProxySettings( _mapNodeOptions.proxySettings().get() );
    }

    // establish global driver options. These are OSG reader-writer options that
    // will make their way to any read* calls down the pipe
    const osgDB::ReaderWriter::Options* global_options = _map->getGlobalOptions();
    osg::ref_ptr<osgDB::ReaderWriter::Options> local_options = global_options ?
            new osgDB::ReaderWriter::Options( *global_options ) :
            NULL;

    if ( local_options.valid() )
    {
        OE_INFO << LC
                << "Options string = "
                << (local_options.valid()? local_options->getOptionString() : "<empty>")
                << std::endl;
    }

    // TODO: not sure why we call this here
    _map->setGlobalOptions( local_options.get() );

    // overlays:
    _pendingOverlayAutoSetTextureUnit = true;

    // load and attach the terrain engine, but don't initialize it until we need it
    const TerrainOptions& terrainOptions = _mapNodeOptions.getTerrainOptions();

    _terrainEngine = TerrainEngineNodeFactory::create( _map.get(), terrainOptions );
    _terrainEngineInitialized = false;

    // the engine needs a container so we can set lighting state on the container and
    // not on the terrain engine itself. Setting the dynamic variance will prevent
    // an optimizer from collapsing the empty group node.
    _terrainEngineContainer = new osg::Group();
    _terrainEngineContainer->setDataVariance( osg::Object::DYNAMIC );
    this->addChild( _terrainEngineContainer.get() );

    // initialize terrain-level lighting:
    if ( terrainOptions.enableLighting().isSet() )
    {
        _terrainEngineContainer->getOrCreateStateSet()->setMode( GL_LIGHTING, terrainOptions.enableLighting().value() ?
                osg::StateAttribute::ON | osg::StateAttribute::PROTECTED :
                osg::StateAttribute::OFF | osg::StateAttribute::PROTECTED );
    }

    if ( _terrainEngine.valid() )
    {
        // inform the terrain engine of the map information now so that it can properly
        // initialize it's CoordinateSystemNode. This is necessary in order to support
        // manipulators and to set up the texture compositor prior to frame-loop
        // initialization.
        _terrainEngine->preInitialize( _map.get(), terrainOptions );

        _terrainEngineContainer->addChild( _terrainEngine.get() );
    }
    else
        OE_WARN << "FAILED to create a terrain engine for this map" << std::endl;

    // make a group for the model layers:
    _models = new osg::Group();
    _models->setName( "osgEarth::MapNode.modelsGroup" );
    addChild( _models.get() );

    // make a group for overlay model layers:
    _overlayModels = new osg::Group();
    _overlayModels->setName( "osgEarth::MapNode.overlayModelsGroup" );

    // a decorator for overlay models:
    _overlayDecorator = new OverlayDecorator();
    if ( _mapNodeOptions.overlayVertexWarping().isSet() )
        _overlayDecorator->setVertexWarping( *_mapNodeOptions.overlayVertexWarping() );
    if ( _mapNodeOptions.overlayBlending().isSet() )
        _overlayDecorator->setOverlayBlending( *_mapNodeOptions.overlayBlending() );
    if ( _mapNodeOptions.overlayTextureSize().isSet() )
        _overlayDecorator->setTextureSize( *_mapNodeOptions.overlayTextureSize() );
    if ( _mapNodeOptions.overlayMipMapping().isSet() )
        _overlayDecorator->setMipMapping( *_mapNodeOptions.overlayMipMapping() );
    addTerrainDecorator( _overlayDecorator.get() );

    // install any pre-existing model layers:
    ModelLayerVector modelLayers;
    _map->getModelLayers( modelLayers );
    int modelLayerIndex = 0;
    for( ModelLayerVector::const_iterator k = modelLayers.begin(); k != modelLayers.end(); k++, modelLayerIndex++ )
    {
        onModelLayerAdded( k->get(), modelLayerIndex );
    }

    _mapCallback = new MapNodeMapCallbackProxy(this);
    // install a layer callback for processing further map actions:
    _map->addMapCallback( _mapCallback.get()  );

    osg::StateSet* ss = getOrCreateStateSet();
    //ss->setAttributeAndModes( new osg::CullFace() ); //, osg::StateAttribute::ON);
    //ss->setAttributeAndModes( new osg::PolygonOffset( -1, -1 ) );

    if ( _mapNodeOptions.enableLighting().isSet() )
    {
        ss->setMode( GL_LIGHTING, _mapNodeOptions.enableLighting().value() ?
                     osg::StateAttribute::ON | osg::StateAttribute::PROTECTED :
                     osg::StateAttribute::OFF | osg::StateAttribute::PROTECTED );
    }

    dirtyBound();

    // register for event traversals so we can deal with blacklisted filenames
    ADJUST_EVENT_TRAV_COUNT( this, 1 );
}