Alternatywa dla MakeValid () dla danych przestrzennych w SQL Server 2016

13

Mam bardzo dużą tabelę LINESTRINGdanych geograficznych, które przenoszę z Oracle na SQL Server. Istnieje szereg ocen, które są wykonywane w odniesieniu do tych danych w Oracle, i będą musiały zostać wykonane również w stosunku do danych w SQL Server.

Problem: SQL Server ma bardziej rygorystyczne wymagania dla ważnego LINESTRINGniż Oracle; „Instancja LineString nie może nakładać się na siebie w odstępie dwóch lub więcej kolejnych punktów”. Zdarza się tak, że procent naszych LINESTRINGnie spełnia tego kryterium, co oznacza, że ​​funkcje potrzebne do oceny danych zawodzą. Muszę dostosować dane, aby można je było pomyślnie zweryfikować w programie SQL Server.

Na przykład:

Sprawdzanie bardzo prostej, LINESTRINGktóra podwaja się:

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

Wykonywanie MakeValidfunkcji przeciwko niemu:

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)

Niestety MakeValidfunkcja zmienia kolejność punktów i usuwa trzeci wymiar, co czyni go dla nas bezużytecznym. Szukam innego rozwiązania, które rozwiązałoby ten problem bez zmiany kolejności lub usunięcia trzeciego wymiaru.

Jakieś pomysły?

Moje rzeczywiste dane zawierają setki / tysiące punktów.

CaptainSlock
źródło

Odpowiedzi:

12

Pozwólcie mi ostrzec, że gram po raz pierwszy danymi przestrzennymi na serwerze SQL (więc prawdopodobnie już znasz tę pierwszą część), ale zajęło mi trochę czasu, aby dowiedzieć się, że SQL Server nie traktuje współrzędnych (xyz) jako prawdziwych Wartości 3D traktuje je jako (długość i szerokość geograficzną) z opcjonalną wartością „wysokości” Z, która jest ignorowana przez walidację i inne funkcje.

Dowód:

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

Twój pierwszy przykład wydawał mi się dziwny, ponieważ (0 0 1), (0 1 2) i (0 -1 3) nie są współliniowe w przestrzeni 3D (jestem matematykiem, więc tak myślałem). IsValidDetailed(i MakeValid) traktuje je jako (0 0), (0 1) i (0, -1), co tworzy nakładającą się linię.

Aby to udowodnić, po prostu zamień X i Z, a to potwierdzi:

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

24400: Valid

Ma to sens, jeśli uważamy je za regiony lub ścieżki wytyczone na powierzchni naszego globu, zamiast punktów w matematycznej przestrzeni 3D.


Druga część problemu polega na tym, że wartości punktowe Z (i M) niezachowywane przez SQL za pomocą funkcji :

Współrzędne Z nie są wykorzystywane w żadnych obliczeniach wykonanych przez bibliotekę i nie są przenoszone przez żadne obliczenia biblioteczne.

Jest to niestety zgodne z projektem. Zgłoszono to firmie Microsoft w 2010 r. Żądanie zostało zamknięte jako „Nie naprawi”. Ta dyskusja może być dla Ciebie istotna, jej uzasadnienie jest następujące:

Przypisywanie Z i M jest niejednoznaczne, ponieważ MakeValid dzieli i łączy elementy przestrzenne. Punkty często są tworzone, usuwane lub przenoszone podczas tego procesu. Dlatego MakeValid (i inne konstrukcje) obniża wartości Z i M.

Na przykład:

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

Wartości Z i M są niejednoznaczne dla punktu (0 0). Postanowiliśmy całkowicie porzucić Z i M zamiast zwracać w połowie poprawny wynik.

Możesz je przypisać później, jeśli dokładnie wiesz, jak to zrobić. Alternatywnie możesz zmienić sposób generowania obiektów, aby były poprawne na wejściu, lub zachować dwie wersje swoich obiektów, jedną prawidłową i drugą, która zachowuje wszystkie funkcje. Jeśli lepiej wyjaśnisz swój scenariusz i co robisz z obiektami, być może będziemy w stanie dać ci dodatkowe obejścia.

