web-dev-qa-db-fra.com

Alternative à MakeValid () pour les données spatiales dans SQL Server 2016

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 LINESTRINGs 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.

13
CaptainSlock

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:

  • 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 essentiellement obligé de travailler sur la représentation textuelle de l'objet géographique.
  • L'utilisation de 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().
  • Vous ne pouvez pas créer une fonction qui appelle 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
12
BradC

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
3
CaptainSlock