SQL Server 2016中用于空间数据的MakeValid()的替代方法

我有一个非常庞大的地理数据表,其中包含了许多LINESTRING数据。我正在将这些数据从Oracle迁移到SQL Server。在Oracle中,对这些数据进行了一些评估,并且需要在SQL Server中执行相同的评估。 问题是:SQL Server对于有效的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函数会改变点的顺序并且移除第三维度,这使得它对我们来说无法使用。我正在寻找另一种方法来解决这个问题,而不需要重新排序或移除第三维度。 有什么想法吗? 我的实际数据包含了数百/数千个点。
2个回答

让我先说明一下,这是我第一次在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

非常好的回答,谢谢BradC。我在问题中没有提到,但我的实际数据包含数百/数千个点,所以"@tinynum * 2"是不可持续的。相反,我完全删除了"@tinynum"并使用了0到0.000000003之间的随机数。我一直在对数据进行测试,到目前为止,在完成的22k个数据中,全部都被验证为LINESTRINGs。 - CaptainSlock

这是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