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