Alternativa a MakeValid () para datos espaciales en SQL Server 2016


13

Tengo una tabla muy grande de LINESTRINGdatos geográficos que estoy trasladando de Oracle a SQL Server. Hay varias evaluaciones que se ejecutan contra estos datos en Oracle, y también deberán ejecutarse contra los datos en SQL Server.

El problema: SQL Server tiene requisitos más estrictos para un válido LINESTRINGque Oracle; "La instancia de LineString no puede solaparse en un intervalo de dos o más puntos consecutivos". Sucede que un porcentaje de nuestros LINESTRINGcorreos electrónicos no cumple con ese criterio, lo que significa que las funciones que necesitamos para evaluar los datos fallan. Necesito ajustar los datos para que puedan validarse con éxito en SQL Server.

Por ejemplo:

Validando un muy simple LINESTRINGque se duplica sobre sí mismo:

select geography::STGeomFromText(
    'LINESTRING (0 0 1, 0 1 2, 0 -1 3)',4326).IsValidDetailed()
24413: Not valid because of two overlapping edges in curve (1).

Ejecutando la MakeValidfunción en su contra:

select geography::STGeomFromText(
    'LINESTRING (0 0 1, 0 1 2, 0 -1 3)',4326).MakeValid().STAsText()
LINESTRING (0 -0.999999999999867, 0 0, 0 0.999999999999867)

Desafortunadamente, la MakeValidfunción cambia el orden de los puntos y elimina la tercera dimensión, lo que la hace inutilizable para nosotros. Estoy buscando otro enfoque que resuelva este problema sin reordenar o eliminar la tercera dimensión.

¿Algunas ideas?

Mis datos reales contienen cientos / miles de puntos.

Respuestas:


12

Permítanme advertir que estoy jugando con datos espaciales en el servidor SQL por primera vez (por lo que probablemente ya conozcan esta primera parte), pero me tomó un tiempo descubrir que SQL Server no trata las coordenadas (xyz) como verdaderas Valores 3D, los trata como (longitud de latitud) con un valor opcional de "elevación", Z, que es ignorado por la validación y otras funciones.

Evidencia:

select geography::STGeomFromText('LINESTRING (0 0 1, 0 1 2, 0 -1 3)', 4326)
    .IsValidDetailed()

24413: Not valid because of two overlapping edges in curve (1).

Tu primer ejemplo me pareció extraño porque (0 0 1), (0 1 2) y (0 -1 3) no son colineales en el espacio 3D (soy matemático, así que estaba pensando en esos términos). IsValidDetailed(y MakeValid) los trata como (0 0), (0 1) y (0, -1), lo que hace una línea superpuesta.

Para probarlo, simplemente cambie la X y la Z, y valida:

select geography::STGeomFromText('LINESTRING (1 0 0, 2 1 0, 3 -1 0)', 4326)
    .IsValidDetailed()

24400: Valid

Esto realmente tiene sentido si pensamos en estas como regiones o caminos trazados en la superficie de nuestro globo, en lugar de puntos en el espacio matemático 3D.


La segunda parte de su problema es que los valores de punto Z (y M) no son preservados por SQL a través de funciones :

Las coordenadas Z no se utilizan en ningún cálculo realizado por la biblioteca y no se realizan a través de ningún cálculo de la biblioteca.

Esto es lamentablemente por diseño. Esto se informó a Microsoft en 2010 , la solicitud se cerró como "No se solucionará". Puede encontrar esa discusión relevante, su razonamiento es:

Asignar Z y M es ambiguo, porque MakeValid divide y combina elementos espaciales. Los puntos a menudo se crean, eliminan o mueven durante este proceso. Por lo tanto, MakeValid (y otras construcciones) elimina los valores Z y M.

Por ejemplo:

DECLARE @a geometry = geometry::Parse('POINT(0 0 2 2)');
DECLARE @b geometry = geometry::Parse('POINT(0 0 1 1)');
SELECT @a.STUnion(@b).AsTextZM()

Los valores Z y M son ambiguos para el punto (0 0). Decidimos eliminar Z y M por completo en lugar de devolver el resultado medio correcto.

Puede asignarlos más adelante si sabe exactamente cómo. Alternativamente, puede cambiar la forma en que genera sus objetos para que sean válidos en la entrada, o mantener dos versiones de sus objetos, una que sea válida y otra que conserve todas sus características. Si explica mejor su escenario y lo que hace con los objetos, tal vez podamos darle soluciones adicionales.

