23. Validez

En el 90% de los casos, la respuesta a la pregunta «¿por qué mi consulta da un error “TopologyException”?» es «una o varias de las entradas no son válidas». Lo que nos lleva a preguntarnos: ¿qué significa que no sea válida y por qué debería importarnos?

23.1. Qué es la Validez

La validez es más importante para los polígonos, que definen áreas delimitadas y requieren mucha estructura. Las líneas son muy sencillas y no pueden ser inválidas, ni tampoco los puntos.

Algunas de las reglas de validez de los polígonos parecen obvias y otras arbitrarias (y, de hecho, lo son).

  • Los anillos del polígono deben cerrarse.

  • Los anillos que definen agujeros deben estar dentro de los anillos que definen límites exteriores.

  • Los anillos no pueden autointersectarse (no pueden tocarse ni cruzarse).

  • Los anillos no pueden tocar a otros anillos, excepto en un punto.

  • Los elementos de los multipolígonos no pueden tocarse entre sí.

Las tres últimas reglas podrían catalogarse como arbitrarias. Hay otras formas de definir polígonos que son igualmente autoconsistentes, pero las reglas anteriores son las utilizadas por el estándar OGC SFSQL que PostGIS cumple.

La razón por la que las reglas son importantes es porque los algoritmos para cálculos geométricos dependen de una estructura consistente en las entradas. Es posible construir algoritmos que no tengan suposiciones estructurales, pero esas rutinas tienden a ser muy lentas, porque el primer paso en cualquier rutina sin estructura es analizar las entradas y construir estructura en ellas.

He aquí un ejemplo del por qué la estructura importa. Este polígono no es válido:

POLYGON((0 0, 0 1, 2 1, 2 2, 1 2, 1 0, 0 0));

Puede ver la invalidez un poco más claramente en este diagrama:

_images/figure_eight.png

El anillo exterior es en realidad una figura de ocho, con una auto-intersección en el centro. Obsérvese que las rutinas gráficas representan correctamente el relleno del polígono, de modo que visualmente parece un «área»: dos cuadrados de una unidad, es decir, un área total de dos unidades de área.

Veamos cuál cree la base de datos que es el área de nuestro polígono:

SELECT ST_Area(ST_GeometryFromText(
         'POLYGON((0 0, 0 1, 1 1, 2 1, 2 2, 1 2, 1 1, 1 0, 0 0))'
       ));
 st_area
---------
       0

¿Qué ocurre aquí? El algoritmo que calcula el área asume que los anillos no se auto-intersecan. Un anillo que se comporte bien siempre tendrá el área delimitada (el interior) a un lado de la línea de delimitación (no importa a qué lado, sólo que sea a un lado). Sin embargo, en nuestro (mal comportamiento) ocho, el área delimitada está a la derecha de la línea para un lóbulo y a la izquierda para el otro. Esto hace que las áreas calculadas para cada lóbulo se anulen (una sale como 1, la otra como -1), de ahí el resultado de «área cero».

23.2. Detectando Validez

En el ejemplo anterior teníamos un polígono que sabíamos era inválido. ¿Cómo detectamos la invalidez en una tabla con millones de geometrías? Con la función ST_IsValid(geometry). Usada contra nuestro «ocho», obtenemos una respuesta rápida:

SELECT ST_IsValid(ST_GeometryFromText(
         'POLYGON((0 0, 0 1, 1 1, 2 1, 2 2, 1 2, 1 1, 1 0, 0 0))'
       ));
f

Ahora sabemos que la entidad es inválida, pero no sabemos por qué. Podemos usar la función ST_IsValidReason(geometry) para encontrar la causa de la invalidez:

SELECT ST_IsValidReason(ST_GeometryFromText(
         'POLYGON((0 0, 0 1, 1 1, 2 1, 2 2, 1 2, 1 1, 1 0, 0 0))'
       ));
Self-intersection[1 1]

