Skip to content

Self-intersecting polygons drawn in osgEarth show extra areas when exported to Shapefile, geoJSON file when viewed in QGIS #2827

@BlessingtonP

Description

@BlessingtonP

osgEarth Version (required): 3.7.2

Description of the problem:
Polygons drawn in osgEarth using FeatureNode appear correct in the application, but when exported to Shapefile and opened in QGIS, they display unexpected extra areas. This occurs because:

The drawing sequence creates self-intersecting polygons (bowtie shapes)
QGIS renders these differently than osgEarth
Standard validation/repair techniques don't preserve the original drawn shape

The Real Need:
We need to preserve the exact drawn shape while eliminating self-intersections - essentially converting a self-intersecting polygon into a simple polygon that matches the visual appearance in osgEarth

How can we export drawn polygons to Shapefile such that:
The visual appearance matches what's seen in osgEarth
No extra areas appear in QGIS
The original point sequence is preserved as much as possible

I need to export to .shp file and .geoJSON file

What you have tried:
I have tried some ways but some of them are depreciated classes also,
Convex hull generation
Triangulation and recombination
Winding order correction
Ring closure enforcement
Geometry repair with OGR's MakeValid()

Screenshot, code block, or data file that will help reproduce the issue:

Image Image

Please check the below code PolygonDrawTool class and DrawTool class.

DrawPolygonTool::DrawPolygonTool(osgEarth::MapNode* mapNode, osg::Group* drawGroup)
: DrawTool(mapNode, drawGroup)
, _featureNode(nullptr)
, _stippleFeatureNode(nullptr)
// , _polygonEdit(nullptr)
{

_polygonStyle.getOrCreate<PolygonSymbol>()
->fill()
    ->color()
    = Color(Color::Yellow, 0.25);

LineSymbol* ls = _polygonStyle.getOrCreate<LineSymbol>();
ls->stroke()->color() = Color::White;
ls->stroke()->width()  = osgEarth::Distance(2.0, osgEarth::Units::PIXELS);
ls->stroke()->widthUnits() = osgEarth::Units::PIXELS;

_polygonStyle.getOrCreate<AltitudeSymbol>()
    ->clamping()
    = AltitudeSymbol::CLAMP_TO_TERRAIN;
_polygonStyle.getOrCreate<AltitudeSymbol>()
    ->technique()
    = AltitudeSymbol::TECHNIQUE_DRAPE;
_polygonStyle.getOrCreate<AltitudeSymbol>()
    ->verticalOffset()
    = 0.1;

_stippleLineStyle.getOrCreate<LineSymbol>()
    ->stroke()
    ->color()
    = Color::Red;
_stippleLineStyle.getOrCreate<LineSymbol>()
    ->stroke()
    ->width()
    = osgEarth::Distance(2.0, osgEarth::Units::PIXELS);
_stippleLineStyle.getOrCreate<LineSymbol>()
    ->tessellation()
    = 20.0;
_stippleLineStyle.getOrCreate<AltitudeSymbol>()
    ->clamping()
    = AltitudeSymbol::CLAMP_TO_TERRAIN;
_stippleLineStyle.getOrCreate<AltitudeSymbol>()
    ->technique()
    = AltitudeSymbol::TECHNIQUE_DRAPE;
_stippleLineStyle.getOrCreate<AltitudeSymbol>()
    ->verticalOffset()
    = 0.1;
_stippleLineStyle.getOrCreate<LineSymbol>()
    ->stroke()
    ->stippleFactor()
    = 255;

}

