コード例 #1
0
      void compute()
      {
          //Tell the calculator about the new start/end points
          _profileCalculator->setStartEnd( GeoPoint(_mapNode->getMapSRS(), _start.x(), _start.y(), 0),
                                           GeoPoint(_mapNode->getMapSRS(), _end.x(), _end.y(), 0));

          if (_featureNode.valid())
          {
              _root->removeChild( _featureNode.get() );
              _featureNode = 0;
          }

          LineString* line = new LineString();
          line->push_back( _start );
          line->push_back( _end );
          Feature* feature = new Feature(line, _mapNode->getMapSRS());
          feature->geoInterp() = GEOINTERP_GREAT_CIRCLE;    

          //Define a style for the line
          Style style;
          LineSymbol* ls = style.getOrCreateSymbol<LineSymbol>();
          ls->stroke()->color() = Color::Yellow;
          ls->stroke()->width() = 2.0f;
          ls->tessellation() = 20;

          style.getOrCreate<AltitudeSymbol>()->clamping() = AltitudeSymbol::CLAMP_TO_TERRAIN;

          feature->style() = style;

          _featureNode = new FeatureNode( _mapNode, feature );
          //Disable lighting
          _featureNode->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF);
          _root->addChild( _featureNode.get() );

      }
コード例 #2
0
TEST(TileCover, DISABLED_FuzzLine) {
    while(1)
    {
        std::srand (time(NULL));
        std::size_t len = std::rand() % 10000 + 3;
        MultiLineString<double> mls;

        std::size_t num_lines = 1;
        num_lines += std::rand() % 5;
        while (num_lines > 0) {
            LineString<double> line;
            for (std::size_t i = 0; i < len; ++i) {
                double x = std::rand() % 180;
                double y = std::rand() % 90;

                line.push_back({x,y});
            }
            mls.emplace_back(line);
            --num_lines;
        }
        
        std::clog << ".";
        util::TileCover tc(mls, 5);
        while(tc.next()) {
        };
    }
}
コード例 #3
0
ファイル: Geometry.cpp プロジェクト: 3dcl/osgearth
Geometry*
Ring::cloneAs( const Geometry::Type& newType ) const
{
    if ( newType == TYPE_LINESTRING )
    {
        LineString* line = new LineString( &this->asVector() );
        if ( line->size() > 1 && line->front() != line->back() )
            line->push_back( front() );
        return line;
    }
    else return Geometry::cloneAs( newType );
}
コード例 #4
0
std::unordered_map<std::string, std::vector<Feature>> Source::Impl::queryRenderedFeatures(const QueryParameters& parameters) const {
    std::unordered_map<std::string, std::vector<Feature>> result;
    if (renderTiles.empty() || parameters.geometry.empty()) {
        return result;
    }

    LineString<double> queryGeometry;

    for (const auto& p : parameters.geometry) {
        queryGeometry.push_back(TileCoordinate::fromScreenCoordinate(
            parameters.transformState, 0, { p.x, parameters.transformState.getSize().height - p.y }).p);
    }

    mapbox::geometry::box<double> box = mapbox::geometry::envelope(queryGeometry);


    auto sortRenderTiles = [](const RenderTile& a, const RenderTile& b) {
        return a.id.canonical.z != b.id.canonical.z ? a.id.canonical.z < b.id.canonical.z :
               a.id.canonical.y != b.id.canonical.y ? a.id.canonical.y < b.id.canonical.y :
               a.id.wrap != b.id.wrap ? a.id.wrap < b.id.wrap : a.id.canonical.x < b.id.canonical.x;
    };
    std::vector<std::reference_wrapper<const RenderTile>> sortedTiles;
    std::transform(renderTiles.cbegin(), renderTiles.cend(), std::back_inserter(sortedTiles),
                   [](const auto& pair) { return std::ref(pair.second); });
    std::sort(sortedTiles.begin(), sortedTiles.end(), sortRenderTiles);

    for (const auto& renderTileRef : sortedTiles) {
        const RenderTile& renderTile = renderTileRef.get();
        GeometryCoordinate tileSpaceBoundsMin = TileCoordinate::toGeometryCoordinate(renderTile.id, box.min);
        if (tileSpaceBoundsMin.x >= util::EXTENT || tileSpaceBoundsMin.y >= util::EXTENT) {
            continue;
        }

        GeometryCoordinate tileSpaceBoundsMax = TileCoordinate::toGeometryCoordinate(renderTile.id, box.max);
        if (tileSpaceBoundsMax.x < 0 || tileSpaceBoundsMax.y < 0) {
            continue;
        }

        GeometryCoordinates tileSpaceQueryGeometry;
        tileSpaceQueryGeometry.reserve(queryGeometry.size());
        for (const auto& c : queryGeometry) {
            tileSpaceQueryGeometry.push_back(TileCoordinate::toGeometryCoordinate(renderTile.id, c));
        }

        renderTile.tile.queryRenderedFeatures(result,
                                              tileSpaceQueryGeometry,
                                              parameters.transformState,
                                              parameters.layerIDs);
    }

    return result;
}
コード例 #5
0
osg::Group*
UTMData::buildGZDTile(const std::string& name, const GeoExtent& extent, const Style& style, const FeatureProfile* featureProfile, const Map* map)
{
    osg::Group* group = new osg::Group();

    Style lineStyle;
    lineStyle.add( const_cast<LineSymbol*>(style.get<LineSymbol>()) );
    lineStyle.add( const_cast<AltitudeSymbol*>(style.get<AltitudeSymbol>()) );

    bool hasText = style.get<TextSymbol>() != 0L;

    GeometryCompiler compiler;
    osg::ref_ptr<Session> session = new Session(map);
    FilterContext context( session.get(), featureProfile, extent );

    // make sure we get sufficient tessellation:
    compiler.options().maxGranularity() = 1.0;

    FeatureList features;

    // longitudinal line:
    LineString* lon = new LineString(2);
    lon->push_back( osg::Vec3d(extent.xMin(), extent.yMax(), 0) );
    lon->push_back( osg::Vec3d(extent.xMin(), extent.yMin(), 0) );
    Feature* lonFeature = new Feature(lon, extent.getSRS());
    lonFeature->geoInterp() = GEOINTERP_GREAT_CIRCLE;
    features.push_back( lonFeature );

    // latitudinal line:
    LineString* lat = new LineString(2);
    lat->push_back( osg::Vec3d(extent.xMin(), extent.yMin(), 0) );
    lat->push_back( osg::Vec3d(extent.xMax(), extent.yMin(), 0) );
    Feature* latFeature = new Feature(lat, extent.getSRS());
    latFeature->geoInterp() = GEOINTERP_RHUMB_LINE;
    features.push_back( latFeature );

    // top lat line at 84N
    if ( extent.yMax() == 84.0 )
    {
        LineString* lat = new LineString(2);
        lat->push_back( osg::Vec3d(extent.xMin(), extent.yMax(), 0) );
        lat->push_back( osg::Vec3d(extent.xMax(), extent.yMax(), 0) );
        Feature* latFeature = new Feature(lat, extent.getSRS());
        latFeature->geoInterp() = GEOINTERP_RHUMB_LINE;
        features.push_back( latFeature );
    }

    osg::Node* geomNode = compiler.compile(features, lineStyle, context);
    if ( geomNode ) 
        group->addChild( geomNode );

    // get the geocentric tile center:
    osg::Vec3d tileCenter;
    extent.getCentroid( tileCenter.x(), tileCenter.y() );

    const SpatialReference* ecefSRS = extent.getSRS()->getGeocentricSRS();
    
    osg::Vec3d centerECEF;
    extent.getSRS()->transform( tileCenter, ecefSRS, centerECEF );

    if ( hasText )
    {
        osg::Vec3d west, east;
        extent.getSRS()->transform( osg::Vec3d(extent.xMin(),tileCenter.y(),0), ecefSRS, west );
        extent.getSRS()->transform( osg::Vec3d(extent.xMax(),tileCenter.y(),0), ecefSRS, east );

        const TextSymbol* textSym_in = style.get<TextSymbol>();
        osg::ref_ptr<TextSymbol> textSym = textSym_in ? new TextSymbol(*textSym_in) : new TextSymbol();
        textSym->size() = (west-east).length() / 3.0;

        TextSymbolizer ts(textSym.get());
        
        osg::Geode* textGeode = new osg::Geode();        
        osg::Drawable* d = ts.create(name);
        d->getOrCreateStateSet()->setRenderBinToInherit();
        textGeode->addDrawable(d);
        Registry::shaderGenerator().run(textGeode, Registry::stateSetCache());

        osg::Matrixd centerL2W;
        ecefSRS->createLocalToWorld( centerECEF, centerL2W );
        osg::MatrixTransform* mt = new osg::MatrixTransform(centerL2W);
        mt->addChild(textGeode);
       
        group->addChild(mt);
    }

    //group = buildGZDChildren( group, name );

    group = ClusterCullingFactory::createAndInstall( group, centerECEF )->asGroup();

    return group;
}
コード例 #6
0
ファイル: UTMGraticule.cpp プロジェクト: chuckshaw/osgearth
osg::Node*
UTMGraticule::buildGZDTile( const std::string& name, const GeoExtent& extent )
{
    osg::Group* group = new osg::Group();

    Style lineStyle;
    lineStyle.add( _options->primaryStyle()->get<LineSymbol>() );
    lineStyle.add( _options->primaryStyle()->get<AltitudeSymbol>() );

    //const Style& lineStyle = *_options->lineStyle();
    //Style textStyle = *_options->textStyle();

    bool hasText = _options->primaryStyle()->get<TextSymbol>() != 0L;

    GeometryCompiler compiler;
    osg::ref_ptr<Session> session = new Session( _mapNode->getMap() );
    FilterContext context( session.get(), _featureProfile.get(), extent );

    // make sure we get sufficient tessellation:
    compiler.options().maxGranularity() = 1.0;

    FeatureList features;

    // longitudinal line:
    LineString* lon = new LineString(2);
    lon->push_back( osg::Vec3d(extent.xMin(), extent.yMax(), 0) );
    lon->push_back( osg::Vec3d(extent.xMin(), extent.yMin(), 0) );
    Feature* lonFeature = new Feature(lon, extent.getSRS());
    lonFeature->geoInterp() = GEOINTERP_GREAT_CIRCLE;
    features.push_back( lonFeature );

    // latitudinal line:
    LineString* lat = new LineString(2);
    lat->push_back( osg::Vec3d(extent.xMin(), extent.yMin(), 0) );
    lat->push_back( osg::Vec3d(extent.xMax(), extent.yMin(), 0) );
    Feature* latFeature = new Feature(lat, extent.getSRS());
    latFeature->geoInterp() = GEOINTERP_RHUMB_LINE;
    features.push_back( latFeature );

    // top lat line at 84N
    if ( extent.yMax() == 84.0 )
    {
        LineString* lat = new LineString(2);
        lat->push_back( osg::Vec3d(extent.xMin(), extent.yMax(), 0) );
        lat->push_back( osg::Vec3d(extent.xMax(), extent.yMax(), 0) );
        Feature* latFeature = new Feature(lat, extent.getSRS());
        latFeature->geoInterp() = GEOINTERP_RHUMB_LINE;
        features.push_back( latFeature );
    }

    osg::Node* geomNode = compiler.compile(features, lineStyle, context);
    if ( geomNode ) 
        group->addChild( geomNode );

    // get the geocentric tile center:
    osg::Vec3d tileCenter;
    extent.getCentroid( tileCenter.x(), tileCenter.y() );
    
    osg::Vec3d centerECEF;
    extent.getSRS()->transformToECEF( tileCenter, centerECEF );

    if ( hasText )
    {
        osg::Vec3d west, east;
        extent.getSRS()->transformToECEF(osg::Vec3d(extent.xMin(),tileCenter.y(),0), west );
        extent.getSRS()->transformToECEF(osg::Vec3d(extent.xMax(),tileCenter.y(),0), east );

        TextSymbol* textSym = _options->primaryStyle()->getOrCreate<TextSymbol>();
        textSym->size() = (west-east).length() / 3.0;

        TextSymbolizer ts( textSym );
        
        osg::Geode* textGeode = new osg::Geode();
        textGeode->getOrCreateStateSet()->setRenderBinDetails( 9998, "DepthSortedBin" );   
        textGeode->getOrCreateStateSet()->setAttributeAndModes( _depthAttribute, 1 );
        
        osg::Drawable* d = ts.create(name);
        d->getOrCreateStateSet()->setRenderBinToInherit();

        textGeode->addDrawable(d);
        osg::MatrixTransform* mt = new osg::MatrixTransform(ECEF::createLocalToWorld(centerECEF));
        mt->addChild(textGeode);
       
        group->addChild(mt);
    }

    group = buildGZDChildren( group, name );

    group = ClusterCullingFactory::createAndInstall( group, centerECEF )->asGroup();

    return group;
}
コード例 #7
0
//
// NOTE: run this sample from the repo/tests directory.
//
int main(int argc, char** argv)
{
    osg::ArgumentParser arguments(&argc,argv);

    osgViewer::Viewer viewer(arguments);
    s_viewer = &viewer;

    // Start by creating the map:
    s_mapNode = MapNode::load(arguments);
    if ( !s_mapNode )
    {
        Map* map = new Map();

        // Start with a basemap imagery layer; we'll be using the GDAL driver
        // to load a local GeoTIFF file:
        GDALOptions basemapOpt;
        basemapOpt.url() = "../data/world.tif";
        map->addImageLayer( new ImageLayer( ImageLayerOptions("basemap", basemapOpt) ) );

        // That's it, the map is ready; now create a MapNode to render the Map:
        MapNodeOptions mapNodeOptions;
        mapNodeOptions.enableLighting() = false;

        s_mapNode = new MapNode( map, mapNodeOptions );
    }
    s_mapNode->setNodeMask( 0x01 );

        
    // Define a style for the feature data. Since we are going to render the
    // vectors as lines, configure the line symbolizer:
    StyleSheet* styleSheet = buildStyleSheet( Color::Yellow, 2.0f );

    s_source = new FeatureListSource();

    LineString* line = new LineString();
    line->push_back( osg::Vec3d(-60, 20, 0) );
    line->push_back( osg::Vec3d(-120, 20, 0) );
    line->push_back( osg::Vec3d(-120, 60, 0) );
    line->push_back( osg::Vec3d(-60, 60, 0) );
    Feature *feature = new Feature(line, s_mapNode->getMapSRS(), Style(), s_fid++);
    s_source->insertFeature( feature );
    s_activeFeature = feature;
  
    s_root = new osg::Group;
    s_root->addChild( s_mapNode.get() );

    Session* session = new Session(s_mapNode->getMap(), styleSheet, s_source.get());

    FeatureModelGraph* graph = new FeatureModelGraph( 
        session,
        FeatureModelSourceOptions(), 
        new GeomFeatureNodeFactory() );

    graph->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF);
    graph->getOrCreateStateSet()->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF);

    s_root->addChild( graph );

    //Setup the controls
    ControlCanvas* canvas = ControlCanvas::get( &viewer );
    s_root->addChild( canvas );
    Grid *toolbar = createToolBar( );
    canvas->addControl( toolbar );
    canvas->setNodeMask( 0x1 << 1 );



    int col = 0;
    LabelControl* addVerts = new LabelControl("Add Verts");
    toolbar->setControl(col++, 0, addVerts );    
    addVerts->addEventHandler( new AddVertsModeHandler( graph ));
    
    LabelControl* edit = new LabelControl("Edit");
    toolbar->setControl(col++, 0, edit );    
    edit->addEventHandler(new EditModeHandler( graph ));

    unsigned int row = 0;
    Grid *styleBar = createToolBar( );
    styleBar->setPosition(0, 50);
    canvas->addControl( styleBar );
    
    //Make a list of styles
    styleBar->setControl(0, row++, new LabelControl("Styles") );    

    unsigned int numStyles = 8;
    for (unsigned int i = 0; i < numStyles; ++i)
    {
        float w = 50;
        osg::Vec4 color = randomColor();

        float widths[3] = {2, 4, 8};

        unsigned int r = row++;
        for (unsigned int j = 0; j < 3; j++) 
        {
            Control* l = new Control();            
            l->setBackColor( color );
            l->addEventHandler(new ChangeStyleHandler(graph, buildStyleSheet( color, widths[j] ) ));
            l->setSize(w,5 * widths[j]);
            styleBar->setControl(j, r, l);
        }
    }
   
    
    viewer.setSceneData( s_root.get() );
    viewer.setCameraManipulator( new EarthManipulator() );

    if ( s_mapNode )
        viewer.getCamera()->addCullCallback( new osgEarth::Util::AutoClipPlaneCullCallback(s_mapNode) );

    // add some stock OSG handlers:
    viewer.addEventHandler(new osgViewer::StatsHandler());
    viewer.addEventHandler(new osgViewer::WindowSizeHandler());
    viewer.addEventHandler(new osgGA::StateSetManipulator(viewer.getCamera()->getOrCreateStateSet()));

    return viewer.run();
}
コード例 #8
0
osg::Node*
GeodeticGraticule::buildTile( const TileKey& key, Map* map ) const
{
    if ( _options->levels().size() <= key.getLevelOfDetail() )
    {
        OE_WARN << LC << "Tried to create cell at non-existant level " << key.getLevelOfDetail() << std::endl;
        return 0L;
    }

    const GeodeticGraticuleOptions::Level& level = _options->levels()[key.getLevelOfDetail()]; //_levels[key.getLevelOfDetail()];


    // the "-2" here is because normal tile paging gives you one subdivision already,
    // so we only need to account for > 1 subdivision factor.
    unsigned cellsPerTile = level._subdivisionFactor <= 2u ? 1u : 1u << (level._subdivisionFactor-2u);
    unsigned cellsPerTileX = std::max(1u, cellsPerTile);
    unsigned cellsPerTileY = std::max(1u, cellsPerTile);


    GeoExtent tileExtent = key.getExtent();

    FeatureList latLines;
    FeatureList lonLines;

    static LatLongFormatter s_llf(LatLongFormatter::FORMAT_DECIMAL_DEGREES);

    double cellWidth = tileExtent.width() / cellsPerTileX;
    double cellHeight = tileExtent.height() / cellsPerTileY;

    const Style& lineStyle = level._lineStyle.isSet() ? *level._lineStyle : *_options->lineStyle();
    const Style& textStyle = level._textStyle.isSet() ? *level._textStyle : *_options->textStyle();

    bool hasText = textStyle.get<TextSymbol>() != 0L;

    osg::ref_ptr<osg::Group> labels;
    if ( hasText )
    {
        labels = new osg::Group();
        //TODO:  This is a bug, if you don't turn on decluttering the text labels are giant.  Need to determine what is wrong with LabelNodes without decluttering.
        Decluttering::setEnabled( labels->getOrCreateStateSet(), true );
    }

    // spatial ref for features:
    const SpatialReference* geoSRS = tileExtent.getSRS()->getGeographicSRS();

    // longitude lines
    for( unsigned cx = 0; cx < cellsPerTileX; ++cx )
    {
        double clon = tileExtent.xMin() + cellWidth * (double)cx;
        LineString* lon = new LineString(2);
        lon->push_back( osg::Vec3d(clon, tileExtent.yMin(), 0) );
        lon->push_back( osg::Vec3d(clon, tileExtent.yMax(), 0) );
        lonLines.push_back( new Feature(lon, geoSRS) );

        if ( hasText )
        {
            for( unsigned cy = 0; cy < cellsPerTileY; ++cy )
            {
                double clat = tileExtent.yMin() + (0.5*cellHeight) + cellHeight*(double)cy;
                LabelNode* label = new LabelNode(
                    _mapNode.get(),
                    GeoPoint(geoSRS, clon, clat),
                    s_llf.format(clon),
                    textStyle );
                labels->addChild( label );
            }
        }
    }

    // latitude lines
    for( unsigned cy = 0; cy < cellsPerTileY; ++cy )
    {
        double clat = tileExtent.yMin() + cellHeight * (double)cy;
        if ( clat == key.getProfile()->getExtent().yMin() )
            continue;

        LineString* lat = new LineString(2);
        lat->push_back( osg::Vec3d(tileExtent.xMin(), clat, 0) );
        lat->push_back( osg::Vec3d(tileExtent.xMax(), clat, 0) );
        latLines.push_back( new Feature(lat, geoSRS) );

        if ( hasText )
        {
            for( unsigned cx = 0; cx < cellsPerTileX; ++cx )
            {
                double clon = tileExtent.xMin() + (0.5*cellWidth) + cellWidth*(double)cy;
                LabelNode* label = new LabelNode(
                    _mapNode.get(),
                    GeoPoint(geoSRS, clon, clat),
                    s_llf.format(clat),
                    textStyle );
                labels->addChild( label );
            }
        }
    }

    osg::Group* group = new osg::Group();

    GeometryCompiler compiler;
    osg::ref_ptr<Session> session = new Session( map );
    FilterContext context( session.get(), _featureProfile.get(), tileExtent );

    // make sure we get sufficient tessellation:
    compiler.options().maxGranularity() = std::min(cellWidth, cellHeight) / 16.0;

    compiler.options().geoInterp() = GEOINTERP_GREAT_CIRCLE;
    osg::Node* lonNode = compiler.compile(lonLines, lineStyle, context);
    if ( lonNode )
        group->addChild( lonNode );

    compiler.options().geoInterp() = GEOINTERP_RHUMB_LINE;
    osg::Node* latNode = compiler.compile(latLines, lineStyle, context);
    if ( latNode )
        group->addChild( latNode );

    // add the labels.
    if ( labels.valid() )
        group->addChild( labels.get() );

    // get the geocentric tile center:
    osg::Vec3d tileCenter;
    tileExtent.getCentroid( tileCenter.x(), tileCenter.y() );

    const SpatialReference* ecefSRS = tileExtent.getSRS()->getECEF();
    osg::Vec3d centerECEF;
    tileExtent.getSRS()->transform( tileCenter, ecefSRS, centerECEF );
    //tileExtent.getSRS()->transformToECEF( tileCenter, centerECEF );

    osg::NodeCallback* ccc = 0L;
    // set up cluster culling.
    if ( tileExtent.getSRS()->isGeographic() && tileExtent.width() < 90.0 && tileExtent.height() < 90.0 )
    {
        ccc = ClusterCullingFactory::create( group, centerECEF );
    }

    // add a paging node for higher LODs:
    if ( key.getLevelOfDetail() + 1 < _options->levels().size() )
    {
        const GeodeticGraticuleOptions::Level& nextLevel = _options->levels()[key.getLevelOfDetail()+1];

        osg::BoundingSphere bs = group->getBound();

        std::string uri = Stringify() << key.str() << "_" << getID() << "." << GRID_MARKER << "." << GRATICULE_EXTENSION;

        osg::PagedLOD* plod = new osg::PagedLOD();
        plod->setCenter( bs.center() );
        plod->addChild( group, std::max(level._minRange,nextLevel._maxRange), FLT_MAX );
        plod->setFileName( 1, uri );
        plod->setRange( 1, 0, nextLevel._maxRange );
        group = plod;
    }

    // or, if this is the deepest level and there's a minRange set, we need an LOD:
    else if ( level._minRange > 0.0f )
    {
        osg::LOD* lod = new osg::LOD();
        lod->addChild( group, level._minRange, FLT_MAX );
        group = lod;
    }

    if ( ccc )
    {
        osg::Group* cccGroup = new osg::Group();
        cccGroup->addCullCallback( ccc );
        cccGroup->addChild( group );
        group = cccGroup;
    }

    return group;
}
コード例 #9
0
static Feature::geometry_type convertGeometry(const GeometryTileFeature& geometryTileFeature, const CanonicalTileID& tileID) {
    const double size = util::EXTENT * std::pow(2, tileID.z);
    const double x0 = util::EXTENT * tileID.x;
    const double y0 = util::EXTENT * tileID.y;

    auto tileCoordinatesToLatLng = [&] (const Point<int16_t>& p) {
        double y2 = 180 - (p.y + y0) * 360 / size;
        return Point<double>(
            (p.x + x0) * 360 / size - 180,
            360.0 / M_PI * std::atan(std::exp(y2 * M_PI / 180)) - 90.0
        );
    };

    GeometryCollection geometries = geometryTileFeature.getGeometries();

    switch (geometryTileFeature.getType()) {
        case FeatureType::Unknown: {
            assert(false);
            return Point<double>(NAN, NAN);
        }

        case FeatureType::Point: {
            MultiPoint<double> multiPoint;
            for (const auto& p : geometries.at(0)) {
                multiPoint.push_back(tileCoordinatesToLatLng(p));
            }
            if (multiPoint.size() == 1) {
                return multiPoint[0];
            } else {
                return multiPoint;
            }
        }

        case FeatureType::LineString: {
            MultiLineString<double> multiLineString;
            for (const auto& g : geometries) {
                LineString<double> lineString;
                for (const auto& p : g) {
                    lineString.push_back(tileCoordinatesToLatLng(p));
                }
                multiLineString.push_back(std::move(lineString));
            }
            if (multiLineString.size() == 1) {
                return multiLineString[0];
            } else {
                return multiLineString;
            }
        }

        case FeatureType::Polygon: {
            MultiPolygon<double> multiPolygon;
            for (const auto& pg : classifyRings(geometries)) {
                Polygon<double> polygon;
                for (const auto& r : pg) {
                    LinearRing<double> linearRing;
                    for (const auto& p : r) {
                        linearRing.push_back(tileCoordinatesToLatLng(p));
                    }
                    polygon.push_back(std::move(linearRing));
                }
                multiPolygon.push_back(std::move(polygon));
            }
            if (multiPolygon.size() == 1) {
                return multiPolygon[0];
            } else {
                return multiPolygon;
            }
        }
    }

    // Unreachable, but placate GCC.
    return Point<double>();
}