web-dev-qa-db-ja.com

SQL Server 2016の空間データに対するMakeValid()の代替

OracleからSQL Serverに移動する、地理LINESTRINGデータの非常に大きなテーブルがあります。 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番目の次元を並べ替えたり削除したりせずにこの問題を解決する別のアプローチを探しています。

何か案は?

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

13
CaptainSlock

SQLサーバーで初めて空間データを操作しているので注意してください(おそらく、すでにこの最初の部分を知っているでしょう)が、SQLサーバーであることがわかるまでに少し時間がかかりました(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年にマイクロソフトに報告されました 、リクエストは「修正されません」としてクローズされました。あなたはその議論が関連していると思うかもしれません、彼らの推論は:

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つのバージョンのオブジェクトを保持したりできます。1つは有効で、もう1つはすべての機能を保持します。シナリオの説明とオブジェクトの操作について説明すると、追加の回避策を提供できる可能性があります。

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


代わりに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を作成する必要があります。これにより、ソースのXまたはYポイントの一部を小さな値でわずかに調整しますが、Zは維持します(そしてt他のオブジェクトタイプに変換するなど、他のクレイジーなことを行います)。

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


[〜#〜] edit [〜#〜]OK、テスト中に見つけたいくつかのこと:

  • ジオメトリオブジェクトが無効である場合、それを使って何もできません。 STGeometryTypeを読み取ったり、STNumPointsを取得したり、STPointNを使用してそれらを反復処理したりすることはできません。 MakeValidを使用できない場合は、基本的に地理オブジェクトのテキスト表現の操作に行き詰まっています。
  • STAsText()を使用すると、無効なオブジェクトのテキスト表現も返されますが、ZまたはMの値は返されません。代わりに、AsTextZM()またはToString()が必要です。
  • Rand()を呼び出すfunctionを作成することはできません(関数は決定論的である必要があります)。私はあなたのデータの精度が何であるか、またはそれが小さな変更に対してどれほど許容できるかを本当に知りません。そのため、この関数を使用するか、またはあなた自身の裁量で変更してください。

このループが永遠に続く原因となる可能性のある入力があるかどうかはわかりません。あなたは警告されました。

_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
_
12
BradC

これは 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
3
CaptainSlock