让我告诫我是第一次使用SQL Server中的空间数据(所以您可能已经知道了第一部分),但是花了我一段时间才弄清楚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)在3D空间中不是共线的(我是数学家,所以我在考虑这些术语)。IsValidDetailed
(and 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空间中的点,则这实际上是有道理的。
问题的第二部分是SQL 无法通过函数保留 Z(和M)点值:
库进行的任何计算都不会使用Z坐标,库计算也不会进行Z坐标处理。
不幸的是,这是设计使然。这是在2010年向Microsoft报告的,该请求被关闭为“ Wo n't Fix”。您可能会发现相关的讨论很重要,其理由是:
分配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。这是一些测试它的代码,其中三个值(包括您的样本)开始无效,但已修复:
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