让我先说明一下,这是我第一次在SQL Server中处理空间数据(所以你可能已经知道这一点),但我花了一些时间才弄清楚SQL Server并不将(x y z)坐标视为真正的三维值,而是将它们视为(纬度 经度)加上可选的“高程”值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坐标在库的计算中没有被使用,也不会通过任何库的计算传递。
这是设计上的不幸。这个问题在2010年向Microsoft报告过,但请求被关闭并标记为“不予修复”。你可能会发现相关讨论有意义,他们的理由是:
赋予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()
对于点(0 0),值Z和M是模糊的。我们决定完全放弃Z和M,而不是返回一半正确的结果。
如果您确切知道如何,可以稍后分配它们。或者,您可以更改生成对象的方式,使其在输入时有效,或者保留两个版本的对象,一个有效,另一个保留所有功能。如果您能更好地解释您的情况以及您对对象的处理方式,也许我们可以提供额外的解决方法。
此外,正如您已经看到的那样,{{link1: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