Skip to content

Commit

Permalink
creates separate function and tests (#2033)
Browse files Browse the repository at this point in the history
  • Loading branch information
george-silva authored Dec 23, 2024
1 parent 1fa1d43 commit dd1c086
Show file tree
Hide file tree
Showing 9 changed files with 103 additions and 16 deletions.
18 changes: 6 additions & 12 deletions src/planscape/planning/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
)
44 changes: 43 additions & 1 deletion src/planscape/planning/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ISO-8859-1
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -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]]
Binary file not shown.
Binary file not shown.
49 changes: 49 additions & 0 deletions src/planscape/planning/tests/test_services.py
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -20,6 +25,7 @@
ScenarioResultStatus,
)
from stands.models import Stand, StandSizeChoices
from utils import geometry


class MaxTreatableAreaTest(TestCase):
Expand Down Expand Up @@ -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,
)
)
6 changes: 3 additions & 3 deletions src/planscape/planning/tests/test_v2_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down

0 comments on commit dd1c086

Please sign in to comment.