void DrawPolygonTool::beginDraw(const osg::Vec3d& lla)
{
_vecPoints.push_back(lla);
if (_vecPoints.size() <= 2) {
return;
}

// if (_polygonEdit != nullptr) {
//     _polygonEdit->removeChildren(0, _polygonEdit->getNumChildren());
//     _polygonEdit = nullptr;
// }

if (_featureNode != nullptr) {
    _featureNode->removeChildren(0, _featureNode->getNumChildren());
    _featureNode = nullptr;
}
if (_featureNode == nullptr) {
    Feature* feature = new Feature(new osgEarth::Polygon, getMapNode()->getMapSRS(), _polygonStyle);
    _featureNode = new FeatureNode(feature, _polygonStyle);
    _lineFeatures.push_back(feature);
}

Geometry* geom = _featureNode->getFeature()->getGeometry();
geom->clear();

qDebug() << "Drawing polygon/line with points:";
for (int i = 0; i < _vecPoints.size(); ++i) {
    qDebug() << "Point" << i << ":" << _vecPoints[i].x() << _vecPoints[i].y();
}

//_featureNode->setStyle(_polygonStyle);
for (auto& n : _vecPoints) {
    geom->push_back(n);
}

// _featureNode->init();
if (_stippleFeatureNode != nullptr) {
    _stippleFeatureNode->getFeature()->getGeometry()->clear();
}
_lineFeatureNodes.push_back(_featureNode);
drawCommand(/*osg::NodeList{*/ _featureNode/*, _polygonEdit}*/ );

}

void DrawPolygonTool::moveDraw(const osg::Vec3d& lla)
{
if (_vecPoints.size() < 2) {
return;
}
if (_stippleFeatureNode == nullptr) {
Feature* feature = new Feature(new LineString, getMapNode()->getMapSRS(), _stippleLineStyle);
_stippleFeatureNode = new FeatureNode(feature, _stippleLineStyle);
drawCommand(_stippleFeatureNode);
}

Geometry* geom = _stippleFeatureNode->getFeature()->getGeometry();
geom->clear();
_stippleFeatureNode->setStyle(_stippleLineStyle);
geom->push_back(_vecPoints[0]);
geom->push_back(lla);
geom->push_back(_vecPoints[_vecPoints.size() - 1]);

}

void DrawPolygonTool::endDraw(const osg::Vec3d& lla)
{
}

void DrawPolygonTool::resetDraw()
{

if (_vecPoints.front() != _vecPoints.back()) {
    _vecPoints.push_back(_vecPoints.front());
}

_vecPoints.clear();
if (_stippleFeatureNode != nullptr) {
    _stippleFeatureNode->getFeature()->getGeometry()->clear();
}
_featureNode = nullptr;

}

bool DrawPolygonTool::exportToShapefile(const std::string& filename)
{
OGRRegisterAll();
const char* driverName = "ESRI Shapefile";
GDALDriver* driver = GetGDALDriverManager()->GetDriverByName(driverName);
if (!driver) {
qDebug("ERROR: Failed to get GDAL driver for ESRI Shapefile");
return false;
}

driver->Delete(filename.c_str());
GDALDataset* ds = driver->Create(filename.c_str(), 0, 0, 0, GDT_Unknown, nullptr);
if (!ds) {
    qDebug("ERROR: Failed to create GDAL dataset");
    return false;
}

OGRSpatialReference srs;
srs.importFromEPSG(4326);

OGRLayer* layer = ds->CreateLayer("polygons", &srs, wkbPolygon, nullptr);
if (!layer) {
    GDALClose(ds);
    return false;
}

OGRFieldDefn fieldDefn("id", OFTInteger);
if (layer->CreateField(&fieldDefn) != OGRERR_NONE) {
    GDALClose(ds);
    return false;
}

for (size_t i = 0; i < _lineFeatures.size(); ++i)
{
    const osgEarth::Geometry* geom = _lineFeatures[i]->getGeometry();
    if (!geom || geom->size() < 3)
        continue;

    OGRFeature* feature = OGRFeature::CreateFeature(layer->GetLayerDefn());
    feature->SetField("id", static_cast<int>(i + 1));

    OGRPolygon ogrPoly;
    OGRLinearRing ring;

    if (!ogrPoly.IsValid()) {
        qDebug() << "Invalid polygon geometry skipped!";
        continue;
    }

    for (const auto& pt : *geom)
        ring.addPoint(pt.x(), pt.y());

    ring.addPoint((*geom)[0].x(), (*geom)[0].y()); // Close the ring

    qDebug() << "Exporting Polygon with points:";
    for (int i = 0; i < ring.getNumPoints(); ++i) {
        qDebug() << "Point" << i << ":" << ring.getX(i) << ring.getY(i);
    }

    ring.closeRings();
    ogrPoly.addRing(&ring);
    feature->SetGeometry(&ogrPoly);

    if (layer->CreateFeature(feature) != OGRERR_NONE)
        qDebug("Failed to create feature");

    OGRFeature::DestroyFeature(feature);
}

GDALClose(ds);
qDebug("Polygon export to shapefile successful");
return true;

}

