From 40c17fb51a610def99e05fffc737e0bb81190063 Mon Sep 17 00:00:00 2001 From: Paul Schreiber Date: Tue, 28 Nov 2023 21:20:13 -0500 Subject: [PATCH] feat: Add support for GPX map uploads (#1017) * feat: add GPX parser * test: add GPX tests --- terraso_backend/apps/core/gis/parsers.py | 16 ++ .../tests/core/gis/test_parsers.py | 186 ++++++++++++++++++ terraso_backend/tests/graphql/conftest.py | 15 ++ terraso_backend/tests/shared_data/conftest.py | 16 ++ 4 files changed, 233 insertions(+) diff --git a/terraso_backend/apps/core/gis/parsers.py b/terraso_backend/apps/core/gis/parsers.py index 17e90f5c9..115bacdf4 100644 --- a/terraso_backend/apps/core/gis/parsers.py +++ b/terraso_backend/apps/core/gis/parsers.py @@ -28,6 +28,7 @@ logger = structlog.get_logger(__name__) supported_drivers["KML"] = "rw" +supported_drivers["GPX"] = "rw" def is_geojson_file_extension(file): @@ -56,6 +57,10 @@ def is_kmz_file_extension(file): return file.name.endswith(".kmz") +def is_gpx_file_extension(file): + return file.name.endswith(".gpx") + + def parse_kml_file(file): gdf = gpd.read_file(file, driver="KML") return json.loads(gdf.to_json()) @@ -106,6 +111,11 @@ def parse_shapefile(file): return json.loads(gdf_transformed.to_json()) +def parse_gpx_file(file): + gdf = gpd.read_file(file, driver="GPX") + return json.loads(gdf.to_json()) + + def parse_file_to_geojson(file): if is_shape_file_extension(file): try: @@ -131,5 +141,11 @@ def parse_file_to_geojson(file): except Exception as e: logger.error("Error parsing geojson file", error=e) raise ValidationError("invalid_geojson_file") + elif is_gpx_file_extension(file): + try: + return parse_gpx_file(file) + except Exception as e: + logger.error("Error parsing gpx file", error=e) + raise ValidationError("invalid_gpx_file") else: raise ValidationError("invalid_file_type") diff --git a/terraso_backend/tests/core/gis/test_parsers.py b/terraso_backend/tests/core/gis/test_parsers.py index 1e54e1f83..1c86d2ebc 100644 --- a/terraso_backend/tests/core/gis/test_parsers.py +++ b/terraso_backend/tests/core/gis/test_parsers.py @@ -68,6 +68,36 @@ """ +GPX_CONTENT = """ + + + 0 + + + + + + 0 + + + + + + 0 + + + + + + 0 + + + + +""" + KML_GEOJSON = { "type": "FeatureCollection", "features": [ @@ -115,6 +145,132 @@ ], } +GPX_GEOJSON = { + "type": "FeatureCollection", + "features": [ + { + "id": "0", + "type": "Feature", + "properties": { + "ele": 0.0, + "time": None, + "magvar": None, + "geoidheight": None, + "name": "Portland", + "cmt": "Waypoint no: 1", + "desc": "This is waypoint no: 1", + "src": None, + "link1_href": None, + "link1_text": None, + "link1_type": None, + "link2_href": None, + "link2_text": None, + "link2_type": None, + "sym": None, + "type": None, + "fix": None, + "sat": None, + "hdop": None, + "vdop": None, + "pdop": None, + "ageofdgpsdata": None, + "dgpsid": None, + }, + "geometry": {"type": "Point", "coordinates": [-122.681944, 45.52]}, + }, + { + "id": "1", + "type": "Feature", + "properties": { + "ele": 0.0, + "time": None, + "magvar": None, + "geoidheight": None, + "name": "Rio de Janeiro", + "cmt": "Waypoint no: 2", + "desc": "This is waypoint no: 2", + "src": None, + "link1_href": None, + "link1_text": None, + "link1_type": None, + "link2_href": None, + "link2_text": None, + "link2_type": None, + "sym": None, + "type": None, + "fix": None, + "sat": None, + "hdop": None, + "vdop": None, + "pdop": None, + "ageofdgpsdata": None, + "dgpsid": None, + }, + "geometry": {"type": "Point", "coordinates": [-43.196389, -22.908333]}, + }, + { + "id": "2", + "type": "Feature", + "properties": { + "ele": 0.0, + "time": None, + "magvar": None, + "geoidheight": None, + "name": "Istanbul", + "cmt": "Waypoint no: 3", + "desc": "This is waypoint no: 3", + "src": None, + "link1_href": None, + "link1_text": None, + "link1_type": None, + "link2_href": None, + "link2_text": None, + "link2_type": None, + "sym": None, + "type": None, + "fix": None, + "sat": None, + "hdop": None, + "vdop": None, + "pdop": None, + "ageofdgpsdata": None, + "dgpsid": None, + }, + "geometry": {"type": "Point", "coordinates": [28.976018, 41.01224]}, + }, + { + "id": "3", + "type": "Feature", + "properties": { + "ele": 0.0, + "time": None, + "magvar": None, + "geoidheight": None, + "name": "Reykjavik", + "cmt": "Waypoint no: 4", + "desc": "This is waypoint no: 4", + "src": None, + "link1_href": None, + "link1_text": None, + "link1_type": None, + "link2_href": None, + "link2_text": None, + "link2_type": None, + "sym": None, + "type": None, + "fix": None, + "sat": None, + "hdop": None, + "vdop": None, + "pdop": None, + "ageofdgpsdata": None, + "dgpsid": None, + }, + "geometry": {"type": "Point", "coordinates": [-21.933333, 64.133333]}, + }, + ], +} + @pytest.fixture def shapefile_zip(request): @@ -181,3 +337,33 @@ def test_parse_kml_file(kml_file): # Assert that the output of the parse_kml_file function is as expected assert kml_json == KML_GEOJSON + + +@pytest.fixture +def gpx_file(request): + gpx_contents, file_extension = request.param + # Create a temporary file + with tempfile.NamedTemporaryFile(mode="w", suffix=f".{file_extension}", delete=False) as f: + # Write the GPX content to the file + f.write(gpx_contents) + + # Return the file path + yield f.name + + # Clean up: delete the temporary file + os.unlink(f.name) + + +@pytest.mark.parametrize( + "gpx_file", + [ + (GPX_CONTENT, "gpx"), + ], + indirect=True, +) +def test_parse_gpx_file(gpx_file): + with open(gpx_file, "rb") as file: + gpx_json = parse_file_to_geojson(file) + + # Assert that the output of the parse_gpx_file function is as expected + assert gpx_json == GPX_GEOJSON diff --git a/terraso_backend/tests/graphql/conftest.py b/terraso_backend/tests/graphql/conftest.py index 02478a8a5..92ffa15aa 100644 --- a/terraso_backend/tests/graphql/conftest.py +++ b/terraso_backend/tests/graphql/conftest.py @@ -333,6 +333,21 @@ def data_entry_kml(users, groups): ) +@pytest.fixture +def data_entry_gpx(users, groups): + creator = users[0] + creator_group = groups[0] + creator_group.members.add(creator) + return mixer.blend( + DataEntry, + created_by=creator, + size=100, + groups=creator_group, + entry_type=DataEntry.ENTRY_TYPE_FILE, + resource_type="gpx", + ) + + @pytest.fixture def data_entry_shapefile(users, groups): creator = users[0] diff --git a/terraso_backend/tests/shared_data/conftest.py b/terraso_backend/tests/shared_data/conftest.py index 910409234..4b01a4f6b 100644 --- a/terraso_backend/tests/shared_data/conftest.py +++ b/terraso_backend/tests/shared_data/conftest.py @@ -101,3 +101,19 @@ def visualization_config_kml(user): ), created_by=user, ) + + +@pytest.fixture +def visualization_config_gpx(user): + return mixer.blend( + VisualizationConfig, + size=1, + data_entry=mixer.blend( + DataEntry, + size=1, + url=f"{settings.DATA_ENTRY_FILE_BASE_URL}/{user.id}/test_data.gpx", + created_by=user, + resource_type="gpx", + ), + created_by=user, + )