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

13

У меня есть очень большая таблица географических LINESTRINGданных, которые я перемещаю из Oracle в SQL Server. Есть ряд оценок, которые выполняются для этих данных в Oracle, и их также нужно будет выполнять для данных в SQL Server.

Проблема: SQL Server предъявляет более строгие требования, LINESTRINGчем Oracle; «Экземпляр LineString не может перекрывать себя через интервал из двух или более последовательных точек». Просто так получается, что процент наших сотрудников LINESTRINGне соответствует этому критерию, а это означает, что функции, которые нам нужны для оценки данных, не работают. Мне нужно настроить данные так, чтобы они могли быть успешно проверены в 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-го измерения.

Есть идеи?

Мои реальные данные содержат сотни / тысячи точек.

CaptainSlock
источник

Ответы:

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

Это действительно имеет смысл, если мы думаем о них как о областях или путях, прорисованных на поверхности нашего земного шара, а не точках в математическом трехмерном пространстве.


Вторая часть вашей проблемы заключается в том, что значения точек 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

Если вам абсолютно необходимо поддерживать их как LINESTRINGS, то вам придется написать свою собственную версию, 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
BradC
источник
Отличный ответ, спасибо BradC. Я не включил это в свой вопрос, но мои фактические данные содержат сотни / тысячи точек, поэтому «@tinynum * 2» не был устойчивым. Вместо этого я полностью удалил «@tinynum» и использовал случайное число от 0 до 0,000000003. Я работал с данными и до сих пор, из 22 тыс. Завершенных, все были проверены как LINESTRING.
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
CaptainSlock
источник
1
Выглядит очень хорошо, я не знал о PWDENCRYPTфункции. Вы могли бы оставитьABS
опустить