Alternative zu MakeValid () für räumliche Daten in SQL Server 2016

13

Ich habe eine sehr große Tabelle mit Geografiedaten LINESTRING, die ich von Oracle zu SQL Server übertrage. Es gibt eine Reihe von Auswertungen, die für diese Daten in Oracle ausgeführt werden, und sie müssen auch für die Daten in SQL Server ausgeführt werden.

Das Problem: Für SQL Server gelten strengere Anforderungen als für LINESTRINGOracle. Msgstr "Die LineString - Instanz kann sich nicht über ein Intervall von zwei oder mehr aufeinanderfolgenden Punkten überlappen". Es ist einfach so, dass ein Prozentsatz von unseremLINESTRING s dieses Kriterium nicht erfüllt, was bedeutet, dass die Funktionen, die wir zur Auswertung der Daten benötigen, fehlschlagen. Ich muss die Daten anpassen, damit sie in SQL Server erfolgreich validiert werden können.

Beispielsweise:

Validierung eines sehr einfachen LINESTRING, das sich auf sich selbst verdoppelt:

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

Ausführen der MakeValidFunktion dagegen:

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)

Leider MakeValidändert die Funktion die Reihenfolge der Punkte und entfernt die dritte Dimension, was sie für uns unbrauchbar macht. Ich suche nach einem anderen Ansatz, der dieses Problem löst, ohne die 3. Dimension neu anzuordnen oder zu entfernen.

Irgendwelche Ideen?

Meine tatsächlichen Daten enthalten Hunderte / Tausende von Punkten.

CaptainSlock
quelle

Antworten:

12

Lassen Sie mich bedenken, dass ich zum ersten Mal mit räumlichen Daten in SQL Server spiele (so dass Sie diesen ersten Teil wahrscheinlich bereits kennen), aber ich habe eine Weile gebraucht, um herauszufinden, dass SQL Server (xyz) -Koordinaten nicht als wahr behandelt 3D-Werte werden als (Längen- und Breitengrad) mit einem optionalen Höhenwert (Z) behandelt, der von der Validierung und anderen Funktionen ignoriert wird.

Beweis:

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

Ihr erstes Beispiel kam mir komisch vor, weil (0 0 1), (0 1 2) und (0 -1 3) im 3D-Raum nicht kollinear sind (ich bin Mathematiker, also habe ich in diesen Begriffen nachgedacht). IsValidDetailed(undMakeValid ) behandelt diese als (0 0), (0 1) und (0, -1), was eine überlappende Linie ergibt.

Um dies zu beweisen, tauschen Sie einfach das X und das Z aus und es wird überprüft:

select geography::STGeomFromText('LINESTRING (1 0 0, 2 1 0, 3 -1 0)', 4326)
    .IsValidDetailed()

24400: Valid

Dies ist tatsächlich sinnvoll, wenn wir uns diese als Regionen oder Pfade vorstellen, die auf der Oberfläche unseres Globus statt als Punkte im mathematischen 3D-Raum gezeichnet werden.


Der zweite Teil Ihres Problems besteht darin, dass Z- (und M-) Punktwerte von SQL nicht über Funktionen beibehalten werden :

Z-Koordinaten werden in den von der Bibliothek durchgeführten Berechnungen nicht verwendet und werden nicht durch Bibliotheksberechnungen geführt.

Dies ist leider beabsichtigt. Dies wurde Microsoft im Jahr 2010 gemeldet , die Anfrage wurde als "Won't Fix" geschlossen. Vielleicht finden Sie diese Diskussion relevant, ihre Argumentation ist:

Das Zuweisen von Z und M ist nicht eindeutig, da MakeValid räumliche Elemente aufteilt und zusammenführt. Während dieses Vorgangs werden häufig Punkte erstellt, entfernt oder verschoben. Daher löscht MakeValid (und andere Konstruktionen) die Z- und M-Werte.

Beispielsweise:

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()

Die Werte Z und M sind für Punkt (0 0) nicht eindeutig. Wir haben beschlossen, Z und M komplett zu streichen, anstatt ein halbwegs korrektes Ergebnis zu liefern.

Sie können sie später zuweisen, wenn Sie genau wissen, wie. Alternativ können Sie die Art und Weise ändern, in der Sie Ihre Objekte generieren, um sie bei der Eingabe gültig zu machen, oder zwei Versionen Ihrer Objekte beibehalten, eine gültige und eine andere, die alle Ihre Funktionen beibehält. Wenn Sie Ihr Szenario besser erklären und wissen, was Sie mit den Objekten tun, können wir Ihnen möglicherweise zusätzliche Problemumgehungen anbieten.

Darüber hinaus können Sie, wie Sie bereits gesehen haben, MakeValidauch andere unerwartete Aktionen ausführen, z. B. die Reihenfolge der Punkte ändern, einen MULTILINESTRING zurückgeben oder sogar ein POINT-Objekt zurückgeben.