Observa que además de la razón (autointersección) también se devuelve la ubicación de la invalidez (coordenada (1 1)).

Podemos usar la función ST_IsValid(geometry) para probar nuestras tablas también:

-- Find all the invalid polygons and what their problem is
SELECT name, boroname, ST_IsValidReason(geom)
FROM nyc_neighborhoods
WHERE NOT ST_IsValid(geom);
          name           |   boroname    |          st_isvalidreason
-------------------------+---------------+-----------------------------------------
 Howard Beach            | Queens        | Self-intersection[597264.08 4499924.54]
 Corona                  | Queens        | Self-intersection[595483.05 4513817.95]
 Steinway                | Queens        | Self-intersection[593545.57 4514735.20]
 Red Hook                | Brooklyn      | Self-intersection[584306.82 4502360.51]

23.3. Reparar la no validez

La reparación de invalidez implica reducir un polígono a sus estructuras más simples (anillos), asegurando que los anillos sigan las reglas de validez, y luego construir nuevos polígonos que sigan las reglas de encierro de anillos. Frecuentemente los resultados son intuitivos, pero en el caso de entradas extremadamente problemáticas, las salidas válidas pueden no ajustarse a tu intuición de cómo deberían verse. Las versiones recientes de PostGIS incluyen diferentes algoritmos para la reparación de geometrías: lee con cuidado la página del manual y elige el que más te guste.

Por ejemplo, aquí hay un caso clásico de invalidez – el «polígono banana» – un único anillo que encierra un área pero se dobla para tocarse a sí mismo, dejando un «agujero» que en realidad no es un agujero.

POLYGON((0 0, 2 0, 1 1, 2 2, 3 1, 2 0, 4 0, 4 4, 0 4, 0 0))
_images/banana.png

Ejecutar ST_MakeValid en el polígono devuelve un polígono OGC válido, consistente en un anillo exterior e interior que se tocan en un punto.

SELECT ST_AsText(
         ST_MakeValid(
           ST_GeometryFromText('POLYGON((0 0, 2 0, 1 1, 2 2, 3 1, 2 0, 4 0, 4 4, 0 4, 0 0))')
         )
       );
POLYGON((0 0,0 4,4 4,4 0,2 0,0 0),(2 0,3 1,2 2,1 1,2 0))

Nota

El «polígono banana» (o «concha invertida») es un caso donde el modelo topológico OGC para geometrías válidas y el modelo usado internamente por ESRI difieren. El modelo de ESRI considera inválidos los anillos que se tocan, y prefiere la forma banana para este tipo de figura. El modelo OGC es lo contrario. Ninguno es «correcto», son solo formas diferentes de modelar la misma situación.

23.4. Reparación de invalidez en lote

Aquí hay un ejemplo de SQL para marcar geometrías inválidas para revisión mientras se agrega una versión reparada a la tabla.

-- Column for old invalid form
ALTER TABLE nyc_neighborhoods
  ADD COLUMN geom_invalid geometry
  DEFAULT NULL;

-- Fix invalid and save the original
UPDATE nyc_neighborhoods
  SET geom = ST_MakeValid(geom),
      geom_invalid = geom
  WHERE NOT ST_IsValid(geom);

-- Review the invalid cases
SELECT geom, ST_IsValidReason(geom_invalid)
  FROM nyc_neighborhoods
  WHERE geom_invalid IS NOT NULL;

Una buena herramienta para reparar visualmente geometrías inválidas es OpenJump (http://openjump.org) que incluye una rutina de validación en Tools->QA->Validate Selected Layers.

23.5. Lista de funciones

ST_IsValid(geometry A): Devuelve un valor booleano que indica si la geometría es válida.

ST_IsValidReason(geometry A): Devuelve una cadena de texto con la razón de la invalidez y una coordenada de invalidez.

ST_MakeValid(geometry A): Devuelve una geometría reconstruida para cumplir con las reglas de validez.