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) nie są zachowywane 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, MakeValid
mogą 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ę, MakeValid
któ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ć STNumPoints
ani użyć STPointN
do 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 MultiPoint
obiekt 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