Alternatif untuk MakeValid () untuk data spasial di SQL Server 2016

13

Saya memiliki tabel LINESTRINGdata geografi yang sangat besar yang saya pindahkan dari Oracle ke SQL Server. Ada sejumlah evaluasi yang dijalankan terhadap data ini di Oracle, dan mereka perlu dieksekusi terhadap data dalam SQL Server juga.

Masalahnya: SQL Server memiliki persyaratan yang lebih ketat untuk valid LINESTRINGdaripada Oracle; "Contoh LineString tidak dapat tumpang tindih dengan interval dua atau lebih titik berurutan". Kebetulan bahwa sebagian dari kita LINESTRINGtidak memenuhi kriteria itu, yang berarti bahwa fungsi yang kita butuhkan untuk mengevaluasi data gagal. Saya perlu menyesuaikan data sehingga dapat berhasil divalidasi dalam SQL Server.

Sebagai contoh:

Memvalidasi yang sangat sederhana LINESTRINGyang menggandakan kembali pada dirinya sendiri:

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).

Menjalankan MakeValidfungsi yang menentangnya:

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)

Sayangnya MakeValidfungsi mengubah urutan poin dan menghilangkan dimensi ketiga, yang membuatnya tidak dapat digunakan untuk kita. Saya mencari pendekatan lain yang menyelesaikan masalah ini tanpa menata ulang atau menghapus dimensi ke-3.

Ada ide?

Data aktual saya mengandung ratusan / ribuan poin.

CaptainSlock
sumber

Jawaban:

12

Biarkan saya perhatikan bahwa saya bermain dengan data spasial di SQL server untuk pertama kalinya (jadi Anda mungkin sudah tahu bagian pertama ini), tetapi butuh beberapa saat untuk mengetahui bahwa SQL Server tidak memperlakukan koordinat (xyz) sebagai benar Nilai 3D, itu memperlakukan mereka sebagai (lintang bujur) dengan nilai "ketinggian" opsional, Z, yang diabaikan oleh validasi dan fungsi lainnya.

Bukti:

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).

Contoh pertama Anda terasa aneh bagi saya karena (0 0 1), (0 1 2), dan (0 -1 3) tidak collinear dalam ruang 3D (saya seorang ahli matematika, jadi saya berpikir dalam istilah itu). IsValidDetailed(dan MakeValid) memperlakukan ini sebagai (0 0), (0 1), dan (0, -1), yang membuat garis tumpang tindih.

Untuk membuktikannya, cukup tukar X dan Z, dan validasi:

select geography::STGeomFromText('LINESTRING (1 0 0, 2 1 0, 3 -1 0)', 4326)
    .IsValidDetailed()

24400: Valid

Ini sebenarnya masuk akal jika kita menganggap ini sebagai wilayah atau jalur yang dilacak di permukaan dunia kita, alih-alih titik dalam ruang 3D matematika.


Bagian kedua dari masalah Anda adalah bahwa nilai-nilai titik Z (dan M) tidak dipertahankan oleh fungsi-fungsi SQL :

Koordinat Z tidak digunakan dalam perhitungan apa pun yang dibuat oleh perpustakaan dan tidak dilakukan melalui perhitungan perpustakaan apa pun.

Sayangnya ini karena desain. Ini dilaporkan ke Microsoft pada 2010 , permintaan ditutup sebagai "Tidak akan Perbaiki". Anda mungkin menemukan diskusi itu relevan, alasannya adalah:

Menetapkan Z dan M bersifat mendua, karena MakeValid memecah dan menggabungkan elemen spasial. Poin sering dibuat, dihapus atau dipindahkan selama proses ini. Oleh karena itu MakeValid (dan konstruksi lainnya) menjatuhkan nilai Z dan M.

Sebagai contoh:

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()

Nilai Z dan M adalah mendua untuk poin (0 0). Kami memutuskan untuk menjatuhkan Z dan M sepenuhnya daripada mengembalikan hasil yang setengah benar.

Anda dapat menetapkannya nanti jika tahu persis bagaimana caranya. Sebagai alternatif, Anda dapat mengubah cara Anda membuat objek menjadi valid pada input, atau menyimpan dua versi objek Anda, satu yang valid dan satu lagi yang mempertahankan semua fitur Anda. Jika Anda menjelaskan skenario Anda dengan lebih baik dan apa yang Anda lakukan dengan objek mungkin kami dapat memberikan Anda solusi tambahan.