bool DrawPolygonTool::exportToGeoJSON(const std::string& filename)
{
Json::Value root(Json::objectValue);
root["type"] = "FeatureCollection";
root["features"] = Json::arrayValue;

for (size_t i = 0; i < _lineFeatures.size(); ++i)
{
    const osgEarth::Geometry* geom = _lineFeatures[i]->getGeometry();
    if (!geom || geom->size() < 3)
        continue;

    Json::Value feature(Json::objectValue);
    feature["type"] = "Feature";

    Json::Value geometry(Json::objectValue);
    geometry["type"] = "Polygon";

    Json::Value coordinates(Json::arrayValue);
    Json::Value ring(Json::arrayValue);

    for (const auto& pt : *geom)
    {
        Json::Value coord(Json::arrayValue);
        coord.append(pt.x()); // Longitude
        coord.append(pt.y()); // Latitude
        ring.append(coord);
    }

    // Close the ring
    Json::Value first(Json::arrayValue);
    first.append((*geom)[0].x());
    first.append((*geom)[0].y());
    ring.append(first);

    coordinates.append(ring);
    geometry["coordinates"] = coordinates;
    feature["geometry"] = geometry;

    Json::Value props(Json::objectValue);
    props["id"] = static_cast<int>(i + 1);
    feature["properties"] = props;

    root["features"].append(feature);
}

std::ofstream ofs(filename);
if (!ofs.is_open()) {
    qDebug("Failed to open file for GeoJSON export: %s", filename.c_str());
    return false;
}

Json::StreamWriterBuilder writer;
writer["indentation"] = "  ";
ofs << Json::writeString(writer, root);
ofs.close();

qDebug("Polygon export to GeoJSON successful");
return true;

}

DrawPolygonTool::~DrawPolygonTool()
{
osg::Group* tempGroup = getDrawGroup();
for (auto node : _lineFeatureNodes)
{
if (node)
{
tempGroup->removeChild(node);
}
}
_lineFeatureNodes.clear();
_lineFeatures.clear();
if (_stippleFeatureNode)
{
tempGroup->removeChild(_stippleFeatureNode);
_stippleFeatureNode = nullptr;
}
}

DrawTool

DrawTool::DrawTool(osgEarth::MapNode* mapNode, osg::Group* drawGroup)
: _active(true)
, _dbClick(false)
, _mapNode(mapNode)
, _intersectionMask(0x1)
, _drawGroup(drawGroup)
, _tmpGroup(new osg::Group)
{

_pnStyle.getOrCreate<osgEarth::IconSymbol>()->url()->setLiteral(":/MapData/icon/release/MapData/icon/location-pin.png");
_pnStyle.getOrCreate<osgEarth::TextSymbol>()->size() = 24;
_mapNode->addChild(_tmpGroup);

}

