Permettez-moi de vous avertir que je joue avec des données spatiales dans SQL Server pour la première fois (donc vous connaissez probablement déjà cette première partie), mais il m'a fallu un certain temps pour comprendre que SQL Server ne traite pas les coordonnées (xyz) comme vraies Les valeurs 3D, il les traite comme (longitude latitude) avec une valeur "d'élévation" optionnelle, Z, qui est ignorée par la validation et d'autres fonctions.
Preuve:
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).
Votre premier exemple m'a semblé bizarre parce que (0 0 1), (0 1 2) et (0 -1 3) ne sont pas colinéaires dans l'espace 3D (je suis mathématicien, donc je pensais en ces termes). IsValidDetailed
(et MakeValid
) les traite comme (0 0), (0 1) et (0, -1), ce qui fait une ligne qui se chevauchent.
Pour le prouver, il suffit d'échanger les X et Z, et cela valide:
select geography::STGeomFromText('LINESTRING (1 0 0, 2 1 0, 3 -1 0)', 4326)
.IsValidDetailed()
24400: Valid
Cela a du sens si nous les considérons comme des régions ou des chemins tracés à la surface de notre globe, au lieu de points dans l'espace mathématique 3D.
La deuxième partie de votre problème est que les valeurs des points Z (et M) ne sont pas conservées par SQL via les fonctions :
Les coordonnées Z ne sont utilisées dans aucun calcul effectué par la bibliothèque et ne sont effectuées par aucun calcul de bibliothèque.
C'est malheureusement par conception. Cela a été signalé à Microsoft en 2010 , la demande a été classée comme «ne résoudra pas». Vous pourriez trouver cette discussion pertinente, leur raisonnement est le suivant:
L'affectation de Z et M est ambiguë, car MakeValid divise et fusionne les éléments spatiaux. Les points sont souvent créés, supprimés ou déplacés au cours de ce processus. Par conséquent, MakeValid (et d'autres constructions) supprime les valeurs Z et M.
Par exemple:
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()
Les valeurs Z et M sont ambiguës pour le point (0 0). Nous avons décidé d'abandonner complètement Z et M au lieu de renvoyer un résultat semi-correct.
Vous pouvez les affecter plus tard si vous savez exactement comment. Vous pouvez également modifier la façon dont vous générez vos objets pour qu'ils soient valides en entrée, ou conserver deux versions de vos objets, une qui est valide et une autre qui préserve toutes vos fonctionnalités. Si vous expliquez mieux votre scénario et ce que vous faites avec les objets, nous pourrions peut-être vous proposer des solutions de contournement supplémentaires.
De plus, comme vous l'avez déjà vu, vous MakeValid
pouvez également faire d'autres choses inattendues , comme changer l'ordre des points, retourner un MULTILINESTRING, ou même retourner un objet POINT.
Une idée que j'ai rencontrée était de les stocker en tant qu'objet MULTIPOINT à la place :
Le problème est lorsque votre chaîne de caractères retrace réellement une section continue de ligne entre deux points qui a été précédemment tracée par la ligne. Par définition, si vous retracez des points existants, la chaîne de lignes n'est plus la géométrie la plus simple qui peut représenter cet ensemble de points, et MakeValid () vous donnera une chaîne multiligne à la place (et perdez vos valeurs Z / M).
Malheureusement, si vous travaillez avec des données GPS ou similaires, il est fort probable que vous ayez retracé votre chemin à un moment donné de l'itinéraire, donc les chaînes de lignes ne sont pas toujours aussi utiles dans ces scénarios :( On peut dire que ces données doivent être stockées sous la forme un multipoint de toute façon puisque vos données représentent l'emplacement discret d'un objet échantillonné à des moments réguliers dans le temps.
Dans votre cas, cela valide très bien:
select geometry::STGeomFromText('MULTIPOINT (0 0 1, 0 1 2, 0 -1 3)',4326)
.IsValidDetailed()
24400: Valid
Si vous devez absolument les conserver en tant que LINESTRINGS, vous devrez écrire votre propre version MakeValid
qui ajuste légèrement certains des points X ou Y source par une valeur minuscule, tout en préservant Z (et ne fait pas d'autres choses folles comme le convertir en d'autres types d'objets).
Je travaille toujours sur du code, mais jetez un œil à certaines des idées de départ ici:
EDIT Ok, quelques choses que j'ai trouvées lors des tests:
- Si l'objet géométrique n'est pas valide, vous ne pouvez pas en faire grand-chose. Vous ne pouvez pas lire le
STGeometryType
, vous ne pouvez pas obtenir le STNumPoints
ou utiliser STPointN
pour les parcourir. Si vous ne pouvez pas utiliser MakeValid
, vous êtes fondamentalement coincé à opérer sur la représentation textuelle de l'objet géographique.
- L'utilisation
STAsText()
renvoie la représentation textuelle d'un objet non valide, mais ne renvoie pas de valeurs Z ou M. Au lieu de cela, nous voulons AsTextZM()
ou ToString()
.
- Vous ne pouvez pas créer une fonction qui appelle
RAND()
(les fonctions doivent être déterministes), donc je viens de la pousser par des valeurs de plus en plus grandes. Je n'ai vraiment aucune idée de la précision de vos données, ni de leur tolérance aux petits changements, alors utilisez ou modifiez cette fonction à votre discrétion.
Je n'ai aucune idée s'il existe des entrées possibles qui feront que cette boucle continuera indéfiniment. Tu étais prévenu.
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
Au lieu d'analyser la chaîne, j'ai choisi de créer un nouvel MultiPoint
objet en utilisant le même ensemble de points, afin de pouvoir les parcourir et les pousser, puis réassembler un nouveau LineString. Voici du code pour le tester, 3 de ces valeurs (y compris votre exemple) commencent non valides mais sont fixes:
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