osgEarth 2.1.1
Public Member Functions | Protected Member Functions | Private Member Functions | Private Attributes

osgEarth::Features::FeatureModelGraph Class Reference

Collaboration diagram for osgEarth::Features::FeatureModelGraph:

List of all members.

Public Member Functions

 FeatureModelGraph (FeatureSource *source, const FeatureModelSourceOptions &options, FeatureNodeFactory *factory, Session *session)
osg::Node * load (unsigned lod, unsigned tileX, unsigned tileY, const std::string &uri)
StyleSheetgetStyles ()
void setStyles (StyleSheet *styles)
void dirty ()
virtual void traverse (osg::NodeVisitor &nv)

Protected Member Functions

virtual ~FeatureModelGraph ()
void setupPaging ()
osg::Group * build (const FeatureLevel &level, const GeoExtent &extent, const TileKey *key)
osg::Group * build (const Style &baseStyle, const Query &baseQuery, const GeoExtent &extent)

Private Member Functions

osg::Group * createNodeForStyle (const Style &style, const Query &query)
osg::BoundingSphered getBoundInWorldCoords (const GeoExtent &extent, const MapFrame *mapf) const
void buildSubTilePagedLODs (unsigned lod, unsigned tileX, unsigned tileY, const MapFrame *mapFrame, osg::Group *parent)
void redraw ()

Private Attributes

FeatureModelSourceOptions _options
osg::ref_ptr< FeatureSource_source
osg::ref_ptr< FeatureNodeFactory_factory
osg::ref_ptr< Session_session
UID _uid
std::set< std::string > _blacklist
Threading::ReadWriteMutex _blacklistMutex
GeoExtent _usableFeatureExtent
bool _featureExtentClamped
GeoExtent _usableMapExtent
osg::BoundingSphered _fullWorldBound
bool _useTiledSource
osgEarth::Revision _revision
bool _dirty
std::vector< const FeatureLevel * > _lodmap

Detailed Description

A graph that gets its contents from a FeatureNodeFactory. This class will handle all the internals of selecting features, gridding feature data if required, and sorting features based on style. Then for each cell and each style, it will invoke the FeatureNodeFactory to create the actual data for each set.

Definition at line 43 of file FeatureModelGraph.


Constructor & Destructor Documentation

FeatureModelGraph::FeatureModelGraph ( FeatureSource source,
const FeatureModelSourceOptions options,
FeatureNodeFactory factory,
Session session 
)

Constructs a new model graph.

Parameters:
sourceSource from which to read feature data
optionsModel source options
factoryNode factory that will be invoked to compile feature data into nodes
sessionSession under which to create elements in this graph

Definition at line 151 of file FeatureModelGraph.cpp.

                                                                               :
_source   ( source ),
_options  ( options ),
_factory  ( factory ),
_session  ( session ),
_dirty    ( false )
{
    _uid = osgEarthFeatureModelPseudoLoader::registerGraph( this );

    // install the stylesheet in the session if it doesn't already have one.
    if ( !session->styles() )
        session->setStyles( _options.styles().get() );

    // initialize lighting on the graph, if necessary.
    osg::StateSet* stateSet = getOrCreateStateSet();

    if ( _options.enableLighting().isSet() )
        stateSet->setMode( GL_LIGHTING, *_options.enableLighting() ? 1 : 0 );
    
    // Calculate the usable extent (in both feature and map coordinates) and bounds.
    const Profile* mapProfile = session->getMapInfo().getProfile();

    // the part of the feature extent that will fit on the map (in map coords):
    _usableMapExtent = mapProfile->clampAndTransformExtent( 
        _source->getFeatureProfile()->getExtent(), 
        &_featureExtentClamped );

    // same, back into feature coords:
    _usableFeatureExtent = _usableMapExtent.transform( _source->getFeatureProfile()->getSRS() );

    // world-space bounds of the feature layer
    _fullWorldBound = getBoundInWorldCoords( _usableMapExtent, 0L );

    // whether to request tiles from the source (if available). if the source is tiled, but the
    // user manually specified schema levels, don't use the tiles.
    _useTiledSource = _source->getFeatureProfile()->getTiled();

    if ( options.levels().isSet() && options.levels()->getNumLevels() > 0 )
    {
        // the user provided a custom levels setup, so don't use the tiled source (which
        // provides its own levels setup)
        _useTiledSource = false;

        // for each custom level, calculate the best LOD match and store it in the level
        // layout data. We will use this information later when constructing the SG in
        // the pager.
        for( unsigned i = 0; i < options.levels()->getNumLevels(); ++i )
        {
            const FeatureLevel* level = options.levels()->getLevel( i );
            unsigned lod = options.levels()->chooseLOD( *level, _fullWorldBound.radius() );
            _lodmap.resize( lod+1, 0L );
            _lodmap[lod] = level;

            OE_INFO << LC << source->getName() 
                << ": F.Level max=" << level->maxRange() << ", min=" << level->minRange()
                << ", LOD=" << lod << std::endl;
        }
    }

    setNumChildrenRequiringUpdateTraversal( 1 );

    redraw();
}

