Skip to content

Commit

Permalink
[TM-799] Finished polygon post endpoint.
Browse files Browse the repository at this point in the history
  • Loading branch information
roguenet committed May 10, 2024
1 parent e2ea75b commit 5a26382
Show file tree
Hide file tree
Showing 18 changed files with 206 additions and 58 deletions.
32 changes: 29 additions & 3 deletions app/Http/Controllers/V2/Sites/StoreBulkSiteGeometryController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,22 @@
namespace App\Http\Controllers\V2\Sites;

use App\Http\Controllers\Controller;
use App\Models\V2\PolygonGeometry;
use App\Models\V2\Sites\Site;
use App\Services\PolygonService;
use App\Validators\SitePolygonValidator;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException;

class StoreBulkSiteGeometryController extends Controller
{
public const VALIDATIONS = [
PolygonService::OVERLAPPING_CRITERIA_ID => 'NOT_OVERLAPPING',
PolygonService::SELF_CRITERIA_ID => 'SELF_INTERSECTION_UUID',
PolygonService::COORDINATE_SYSTEM_CRITERIA_ID => 'FEATURE_BOUNDS_UUID',
PolygonService::SIZE_CRITERIA_ID => 'POLYGON_SIZE_UUID',
PolygonService::WITHIN_COUNTRY_CRITERIA_ID => 'WITHIN_COUNTRY',
PolygonService::SPIKE_CRITERIA_ID => 'SPIKES_UUID',
Expand All @@ -41,22 +44,45 @@ public function __invoke(Request $request, Site $site): JsonResponse

// Do the validation in a separate step so that all of the existing polygons are taken into account
// for things like overlapping and estimated area.
$polygonErrors = [];
foreach ($polygonUuids as $polygonUuid) {
$data = ['polygon_uuid' => $polygonUuid];
$data = ['geometry' => $polygonUuid];
foreach (self::VALIDATIONS as $criteriaId => $validation) {
$valid = true;

try {
SitePolygonValidator::validate($validation, $data);
} catch (ValidationException $exception) {
$valid = false;
Log::info('ValidationException: ' . $validation . ', ' . $exception->getMessage());
$polygonErrors[$polygonUuid][] = json_decode($exception->errors()['geometry'][0]);
}

$service->createCriteriaSite($polygonUuid, $criteriaId, $valid);
// TODO: accumulate errors and add to response
}

// For these two, the createGeojsonModels already handled creating the site criteria, so we just need to
// report on them if not valid
$polygon = PolygonGeometry::isUuid($polygonUuid)->select('uuid')->first();
$schemaCriteria = $polygon->criteriaSite()->forCriteria(PolygonService::SCHEMA_CRITERIA_ID)->first();
if ($schemaCriteria != null && ! $schemaCriteria->valid) {
$polygonErrors[$polygonUuid][] = [
'key' => 'TABLE_SCHEMA',
'message' => 'The properties for the geometry are missing some required values.',
];
} else {
// only report data validation if the schema was valid. When the schema is invalid, the data is
// always invalid as well.
$dataCriteria = $polygon->criteriaSite()->forCriteria(PolygonService::DATA_CRITERIA_ID)->first();
if ($dataCriteria != null && ! $dataCriteria->valid) {
$polygonErrors[$polygonUuid][] = [
'key' => 'DATA_COMPLETED',
'message' => 'The properties for the geometry have some invalid values.',
];
}
}
}

return response()->json(['polygon_uuids' => $polygonUuids]);
return response()->json(['polygon_uuids' => $polygonUuids, 'errors' => $polygonErrors], 201);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
use App\Models\V2\WorldCountryGeneralized;
use App\Services\PolygonService;
use App\Validators\Extensions\Polygons\EstimatedArea;
use App\Validators\Extensions\Polygons\GeometryType;
use App\Validators\Extensions\Polygons\NotOverlapping;
use App\Validators\Extensions\Polygons\PolygonSize;
use App\Validators\Extensions\Polygons\GeometryType;
use App\Validators\Extensions\Polygons\SelfIntersection;
use App\Validators\Extensions\Polygons\Spikes;
use App\Validators\Extensions\Polygons\WithinCountry;
Expand Down
14 changes: 11 additions & 3 deletions app/Models/V2/PolygonGeometry.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@

use App\Models\Traits\HasUuid;
use App\Models\V2\Sites\CriteriaSite;
use App\Models\V2\Sites\SitePolygon;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;

/**
Expand All @@ -24,11 +27,16 @@ class PolygonGeometry extends Model
'polygon_id', 'geom',
];

public function criteriaSite()
public function criteriaSite(): HasMany
{
return $this->hasMany(CriteriaSite::class, 'polygon_id', 'uuid');
}

public function sitePolygon(): BelongsTo
{
return $this->belongsTo(SitePolygon::class, 'uuid', 'poly_id');
}

public static function getGeoJson(string $uuid): ?array
{
$geojson_string = PolygonGeometry::isUuid($uuid)
Expand All @@ -47,9 +55,9 @@ public function getGeoJsonAttribute(): array
public static function getGeometryType(string $uuid): ?string
{
return PolygonGeometry::isUuid($uuid)
->selectRaw('ST_GeometryType(geom) as geometry_type')
->selectRaw('ST_GeometryType(geom) as geometry_type_string')
->first()
?->geometry_type;
?->geometry_type_string;
}

public function getGeometryTypeAttribute(): string
Expand Down
5 changes: 5 additions & 0 deletions app/Models/V2/Sites/CriteriaSite.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,9 @@ class CriteriaSite extends Model
'deleted_at',
'date_created',
];

public function scopeForCriteria($query, $criteriaId)
{
return $query->where('criteria_id', $criteriaId);
}
}
26 changes: 22 additions & 4 deletions app/Services/PolygonService.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
use App\Models\V2\Sites\CriteriaSite;
use App\Models\V2\Sites\SitePolygon;
use App\Validators\SitePolygonValidator;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

class PolygonService
{
public const OVERLAPPING_CRITERIA_ID = 3;
public const SELF_CRITERIA_ID = 4;
public const COORDINATE_SYSTEM_CRITERIA_ID = 5;
public const SIZE_CRITERIA_ID = 6;
public const WITHIN_COUNTRY_CRITERIA_ID = 7;
public const SPIKE_CRITERIA_ID = 8;
Expand Down Expand Up @@ -98,6 +100,22 @@ private function insertSinglePolygon(array $geometry): array
private function insertSitePolygon(string $polygonUuid, array $properties, float $area)
{
try {
// Avoid trying to store an invalid date string or int in the DB, as that will throw an exception and prevent
// the site polygon from storing. With an invalid date, this will end up reporting schema invalid and data
// invalid, which isn't necessarily correct for the payload given, but it does reflect the status in the DB
try {
$properties['plantstart'] = empty($properties['plantstart']) ? null : Carbon::parse($properties['plantstart']);
} catch (\Exception $e) {
$properties['plantstart'] = null;
}

try {
$properties['plantend'] = empty($properties['plantend']) ? null : Carbon::parse($properties['plantend']);
} catch (\Exception $e) {
$properties['plantend'] = null;
}
$properties['num_trees'] = is_int($properties['num_trees'] ?? null) ? $properties['num_trees'] : null;

$validationGeojson = ['features' => [
'feature' => ['properties' => $properties],
]];
Expand All @@ -116,12 +134,12 @@ private function insertSitePolygon(string $polygonUuid, array $properties, float
$sitePolygon->site_id = $properties['site_id'] ?? null;
$sitePolygon->site_name = $properties['site_name'] ?? null;
$sitePolygon->poly_label = $properties['poly_label'] ?? null;
$sitePolygon->plantstart = ! empty($properties['plantstart']) ? $properties['plantstart'] : null;
$sitePolygon->plantend = ! empty($properties['plantend']) ? $properties['plantend'] : null;
$sitePolygon->practice = $properties['practice'] ?? null;
$sitePolygon->plantstart = $properties['plantstart'] ?? null;
$sitePolygon->plantend = $properties['plantend'];
$sitePolygon->practice = $properties['practice'];
$sitePolygon->target_sys = $properties['target_sys'] ?? null;
$sitePolygon->distr = $properties['distr'] ?? null;
$sitePolygon->num_trees = $properties['num_trees'] ?? null;
$sitePolygon->num_trees = $properties['num_trees'];
$sitePolygon->est_area = $area ?? null;
$sitePolygon->save();

Expand Down
6 changes: 2 additions & 4 deletions app/Validators/Extensions/Polygons/EstimatedArea.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@ class EstimatedArea extends Extension
public static $name = 'estimated_area';

public static $message = [
'ESTIMATED_AREA',
'The {{attribute}} field must represent a polygon that matches the site\'s estimated area',
['attribute' => ':attribute'],
'The :attribute field must represent a polygon that matches the site\'s estimated area',
'key' => 'TOTAL_AREA_EXPECTED',
'message' => 'The project\'s total geometry must match the project\'s estimated area.',
];

public static function passes($attribute, $value, $parameters, $validator): bool
Expand Down
29 changes: 22 additions & 7 deletions app/Validators/Extensions/Polygons/FeatureBounds.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,41 @@

namespace App\Validators\Extensions\Polygons;

use App\Models\V2\PolygonGeometry;
use App\Validators\Extensions\Extension;

class FeatureBounds extends Extension
{
public static $name = 'polygon_feature_bounds';

public static $message = [
'FEATURE_BOUNDS',
'The {{attribute}} field must have valid feature polygon bounds.',
['attribute' => ':attribute'],
'The :attribute field must have valid feature polygon bounds.',
'key' => 'COORDINATE_SYSTEM',
'message' => 'The coordinates must have valid lat/lng values.',
];

public static function passes($attribute, $value, $parameters, $validator): bool
{
$type = data_get($value, 'geometry.type');
if (is_string($value)) {
// assume we have a DB UUID
return self::uuidValid($value);
}

// assume we have a GeoJSON
return self::geoJsonValid($value);
}

public static function uuidValid($uuid): bool
{
return self::geoJsonValid(PolygonGeometry::getGeoJson($uuid));
}

public static function geoJsonValid($geojson): bool
{
$type = data_get($geojson, 'geometry.type');
if ($type === 'Polygon') {
return self::hasValidPolygonBounds(data_get($value, 'geometry.coordinates.0'));
return self::hasValidPolygonBounds(data_get($geojson, 'geometry.coordinates.0'));
} elseif ($type === 'MultiPolygon') {
foreach (data_get($value, 'geometry.coordinates') as $coordinates) {
foreach (data_get($geojson, 'geometry.coordinates') as $coordinates) {
if (! self::hasValidPolygonBounds($coordinates)) {
return false;
}
Expand Down
6 changes: 2 additions & 4 deletions app/Validators/Extensions/Polygons/GeometryType.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,8 @@ class GeometryType extends Extension
public static $name = 'geometry_type';

public static $message = [
'GEOMETRY_TYPE',
'The {{attribute}} field must represent geojson that is polygon geometry',
['attribute' => ':attribute'],
'The :attribute field must represent geojson that is polygon geometry',
'key' => 'GEOMETRY_TYPE',
'message' => 'The geometry must by of polygon type',
];

public const VALID_TYPE = 'POLYGON';
Expand Down
6 changes: 2 additions & 4 deletions app/Validators/Extensions/Polygons/HasPolygonSite.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@ class HasPolygonSite extends Extension
public static $name = 'has_polygon_site';

public static $message = [
'HAS_POLYGON_SITE',
'The {{attribute}} field must represent a polygon with an attached site',
['attribute' => ':attribute'],
'The :attribute field must represent a polygon with an attached site',
'key' => 'HAS_POLYGON_SITE',
'message' => 'The geometry must have an attached site',
];

public static function passes($attribute, $value, $parameters, $validator): bool
Expand Down
6 changes: 2 additions & 4 deletions app/Validators/Extensions/Polygons/NotOverlapping.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,8 @@ class NotOverlapping extends Extension
public static $name = 'not_overlapping';

public static $message = [
'NOT_OVERLAPPING',
'The {{attribute}} field must represent a polygon that does not overlap with other site polygons',
['attribute' => ':attribute'],
'The :attribute field must represent a polygon that does not overlap with other site polygons',
'key' => 'OVERLAPPING_POLYGON',
'message' => 'The geometry must not overlap with other project geometry',
];

public static function passes($attribute, $value, $parameters, $validator): bool
Expand Down
6 changes: 2 additions & 4 deletions app/Validators/Extensions/Polygons/PolygonSize.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,8 @@ class PolygonSize extends Extension
public static $name = 'polygon_size';

public static $message = [
'POLYGON_SIZE',
'The {{attribute}} field must not represent a polygon that is too large.',
['attribute' => ':attribute'],
'The :attribute field must not represent a polygon that is too large.',
'key' => 'SIZE_LIMIT',
'message' => 'The geometry must not be larger than ' . self::SIZE_LIMIT . 'square kilometers',
];

public const SIZE_LIMIT = 10000000;
Expand Down
6 changes: 2 additions & 4 deletions app/Validators/Extensions/Polygons/SelfIntersection.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@ class SelfIntersection extends Extension
public static $name = 'polygon_self_intersection';

public static $message = [
'SELF_INTERSECTION',
'The {{attribute}} geometry field must not self intersect.',
['attribute' => ':attribute'],
'The :attribute geometry field must not self intersect.',
'key' => 'SELF_INTERSECTION',
'message' => 'The geometry must not self intersect.',
];

public static function passes($attribute, $value, $parameters, $validator): bool
Expand Down
6 changes: 2 additions & 4 deletions app/Validators/Extensions/Polygons/Spikes.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@ class Spikes extends Extension
public static $name = 'polygon_spikes';

public static $message = [
'POLYGON_SPIKES',
'The {{attribute}} field must not represent a polygon with spikes',
['attribute' => ':attribute'],
'The :attribute field must not represent a polygon with spikes',
'key' => 'SPIKE',
'message' => 'The geometry must not have spikes',
];

public static function passes($attribute, $value, $parameters, $validator): bool
Expand Down
6 changes: 2 additions & 4 deletions app/Validators/Extensions/Polygons/WithinCountry.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,8 @@ class WithinCountry extends Extension
public static $name = 'within_country';

public static $message = [
'WITHIN_COUNTRY',
'The {{attribute}} field must represent a polygon that is within its assigned country',
['attribute' => ':attribute'],
'The :attribute field must represent a polygon that is within its assigned country',
'key' => 'WITHIN_COUNTRY',
'message' => 'The geometry must be within the project\'s assigned country',
];

public const THRESHOLD_PERCENTAGE = 75;
Expand Down
4 changes: 4 additions & 0 deletions app/Validators/SitePolygonValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ class SitePolygonValidator extends Validator
'features.*' => 'polygon_feature_bounds',
];

public const FEATURE_BOUNDS_UUID = [
'*' => 'string|uuid||polygon_feature_bounds',
];

public const SPIKES = [
'features' => 'required|array',
'features.*.geometry' => 'polygon_spikes',
Expand Down
38 changes: 36 additions & 2 deletions openapi-src/V2/definitions/SiteGeometryPost.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,39 @@
title: SiteGeometryPost
type: object
properties:
uuid:
type: string
polygon_uuids:
type: array
items:
type: string
description:
The UUIDs generated by the system for the uploaded polygons. They are in the same order as the polygons in the
request payload.
errors:
type: object
description:
Mapping of geometry UUID to the errors associated with the geometry. The geometry was saved in the DB and must
be updated instead of created once the issues are resolved.
additionalProperties:
type: array
items:
type: object
properties:
key:
type: string
enum:
- OVERLAPPING_POLYGON
- SELF_INTERSECTION
- COORDINATE_SYSTEM
- SIZE_LIMIT
- WITHIN_COUNTRY
- SPIKE
- GEOMETRY_TYPE
- TOTAL_AREA_EXPECTED
- TABLE_SCHEMA
- DATA_COMPLETED
message:
type: string
description: Human readable string in english to describe the error.



Loading

0 comments on commit 5a26382

Please sign in to comment.