J'ai une très grande table de données géographiques LINESTRING
que je déplace d'Oracle vers SQL Server. Il existe un certain nombre d'évaluations qui sont exécutées par rapport à ces données dans Oracle, et elles devront également être exécutées par rapport aux données dans SQL Server.
Le problème: SQL Server a des exigences plus strictes pour un LINESTRING
valide qu'Oracle; "L'instance LineString ne peut pas se chevaucher sur un intervalle de deux points consécutifs ou plus". Il se trouve qu'un pourcentage de nos LINESTRING
s ne remplit pas ce critère, ce qui signifie que le fonctions dont nous avons besoin pour évaluer l'échec des données. J'ai besoin d'ajuster les données afin qu'elles puissent être validées avec succès dans SQL Server.
Par exemple:
Validation d'un LINESTRING
très simple qui se replie sur lui-même:
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).
Exécuter la fonction MakeValid
contre elle:
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)
Malheureusement, la fonction MakeValid
change l'ordre des points et supprime la troisième dimension, ce qui la rend inutilisable pour nous. Je cherche une autre approche qui résout ce problème sans réorganiser ou supprimer la 3ème dimension.
Des idées?
Mes données réelles contiennent des centaines/milliers de points.
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 n'est pas ne traite pas les coordonnées (xyz) comme de vraies valeurs 3D, il les traite comme (longitude latitude) avec une valeur "d'élévation" facultative, 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) sont non préservé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 201 , la demande a été clôturée sous le nom "Won't Fix". 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é de supprimer 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, MakeValid
peut aussi 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 comme un 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, alors 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, alors vous devrez écrire votre propre version de MakeValid
qui ajuste légèrement certains des points X ou Y source par une valeur minuscule, tout en préservant Z (et ne le fait pas) t faire 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:
[~ # ~] modifier [~ # ~] Ok, quelques choses que j'ai trouvées lors des tests:
STGeometryType
, vous ne pouvez pas obtenir le STNumPoints
ou utiliser STPointN
pour les parcourir. Si vous ne pouvez pas utiliser MakeValid
, vous êtes essentiellement obligé de travailler sur la représentation textuelle de l'objet géographique.STAsText()
renverra la représentation textuelle d'un objet non valide, mais ne renverra pas de valeurs Z ou M. Au lieu de cela, nous voulons AsTextZM()
ou ToString()
.Rand()
(les fonctions doivent être déterministes), donc je viens de la rendre Nudge par valeurs successivement 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 y a 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 objet MultiPoint
en utilisant le même ensemble de points, afin de pouvoir les parcourir et les déplacer, 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
Il s'agit de fonction FixBadLineString
de BradC modifié pour utiliser un nombre aléatoire compris entre 0 et 0,00000000003, ce qui lui permet d'évoluer pour LINESTRINGs
avec un grand nombre de points, tout en minimisant le changement des coordonnées:
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