Ponadto, jak już widzieliście, MakeValidmogą również robić inne nieoczekiwane rzeczy , takie jak zmiana kolejności punktów, zwracanie MULTILINESTRING, a nawet zwracanie obiektu POINT.


Jednym z pomysłów, na które natknąłem, było przechowywanie ich jako obiektu MULTIPOINT :

Problem polega na tym, że linia odniesienia faktycznie pobiera ciągły odcinek linii między dwoma punktami, który wcześniej był śledzony przez linię. Z definicji, jeśli przeglądasz istniejące punkty, to linia nie jest już najprostszą geometrią, która może reprezentować ten zestaw punktów, a MakeValid () da ci zamiast tego multilinestring (i stracisz wartości Z / M).

Niestety, jeśli pracujesz z danymi GPS lub podobnymi, jest całkiem prawdopodobne, że mogłeś prześledzić swoją ścieżkę w pewnym punkcie trasy, więc linie nie zawsze są przydatne w tych scenariuszach :( Prawdopodobnie takie dane powinny być przechowywane jako i tak wielopunktowy, ponieważ dane reprezentują dyskretną lokalizację obiektu próbkowanego w regularnych punktach czasowych.

W twoim przypadku sprawdza się dobrze:

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

24400: Valid

Jeśli absolutnie musisz zachować je jako LINESTRINGS, będziesz musiał napisać własną wersję, MakeValidktóra nieznacznie dostosowuje niektóre źródłowe punkty X lub Y o niewielką wartość, zachowując jednocześnie Z (i nie robi innych szalonych rzeczy, takich jak przekonwertować na inne typy obiektów).

Nadal pracuję nad kodem, ale spójrz na niektóre z początkowych pomysłów tutaj:


EDYCJA Ok, kilka rzeczy znalazłem podczas testowania:

  • Jeśli obiekt geometrii jest nieprawidłowy, po prostu niewiele możesz z nim zrobić. Nie możesz odczytać STGeometryType, nie możesz uzyskać STNumPointsani użyć STPointNdo ich powtarzania. Jeśli nie możesz użyć MakeValid, po prostu utkniesz w operowaniu tekstową reprezentacją obiektu geograficznego.
  • Użycie STAsText()spowoduje zwrócenie reprezentacji tekstowej nawet nieprawidłowego obiektu, ale nie zwróci wartości Z lub M. Zamiast tego chcemy AsTextZM()lub ToString().
  • Nie można utworzyć funkcji, która będzie wywoływać RAND()(funkcje muszą być deterministyczne), dlatego właśnie zmusiłem ją do przesuwania się o kolejne coraz większe wartości. Naprawdę nie mam pojęcia, jaka jest dokładność twoich danych ani jaka jest tolerancja na małe zmiany, więc używaj lub modyfikuj tę funkcję według własnego uznania.

Nie mam pojęcia, czy są możliwe dane wejściowe, które spowodują, że ta pętla będzie działać wiecznie. Zostałeś ostrzeżony.

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

Zamiast analizować ciąg, zdecydowałem się utworzyć nowy MultiPointobiekt przy użyciu tego samego zestawu punktów, aby móc iterować je i szturchać, a następnie ponownie złożyć nowy LineString. Oto kod do przetestowania, 3 z tych wartości (w tym Twoja próbka) zaczynają się niepoprawne, ale zostały naprawione:

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
źródło
Świetna odpowiedź, dziękuję BradC. Nie uwzględniłem tego w moim pytaniu, ale moje rzeczywiste dane zawierają setki / tysiące punktów, więc „@tinynum * 2” nie był zrównoważony. Zamiast tego całkowicie porzuciłem „@tinynum” i użyłem losowej liczby od 0 do 0,000000003. Uruchomiłem to z danymi i do tej pory, z 22 tys. Ukończonych, wszystkie zostały zatwierdzone jako LINESTRING.
CaptainSlock
3

Jest to funkcja BradCFixBadLineString dostosowana do używania losowej liczby od 0 do 0,000000003, umożliwiając w ten sposób skalowanie LINESTRINGsz dużą liczbą punktów, a także minimalizując zmianę współrzędnych:

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
źródło
1
Wygląda naprawdę dobrze, nie wiedziałem o tej PWDENCRYPTfunkcji. Mogłeś ABS
pominąć