Ich bin auf die Idee gekommen, sie stattdessen als MULTIPOINT-Objekt zu speichern :

Das Problem ist, wenn Ihre Linienfolge tatsächlich einen durchgehenden Linienabschnitt zwischen zwei Punkten zurückverfolgt, der zuvor von der Linie verfolgt wurde. Per Definition ist die Linienfolge nicht mehr die einfachste Geometrie, die diese Punktmenge darstellen kann, wenn Sie vorhandene Punkte nachzeichnen, und MakeValid () gibt Ihnen stattdessen eine Mehrfachlinienfolge (und verliert Ihre Z / M-Werte).

Wenn Sie mit GPS-Daten oder Ähnlichem arbeiten, ist es leider sehr wahrscheinlich, dass Sie Ihren Weg an einem bestimmten Punkt der Route zurückverfolgt haben. Daher sind Linienfolgen in den folgenden Szenarien nicht immer so nützlich: (Vermutlich sollten solche Daten wie folgt gespeichert werden Ein Multipoint, da Ihre Daten den diskreten Ort eines Objekts darstellen, das zu regelmäßigen Zeitpunkten abgetastet wird.

In Ihrem Fall ist es ganz gut validiert:

select geometry::STGeomFromText('MULTIPOINT (0 0 1, 0 1 2, 0 -1 3)',4326)
    .IsValidDetailed()

24400: Valid

Wenn Sie diese unbedingt als LINESTRINGS pflegen müssen, müssen Sie Ihre eigene Version von schreiben MakeValid , die einige der X - oder Y - Punkte der Quelle geringfügig um einen winzigen Wert anpasst, während Z erhalten bleibt (und andere verrückte Dinge wie z in andere Objekttypen konvertieren).

Ich arbeite immer noch an Code, aber wirf hier einen Blick auf einige der ersten Ideen:


EDIT Ok, ein paar Dinge, die ich beim Testen gefunden habe:

  • Wenn das Geometrieobjekt ungültig ist, können Sie nicht viel damit anfangen. Sie können das nicht lesen STGeometryType, Sie können das nicht bekommen STNumPointsoder verwenden STPointN, um durch sie zu iterieren. Wenn Sie nicht verwenden können MakeValid, müssen Sie im Grunde nur die Textdarstellung des geografischen Objekts bearbeiten.
  • Bei Verwendung von STAsText()wird die Textdarstellung auch eines ungültigen Objekts zurückgegeben, es werden jedoch keine Z- oder M-Werte zurückgegeben. Stattdessen wollen wir AsTextZM()oder ToString().
  • Sie können keine Funktion erstellen , die aufruft RAND()(Funktionen müssen deterministisch sein), also habe ich sie nur durch sukzessive größere und größere Werte angestupst. Ich habe wirklich keine Ahnung, wie genau Ihre Daten sind oder wie tolerant sie gegenüber kleinen Änderungen sind. Verwenden oder ändern Sie diese Funktion daher nach eigenem Ermessen.

Ich habe keine Ahnung, ob es mögliche Eingänge gibt, die dazu führen, dass diese Schleife für immer weitergeht. Du wurdest gewarnt.

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

Anstatt die Zeichenfolge zu analysieren, habe ich mich dafür entschieden, ein neues MultiPointObjekt mit der gleichen Anzahl von Punkten zu erstellen , damit ich sie durchlaufen und anstoßen und dann einen neuen LineString zusammensetzen kann. Hier ist ein Code zum Testen: 3 dieser Werte (einschließlich Ihres Beispiels) beginnen ungültig, sind jedoch behoben:

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
BradC
quelle
Tolle Antwort, danke BradC. Ich habe dies nicht in meine Frage aufgenommen, aber meine tatsächlichen Daten enthalten Hunderte / Tausende von Punkten, sodass "@tinynum * 2" nicht nachhaltig war. Stattdessen habe ich "@tinynum" komplett fallen gelassen und eine Zufallszahl zwischen 0 und 0,000000003 verwendet. Ich habe dies mit den Daten verglichen und bis jetzt wurden alle 22.000 als LINESTRINGs validiert.
CaptainSlock
3

Dies ist die FixBadLineStringFunktion von BradC, die dahingehend optimiert wurde, dass eine Zufallszahl zwischen 0 und 0,000000003 verwendet wird, um eine Skalierung LINESTRINGsmit einer großen Anzahl von Punkten zu ermöglichen und die Änderung der Koordinaten zu minimieren:

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
CaptainSlock
quelle
1
Sieht wirklich gut aus, ich wusste nichts über die PWDENCRYPTFunktion. Du hättest das weglassen könnenABS und es hätte entweder eine positive oder eine negative Zahl zurückgegeben, sodass wir nicht immer zu X addieren und von Y subtrahieren.
BradC,