Here is the call graph for this function:

FeatureModelGraph::~FeatureModelGraph ( ) [protected, virtual]

Definition at line 218 of file FeatureModelGraph.cpp.

Here is the call graph for this function:


Member Function Documentation

osg::Group * FeatureModelGraph::build ( const FeatureLevel level,
const GeoExtent extent,
const TileKey key 
) [protected]

Definition at line 462 of file FeatureModelGraph.cpp.

{
    osg::ref_ptr<osg::Group> group = new osg::Group();

    // form the baseline query, which does a spatial query based on the working extent.
    Query query;
    if ( extent.isValid() )
        query.bounds() = extent.bounds();

    // add a tile key to the query if there is one, to support TFS-style queries
    if ( key )
        query.tileKey() = *key;

    // now, go through any level-based selectors.
    const StyleSelectorVector& levelSelectors = level.selectors();
    
    // if there are none, just build once with the default style and query.
    if ( levelSelectors.size() == 0 )
    {
        // attempt to glean the style from the feature source name:
        const Style style = *_session->styles()->getStyle( *_source->getFeatureSourceOptions().name() );

        osg::Node* node = build( style, query, extent );
        if ( node )
            group->addChild( node );
    }

    else
    {
        for( StyleSelectorVector::const_iterator i = levelSelectors.begin(); i != levelSelectors.end(); ++i )
        {
            const StyleSelector& selector = *i;

            // fetch the selector's style:
            const Style* selectorStyle = _session->styles()->getStyle( selector.getSelectedStyleName() );

            // combine the selector's query, if it has one:
            Query selectorQuery = 
                selector.query().isSet() ? query.combineWith( *selector.query() ) : query;

            osg::Node* node = build( *selectorStyle, selectorQuery, extent );
            if ( node )
                group->addChild( node );
        }
    }

    if ( group->getNumChildren() > 0 )
    {
        // account for a min-range here.
        if ( level.minRange() > 0.0f )
        {
            osg::LOD* lod = new osg::LOD();
            lod->addChild( group.get(), level.minRange(), FLT_MAX );
            group = lod;
        }

        if ( _session->getMapInfo().isGeocentric() && _options.clusterCulling() == true )
        {
            const GeoExtent& ccExtent = extent.isValid() ? extent : _source->getFeatureProfile()->getExtent();
            if ( ccExtent.isValid() )
            {
                // if the extent is more than 90 degrees, bail
                GeoExtent geodeticExtent = ccExtent.transform( ccExtent.getSRS()->getGeographicSRS() );
                if ( geodeticExtent.width() < 90.0 && geodeticExtent.height() < 90.0 )
                {
#if 1
                    // get the geocentric tile center:
                    osg::Vec3d tileCenter;
                    ccExtent.getCentroid( tileCenter.x(), tileCenter.y() );
                    osg::Vec3d centerECEF;
                    ccExtent.getSRS()->transformToECEF( tileCenter, centerECEF );

                    osg::NodeCallback* ccc = ClusterCullerFactory::create( group.get(), centerECEF );
                    if ( ccc )
                        group->addCullCallback( ccc );
#endif
                }
            }
        }

        return group.release();
    }

    else
    {
        return 0L;
    }
}

Here is the call graph for this function:

Here is the caller graph for this function:

osg::Group * FeatureModelGraph::build ( const Style baseStyle,
const Query baseQuery,
const GeoExtent extent 
) [protected]

Definition at line 552 of file FeatureModelGraph.cpp.

