From dd1c086ee15869d488698ab3be592d7cdb48761a Mon Sep 17 00:00:00 2001 From: George Silva <863039+george-silva@users.noreply.github.com> Date: Mon, 23 Dec 2024 10:32:55 -0300 Subject: [PATCH] creates separate function and tests (#2033) --- src/planscape/planning/serializers.py | 18 +++---- src/planscape/planning/services.py | 44 +++++++++++++++- .../test_data/project_areas_for_pa_covers.cpg | 1 + .../test_data/project_areas_for_pa_covers.dbf | Bin 0 -> 8616 bytes .../test_data/project_areas_for_pa_covers.prj | 1 + .../test_data/project_areas_for_pa_covers.shp | Bin 0 -> 14100 bytes .../test_data/project_areas_for_pa_covers.shx | Bin 0 -> 180 bytes src/planscape/planning/tests/test_services.py | 49 ++++++++++++++++++ src/planscape/planning/tests/test_v2_views.py | 6 +-- 9 files changed, 103 insertions(+), 16 deletions(-) create mode 100644 src/planscape/planning/tests/test_data/project_areas_for_pa_covers.cpg create mode 100644 src/planscape/planning/tests/test_data/project_areas_for_pa_covers.dbf create mode 100644 src/planscape/planning/tests/test_data/project_areas_for_pa_covers.prj create mode 100644 src/planscape/planning/tests/test_data/project_areas_for_pa_covers.shp create mode 100644 src/planscape/planning/tests/test_data/project_areas_for_pa_covers.shx diff --git a/src/planscape/planning/serializers.py b/src/planscape/planning/serializers.py index 56ff3ca92..96522ba58 100644 --- a/src/planscape/planning/serializers.py +++ b/src/planscape/planning/serializers.py @@ -19,7 +19,7 @@ User, UserPrefs, ) -from planning.services import get_acreage, union_geojson +from planning.services import get_acreage, planning_area_covers, union_geojson from planscape.exceptions import InvalidGeometry from stands.models import Stand, StandSizeChoices @@ -679,14 +679,8 @@ def _is_inside_planning_area(self, geometry, planning_area_id, stand_size) -> bo except PlanningArea.DoesNotExist: raise serializers.ValidationError("Planning area does not exist.") - if planning_area.geometry.covers(uploaded_geos): - return True - - all_stands_geometry = Stand.objects.within_polygon( - planning_area.geometry, stand_size - ).aggregate(geometry=UnionOp("geometry"))["geometry"] - - if all_stands_geometry and all_stands_geometry.covers(uploaded_geos): - return True - - return False + return planning_area_covers( + planning_area=planning_area, + geometry=uploaded_geos, + stand_size=stand_size, + ) diff --git a/src/planscape/planning/services.py b/src/planscape/planning/services.py index a3f20e5a5..37e63e464 100644 --- a/src/planscape/planning/services.py +++ b/src/planscape/planning/services.py @@ -11,6 +11,7 @@ from typing import Any, Dict, Optional, Tuple, Type, Union from django.conf import settings from django.contrib.gis.geos import GEOSGeometry, MultiPolygon, Polygon +from django.contrib.gis.db.models import Union as UnionOp from django.db import transaction from django.utils.timezone import now from fiona.crs import from_epsg @@ -29,7 +30,7 @@ ) from planning.tasks import async_forsys_run from planscape.exceptions import InvalidGeometry -from stands.models import StandSizeChoices, area_from_size +from stands.models import Stand, StandSizeChoices, area_from_size from utils.geometry import to_multi from actstream import action @@ -394,3 +395,44 @@ def create_projectarea_note(user: TUser, **kwargs) -> Scenario: } note = ProjectAreaNote.objects.create(**data) return note + + +def planning_area_covers( + planning_area: PlanningArea, + geometry: GEOSGeometry, + stand_size: StandSizeChoices, + buffer_size: float = -1.0, +) -> bool: + """Specialized version of `covers` predicate for Planning Area. + This is necessary because some times our users want to upload + project areas that are slightly off the planning area. So this + function first considers the Planning Area itself, then all the + stands that make up the planning area and lastly it considers + a buffered version of the test geometry (negative means smaller). + """ + if planning_area.geometry.covers(geometry): + logger.info("Planning Area covers geometry using DE9IM matrix.") + return True + + all_stands = Stand.objects.within_polygon( + planning_area.geometry, + stand_size, + ).aggregate(geometry=UnionOp("geometry"))["geometry"] + + if all_stands is None: + return False + + if all_stands.covers(geometry): + logger.info("Planning Area covers geometry using stands DE9IM matrix.") + return True + + # units here are in meters + test_geometry = geometry.transform(settings.AREA_SRID, clone=True) + test_geometry = test_geometry.buffer(buffer_size).transform(4269, clone=True) + + if all_stands.covers(test_geometry): + logger.info( + "Planning Area covers geometry using a buffered version of test geometry." + ) + return True + return False diff --git a/src/planscape/planning/tests/test_data/project_areas_for_pa_covers.cpg b/src/planscape/planning/tests/test_data/project_areas_for_pa_covers.cpg new file mode 100644 index 000000000..cd89cb975 --- /dev/null +++ b/src/planscape/planning/tests/test_data/project_areas_for_pa_covers.cpg @@ -0,0 +1 @@ +ISO-8859-1 \ No newline at end of file diff --git a/src/planscape/planning/tests/test_data/project_areas_for_pa_covers.dbf b/src/planscape/planning/tests/test_data/project_areas_for_pa_covers.dbf new file mode 100644 index 0000000000000000000000000000000000000000..090b6490d8da9249f8497e82195ab05cad9611bb GIT binary patch literal 8616 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 R%ltr#j=y>D|M#n}{{w2|xPt%y literal 0 HcmV?d00001 diff --git a/src/planscape/planning/tests/test_data/project_areas_for_pa_covers.prj b/src/planscape/planning/tests/test_data/project_areas_for_pa_covers.prj new file mode 100644 index 000000000..5ded4bcac --- /dev/null +++ b/src/planscape/planning/tests/test_data/project_areas_for_pa_covers.prj @@ -0,0 +1 @@ +GEOGCS["GCS_North_American_1983",DATUM["D_North_American_1983",SPHEROID["GRS_1980",6378137.0,298.257222101]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]] \ No newline at end of file diff --git a/src/planscape/planning/tests/test_data/project_areas_for_pa_covers.shp b/src/planscape/planning/tests/test_data/project_areas_for_pa_covers.shp new file mode 100644 index 0000000000000000000000000000000000000000..14c6f3d0b179fbda650c54e2aef5e648e87bd96f GIT binary patch literal 14100 zcmZwOcU;Zy|3C0{N*bCXEwuM8ajFxgC8Fsxq#_y`iWW_UG!&6&OK3=vbTnwtAe3kk z6-q-&>-RX{@3-6M_dCCDx6k{J`{#L`*SN0d^SZ9r>l`#R609`;>)*!s5qcULM#NaV zI5TJUNo1|N!)L1`hy*sXx|y9cB=Q5%vck9oBH@4i`2YKV($dh--J%@J_BO}#Z+Q}V zVD0$*kOM?Q1VcaZgD8n?9j>>mdW=XwLmJEf`RvrAmvgEzB=WgW?nzBHM8clbZ5nIf z-!e^(WLqL(R+?aN7FM*owJXq$NKpQ8<*XHKlIp|fWlto~Is8g}4Le=&TM&035s^UJ4__JcT6_h44cIl-8P38 z99NPjaXuS=wvb(}M1r2>`D-R{(yr>4qnZT zH3cs#QYA=az8wQ|VTcK$e1{ZOrudW>4*N<&5-tn2zv+3#8ig=1&kMGef4U3j?w%k2j#|v{E&J1~dVVz`kt>dWU& zg$2fgi;?{+6Kwj2FRckaUEsF#6u#Y@e%K4+dr-Xkz!`W!%FN_D9CyCRO$o<4D%vY> z5&1!g)hnMse2et)zg4jQQJJZ~2Z@BTeYG`?$glO`yVW1Tv|%h)F2I7Orhd;bS4pvn zSt%nQSLd$i>*@w~;RZ9KB4xBUvazGHM1BYEll%A^?eDyJIM)m>KG^g+A90P5p-OMm z!6I1OO+`G06XVqN zfj8$Z{M|DoYz@Y7=RKJfQ*G4gRP{^^ywy=4M+6S|6g^&nad#N@Wy*jHE{?i*A}^F> zJ~!^vA(3yrAA2te$EXJ@R>22Xdi49?&*H_s7vQ3IPI~W=SF6`vsNsJ%0eN^ar*5tX zaaTI8yOGG-W$8|7Rrrbk%Z?ZD$Y5O?Gpx)lv;7(Je8M_m-Wlg-D%t;n26Z8xrkSP& zr+>+;n}atrJgG{={pbYjC+eV%LY|3P9ER_YHSSPB-L;({dDdu>$nyQnj{{%@+lcwg zuzgl`$qCe{_5J|%9~xNSTzy1K;4`j!uRMeOf>Z|(nUD(+V{leqlGsmm z5;@}MsemEO6Jo7%lDlALj@1y~S^0me zByzagXv7_~ztEE{X9bsaB)|Fr&#qyu|ENMDr{&6QRfl65y;Q8t9hg~B>CqS5f0PU7T1D6`<<_(uEV5gu(gAjkyrqc?A0VBVlqAC*tz`IbLxXkIjE*`qB{R$~{*2=6_lDsMkg6b(4BsHFJuXw8pwv zCfV;^g?u{MGWdE4=Bg;SKLsaQz7Lkh`5MTf5#h+cN`t@V9q?krM^9b2MlEl85w?2O zc*zj?Y&9JHJR8@0aNk{DdiWvRIH?^z`mQp0E%N_aP=(q-c&(Rw*8$YaqO8E%bMR)3 zyR(N-Pj@!>CrhG#O}4mnZG^pCk|htoy)G*=;;7?6y$1vT;0a4Y%OL99dGx%u1jgsz z($dH%<^jn`cahg{Ohj`<0etcKO2It*C8^-e1I#14C9*8DFy8K3%V{Sl^UByvXDd8e zuvi*^+GqNDz~2}7vN{)(24rWB!MvrX*N!3{wZKTYjC}KdeXBYjw#i8GVL?7#?{D4f zfa4i;S1*4;T>RFN?N&HH6We+9gNUDb@UWc)*P9feFt7lxx!vXS67g!D9l`;~|DKIy z3n7RH6NZ}vQ9sXL@g!-(?j_r0cf)6szuweG`^UIBe;EBoJ%3?J!_Jhhgfb7(?YpJ8h&4coE%nF> z#_pPjO3NmG5;-wFY10PZ|DFf;Qzk+DQLm5G>nHX4O1=J4ug}!$H}(2Xz5Y}052*JK z)cXtS{Rj2_gnIw7a9DvO5`CesHd)q=d1ke5PD{45M`Lfj@2;#|JK`cg`BFkXiG)K; zOteyP#dRh1TDYHEe|7=uEjuym@1x^H0zFqx-2r%}O*?rGj#!Sm{R!)JVVanSKFmZT z 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 literal 0 HcmV?d00001 diff --git a/src/planscape/planning/tests/test_data/project_areas_for_pa_covers.shx b/src/planscape/planning/tests/test_data/project_areas_for_pa_covers.shx new file mode 100644 index 0000000000000000000000000000000000000000..f875310234d384a5a9504f8b2688aea4bedea8a9 GIT binary patch literal 180 zcmZQzQ0HR64x(N#Gcd3M<+!d0%>CvRcffM>)5e|ZE)G1thrZY|#2r|#xM@X4w~GUI z?I@x~3=B*QfcP2^HvmnI0@5287&!PKe6C3lKCc@C12a%w;1q-}TnD6q`oxrg^aTb6 JiElu90s!hE8=L?D literal 0 HcmV?d00001 diff --git a/src/planscape/planning/tests/test_services.py b/src/planscape/planning/tests/test_services.py index 3a3e1454e..6756118fd 100644 --- a/src/planscape/planning/tests/test_services.py +++ b/src/planscape/planning/tests/test_services.py @@ -1,15 +1,20 @@ from datetime import date, datetime +import json import shutil +from turtle import st from django.contrib.gis.geos import GEOSGeometry, MultiPolygon from django.test import TestCase, TransactionTestCase import fiona +import shapely from planscape.tests.factories import UserFactory +from planning.tests.factories import PlanningAreaFactory from fiona.crs import to_string from planning.services import ( export_to_shapefile, get_max_treatable_area, get_max_treatable_stand_count, get_schema, + planning_area_covers, validate_scenario_treatment_ratio, ) from planning.models import ( @@ -20,6 +25,7 @@ ScenarioResultStatus, ) from stands.models import Stand, StandSizeChoices +from utils import geometry class MaxTreatableAreaTest(TestCase): @@ -248,3 +254,46 @@ def test_export_creates_file(self): self.assertEqual(1, len(source)) self.assertEqual(to_string(source.crs), "EPSG:4269") shutil.rmtree(str(output)) + + +class TestPlanningAreaCovers(TestCase): + def setUp(self): + self.real_world_geom = "MULTIPOLYGON (((-120.592804 40.388397, -120.653229 40.089629, -121.098175 40.043386, -121.308289 40.179923, -121.059723 40.687928, -120.433502 41.088667, -120.013275 41.096947, -120.009155 40.701464, -120.010529 39.949753, -120.592804 40.388397)))" + self.real_world_planning_area = PlanningAreaFactory.create( + geometry=GEOSGeometry(self.real_world_geom, srid=4269) + ) + self.covers_de9im = GEOSGeometry( + "POLYGON ((-121.13497533859726 40.378055548860004, -120.5974285753569 40.45918503498109, -120.0351560371661 40.02304123541387, -120.0295412055665 41.05622374781322, -120.40813814721828 41.0493352414621, -120.99739165146956 40.63587551109521, -121.13497533859726 40.378055548860004))", + srid=4269, + ) + # in this is not necessary to create the stands, they are present by the usage of a migration + # that autoloads the LARGE stands. + + def test_real_world(self): + with fiona.open( + "planning/tests/test_data/project_areas_for_pa_covers.shp" + ) as shapefile: + features = [f for f in shapefile] + # this convoluted conversion step is because Django automatically + # considers geometries coming FROM geojson to be 4326 + geometries = [shapely.geometry.shape(f.geometry) for f in features] + geometries = MultiPolygon( + [GEOSGeometry(g.wkt, srid=4269) for g in geometries], srid=4269 + ) + test_geometry = geometries.unary_union + self.assertTrue( + planning_area_covers( + self.real_world_planning_area, + test_geometry, + stand_size=StandSizeChoices.LARGE, + ) + ) + + def test_de9im_covers(self): + self.assertTrue( + planning_area_covers( + self.real_world_planning_area, + self.covers_de9im, + stand_size=StandSizeChoices.LARGE, + ) + ) diff --git a/src/planscape/planning/tests/test_v2_views.py b/src/planscape/planning/tests/test_v2_views.py index 92a9ffd79..89eab35be 100644 --- a/src/planscape/planning/tests/test_v2_views.py +++ b/src/planscape/planning/tests/test_v2_views.py @@ -1088,7 +1088,7 @@ def test_confirm_permissions_required(self): payload = { "geometry": json.dumps(self.riverside), "name": "new scenario", - "stand_size": "SMALL", + "stand_size": "LARGE", "planning_area": self.planning_area.pk, } response = self.client.post( @@ -1127,7 +1127,7 @@ def test_create_from_multi_feature_shpjs(self): payload = { "geometry": json.dumps(self.pasadena_pomona), "name": "new scenario", - "stand_size": "SMALL", + "stand_size": "LARGE", "planning_area": self.planning_area.pk, } response = self.client.post( @@ -1155,7 +1155,7 @@ def test_create_uncontained_geometry(self): payload = { "geometry": json.dumps(self.sandiego), "name": "new scenario", - "stand_size": "SMALL", + "stand_size": "LARGE", "planning_area": self.planning_area.pk, } response = self.client.post(