Дозвольте мені зауважити, що я граю з просторовими даними на SQL-сервері вперше (тому ви, напевно, вже знаєте цю першу частину), але мені знадобилося певний час, щоб зрозуміти, що SQL Server не трактує (xyz) координати як істинні 3D-значення, він розглядає їх як (широту довготи) з необов'язковим значенням "висота", Z, яке ігнорується валідацією та іншими функціями.
Докази:
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).
Ваш перший приклад здався мені дивним, тому що (0 0 1), (0 1 2) і (0 -1 3) не є колінеарними в тривимірному просторі (я математик, тому я думав у цих термінах). IsValidDetailed
(і MakeValid
) розглядає їх як (0 0), (0 1) і (0, -1), що робить лінію, що перекривається.
Щоб довести це, просто поміняйте місцями X і Z і це підтверджує:
select geography::STGeomFromText('LINESTRING (1 0 0, 2 1 0, 3 -1 0)', 4326)
.IsValidDetailed()
24400: Valid
Це насправді має сенс, якщо ми розглянемо це як регіони або шляхи, простежені на поверхні нашої земної кулі, а не точки в математичному 3D-просторі.
Друга частина вашого питання полягає в тому, що значення Z (і M) SQL не зберігаються через функції :
Z-координати не використовуються в будь-яких обчисленнях, зроблених бібліотекою, і не проводяться через будь-які бібліотечні обчислення.
На жаль, це дизайн. Про це повідомили Microsoft у 2010 році , запит закрито як "Не виправлено". Ви можете вважати цю дискусію актуальною, їх міркування такі:
Призначення Z і M неоднозначне, тому що MakeValid розбиває і об'єднує просторові елементи. Під час цього процесу очки часто створюються, видаляються або переміщуються. Тому MakeValid (та інші конструкції) знижує значення Z та M.
Наприклад:
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()
Значення Z і M неоднозначні для точки (0 0). Ми вирішили повністю відмовитись від Z і M, замість того, щоб повернути напівправильний результат.
Ви можете призначити їх згодом, якщо ви точно знаєте як. Крім того, ви можете змінити спосіб створення ваших об'єктів, щоб вони були дійсними при введенні, або зберегти дві версії об'єктів, одну, що є дійсною, та іншу, яка зберігає всі ваші функції. Якщо ви поясните свій сценарій краще і що ви робите з об'єктами, можливо, ми могли б дати вам додаткові шляхи вирішення.
Крім того, як ви вже бачили, MakeValid
ви також можете робити інші несподівані речі , наприклад змінити порядок балів, повернути MULTILINESTRING або навіть повернути об’єкт POINT.
Одна з ідей, на яку я натрапила, - це зберігати їх як об’єкт MULTIPOINT :
Проблема полягає в тому, що ваша лінія рядків насправді відтягує безперервний відрізок лінії між двома точками, який раніше був простежений лінією. За визначенням, якщо ви перетягуєте наявні точки, то рядкова лінія вже не є найпростішою геометрією, яка може представляти цей набір точок, і MakeValid () надасть вам багаторядковий рядок (і втратить значення Z / M).
На жаль, якщо ви працюєте з даними GPS або подібними, то цілком ймовірно, що ви могли перетягнути свій шлях в якийсь момент маршруту, тому рядки рядків не завжди корисні в цих сценаріях :( Можливо, такі дані повинні зберігатися як так чи інакше, багатоточкові дані, оскільки ваші дані представляють дискретні місця об'єкта, відібраного у вибірку в регулярні моменти часу.
У вашому випадку це добре підтверджує:
select geometry::STGeomFromText('MULTIPOINT (0 0 1, 0 1 2, 0 -1 3)',4326)
.IsValidDetailed()
24400: Valid
Якщо вам абсолютно потрібно підтримувати це як ПІДКЛЮЧЕННЯ, то вам доведеться написати свою власну версію, MakeValid
яка злегка коригує деякі джерельні точки X або Y на деяке крихітне значення, зберігаючи при цьому Z (і не робить інших божевільних речей, як перетворити його в інші типи об’єктів).
Я все ще працюю над деяким кодом, але погляньте на деякі вихідні ідеї тут:
РЕДАКТУЙ Добре, кілька речей, які я знайшов під час тестування:
- Якщо об’єкт геометрії недійсний, ви просто не зможете багато з цим зробити. Ви не можете прочитати
STGeometryType
, ви не можете отримати STNumPoints
або використовувати STPointN
для повторення через них. Якщо ви не можете скористатися MakeValid
, ви в основному затримані в роботі над текстовим поданням географічного об'єкта.
- Використання
STAsText()
поверне текстове зображення навіть недійсного об'єкта, але не поверне значення Z або M. Натомість ми хочемо AsTextZM()
або ToString()
.
- Ви не можете створити функцію, яка викликає
RAND()
(функції повинні бути детермінованими), тому я просто змусив її натискати на послідовно більші та більші значення. Я дійсно не маю уявлення, яка точність ваших даних чи наскільки толерантна до невеликих змін, тому використовуйте або змінюйте цю функцію на власний розсуд.
Я не маю уявлення, чи є можливі входи, які змусять цю петлю продовжуватися назавжди. Вас попередили
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
Замість розбору рядка я вирішив створити новий MultiPoint
об’єкт, використовуючи той самий набір точок, щоб я міг перебирати їх і натискати на них, а потім збирати новий LineString. Ось код, який можна перевірити, 3 з цих значень (включаючи зразок) починаються недійсними, але фіксуються:
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