Альтернатива MakeValid () для просторових даних у SQL Server 2016


13

У мене дуже велика таблиця географічних LINESTRINGданих, що я переходжу від Oracle до SQL Server. Існує ряд оцінок, які виконуються на основі цих даних в Oracle, і їх також потрібно буде виконати на даних у SQL Server.

Проблема: SQL Server має більш жорсткі вимоги до дійсних, LINESTRINGніж Oracle; "Екземпляр LineString не може перекриватися через інтервал двох або більше послідовних точок". Так буває, що відсоток наших LINESTRINGs не відповідає цьому критерію, це означає, що функції, необхідні нам для оцінки даних, не відповідають. Мені потрібно відкоригувати дані, щоб вони могли бути успішно перевірені в SQL Server.

Наприклад:

Перевірка дуже простої, LINESTRINGщо подвоюється на собі:

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

Виконання MakeValidфункції проти нього:

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)

На жаль, MakeValidфункція змінює порядок точок і видаляє третій вимір, що робить його непридатним для нас. Я шукаю інший підхід, який вирішує цю проблему без переупорядкування чи видалення 3-го виміру.

Будь-які ідеї?

Мої фактичні дані містять сотні / тисячі балів.

Відповіді:


12

Дозвольте мені зауважити, що я граю з просторовими даними на 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) не є колінеарними в тривимірному просторі (я математик, тому я думав у цих термінах). IsValidDetailedMakeValid) розглядає їх як (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

Чудова відповідь, дякую БредК. Я не включив це до свого запитання, але мої фактичні дані містять сотні / тисячі пунктів, тому "@tinynum * 2" не був стійким. Натомість я повністю скинув "@tinynum" і використав випадкове число від 0 до 0,000000003. Я запускав це проти даних, і до цього часу, з 22 к. Завершених, усі були підтверджені як ЛІНІСТРИНГ.
CaptainSlock

3

Це BradC в FixBadLineStringфункції перероблено , щоб використовувати випадкове число між 0 і 0.000000003, тим самим дозволяючи йому масштабувати для LINESTRINGsз великою кількістю точок, а також звести до мінімуму зміни координат:

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
Виглядає дуже добре, я не знав про PWDENCRYPTфункцію. Ви могли б вийти з цього місця, ABSі воно повернуло б або додатне, або негативне число, тому ми не завжди додаємо до X і віднімаємо від Y.
BradC
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.