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


13

J'ai une très grande table de LINESTRINGdonnées géographiques que je déplace d'Oracle vers SQL Server. Un certain nombre d'évaluations 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 valide LINESTRINGqu'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 notreLINESTRING s ne remplit pas ce critère, ce qui signifie que les fonctions dont nous avons besoin pour évaluer les données échouent. J'ai besoin d'ajuster les données afin qu'elles puissent être validées avec succès dans SQL Server.

Par exemple:

Valider un très simple LINESTRINGqui 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 MakeValidfonction 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 MakeValidfonction 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.

Réponses:


12

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 MakeValidpouvez é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 MakeValidqui 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 STNumPointsou utiliser STPointNpour 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 MultiPointobjet 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

Excellente réponse, merci BradC. Je n'ai pas inclus cela dans ma question, mais mes données réelles contiennent des centaines / milliers de points, donc "@tinynum * 2" n'était pas viable. Au lieu de cela, j'ai complètement supprimé "@tinynum" et utilisé un nombre aléatoire compris entre 0 et 0,00000000003. J'ai couru cela contre les données et jusqu'à présent, sur 22k terminés, tous ont été validés en tant que LINESTRING.
CaptainSlock

3

Il s'agit de la FixBadLineStringfonction de BradC modifiée pour utiliser un nombre aléatoire compris entre 0 et 0,00000000003, ce qui lui permet de s'adapter LINESTRINGsà un grand nombre de points et de minimiser également la modification 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

1
Ça a l'air vraiment bien, je ne connaissais pas la PWDENCRYPTfonction. Vous auriez pu laisser de côté le ABSet il aurait retourné un nombre positif ou négatif, donc nous ne sommes pas toujours en train d'ajouter à X et de soustraire de Y.
BradC
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.