{
    osg::ref_ptr<osg::Group> group = new osg::Group();

    if ( _source->hasEmbeddedStyles() )
    {
        const FeatureProfile* profile = _source->getFeatureProfile();

        // each feature has its own style, so use that and ignore the style catalog.
        osg::ref_ptr<FeatureCursor> cursor = _source->createFeatureCursor( baseQuery );
        while( cursor->hasMore() )
        {
            Feature* feature = cursor->nextFeature();
            if ( feature )
            {
                FeatureList list;
                list.push_back( feature );
                osg::ref_ptr<FeatureCursor> cursor = new FeatureListCursor(list);

                FilterContext context( _session.get(), _source->getFeatureProfile(), workingExtent );

                // note: gridding is not supported for embedded styles.
                osg::ref_ptr<osg::Node> node;

                // Get the Group that parents all features of this particular style. Note, this
                // might be NULL if the factory does not support style groups.
                osg::Group* styleGroup = _factory->getOrCreateStyleGroup(*feature->style(), _session.get());
                if ( styleGroup )
                {
                    if ( !group->containsNode( styleGroup ) )
                        group->addChild( styleGroup );
                }

                if ( _factory->createOrUpdateNode( cursor.get(), *feature->style(), context, node ) )
                {
                    if ( node.valid() )
                    {
                        if ( styleGroup )
                            styleGroup->addChild( node.get() );
                        else
                            group->addChild( node.get() );
                    }
                }
            }
        }
    }

    else
    {
        const StyleSheet* styles = _session->styles();

        // if we have selectors, sort the features into style groups and create a node for each group.
        if ( styles->selectors().size() > 0 )
        {
            for( StyleSelectorList::const_iterator i = styles->selectors().begin(); i != styles->selectors().end(); ++i )
            {
                // pull the selected style...
                const StyleSelector& sel = *i;

                // combine the selection style with the incoming base style:
                Style selectedStyle = *styles->getStyle( sel.getSelectedStyleName() );
                Style combinedStyle = baseStyle.combineWith( selectedStyle );

                // .. and merge it's query into the existing query
                Query combinedQuery = baseQuery.combineWith( *sel.query() );

                // then create the node.
                osg::Group* styleGroup = createNodeForStyle( combinedStyle, combinedQuery );
                if ( styleGroup && !group->containsNode(styleGroup) )
                    group->addChild( styleGroup );
            }
        }

        // otherwise, render all the features with a single style
        else
        {
            Style combinedStyle = baseStyle;

            // if there's no base style defined, choose a "default" style from the stylesheet.
            if ( baseStyle.empty() )
                combinedStyle = *styles->getDefaultStyle();

            osg::Group* styleGroup = createNodeForStyle( combinedStyle, baseQuery );
            if ( styleGroup && !group->containsNode(styleGroup) )
                group->addChild( styleGroup );
        }
    }

    return group->getNumChildren() > 0 ? group.release() : 0L;
}

Here is the call graph for this function:

void FeatureModelGraph::buildSubTilePagedLODs ( unsigned  lod,
unsigned  tileX,
unsigned  tileY,
const MapFrame mapFrame,
osg::Group *  parent 
) [private]

Definition at line 412 of file FeatureModelGraph.cpp.

{
    unsigned subtileLOD = parentLOD + 1;
    unsigned subtileX = parentTileX * 2;
    unsigned subtileY = parentTileY * 2;

    // make a paged LOD for each subtile:
    for( unsigned u = subtileX; u <= subtileX + 1; ++u )
    {
        for( unsigned v = subtileY; v <= subtileY + 1; ++v )
        {
            GeoExtent subtileFeatureExtent = s_getTileExtent( subtileLOD, u, v, _usableFeatureExtent );
            osg::BoundingSphered subtile_bs = getBoundInWorldCoords( subtileFeatureExtent, mapf );

            // Camera range for the PLODs. This should always be sufficient because
            // the max range of a FeatureLevel below this will, by definition, have a max range
            // less than or equal to this number -- based on how the LODs were chosen in 
            // setupPaging.
            float maxRange = subtile_bs.radius() * _options.levels()->tileSizeFactor().value();

            std::string uri = s_makeURI( _uid, subtileLOD, u, v );

            // check the blacklist to make sure we haven't unsuccessfully tried
            // this URI before
            bool blacklisted = false;
            {
                Threading::ScopedReadLock sharedLock( _blacklistMutex );
                blacklisted = _blacklist.find( uri ) != _blacklist.end();
            }

            if ( !blacklisted )
            {
                OE_DEBUG << LC << "    " << uri
                    << std::fixed
                    << "; center = " << subtile_bs.center().x() << "," << subtile_bs.center().y() << "," << subtile_bs.center().z()
                    << "; radius = " << subtile_bs.radius()
                    << std::endl;

                osg::Group* pagedNode = createPagedNode( subtile_bs, uri, 0.0f, maxRange );
                parent->addChild( pagedNode );
            }
        }
    }
}

Here is the call graph for this function:

Here is the caller graph for this function:

osg::Group * FeatureModelGraph::createNodeForStyle ( const Style style,
const Query query 
) [private]

Definition at line 644 of file FeatureModelGraph.cpp.

{
    osg::Group* styleGroup = 0L;

    // the profile of the features
    const FeatureProfile* profile = _source->getFeatureProfile();

    // get the extent of the full set of feature data:
    const GeoExtent& extent = profile->getExtent();
    
    // query the feature source:
    osg::ref_ptr<FeatureCursor> cursor = _source->createFeatureCursor( query );

    if ( cursor->hasMore() )
    {
        Bounds cellBounds =
            query.bounds().isSet() ? *query.bounds() : extent.bounds();

        FilterContext context( _session.get(), profile, GeoExtent(profile->getSRS(), cellBounds) );

        // start by culling our feature list to the working extent. By default, this is done by
        // checking feature centroids. But the user can override this to crop feature geometry to
        // the cell boundaries.
        FeatureList workingSet;
        cursor->fill( workingSet );

        CropFilter crop( 
            _options.levels().isSet() && _options.levels()->cropFeatures() == true ? 
            CropFilter::METHOD_CROPPING : CropFilter::METHOD_CENTROID );
        context = crop.push( workingSet, context );

        // next, if the usable extent is less than the full extent (i.e. we had to clamp the feature
        // extent to fit on the map), calculate the extent of the features in this tile and 
        // crop to the map extent if necessary. (Note, if cropFeatures was set to true, this is
        // already done)
        if ( _featureExtentClamped && _options.levels().isSet() && _options.levels()->cropFeatures() == false )
        {
            context.extent() = _usableFeatureExtent;
            CropFilter crop2( CropFilter::METHOD_CROPPING );
            context = crop2.push( workingSet, context );
        }

        if ( workingSet.size() > 0 )
        {
            // next ask the implementation to construct OSG geometry for the cell features.
            osg::ref_ptr<osg::Node> node;

            osg::ref_ptr<FeatureCursor> newCursor = new FeatureListCursor(workingSet);

            if ( _factory->createOrUpdateNode( newCursor.get(), style, context, node ) )
            {
                if ( !styleGroup )
                    styleGroup = _factory->getOrCreateStyleGroup( style, _session.get() );

                // if it returned a node, add it. (it doesn't necessarily have to)
                if ( node.valid() )
                    styleGroup->addChild( node.get() );
            }
        }

        CacheStats stats = context.resourceCache()->getSkinStats();
        OE_DEBUG << LC << "Resource Cache skins: "
            << " num=" << stats._entries << ", max=" << stats._maxEntries
            << ", queries=" << stats._queries << ", hits=" << (100.0f*stats._hitRatio) << "%"
            << std::endl;

    }


    return styleGroup;
}

Here is the call graph for this function:

Here is the caller graph for this function:

void FeatureModelGraph::dirty ( )

Definition at line 224 of file FeatureModelGraph.cpp.

{
    _dirty = true;
}

Here is the caller graph for this function:

osg::BoundingSphered FeatureModelGraph::getBoundInWorldCoords ( const GeoExtent extent,
const MapFrame mapf 
) const [private]

Definition at line 230 of file FeatureModelGraph.cpp.