Además, como ya ha visto, MakeValidtambién puede hacer otras cosas inesperadas , como cambiar el orden de los puntos, devolver un MULTILINESTRING o incluso devolver un objeto POINT.


Una idea que encontré fue almacenarlos como un objeto MULTIPUNTO en su lugar :

El problema es cuando su cadena de líneas en realidad vuelve sobre una sección continua de línea entre dos puntos que la línea trazó previamente. Por definición, si está volviendo sobre los puntos existentes, entonces la cadena lineal ya no es la geometría más simple que puede representar este conjunto de puntos, y MakeValid () le dará una multilínea (y perderá sus valores Z / M).

Desafortunadamente, si está trabajando con datos GPS o similares, es muy probable que haya vuelto sobre su camino en algún punto de la ruta, por lo que las cadenas de líneas no siempre son tan útiles en estos escenarios :( Podría decirse que dichos datos deben almacenarse como un multipunto de todos modos ya que sus datos representan la ubicación discreta de un objeto muestreado en puntos regulares en el tiempo.

En su caso, valida muy bien:

select geometry::STGeomFromText('MULTIPOINT (0 0 1, 0 1 2, 0 -1 3)',4326)
    .IsValidDetailed()

24400: Valid

Si absolutamente necesita mantenerlos como LINESTRINGS, entonces tendrá que escribir su propia versión MakeValidque ajuste ligeramente algunos de los puntos X o Y de origen en un pequeño valor, mientras conserva Z (y no hace otras locuras como convertirlo a otros tipos de objetos).

Todavía estoy trabajando en algún código, pero eche un vistazo a algunas de las ideas iniciales aquí:


EDITAR Ok, algunas cosas que encontré durante las pruebas:

  • Si el objeto de geometría no es válido, simplemente no puede hacer mucho con él. No puede leer el STGeometryType, no puede obtener el STNumPointso usar STPointNpara iterar a través de ellos. Si no puede usar MakeValid, básicamente está atascado con la operación en la representación de texto del objeto geográfico.
  • El uso STAsText()devolverá la representación de texto incluso de un objeto no válido, pero no devolverá los valores Z o M. En cambio, queremos AsTextZM()o ToString().
  • No puede crear una función que invoque RAND()(las funciones deben ser deterministas), por lo que acabo de empujarla con valores cada vez más grandes. Realmente no tengo idea de cuál es la precisión de sus datos, o cuán tolerante es de pequeños cambios, así que use o modifique esta función a su propia discreción.

No tengo idea de si hay posibles entradas que harán que este ciclo continúe para siempre. Usted ha sido advertido.

CREATE FUNCTION dbo.FixBadLineString (@input geography) RETURNS geography
AS BEGIN
DECLARE @output geography

IF @input.STIsValid() = 1   --send valid objects back as-is
  SET @output = @input;
ELSE IF LEFT(@input.IsValidDetailed(),6) = '24413:'
--"Not valid because of two overlapping edges in curve"
BEGIN
  --make a new MultiPoint object from the LineString text
  DECLARE @mp geography = geography::STGeomFromText(
      REPLACE(@input.AsTextZM(), 'LINESTRING', 'MULTIPOINT'), 4326);
  DECLARE @newText nvarchar(max); --to build output
  DECLARE @point int 
  DECLARE @tinynum float = 0;

  SET @output = @input;
  --keep going until it validates
  WHILE @output.STIsValid() = 0
  BEGIN
    SET @newText = 'LINESTRING (';
    SET @point = 1
    SET @tinynum = @tinynum + 0.00000001

    --Loop through the points, add a bit and append to the new string
    WHILE @point <= @mp.STNumPoints()
    BEGIN
      SET @newText = @newText + convert(varchar(50),
               @mp.STPointN(@point).Long + @tinynum) + ' ';
      SET @newText = @newText + convert(varchar(50),
               @mp.STPointN(@point).Lat - @tinynum) + ' ';
      SET @newText = @newText + convert(varchar(50), 
               @mp.STPointN(@point).Z) + ', ';
      SET @tinynum = @tinynum * -2
      SET @point = @point + 1
    END

    --close the parens and make the new LineString object
    SET @newText = LEFT(@newText, LEN(@newText) - 1) + ')'
    SET @output = geography::STGeomFromText(@newText, 4326);
  END; --this will loop if it is still invalid
  RETURN @output;
END;
--Any other unhandled error, just send back NULL
ELSE SET @output = NULL;

RETURN @output;
END

En lugar de analizar la cadena, elegí crear un nuevo MultiPointobjeto usando el mismo conjunto de puntos, para poder iterar a través de ellos y empujarlos, luego volver a armar un nuevo LineString. Aquí hay un código para probarlo, 3 de estos valores (incluida su muestra) comienzan inválidos pero son corregidos:

declare @geostuff table (baddata geography)

INSERT INTO @geostuff (baddata)
          SELECT geography::STGeomFromText('LINESTRING (0 0 1, 0 1 2, 0 -1 3)',4326)
UNION ALL SELECT geography::STGeomFromText('LINESTRING (0 2 0, 0 1 0.5, 0 -1 -14)',4326)
UNION ALL SELECT geography::STGeomFromText('LINESTRING (0 0 4, 1 1 40, -1 -1 23)',4326)
UNION ALL SELECT geography::STGeomFromText('LINESTRING (1 1 9, 0 1 -.5, 0 -1 3)',4326)
UNION ALL SELECT geography::STGeomFromText('LINESTRING (6 6 26.5, 4 4 42, 12 12 86)',4326)
UNION ALL SELECT geography::STGeomFromText('LINESTRING (0 0 2, -4 4 -2, 4 -4 0)',4326)

SELECT baddata.AsTextZM() as before, baddata.IsValidDetailed() as pretest,
 dbo.FixBadLineString(baddata).AsTextZM() as after,
 dbo.FixBadLineString(baddata).IsValidDetailed() as posttest 
FROM @geostuff

Gran respuesta, gracias BradC. No incluí esto en mi pregunta, pero mis datos reales contienen cientos / miles de puntos, por lo que "@tinynum * 2" no era sostenible. En cambio, solté "@tinynum" por completo y usé un número aleatorio entre 0 y 0.000000003. He estado ejecutando esto contra los datos y hasta ahora, de 22k completados, todos fueron validados como LINESTRING.
CaptainSlock

3

Esta es la FixBadLineStringfunción de BradC ajustada para usar un número aleatorio entre 0 y 0.000000003, lo que le permite escalar LINESTRINGscon una gran cantidad de puntos y también minimizar el cambio en las coordenadas:

CREATE FUNCTION dbo.FixBadLineString (@input geography) RETURNS geography
AS BEGIN
DECLARE @output geography

IF @input.STIsValid() = 1   --send valid objects back as-is
  SET @output = @input;
ELSE IF LEFT(@input.IsValidDetailed(),6) = '24413:'
--"Not valid because of two overlapping edges in curve"
BEGIN
  --make a new MultiPoint object from the LineString text
  DECLARE @mp geography = geography::STGeomFromText(
      REPLACE(@input.AsTextZM(), 'LINESTRING', 'MULTIPOINT'), 4326);
  DECLARE @newText nvarchar(max); --to build output
  DECLARE @point int 

  SET @output = @input;
  --keep going until it validates
  WHILE @output.STIsValid() = 0
  BEGIN
    SET @newText = 'LINESTRING (';
    SET @point = 1

    --Loop through the points, add/subtract a random value between 0 and 3E-9 and append to the new string
    WHILE @point <= @mp.STNumPoints()
    BEGIN
      SET @newText = @newText + convert(varchar(50),
        CAST(@mp.STPointN(@point).Long AS NUMERIC(18,9)) + 
          CAST(ABS(CHECKSUM(PWDENCRYPT(N''))) / 644245094100000000 AS NUMERIC(18,9))) + ' ';
      SET @newText = @newText + convert(varchar(50),
        CAST(@mp.STPointN(@point).Lat AS NUMERIC(18,9)) - 
          CAST(ABS(CHECKSUM(PWDENCRYPT(N''))) / 644245094100000000 AS NUMERIC(18,9))) + ' ';
      SET @newText = @newText + convert(varchar(50), 
               @mp.STPointN(@point).Z) + ', ';
      SET @point = @point + 1
    END

    --close the parens and make the new LineString object
    SET @newText = LEFT(@newText, LEN(@newText) - 1) + ')'
    SET @output = geography::STGeomFromText(@newText, 4326);
  END; --this will loop if it is still invalid
  RETURN @output;
END;
--Any other unhandled error, just send back NULL
ELSE SET @output = NULL;

RETURN @output;
END

1
Se ve muy bien, no sabía sobre la PWDENCRYPTfunción. Podrías haber dejado fuera elABS y habría devuelto un número positivo o negativo, por lo que no siempre estamos sumando a X y restando de Y.
BradC
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.