Selain itu, seperti yang sudah Anda lihat, MakeValidjuga dapat melakukan hal-hal tak terduga lainnya , seperti mengubah urutan poin, mengembalikan MULTILINESTRING, atau bahkan mengembalikan objek TITIK.


Satu ide yang saya temui adalah untuk menyimpannya sebagai objek MULTIPOINT :

Masalahnya adalah ketika linestring Anda benar-benar menelusuri bagian garis kontinu antara dua titik yang sebelumnya dilacak oleh garis. Menurut definisi, jika Anda menelusuri kembali titik yang ada, maka linestring tidak lagi menjadi geometri paling sederhana yang dapat mewakili titik ini, dan MakeValid () akan memberi Anda multilinestring sebagai gantinya (dan kehilangan nilai Z / M Anda).

Sayangnya, jika Anda bekerja dengan data GPS atau sejenisnya maka sangat mungkin bahwa Anda mungkin telah menelusuri kembali jalur Anda di beberapa titik dalam rute, jadi linestrings tidak selalu berguna dalam skenario ini :( Bisa dibilang, data tersebut harus disimpan sebagai bagaimanapun, multipoint karena data Anda mewakili lokasi diskrit suatu objek yang diambil sampelnya pada titik reguler dalam waktu.

Dalam kasus Anda itu memvalidasi dengan baik:

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

24400: Valid

Jika Anda benar-benar perlu mempertahankan ini sebagai LINESTRING, maka Anda harus menulis versi Anda sendiri MakeValidyang sedikit menyesuaikan beberapa sumber X atau poin Y dengan nilai kecil, sambil tetap mempertahankan Z (dan tidak melakukan hal-hal gila lainnya seperti mengubahnya menjadi tipe objek lain).

Saya masih mengerjakan beberapa kode, tetapi coba lihat beberapa ide awal di sini:


EDIT Ok, beberapa hal yang saya temukan saat menguji:

  • Jika objek geometri tidak valid, Anda tidak bisa berbuat banyak dengannya. Anda tidak dapat membaca STGeometryType, Anda tidak bisa mendapatkan STNumPointsatau menggunakannya STPointNuntuk mengulanginya. Jika Anda tidak dapat menggunakan MakeValid, Anda pada dasarnya terjebak dengan operasi pada representasi teks dari objek geografis.
  • Menggunakan STAsText()akan mengembalikan representasi teks bahkan objek yang tidak valid, tetapi tidak mengembalikan nilai Z atau M. Sebaliknya, kami ingin AsTextZM()atau ToString().
  • Anda tidak dapat membuat fungsi yang memanggil RAND()(fungsi harus bersifat deterministik), jadi saya hanya membuatnya mendorong dengan nilai yang lebih besar dan lebih besar secara berturut-turut. Saya benar-benar tidak tahu apa presisi data Anda, atau seberapa tolerannya perubahan kecil, jadi gunakan atau modifikasi fungsi ini atas kebijakan Anda sendiri.

Saya tidak tahu apakah ada input yang mungkin menyebabkan loop ini berlangsung selamanya. Anda telah diperingatkan.

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

Alih-alih mengurai string, saya memilih untuk membuat MultiPointobjek baru menggunakan set poin yang sama, jadi saya bisa beralih melalui mereka dan mendorong mereka, lalu memasang kembali LineString baru. Berikut beberapa kode untuk mengujinya, 3 dari nilai-nilai ini (termasuk sampel Anda) mulai tidak valid tetapi sudah diperbaiki:

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
sumber
Jawaban yang bagus, terima kasih BradC. Saya tidak memasukkan ini dalam pertanyaan saya, tetapi data aktual saya mengandung ratusan / ribuan poin, jadi "@tinynum * 2" tidak berkelanjutan. Sebagai gantinya saya menjatuhkan "@tinynum" seluruhnya dan menggunakan angka acak antara 0 dan 0,000000003. Saya sudah menjalankan ini terhadap data dan sejauh ini, dari 22k selesai, semua divalidasi sebagai LINESTRINGs.
CaptainSlock
3

Ini adalah fungsi BradC yang di-FixBadLineString tweak untuk menggunakan angka acak antara 0 dan 0,000000003, sehingga memungkinkannya untuk menskala LINESTRINGsdengan sejumlah besar poin, dan juga meminimalkan perubahan pada koordinat:

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
CaptainSlock
sumber
1
Terlihat sangat bagus, saya tidak tahu tentang PWDENCRYPTfungsinya. Anda dapat mengabaikan ABSdan akan mengembalikan angka positif atau negatif, jadi kami tidak selalu menambahkan ke X dan mengurangi dari Y.
BradC