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, MakeValid
tambié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 MakeValid
que 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 STNumPoints
o usar STPointN
para 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 MultiPoint
objeto 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