Skip to content

Commit

Permalink
Merge pull request #60410 from elpaso/bugfix-gh_60396-mapinfo-multipo…
Browse files Browse the repository at this point in the history
…lygon

[ogr] Fix mapinfo (multi)polygon type  layers
  • Loading branch information
elpaso authored Feb 7, 2025
2 parents 669e1a0 + 58b9c3e commit c1e5e62
Show file tree
Hide file tree
Showing 7 changed files with 274 additions and 30 deletions.
124 changes: 102 additions & 22 deletions src/core/providers/ogr/qgsogrproviderutils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2663,7 +2663,7 @@ QList< QgsProviderSublayerDetails > QgsOgrProviderUtils::querySubLayerList( int
QgsDebugMsgLevel( QStringLiteral( "Unknown geometry type, count features for each geometry type" ), 2 );
// Add virtual sublayers for supported geometry types if layer type is unknown
// Count features for geometry types
QMap<OGRwkbGeometryType, int64_t> fCount;
QMap<OGRwkbGeometryType, size_t> fCount;
QSet<OGRwkbGeometryType> fHasZ;
// TODO: avoid reading attributes, setRelevantFields cannot be called here because it is not constant

Expand Down Expand Up @@ -2707,7 +2707,7 @@ QList< QgsProviderSublayerDetails > QgsOgrProviderUtils::querySubLayerList( int
if ( gType == wkbTINZ )
gType = wkbMultiPolygon25D;
bool hasZ = wkbHasZ( gType );
gType = QgsOgrProviderUtils::ogrWkbSingleFlatten( gType );
gType = wkbFlatten( gType );
fCount[gType] = fCount.value( gType ) + pasCounter[i].nCount;
if ( hasZ )
fHasZ.insert( gType );
Expand All @@ -2728,7 +2728,7 @@ QList< QgsProviderSublayerDetails > QgsOgrProviderUtils::querySubLayerList( int
if ( gType != wkbNone )
{
bool hasZ = wkbHasZ( gType );
gType = QgsOgrProviderUtils::ogrWkbSingleFlatten( gType );
gType = wkbFlatten( gType );
fCount[gType] = fCount.value( gType ) + 1;
if ( hasZ )
fHasZ.insert( gType );
Expand All @@ -2754,40 +2754,120 @@ QList< QgsProviderSublayerDetails > QgsOgrProviderUtils::querySubLayerList( int
}
}

// List TIN and PolyhedralSurface as Polygon
if ( fCount.contains( wkbTIN ) )
QMap<Qgis::GeometryType, size_t> baseTypeCount;
baseTypeCount[Qgis::GeometryType::Point] = 0;
baseTypeCount[Qgis::GeometryType::Line] = 0;
baseTypeCount[Qgis::GeometryType::Polygon] = 0;

OGRwkbGeometryType polyBaseType { wkbPolygon };
OGRwkbGeometryType lineBaseType { wkbLineString };
OGRwkbGeometryType pointBaseType { wkbPoint };

// Last type in the list is the winner
const static QList<OGRwkbGeometryType> pointHierarchy { wkbPoint, wkbMultiPoint };
const static QList<OGRwkbGeometryType> lineHierarchy { wkbLineString, wkbCircularString, wkbMultiLineString, wkbCompoundCurve, wkbMultiCurve };
const static QList<OGRwkbGeometryType> polyHierarchy { wkbPolyhedralSurface, wkbTIN, wkbPolygon, wkbCurvePolygon, wkbMultiPolygon, wkbMultiSurface };

for ( const auto t : std::as_const( pointHierarchy ) )
{
fCount[wkbPolygon] = fCount.value( wkbPolygon ) + fCount[wkbTIN];
fCount.remove( wkbTIN );
if ( fCount.contains( t ) )
{
baseTypeCount[Qgis::GeometryType::Point] += fCount.value( t );
pointBaseType = t;
fCount.remove( t );
}
}
if ( fCount.contains( wkbPolyhedralSurface ) )

// For lines use a three-step approach
// 1. First collapse linestring and circularstring into compoundcurve
if ( fCount.contains( wkbLineString ) && fCount.contains( wkbCircularString ) )
{
fCount[wkbPolygon] = fCount.value( wkbPolygon ) + fCount[wkbPolyhedralSurface];
fCount.remove( wkbPolyhedralSurface );
baseTypeCount[Qgis::GeometryType::Line] += fCount.value( wkbLineString );
baseTypeCount[Qgis::GeometryType::Line] += fCount.value( wkbCircularString );
lineBaseType = wkbCompoundCurve;
if ( ! fCount.contains( wkbCompoundCurve ) )
{
fCount[wkbCompoundCurve] = baseTypeCount[Qgis::GeometryType::Line];
baseTypeCount[Qgis::GeometryType::Line] = 0;
}
fCount.remove( wkbLineString );
fCount.remove( wkbCircularString );
}
// When there are CurvePolygons, promote Polygons
if ( fCount.contains( wkbPolygon ) && fCount.contains( wkbCurvePolygon ) )

// 2. Then collapse multilinestring and compoundcurve into multicurve
if ( fCount.contains( wkbMultiLineString ) && fCount.contains( wkbCompoundCurve ) )
{
fCount[wkbCurvePolygon] += fCount.value( wkbPolygon );
fCount.remove( wkbPolygon );
baseTypeCount[Qgis::GeometryType::Line] += fCount.value( wkbMultiLineString );
baseTypeCount[Qgis::GeometryType::Line] += fCount.value( wkbCompoundCurve );
lineBaseType = wkbMultiCurve;
if ( ! fCount.contains( wkbMultiCurve ) )
{
fCount[wkbMultiCurve] = baseTypeCount[Qgis::GeometryType::Line];
baseTypeCount[Qgis::GeometryType::Line] = 0;
}
fCount.remove( wkbMultiLineString );
fCount.remove( wkbCompoundCurve );
}
// When there are CompoundCurves, promote LineStrings and CircularStrings
if ( fCount.contains( wkbLineString ) && fCount.contains( wkbCompoundCurve ) )

// 3. Then follow the hierarchy
for ( const auto t : std::as_const( lineHierarchy ) )
{
fCount[wkbCompoundCurve] += fCount.value( wkbLineString );
fCount.remove( wkbLineString );
if ( fCount.contains( t ) )
{
baseTypeCount[Qgis::GeometryType::Line] += fCount.value( t );
lineBaseType = t;
fCount.remove( t );
}
}


// For polygons use a two-step approach:
// 1. First collapse multipolygon and curvepolygon into multisurface
if ( fCount.contains( wkbMultiPolygon ) && fCount.contains( wkbCurvePolygon ) )
{
baseTypeCount[Qgis::GeometryType::Polygon] += fCount.value( wkbMultiPolygon );
baseTypeCount[Qgis::GeometryType::Polygon] += fCount.value( wkbMultiSurface );
polyBaseType = wkbMultiSurface;
if ( ! fCount.contains( wkbMultiSurface ) )
{
fCount[wkbMultiSurface] = baseTypeCount[Qgis::GeometryType::Polygon];
baseTypeCount[Qgis::GeometryType::Polygon] = 0;
}
fCount.remove( wkbMultiPolygon );
fCount.remove( wkbCurvePolygon );
}
if ( fCount.contains( wkbCircularString ) && fCount.contains( wkbCompoundCurve ) )

// 2. Then collapse following the hierarchy
for ( const auto t : std::as_const( polyHierarchy ) )
{
fCount[wkbCompoundCurve] += fCount.value( wkbCircularString );
fCount.remove( wkbCircularString );
if ( fCount.contains( t ) )
{
baseTypeCount[Qgis::GeometryType::Polygon] += fCount.value( t );
polyBaseType = t;
fCount.remove( t );
}
}

if ( baseTypeCount[Qgis::GeometryType::Point] > 0 )
{
fCount[pointBaseType] = baseTypeCount[Qgis::GeometryType::Point];
}

if ( baseTypeCount[Qgis::GeometryType::Line] > 0 )
{
fCount[lineBaseType] = baseTypeCount[Qgis::GeometryType::Line];
}

if ( baseTypeCount[Qgis::GeometryType::Polygon] > 0 )
{
fCount[polyBaseType] = baseTypeCount[Qgis::GeometryType::Polygon];
}

QList< QgsProviderSublayerDetails > res;
res.reserve( fCount.size() );

bool bIs25D = wkbHasZ( layerGeomType );
QMap<OGRwkbGeometryType, int64_t>::const_iterator countIt = fCount.constBegin();
QMap<OGRwkbGeometryType, size_t>::const_iterator countIt = fCount.constBegin();
for ( ; countIt != fCount.constEnd(); ++countIt )
{
if ( feedback && feedback->isCanceled() )
Expand Down
161 changes: 156 additions & 5 deletions tests/src/python/test_provider_ogr.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ def testMixOfPolygonCurvePolygon(self):
self.assertEqual(
vl.dataProvider().subLayers()[0],
QgsDataProvider.SUBLAYER_SEPARATOR.join(
["0", "testMixOfPolygonCurvePolygon", "4", "CurvePolygon", "", ""]
["0", "testMixOfPolygonCurvePolygon", "4", "MultiSurface", "", ""]
),
)

Expand All @@ -188,7 +188,6 @@ def testMixOfLineStringCompoundCurve(self):
f.write('1,"LINESTRING(0 0,0 1)"\n')
f.write('2,"COMPOUNDCURVE((0 0,0 1))"\n')
f.write('3,"MULTILINESTRING((0 0,0 1))"\n')
f.write('4,"MULTICURVE((0 0,0 1))"\n')
f.write('5,"CIRCULARSTRING(0 0,1 1,2 0)"\n')

vl = QgsVectorLayer(f"{datasource}|layerid=0", "test", "ogr")
Expand All @@ -197,7 +196,146 @@ def testMixOfLineStringCompoundCurve(self):
self.assertEqual(
vl.dataProvider().subLayers()[0],
QgsDataProvider.SUBLAYER_SEPARATOR.join(
["0", "testMixOfLineStringCompoundCurve", "5", "CompoundCurve", "", ""]
["0", "testMixOfLineStringCompoundCurve", "4", "MultiCurve", "", ""]
),
)

def testMixOfCurvePolygonAndMultiPolygon(self):

datasource = os.path.join(
self.basetestpath, "testMixOfCurvePolygonAndMultiPolygon.csv"
)
with open(datasource, "w") as f:
f.write("id,WKT\n")
f.write('1,"CURVEPOLYGON((0 0,0 1,1 1,0 0))"\n')
f.write('2,"MULTIPOLYGON(((0 0,0 1,1 1,0 0)))"\n')

vl = QgsVectorLayer(f"{datasource}|layerid=0", "test", "ogr")
self.assertTrue(vl.isValid())
self.assertEqual(len(vl.dataProvider().subLayers()), 1)
self.assertEqual(
vl.dataProvider().subLayers()[0],
QgsDataProvider.SUBLAYER_SEPARATOR.join(
[
"0",
"testMixOfCurvePolygonAndMultiPolygon",
"2",
"MultiSurface",
"",
"",
]
),
)

def testMixOfLineStringAndCircularString(self):

datasource = os.path.join(
self.basetestpath, "testMixOfLineStringAndCircularString.csv"
)
with open(datasource, "w") as f:
f.write("id,WKT\n")
f.write('1,"LINESTRING(0 0,0 1)"\n')
f.write('2,"CIRCULARSTRING(0 0,1 1,2 0)"\n')

vl = QgsVectorLayer(f"{datasource}|layerid=0", "test", "ogr")
self.assertTrue(vl.isValid())
self.assertEqual(len(vl.dataProvider().subLayers()), 1)
self.assertEqual(
vl.dataProvider().subLayers()[0],
QgsDataProvider.SUBLAYER_SEPARATOR.join(
[
"0",
"testMixOfLineStringAndCircularString",
"2",
"CompoundCurve",
"",
"",
]
),
)

def testMixOfLineStringAndCircularStringAndCompoundCurve(self):

datasource = os.path.join(
self.basetestpath,
"testMixOfLineStringAndCircularStringAndCompoundCurve.csv",
)
with open(datasource, "w") as f:
f.write("id,WKT\n")
f.write('1,"LINESTRING(0 0,0 1)"\n')
f.write('2,"CIRCULARSTRING(0 0,1 1,2 0)"\n')
f.write('3,"COMPOUNDCURVE((0 0,0 1))"\n')

vl = QgsVectorLayer(f"{datasource}|layerid=0", "test", "ogr")
self.assertTrue(vl.isValid())
self.assertEqual(len(vl.dataProvider().subLayers()), 1)
self.assertEqual(
vl.dataProvider().subLayers()[0],
QgsDataProvider.SUBLAYER_SEPARATOR.join(
[
"0",
"testMixOfLineStringAndCircularStringAndCompoundCurve",
"3",
"CompoundCurve",
"",
"",
]
),
)

def testMixOfMultiLineStringAndCompoundCurve(self):

datasource = os.path.join(
self.basetestpath, "testMixOfMultiLineStringAndCompoundCurve.csv"
)
with open(datasource, "w") as f:
f.write("id,WKT\n")
f.write('1,"MULTILINESTRING((0 0,0 1))"\n')
f.write('2,"COMPOUNDCURVE((0 0,0 1))"\n')

vl = QgsVectorLayer(f"{datasource}|layerid=0", "test", "ogr")
self.assertTrue(vl.isValid())
self.assertEqual(len(vl.dataProvider().subLayers()), 1)
self.assertEqual(
vl.dataProvider().subLayers()[0],
QgsDataProvider.SUBLAYER_SEPARATOR.join(
[
"0",
"testMixOfMultiLineStringAndCompoundCurve",
"2",
"MultiCurve",
"",
"",
]
),
)

def testMixOfMultiLineStringAndCompoundCurveAndMultiCurve(self):

datasource = os.path.join(
self.basetestpath,
"testMixOfMultiLineStringAndCompoundCurveAndMultiCurve.csv",
)
with open(datasource, "w") as f:
f.write("id,WKT\n")
f.write('1,"MULTILINESTRING((0 0,0 1))"\n')
f.write('2,"COMPOUNDCURVE((0 0,0 1))"\n')
f.write('3,"MULTICURVE((0 0,0 1))"\n')

vl = QgsVectorLayer(f"{datasource}|layerid=0", "test", "ogr")
self.assertTrue(vl.isValid())
self.assertEqual(len(vl.dataProvider().subLayers()), 1)
self.assertEqual(
vl.dataProvider().subLayers()[0],
QgsDataProvider.SUBLAYER_SEPARATOR.join(
[
"0",
"testMixOfMultiLineStringAndCompoundCurveAndMultiCurve",
"3",
"MultiCurve",
"",
"",
]
),
)

Expand Down Expand Up @@ -2535,11 +2673,11 @@ def test_provider_sublayer_details(self):
self.assertEqual(
res[0].uri(),
TEST_DATA_DIR
+ "/multipatch.shp|geometrytype=Polygon25D|uniqueGeometryType=yes",
+ "/multipatch.shp|geometrytype=MultiPolygon25D|uniqueGeometryType=yes",
)
self.assertEqual(res[0].providerKey(), "ogr")
self.assertEqual(res[0].type(), QgsMapLayerType.VectorLayer)
self.assertEqual(res[0].wkbType(), QgsWkbTypes.Type.PolygonZ)
self.assertEqual(res[0].wkbType(), QgsWkbTypes.Type.MultiPolygonZ)
self.assertEqual(res[0].geometryColumnName(), "")
self.assertEqual(res[0].driverName(), "ESRI Shapefile")

Expand Down Expand Up @@ -3127,6 +3265,19 @@ def test_provider_sublayer_details(self):
self.assertEqual(res[0].type(), QgsMapLayerType.VectorLayer)
self.assertFalse(res[0].skippedContainerScan())

res = metadata.querySublayers(
os.path.join(TEST_DATA_DIR, "mapinfo", "multipoly.tab"),
Qgis.SublayerQueryFlag.ResolveGeometryType,
)
self.assertEqual(len(res), 1)
self.assertEqual(res[0].wkbType(), QgsWkbTypes.MultiPolygon)
layer = res[0].toLayer(options)
self.assertTrue(layer.isValid())
self.assertEqual(layer.wkbType(), QgsWkbTypes.MultiPolygon)
# Check feature geometries
for feature in layer.getFeatures():
self.assertEqual(feature.geometry().wkbType(), QgsWkbTypes.MultiPolygon)

@unittest.skipIf(
int(gdal.VersionInfo("VERSION_NUM")) < GDAL_COMPUTE_VERSION(3, 4, 0),
"GDAL 3.4 required",
Expand Down
Loading

0 comments on commit c1e5e62

Please sign in to comment.