Skip to content

Support multiple file types #9

Description

@zachflanders

From copilot

Yes, there are several other geospatial file formats that you might consider supporting, depending on your use case and the needs of your users. Here are some common ones:

KML (Keyhole Markup Language): Often used with Google Earth.
GML (Geography Markup Language): An XML-based format for geographic data.
GPX (GPS Exchange Format): Commonly used for GPS data.
CSV with WKT (Well-Known Text): A simple text format for representing vector geometry objects.
GeoTIFF: A format for raster data.
WFS (Web Feature Service): A standard for serving geospatial features over the web.
Here's how you can extend your ingest_file_to_db function to support KML and CSV with WKT, in addition to GeoJSON and Shapefiles:

import json
import tempfile
import zipfile
import fiona
import csv
from pathlib import Path
from pyproj import CRS, Transformer
from django.contrib.gis.geos import GEOSGeometry, Point, Polygon, LineString
from .models import Feature, Property, Layer, User
from fastkml import kml

def ingest_file_to_db(gcs_path, layer_name, user_email):
    """Ingests a file into the specified database table."""
    with tempfile.NamedTemporaryFile(delete=True) as temp_file:
        download_from_gcs(gcs_path, temp_file.name)
        temp_file.seek(0)  # Ensure the file pointer is at the beginning

        # Determine the file type
        file_extension = Path(gcs_path).suffix.lower()

        if file_extension == '.geojson':
            # Read the GeoJSON file
            geojson_data = json.load(temp_file)
            features = geojson_data["features"]
        elif file_extension == '.zip':
            # Handle shapefile in a .zip file
            with zipfile.ZipFile(temp_file, 'r') as zip_ref:
                zip_ref.extractall(temp_file.name)
                shapefile_path = next(
                    (Path(temp_file.name) / name for name in zip_ref.namelist() if name.endswith('.shp')),
                    None
                )
                if shapefile_path is None:
                    raise ValueError("No shapefile found in the .zip archive")

                with fiona.open(shapefile_path) as shapefile:
                    crs = CRS(shapefile.crs)
                    transformer = Transformer.from_crs(crs, CRS("EPSG:4326"), always_xy=True)
                    features = []
                    for feature in shapefile:
                        geom = feature["geometry"]
                        if crs != CRS("EPSG:4326"):
                            geom = transform_geom(transformer, geom)
                        features.append({
                            "type": "Feature",
                            "geometry": geom,
                            "properties": feature["properties"]
                        })
        elif file_extension == '.kml':
            # Handle KML file
            kml_data = kml.KML()
            kml_data.from_string(temp_file.read().decode('utf-8'))
            features = []
            for feature in kml_data.features():
                geom = feature.geometry
                if geom.crs != CRS("EPSG:4326"):
                    transformer = Transformer.from_crs(geom.crs, CRS("EPSG:4326"), always_xy=True)
                    geom = transform_geom(transformer, geom)
                features.append({
                    "type": "Feature",
                    "geometry": geom.__geo_interface__,
                    "properties": feature.extended_data
                })
        elif file_extension == '.csv':
            # Handle CSV with WKT
            features = []
            reader = csv.DictReader(temp_file)
            for row in reader:
                wkt = row.pop('wkt')
                geom = GEOSGeometry(wkt)
                if geom.srid != 4326:
                    geom.transform(4326)
                features.append({
                    "type": "Feature",
                    "geometry": json.loads(geom.geojson),
                    "properties": row
                })
        else:
            raise ValueError("Unsupported file type")

    user = User.objects.get(email=user_email)
    # Create or get the Layer instance
    layer, created = Layer.objects.get_or_create(name=layer_name, user=user)

    # Create Feature instances
    feature_instances = [
        Feature(
            name=feature["properties"].get("name", "Unnamed Feature"),
            geometry=GEOSGeometry(json.dumps(feature["geometry"])),
            layer=layer,
        )
        for feature in features
    ]

    # Bulk create Feature instances
    Feature.objects.bulk_create(feature_instances)

    # Create Property instances
    property_instances = [
        Property(key=key, value=value, feature=feature_instance)
        for feature_instance, feature in zip(feature_instances, features)
        for key, value in feature["properties"].items()
    ]

    # Bulk create Property instances
    Property.objects.bulk_create(property_instances)
    return layer.id

def transform_geom(transformer, geom):
    """Transforms the geometry to EPSG:4326."""
    if geom["type"] == "Point":
        x, y = transformer.transform(geom["coordinates"][0], geom["coordinates"][1])
        return {"type": "Point", "coordinates": [x, y]}
    elif geom["type"] == "LineString":
        coords = [transformer.transform(x, y) for x, y in geom["coordinates"]]
        return {"type": "LineString", "coordinates": coords}
    elif geom["type"] == "Polygon":
        coords = [[transformer.transform(x, y) for x, y in ring] for ring in geom["coordinates"]]
        return {"type": "Polygon", "coordinates": coords}
    # Add support for other geometry types as needed
    else:
        raise ValueError(f"Unsupported geometry type: {geom['type']}")

Explanation:
Determine File Type: The file extension is checked to determine if the file is a GeoJSON, zipped shapefile, KML, or CSV with WKT.
Handle GeoJSON: If the file is a GeoJSON, it is read directly.
Handle Shapefile: If the file is a zipped shapefile, it is extracted and read using fiona. The CRS is checked and reprojected to

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