bool DrawTool::handle(const osgGA::GUIEventAdapter& ea, osgGA::GUIActionAdapter& aa)
{
if (!_active)
{
return false;
}
_view = static_castosgViewer::View*(aa.asView());
const osgGA::GUIEventAdapter::EventType eventType = ea.getEventType();
if (eventType == osgGA::GUIEventAdapter::KEYDOWN && ea.getKey() == osgGA::GUIEventAdapter::KEY_Escape)
{
resetDraw();
}

switch (ea.getEventType())
{
case osgGA::GUIEventAdapter::PUSH:
{
    if (ea.getButton() == osgGA::GUIEventAdapter::LEFT_MOUSE_BUTTON)
    {
        _mouseDownX = ea.getX();
        _mouseDownY = ea.getY();
    }
    break;
}

case osgGA::GUIEventAdapter::MOVE:
{
    osg::Vec3d pos;
    getLocationAt(_view, ea.getX(), ea.getY(), pos.x(), pos.y(), pos.z());
    std::string coord = osgEarth::Stringify() << pos.x() << " " << pos.y() << " " << pos.z();
    if (_coordPn.valid())
    {
        _coordPn->setPosition(osgEarth::GeoPoint(getMapNode()->getMapSRS(), pos));
        _coordPn->setText(coord);
    }
    moveDraw(pos);
    aa.requestRedraw();
    break;
}
case osgGA::GUIEventAdapter::RELEASE:
{
    osg::Vec3d pos;
    getLocationAt(_view, ea.getX(), ea.getY(), pos.x(), pos.y(), pos.z());
    float eps = 1.0f;
    if (ea.getButton() == osgGA::GUIEventAdapter::LEFT_MOUSE_BUTTON)
    {
        if (osg::equivalent(ea.getX(), _mouseDownX, eps) && osg::equivalent(ea.getY(), _mouseDownY, eps))
        {
            if (!_coordPn.valid())
            {
                std::string coord = osgEarth::Stringify() << pos.x() << " " << pos.y() << " " << pos.z();
                _coordPn = new osgEarth::PlaceNode(osgEarth::GeoPoint(getMapNode()->getMapSRS(),
                                pos), coord, _pnStyle);
                _tmpGroup->addChild(_coordPn);
            }
            beginDraw(pos);
            aa.requestRedraw();
        }
    }
    else if (ea.getButton() == osgGA::GUIEventAdapter::RIGHT_MOUSE_BUTTON)
    {
        if (_coordPn.valid())
        {
            _coordPn = NULL;
        }
        resetDraw();
        aa.requestRedraw();
    }
    break;
}
case osgGA::GUIEventAdapter::NONE:
case osgGA::GUIEventAdapter::DOUBLECLICK:
case osgGA::GUIEventAdapter::DRAG:
case osgGA::GUIEventAdapter::KEYDOWN:
case osgGA::GUIEventAdapter::KEYUP:
case osgGA::GUIEventAdapter::FRAME:
case osgGA::GUIEventAdapter::RESIZE:
case osgGA::GUIEventAdapter::SCROLL:
case osgGA::GUIEventAdapter::PEN_PRESSURE:
case osgGA::GUIEventAdapter::PEN_ORIENTATION:
case osgGA::GUIEventAdapter::PEN_PROXIMITY_ENTER:
case osgGA::GUIEventAdapter::PEN_PROXIMITY_LEAVE:
case osgGA::GUIEventAdapter::CLOSE_WINDOW:
case osgGA::GUIEventAdapter::QUIT_APPLICATION:
case osgGA::GUIEventAdapter::USER:
    break;
}

return false;

}

void DrawTool::drawCommand(osg::Node *node)
{
if (!_drawGroup->containsNode(node)) {
_drawGroup->addChild(node);
qDebug() << "[DRAW COMMAND] Node added to group.";
} else {
qDebug() << "[DRAW COMMAND] Node already exists, skipping.";
}
}

void DrawTool::drawCommand(const osg::NodeList &nodes)
{
for (auto& node : nodes)
_drawGroup->addChild(node);
qDebug() << "[DRAW COMMAND] Node added to group.2";

}

bool DrawTool::getLocationAt(osgViewer::View* view, double x, double y, double& lon, double& lat, double& alt)
{
osgUtil::LineSegmentIntersector::Intersections results;
if (getMapNode() && view->computeIntersections(x, y, results, _intersectionMask))
{
osgUtil::LineSegmentIntersector::Intersection first = *(results.begin());
osg::Vec3d point = first.getWorldIntersectPoint();
osg::Vec3d geodetic = _mapNode->getMapSRS()->getGeodeticSRS()->getEllipsoid().geocentricToGeodetic(point);
lon = geodetic.x();
lat = geodetic.y();
alt = geodetic.z();
return true;
}
return false;
}

DrawTool::~DrawTool()
{
if(_coordPn.valid())
{
_tmpGroup->removeChild(_coordPn);
}
_coordPn = nullptr;
if (_tmpGroup.valid()) {
_mapNode->removeChild(_tmpGroup);
}
_tmpGroup = nullptr;
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions