|
22 | 22 | QgsProject, |
23 | 23 | QgsRaster, |
24 | 24 | QgsRasterLayer, |
| 25 | + QgsVectorLayer, |
25 | 26 | QgsWkbTypes, |
26 | 27 | ) |
27 | 28 | from qgis.PyQt.QtCore import QDateTime, QVariant |
@@ -487,6 +488,193 @@ def _fields_from_dataframe(df, drop_cols=None) -> QgsFields: |
487 | 488 | return fields |
488 | 489 |
|
489 | 490 |
|
| 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 | + |
490 | 678 | # ---------- main function you'll call inside processAlgorithm ---------- |
491 | 679 |
|
492 | 680 |
|
|
0 commit comments