From f5ce01996297c7c26f3d424e3ddbc13a5da0f6be Mon Sep 17 00:00:00 2001 From: lastminutediorama Date: Wed, 21 Aug 2024 22:29:21 -0500 Subject: [PATCH] simplify union function --- .../palmsprings_sanbernardino.geojson | 120 ++++++++++++++++++ src/planscape/planning/serializers.py | 2 +- src/planscape/planning/services.py | 34 +++-- src/planscape/planning/views.py | 1 - src/planscape/planning/views_v2.py | 44 ++----- 5 files changed, 159 insertions(+), 42 deletions(-) create mode 100644 src/planscape/planning/fixtures/palmsprings_sanbernardino.geojson diff --git a/src/planscape/planning/fixtures/palmsprings_sanbernardino.geojson b/src/planscape/planning/fixtures/palmsprings_sanbernardino.geojson new file mode 100644 index 000000000..5af271037 --- /dev/null +++ b/src/planscape/planning/fixtures/palmsprings_sanbernardino.geojson @@ -0,0 +1,120 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "bbox": [ + -116.59000730822105, + 33.76094007811721, + -116.46372359000742, + 33.86288159982813 + ], + "type": "Polygon", + "coordinates": [ + [ + [ + -116.5301572048023, + 33.86139064872427 + ], + [ + -116.53075570583647, + 33.85840866841697 + ], + [ + -116.53195270790485, + 33.86288159982813 + ], + [ + -116.52955870376809, + 33.86089365923777 + ], + [ + -116.46551909311, + 33.83454907768415 + ], + [ + -116.46372359000742, + 33.792281376404524 + ], + [ + -116.52058118825529, + 33.76094007811721 + ], + [ + -116.58521929994758, + 33.776860900708314 + ], + [ + -116.59000730822105, + 33.82709158924555 + ], + [ + -116.57624178443476, + 33.84449138356989 + ], + [ + -116.5301572048023, + 33.86139064872427 + ] + ] + ] + }, + "properties": { + "id": null + } + }, + { + "type": "Feature", + "geometry": { + "bbox": [ + -117.36393144521908, + 34.03950544279183, + -117.19620895578856, + 34.14367901861189 + ], + "type": "Polygon", + "coordinates": [ + [ + [ + -117.30765613626542, + 34.14367901861189 + ], + [ + -117.28117363793427, + 34.14093925150775 + ], + [ + -117.22048457925877, + 34.13363277157728 + ], + [ + -117.19620895578856, + 34.08886680080183 + ], + [ + -117.22158801668924, + 34.03950544279183 + ], + [ + -117.34517300890116, + 34.07058816903606 + ], + [ + -117.36393144521908, + 34.10074579569706 + ], + [ + -117.30765613626542, + 34.14367901861189 + ] + ] + ] + }, + "properties": { + "id": null + } + } + ], + "fileName": "palmsprings_sanbernardino/palm_springs_san_bernardino" +} \ No newline at end of file diff --git a/src/planscape/planning/serializers.py b/src/planscape/planning/serializers.py index bbb910c4c..8a01a3551 100644 --- a/src/planscape/planning/serializers.py +++ b/src/planscape/planning/serializers.py @@ -14,6 +14,7 @@ User, UserPrefs, ) +from planning.services import get_acreage from planscape.exceptions import InvalidGeometry from stands.models import StandSizeChoices @@ -351,7 +352,6 @@ class Meta: "uuid", "scenario", "name", - "origin", "data", "geometry", "created_by", diff --git a/src/planscape/planning/services.py b/src/planscape/planning/services.py index d60060973..8f0d88fec 100644 --- a/src/planscape/planning/services.py +++ b/src/planscape/planning/services.py @@ -9,7 +9,7 @@ from pathlib import Path from typing import Any, Dict, Tuple, Type, Union from django.conf import settings -from django.contrib.gis.geos import GEOSGeometry, MultiPolygon +from django.contrib.gis.geos import GEOSGeometry, MultiPolygon, Polygon from django.db import transaction from django.utils.timezone import now from fiona.crs import from_epsg @@ -26,7 +26,6 @@ ScenarioResultStatus, ScenarioStatus, ) -from planning.serializers import ProjectAreaSerializer from planning.tasks import async_forsys_run from planscape.exceptions import InvalidGeometry from stands.models import StandSizeChoices, area_from_size @@ -116,6 +115,27 @@ def create_scenario(user: UserType, **kwargs) -> Scenario: return scenario +def union_geojson(uploaded_geojson): + geometries = [] + if "features" in uploaded_geojson: + for feature in uploaded_geojson["features"]: + try: + geom = GEOSGeometry(json.dumps(feature["geometry"]), srid=4326) + if isinstance(geom, (Polygon, MultiPolygon)): + geometries.append(geom) + except Exception as e: + print(f"Error processing feature: {e}") + else: + geometries.append(GEOSGeometry(json.dumps(uploaded_geojson), srid=4326)) + if not geometries: + raise ValueError("No valid polygon geometries found") + unioned_geometry = geometries[0] + for geom in geometries[1:]: + unioned_geometry = unioned_geometry.union(geom) + + return unioned_geometry + + def feature_to_project_area(idx: int, user_id: int, scenario, feature): try: project_area = { @@ -154,6 +174,7 @@ def create_scenario_from_upload( target=scenario.planning_area, ) + # handle just a polygon if "type" in uploaded_geom and uploaded_geom["type"] == "Polygon": new_feature = feature_to_project_area( 1, scenario.user, scenario, json.dumps(uploaded_geom) @@ -161,16 +182,9 @@ def create_scenario_from_upload( uploaded_geom.setdefault("properties", {}) uploaded_geom["properties"]["project_id"] = new_feature.pk - # # this handles a format provided by shpjs when a shapefile has multiple features - # if "geometry" in uploaded_geom and "coordinates" in uploaded_geom["geometry"]: - # for idx, f in enumerate(uploaded_geom["geometry"]["coordinates"], 1): - # feature_obj = {"type": "Polygon", "coordinates": [f]} - # feature_to_project_area(idx, scenario.user, scenario, feature_obj) - # this handles a more standard FeatureCollection if "features" in uploaded_geom: for idx, f in enumerate(uploaded_geom["features"], 1): - print(f"do we have...mutliple feature? {f}") new_feature = feature_to_project_area( idx, scenario.user, scenario, json.dumps(f["geometry"]) ) @@ -178,7 +192,7 @@ def create_scenario_from_upload( f.setdefault("properties", {}) f["properties"]["project_id"] = new_feature.pk - # Store updated in ScenarioResult.result + # Store geometry with added properties into ScenarioResult.result ScenarioResult.objects.create(scenario=scenario, result=uploaded_geom) return scenario diff --git a/src/planscape/planning/views.py b/src/planscape/planning/views.py index ee1bf45ee..c73212fcd 100644 --- a/src/planscape/planning/views.py +++ b/src/planscape/planning/views.py @@ -3,7 +3,6 @@ import os from base.region_name import display_name_to_region from django.conf import settings -from planning.geometry import coerce_geometry from django.db import transaction from django.db import IntegrityError from django.db.models import Count, Max diff --git a/src/planscape/planning/views_v2.py b/src/planscape/planning/views_v2.py index 55fc9fbb1..34c783c37 100644 --- a/src/planscape/planning/views_v2.py +++ b/src/planscape/planning/views_v2.py @@ -27,6 +27,7 @@ ScenarioSerializer, ListCreatorSerializer, UploadedScenarioSerializer, + UploadedFeatureCollectionSerializer, ) from planning.services import ( create_planning_area, @@ -35,6 +36,7 @@ delete_scenario, toggle_scenario_status, create_scenario_from_upload, + union_geojson, ) User = get_user_model() @@ -174,39 +176,21 @@ def toggle_status(self, request, pk=None): @action(methods=["POST"], detail=False) def upload_shapefiles(self, request, pk=None, *args, **kwargs): stand_size = request.data["stand_size"] - - # TODO: cleanup dict vs string here - uploaded_geom = request.data["geometry"] - if isinstance(uploaded_geom, str): - uploaded_geojson = json.loads(uploaded_geom) - else: - uploaded_geojson = uploaded_geom - uploaded_geos = None - - # TODO: refactor this to a service - if "features" in uploaded_geojson: - geometries = [ - GEOSGeometry(json.dumps(feature["geometry"])) - for feature in uploaded_geojson["features"] - ] - # Combine all polygons into a single polygon or multipolygon - combined_geometry = geometries[0] - for geom in geometries[1:]: - combined_geometry = combined_geometry.union(geom) - uploaded_geos = GEOSGeometry(combined_geometry, srid=4326) - else: - uploaded_geos = GEOSGeometry(uploaded_geom, srid=4326) - scenario_name = request.data["name"] planning_area_pk = request.data["planning_area"] + uploaded_geom = request.data["geometry"] - pa = PlanningArea.objects.get(pk=planning_area_pk) - - # TODO: validate uploaded geom w serializer - - if uploaded_geos.geom_type == "MultiPolygon": - uploaded_geos = uploaded_geos.union(uploaded_geos) + # ensure we have a geojson obj + if isinstance(uploaded_geom, str): + try: + uploaded_geom = json.loads(uploaded_geom) + except json.JSONDecodeError: + raise ValueError("Invalid JSON string") + # Union features and confirm that they're inside the planning area + uploaded_geos = union_geojson(uploaded_geom) + pa = PlanningArea.objects.get(pk=planning_area_pk) + # TODO: check if it's inside pa.geometry if not pa.geometry.contains(uploaded_geos): return Response( { @@ -229,7 +213,7 @@ def upload_shapefiles(self, request, pk=None, *args, **kwargs): # now we create a scenario new_scenario = create_scenario_from_upload( scenario_data=scenario_serializer.validated_data, - uploaded_geom=uploaded_geojson, + uploaded_geom=uploaded_geom, ) out_serializer = ScenarioProjectAreasSerializer(instance=new_scenario)