{
    osg::Vec3d center, corner;
    double z = 0.0;
    GeoExtent workingExtent;

    if ( extent.getSRS()->isEquivalentTo( _usableMapExtent.getSRS() ) )
    {
        workingExtent = extent;
    }
    else
    {
        workingExtent = extent.transform( _usableMapExtent.getSRS() ); // safe.
    }

    workingExtent.getCentroid( center.x(), center.y() );
    
    if ( mapf )
    {
        // note: use the lowest possible resolution to speed up queries
        ElevationQuery query( *mapf );
        query.getElevation( center, mapf->getProfile()->getSRS(), center.z(), DBL_MAX );
    }

    corner.x() = workingExtent.xMin();
    corner.y() = workingExtent.yMin();
    corner.z() = z;

    if ( _session->getMapInfo().isGeocentric() )
    {
        workingExtent.getSRS()->transformToECEF( center, center );
        workingExtent.getSRS()->transformToECEF( corner, corner );
    }

    return osg::BoundingSphered( center, (center-corner).length() );
}

Here is the call graph for this function:

Here is the caller graph for this function:

StyleSheet* osgEarth::Features::FeatureModelGraph::getStyles ( ) [inline]

Definition at line 69 of file FeatureModelGraph.

{ return _session->styles(); }
osg::Node * FeatureModelGraph::load ( unsigned  lod,
unsigned  tileX,
unsigned  tileY,
const std::string &  uri 
)

Loads and returns a subnode. Used internally for paging.

Definition at line 287 of file FeatureModelGraph.cpp.

{
    OE_DEBUG << LC
        << "load: " << lod << "_" << tileX << "_" << tileY << std::endl;

    osg::Group* result = 0L;
    
    if ( _useTiledSource )
    {        
        // A "tiled" source has a pre-generted tile hierarchy, but no range information.
        // We will be calculating the LOD ranges here.

        // The extent of this tile:
        GeoExtent tileExtent = s_getTileExtent( lod, tileX, tileY, _usableFeatureExtent );

        // Calculate the bounds of this new tile:
        MapFrame mapf = _session->createMapFrame();
        osg::BoundingSphered tileBound = getBoundInWorldCoords( tileExtent, &mapf );

        // Apply the tile range multiplier to calculate a max camera range. The max range is
        // the geographic radius of the tile times the multiplier.
        float tileFactor = _options.levels().isSet() ? _options.levels()->tileSizeFactor().get() : 15.0f;
        double maxRange =  tileBound.radius() * tileFactor;
        FeatureLevel level( 0, maxRange );
        
        // Construct a tile key that will be used to query the source for this tile.
        TileKey key(lod, tileX, tileY, _source->getFeatureProfile()->getProfile());
        osg::Group* geometry = build( level, tileExtent, &key );
        result = geometry;

        if (lod < _source->getFeatureProfile()->getMaxLevel())
        {
            // see if there are any more levels. If so, build some pagedlods to bring the
            // next level in.
            FeatureLevel nextLevel(0, maxRange/2.0);

            osg::ref_ptr<osg::Group> group = new osg::Group();

            // calculate the LOD of the next level:
            if ( lod+1 != ~0 )
            {
                MapFrame mapf = _session->createMapFrame();
                buildSubTilePagedLODs( lod, tileX, tileY, &mapf, group.get() );

                // slap the geometry in there afterwards, if there is any
                if ( geometry )
                    group->addChild( geometry );

                result = group.release();
            }   
        }
    }

    else if ( !_options.levels().isSet() || _options.levels()->getNumLevels() == 0 )
    {
        // This is a non-tiled data source that has NO level details. In this case, 
        // we simply want to load all features at once and make them visible at
        // maximum camera range.
        FeatureLevel all( 0.0f, FLT_MAX );
        result = build( all, GeoExtent::INVALID, 0 );
    }

    else if ( lod < _lodmap.size() )
    {
        // This path computes the SG for a model graph with explicity-defined levels of
        // detail. We already calculated the LOD level map in setupPaging(). If the
        // current LOD points to an actual FeatureLevel, we build the geometry for that
        // level in the tile.

        osg::Group* geometry = 0L;
        const FeatureLevel* level = _lodmap[lod];
        if ( level )
        {
            // There exists a real data level at this LOD. So build the geometry that will
            // represent this tile.
            GeoExtent tileExtent = 
                lod > 0 ?
                s_getTileExtent( lod, tileX, tileY, _usableFeatureExtent ) :
                _usableFeatureExtent;

            geometry = build( *level, tileExtent, 0 );
            result = geometry;
        }

        if ( lod < _lodmap.size()-1 )
        {
            // There are more populated levels below this one. So build the subtile
            // PagedLODs that will load them.
            osg::ref_ptr<osg::Group> group = new osg::Group();

            MapFrame mapf = _session->createMapFrame();
            buildSubTilePagedLODs( lod, tileX, tileY, &mapf, group.get() );

            if ( geometry )
                group->addChild( geometry );

            result = group.release();
        }
    }

    if ( !result )
    {
        // If the read resulting in nothing, create an empty group so that the read
        // (technically) succeeds and the pager won't try to load the null child
        // over and over.
        result = new osg::Group();
    }
    else
    {
        RemoveEmptyGroupsVisitor::run( result );
    }

    if ( result->getNumChildren() == 0 )
    {
        // if the result group contains no data, blacklist it so we never try to load it again.
        Threading::ScopedWriteLock exclusiveLock( _blacklistMutex );
        _blacklist.insert( uri );
        OE_DEBUG << LC << "Blacklisting: " << uri << std::endl;
    }

    return result;
}

