SQL Server 2016の空間データ用のMakeValid()の代替


13

LINESTRINGOracleからSQL Serverに移行する地理データの非常に大きなテーブルがあります。Oracleのこのデータに対して実行される評価は多数あり、SQL Serverのデータに対しても実行する必要があります。

問題:SQL ServerにはLINESTRING、Oracleよりも有効な要件が厳しい。「LineStringインスタンスは、2つ以上の連続したポイントの間隔で自身をオーバーラップさせることはできません」。LINESTRINGsの パーセンテージがその基準を満たしていないということが起こります。つまり、データを評価するために必要な関数が失敗します。データを調整して、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番目の次元を削除するため、使用できなくなります。3次元を並べ替えたり削除したりせずにこの問題を解決する別のアプローチを探しています。

何か案は?

私の実際のデータには数百/数千のポイントが含まれています。

回答:


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)が3D空間で同一線上にないために奇妙に見えました(私は数学者なので、それらの用語で考えていました)。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

これは、これらを数学的な3D空間の点ではなく、地球の表面上でトレースされた領域またはパスと考える場合、実際に意味があります。


問題の2番目の部分は、Z(およびM)ポイント値が関数によってSQLによって保存さないことです:

Z座標は、ライブラリによって行われた計算では使用されず、ライブラリ計算を介して実行されません。

残念ながら、これは仕様です。これは2010年にMicrosoftに報告され、リクエストは「修正しない」としてクローズされました。あなたは議論が関連していると思うかもしれません、彼らの理由は次のとおりです:

MakeValidは空間要素を分割および結合するため、ZとMの割り当てはあいまいです。多くの場合、このプロセス中にポイントが作成、削除、または移動されます。したがって、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を完全に削除することにしました。

方法を正確に知っていれば、後で割り当てることができます。または、入力時に有効になるようにオブジェクトを生成する方法を変更したり、有効なオブジェクトとすべての機能を保持するオブジェクトの2つのバージョンを保持したりできます。シナリオをよりよく説明し、オブジェクトで何をするかを説明すれば、追加の回避策を提供できるかもしれません。

さらに、既に見たように、ポイントの順序を変更したり、MULTILINESTRINGを返したり、POINTオブジェクトを返したりするなど、MakeValid他の予期しないこともできます。


私が出会った1つのアイデアは、代わりにMULTIPOINTオブジェクトとして保存することでした:

問題は、ラインストリングが、以前にラインでトレースされた2つのポイント間のラインの連続セクションを実際にリトレースするときです。定義により、既存のポイントをトレースしている場合、ラインストリングはこのポイントセットを表すことができる最も単純なジオメトリではなくなり、MakeValid()は代わりにマルチラインストリングを提供します(そしてZ / M値を失います)。

残念ながら、GPSデータなどで作業している場合は、ルートのあるポイントでパスをトレースした可能性が高いため、これらのシナリオでは線ストリングは必ずしも有用ではありません:(おそらく、そのようなデータはとにかくマルチポイント。データは、一定の時点でサンプリングされたオブジェクトの個別の位置を表すためです。

あなたの場合、それはうまく検証されます:

select geometry::STGeomFromText('MULTIPOINT (0 0 1, 0 1 2, 0 -1 3)',4326)
    .IsValidDetailed()

24400: Valid

これらをLINESTRINGSとして絶対に維持する必要がある場合MakeValidは、Zを維持しながら、ソースXまたはYポイントの一部をわずかな値でわずかに調整する独自のバージョンを記述する必要があります他のオブジェクトタイプに変換します)。

私はまだいくつかのコードに取り組んでいますが、ここでいくつかの最初のアイデアを見てみましょう。


編集 OK、テスト中に見つけたいくつかのこと:

  • ジオメトリオブジェクトが無効である場合、それを使用して多くのことを行うことはできません。を読む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が完了し、すべてがLINESTRINGとして検証されました。
キャプテンロック

3

これは、0から0.000000003の間の乱数を使用するように調整されたBradCのFixBadLineString関数です。これにより、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
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.