Skip to content

Commit 47ce01b

Browse files
refactor: update geodataframe conversion
1 parent 1da4630 commit 47ce01b

File tree

1 file changed

+188
-0
lines changed

1 file changed

+188
-0
lines changed

loopstructural/main/vectorLayerWrapper.py

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
QgsProject,
2323
QgsRaster,
2424
QgsRasterLayer,
25+
QgsVectorLayer,
2526
QgsWkbTypes,
2627
)
2728
from qgis.PyQt.QtCore import QDateTime, QVariant
@@ -487,6 +488,193 @@ def _fields_from_dataframe(df, drop_cols=None) -> QgsFields:
487488
return fields
488489

489490

491+
def _geometry_from_value(value):
492+
"""Convert shapely/QGIS geometry objects into QgsGeometry instances."""
493+
if value is None:
494+
return None
495+
if isinstance(value, QgsGeometry):
496+
return QgsGeometry(value)
497+
# QgsGeometry with asWkb
498+
for attr in ("asWkb", "exportToWkb"):
499+
method = getattr(value, attr, None)
500+
if callable(method):
501+
try:
502+
data = method()
503+
except Exception:
504+
data = None
505+
if data:
506+
try:
507+
data = bytes(data)
508+
except Exception:
509+
pass
510+
try:
511+
return QgsGeometry.fromWkb(data)
512+
except Exception:
513+
continue
514+
# Shapely geometries expose wkb/wkt attributes
515+
wkb_data = getattr(value, "wkb", None)
516+
if wkb_data is not None:
517+
try:
518+
return QgsGeometry.fromWkb(bytes(wkb_data))
519+
except Exception:
520+
pass
521+
wkt_data = getattr(value, "wkt", None)
522+
if wkt_data:
523+
try:
524+
return QgsGeometry.fromWkt(str(wkt_data))
525+
except Exception:
526+
pass
527+
return None
528+
529+
530+
def _infer_wkb_type_from_geoms(geoms) -> QgsWkbTypes.Type:
531+
"""Infer a WKB type from a GeoSeries or iterable of geometries."""
532+
for geom in geoms:
533+
qgs_geom = _geometry_from_value(geom)
534+
if qgs_geom is not None and not qgs_geom.isEmpty():
535+
return qgs_geom.wkbType()
536+
return QgsWkbTypes.Point
537+
538+
539+
def _crs_from_geodataframe_crs(crs_info) -> QgsCoordinateReferenceSystem:
540+
"""Best-effort conversion of GeoPandas CRS metadata to QgsCoordinateReferenceSystem."""
541+
crs = QgsCoordinateReferenceSystem()
542+
if crs_info is None:
543+
return crs
544+
# pyproj CRS exposes helpers like to_wkt/to_epsg
545+
text = None
546+
for attr in ("to_wkt", "to_string"):
547+
method = getattr(crs_info, attr, None)
548+
if callable(method):
549+
try:
550+
text = method()
551+
except Exception:
552+
text = None
553+
if text:
554+
break
555+
if text:
556+
try:
557+
return QgsCoordinateReferenceSystem.fromWkt(text)
558+
except Exception:
559+
temp = QgsCoordinateReferenceSystem()
560+
if hasattr(temp, "createFromWkt"):
561+
try:
562+
if temp.createFromWkt(text):
563+
return temp
564+
except Exception:
565+
pass
566+
try:
567+
epsg = crs_info.to_epsg()
568+
if epsg:
569+
return QgsCoordinateReferenceSystem.fromEpsgId(int(epsg))
570+
except Exception:
571+
pass
572+
if isinstance(crs_info, str):
573+
try:
574+
temp = QgsCoordinateReferenceSystem(crs_info)
575+
if temp.isValid():
576+
return temp
577+
except Exception:
578+
pass
579+
return crs
580+
581+
582+
def QgsLayerFromGeoDataFrame(geodataframe, layer_name: str = "Converted Data"):
583+
"""Create an in-memory QgsVectorLayer from a GeoPandas GeoDataFrame."""
584+
if geodataframe is None:
585+
return None
586+
geometry_series = getattr(geodataframe, "geometry", None)
587+
if geometry_series is None:
588+
raise ValueError("GeoDataFrame must include a geometry column.")
589+
geometry_column = geometry_series.name
590+
wkb_type = _infer_wkb_type_from_geoms(geometry_series)
591+
geom_name = QgsWkbTypes.displayString(wkb_type) or "Point"
592+
crs = _crs_from_geodataframe_crs(getattr(geodataframe, "crs", None))
593+
uri = geom_name
594+
if crs.isValid():
595+
authid = crs.authid()
596+
if authid:
597+
uri = f"{geom_name}?crs={authid}"
598+
layer = QgsVectorLayer(uri, layer_name, "memory")
599+
if crs.isValid():
600+
layer.setCrs(crs)
601+
provider = layer.dataProvider()
602+
attribute_fields = []
603+
for column in geodataframe.columns:
604+
if column == geometry_column:
605+
continue
606+
attribute_fields.append(QgsField(str(column), _qvariant_type_from_dtype(geodataframe[column].dtype)))
607+
if attribute_fields:
608+
provider.addAttributes(attribute_fields)
609+
layer.updateFields()
610+
non_geom_cols = [col for col in geodataframe.columns if col != geometry_column]
611+
features = []
612+
for _, row in geodataframe.iterrows():
613+
feat = QgsFeature(layer.fields())
614+
attrs = []
615+
for column in non_geom_cols:
616+
val = row[column]
617+
if isinstance(val, np.generic):
618+
try:
619+
val = val.item()
620+
except Exception:
621+
pass
622+
if pd.isna(val):
623+
val = None
624+
attrs.append(val)
625+
feat.setAttributes(attrs)
626+
geom = _geometry_from_value(row[geometry_column])
627+
if geom is not None and not geom.isEmpty():
628+
feat.setGeometry(geom)
629+
features.append(feat)
630+
if features:
631+
provider.addFeatures(features)
632+
layer.updateExtents()
633+
return layer
634+
635+
636+
def QgsLayerFromDataFrame(dataframe, layer_name: str = "Converted Table"):
637+
"""Create an attribute-only memory layer from a pandas-compatible DataFrame."""
638+
if dataframe is None:
639+
return None
640+
df = dataframe.copy()
641+
geometry_series = getattr(df, "geometry", None)
642+
geometry_name = getattr(geometry_series, "name", None)
643+
if geometry_name and geometry_name in df.columns:
644+
df = df.drop(columns=[geometry_name])
645+
646+
layer = QgsVectorLayer("None", layer_name, "memory")
647+
provider = layer.dataProvider()
648+
649+
attributes = []
650+
for column in df.columns:
651+
attributes.append(QgsField(str(column), _qvariant_type_from_dtype(df[column].dtype)))
652+
if attributes:
653+
provider.addAttributes(attributes)
654+
layer.updateFields()
655+
656+
features = []
657+
for _, row in df.iterrows():
658+
feat = QgsFeature(layer.fields())
659+
attrs = []
660+
for column in df.columns:
661+
val = row[column]
662+
if pd.isna(val):
663+
val = None
664+
elif isinstance(val, np.generic):
665+
try:
666+
val = val.item()
667+
except Exception:
668+
pass
669+
attrs.append(val)
670+
feat.setAttributes(attrs)
671+
features.append(feat)
672+
if features:
673+
provider.addFeatures(features)
674+
layer.updateExtents()
675+
return layer
676+
677+
490678
# ---------- main function you'll call inside processAlgorithm ----------
491679

492680

0 commit comments

Comments
 (0)