Here is the call graph for this function:

void FeatureModelGraph::redraw ( ) [private]

Definition at line 730 of file FeatureModelGraph.cpp.

{
    removeChildren( 0, getNumChildren() );
    // if there's a display schema in place, set up for quadtree paging.
    if ( _options.levels().isSet() || _useTiledSource ) //_source->getFeatureProfile()->getTiled() )
    {
        setupPaging();
    }
    else
    {
        FeatureLevel defaultLevel( 0.0f, FLT_MAX );
        
        //Remove all current children        
        osg::Node* node = build( defaultLevel, GeoExtent::INVALID, 0 );
        if ( node )
            addChild( node );
    }

    _source->sync( _revision );
    _dirty = false;
}

Here is the call graph for this function:

Here is the caller graph for this function:

void FeatureModelGraph::setStyles ( StyleSheet styles)

Definition at line 753 of file FeatureModelGraph.cpp.

{
    _session->setStyles( styles );
    dirty();
}

Here is the call graph for this function:

void FeatureModelGraph::setupPaging ( ) [protected]

Definition at line 269 of file FeatureModelGraph.cpp.

{
    // calculate the bounds of the full data extent:
    MapFrame mapf = _session->createMapFrame();
    osg::BoundingSphered bs = getBoundInWorldCoords( _usableMapExtent, &mapf );

    // calculate the max range for the top-level PLOD:
    float maxRange = bs.radius() * _options.levels()->tileSizeFactor().value();

    // build the URI for the top-level paged LOD:
    std::string uri = s_makeURI( _uid, 0, 0, 0 );

    // bulid the top level Paged LOD:
    osg::Group* pagedNode = createPagedNode( bs, uri, 0.0f, maxRange );
    this->addChild( pagedNode );
}

Here is the call graph for this function:

Here is the caller graph for this function:

void FeatureModelGraph::traverse ( osg::NodeVisitor &  nv) [virtual]

Definition at line 717 of file FeatureModelGraph.cpp.

{
    if ( nv.getVisitorType() == osg::NodeVisitor::UPDATE_VISITOR)
    {
        if (_source->outOfSyncWith(_revision) || _dirty)
        {
            redraw();
        }
    }
    osg::Group::traverse(nv);
}

Here is the call graph for this function:


Member Data Documentation

std::set<std::string> osgEarth::Features::FeatureModelGraph::_blacklist [private]

Definition at line 105 of file FeatureModelGraph.

Definition at line 106 of file FeatureModelGraph.

Definition at line 113 of file FeatureModelGraph.

Definition at line 102 of file FeatureModelGraph.

Definition at line 108 of file FeatureModelGraph.

Definition at line 110 of file FeatureModelGraph.

Definition at line 114 of file FeatureModelGraph.

Definition at line 100 of file FeatureModelGraph.

Definition at line 112 of file FeatureModelGraph.

Definition at line 103 of file FeatureModelGraph.

Definition at line 101 of file FeatureModelGraph.

Definition at line 104 of file FeatureModelGraph.

Definition at line 107 of file FeatureModelGraph.

Definition at line 109 of file FeatureModelGraph.

Definition at line 111 of file FeatureModelGraph.


The documentation for this class was generated from the following files:
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Defines