Skip to content
Snippets Groups Projects
Commit 93c1cf73 authored by Valentin Rigal's avatar Valentin Rigal Committed by Erwan Rouchet
Browse files

Limit zone polygons to 164 points

parent 9aa0ab14
No related branches found
No related tags found
1 merge request!1314Limit zone polygons to 164 points
......@@ -148,6 +148,25 @@ class TestCreateElements(FixtureAPITestCase):
self.assertEqual(act.type, self.act_type)
self.assertEqual(act.worker_version, self.worker_version)
def test_create_element_polygon_max_size(self):
# Element polygon is limited to 164 points
polygon = [(0, 0), *((i, i) for i in range(163, -1, -1))]
self.assertEqual(len(polygon), 165)
self.client.force_login(self.user)
request = self.make_create_request(
parent=str(self.vol.id),
elt_type='page',
name='The castle of my dreams again',
image=str(self.image.id),
polygon=polygon
)
with self.assertNumQueries(8):
response = self.client.post(**request)
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertDictEqual(
response.json(), {'polygon': ['Ensure this field has no more than 164 elements.']}
)
def test_create_element_polygon(self):
# Create an element with a polygon to an existing volume
polygon = [[10, 10], [10, 40], [40, 40], [40, 10], [10, 10]]
......
......@@ -28,10 +28,13 @@ from arkindex.project.tools import bounding_box
# This leaves 2704 bytes per tuple.
# Docs on the database page layout: https://www.postgresql.org/docs/current/storage-page-layout.html
# An index tuple on the (image_id, polygon) unique constraint has a 8-byte header then 16 bytes for the image UUID,
# leaving 2680 bytes for a geometry. Each geometry has a 16 byte header, and each 2D points adds 16 more bytes
# (8 bytes per coordinate); 2664 bytes can therefore be used for all points.
# This translates to up to 166 points (165 distinct points as the last point must be equal to the first).
# leaving 2680 bytes for a geometry. Each geometry has a 16 byte header; 2664 bytes can therefore be used for all points.
# Each 2D points adds 16 more bytes (8 bytes per coordinate). This translates to up to 166 points, however in practice
# polygons size calculation with ST_MemSize may reach the limit with a 164 points polygon. As we can't find a reasonable
# way to detect this behaviour nor its source, we have to lower the maximum number of points for new polygons to 164.
# Therefore, polygons are limited to 2664 bytes and 164 points (163 distinct points as the last point must be equal to the first).
POLYGON_MAX_MEM_SIZE = 2664
POLYGON_MAX_POINTS = 164
logger = logging.getLogger(__name__)
profile_uri_validator = URLValidator(schemes=['http', 'https'], message='Invalid IIIF profile URI')
......@@ -380,10 +383,10 @@ class Zone(IndexableModel):
check=models.Q(polygon=SnapToGrid('polygon', 1)),
name='zone_polygon_integer_coordinates',
),
# Restrict to between 4 and 166 points (3+1 points for a triangle, and BTree indexes limit us to 2680 bits)
# Restrict to between 4 and 164 points (3+1 points for a triangle, and BTree indexes limit us to 164 points)
# Checking against the memory size is faster than checking the number of points
models.CheckConstraint(
check=models.Q(polygon__memsize__gte=96, polygon__memsize__lte=2664),
check=models.Q(polygon__memsize__gte=96, polygon__memsize__lte=POLYGON_MAX_MEM_SIZE),
name='zone_polygon_size',
)
]
......
from django.db.utils import IntegrityError
from arkindex.images.models import Image, Zone
from arkindex.project.tests import FixtureAPITestCase
class TestImagePolygon(FixtureAPITestCase):
@classmethod
def setUpTestData(cls):
cls.image = Image.objects.get(path='img1')
def test_polygon_165_pts_constraint(self):
"""
Polygon maximum allowed memory size is not sufficient to determine the maximum
allowed number of points. This tests covers a case where a 165 points polygon
overflow ST_MemSize maximum allowed value.
"""
polygon = [(0, 0), *((i, i) for i in range(163, -1, -1))]
self.assertEqual(len(polygon), 165)
self.assertEqual(polygon[0], polygon[-1])
with self.assertRaisesRegex(IntegrityError, '"images_zone" violates check constraint "zone_polygon_size"'):
Zone(image=self.image, polygon=polygon).save()
def test_polygon_164_pts(self):
polygon = [(0, 0), *((i, i) for i in range(162, -1, -1))]
self.assertEqual(len(polygon), 164)
self.assertEqual(polygon[0], polygon[-1])
Zone(image=self.image, polygon=polygon).save()
......@@ -6,6 +6,7 @@ from django.contrib.gis.geos import Point
from drf_spectacular.utils import extend_schema_field
from rest_framework import serializers
from arkindex.images.models import POLYGON_MAX_POINTS
from arkindex.project.gis import ensure_linear_ring
......@@ -52,9 +53,7 @@ class LinearRingField(serializers.ListField):
def __init__(self, *args, **kwargs):
# The maximum length comes from the maximum amount of points the (image_id, polygon) unique index can store:
# (Maximum BTree index tuple size - tuple header size - Image UUID size - Empty LineString size) // Point size
# (2704 - 8 - 16 - 16) // 16 = 166 points
kwargs.update(min_length=3, max_length=166)
kwargs.update(min_length=3, max_length=164)
super().__init__(*args, **kwargs)
def run_validation(self, data):
......@@ -62,9 +61,9 @@ class LinearRingField(serializers.ListField):
# Ensure the LineString is closed
if value[0] != value[-1]:
if len(value) == 166:
if len(value) == POLYGON_MAX_POINTS:
raise serializers.ValidationError(
'166 points are allowed only when the first and last points are equal.'
f'{POLYGON_MAX_POINTS} points are allowed only when the first and last points are equal.'
)
value.append(value[0])
......
......@@ -80,19 +80,19 @@ class TestLinearRingSerializerField(TestCase):
LinearRingField().run_validation([[1, 2]])
def test_linear_ring_max_size(self):
# 0,0 + 164 points from 164,164 to 1,1
coords = [[0, 0]] + [[i, i] for i in range(164, 0, -1)]
# 0,0 + 162 points from 164,164 to 1,1
coords = [[0, 0]] + [[i, i] for i in range(162, 0, -1)]
self.assertEqual(
LinearRingField().run_validation(coords).coords,
LinearRing(coords + [[0, 0]]).coords,
)
# 166th point, should fail if it is not equal to the first one
coords.append([165, 165])
with self.assertRaisesRegex(ValidationError, '166 points are allowed only when the first and last points are equal.'):
# 164th point, should fail if it is not equal to the first one
coords.append([163, 163])
with self.assertRaisesRegex(ValidationError, '164 points are allowed only when the first and last points are equal.'):
LinearRingField().run_validation(coords)
coords[165] = [0, 0]
coords[163] = [0, 0]
self.assertEqual(
LinearRingField().run_validation(coords).coords,
LinearRing(coords).coords,
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment