SQL Server 2016'daki uzamsal veriler için MakeValid () alternatifi


13

LINESTRINGOracle'dan SQL Server'a taşıdığım çok büyük bir coğrafya verisi tablosu var . Oracle'da bu verilere karşı yürütülen bir dizi değerlendirme vardır ve bunların da SQL Server'daki verilere karşı yürütülmesi gerekir.

Sorun: SQL Server, geçerli bir LINESTRINGOracle için daha sıkı gereksinimleri vardır ; "LineString örneği birbirini izleyen iki veya daha fazla noktanın üstesinden gelemez". Öyle olur ki, bizim LINESTRINGyüzdemiz bu kriteri karşılamıyor, yani verileri değerlendirmek için ihtiyaç duyduğumuz işlevler başarısız oluyor. SQL Server'da başarıyla doğrulanabilmesi için verileri ayarlamam gerekiyor.

Örneğin:

LINESTRINGKendini iki katına çıkaran çok basit bir doğrulama :

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İşlevin ona karşı yürütülmesi :

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)

Ne yazık ki MakeValidfonksiyon noktaların sırasını değiştirir ve üçüncü boyutu kaldırır, bu da bizim için kullanılamaz hale getirir. 3. boyutu yeniden sıralama veya kaldırmadan bu sorunu çözen başka bir yaklaşım arıyorum.

Herhangi bir fikir?

Gerçek verilerim yüzlerce / binlerce puan içeriyor.

Yanıtlar:


12

SQL sunucusunda ilk kez uzamsal verilerle oynadığımı söyleyeyim (muhtemelen bu ilk kısmı zaten biliyorsunuz), ancak SQL Server'ın (xyz) koordinatlarını doğru olarak işlemediğini anlamak biraz zaman aldı 3B değerleri, doğrulama ve diğer işlevler tarafından göz ardı edilen isteğe bağlı bir "yükseklik" değeri olan Z'ye (enlem boylamı) muamele eder.

Kanıt:

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

İlk örneğiniz bana garip geldi çünkü (0 0 1), (0 1 2) ve (0 -1 3) 3D uzayda eş zamanlı değiller (ben bir matematikçiyim, bu yüzden bu terimleri düşünüyordum). IsValidDetailed(ve MakeValid) bunları üst üste binen bir çizgi yapan (0 0), (0 1) ve (0, -1) olarak muamele etmektedir.

Bunu kanıtlamak için X ve Z'yi değiştirin ve geçerli kılar:

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

24400: Valid

Bunları, matematiksel 3B uzaydaki noktalar yerine, dünyamızın yüzeyinde izlenen bölgeler veya yollar olarak düşünürsek, bu gerçekten mantıklı.


Sorununuzun ikinci kısmı, Z (ve M) nokta değerlerinin işlevler aracılığıyla SQL tarafından korunmamasıdır :

Z-koordinatları, kütüphane tarafından yapılan hesaplamalarda kullanılmaz ve kütüphane hesaplamaları ile taşınmaz.

Bu maalesef tasarımdan kaynaklanıyor. Bu, 2010 yılında Microsoft'a bildirildi , istek "Düzeltilmeyecek" olarak kapatıldı. Bu tartışmayı alakalı bulabilirsiniz, gerekçeleri:

Z ve M atamak belirsizdir, çünkü MakeValid mekansal unsurları böler ve birleştirir. Puanlar genellikle bu işlem sırasında oluşturulur, kaldırılır veya taşınır. Bu nedenle MakeValid (ve diğer yapılar) Z ve M değerlerini düşürür.

Örneğin:

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 ve M değerleri nokta (0 0) için belirsizdir. Yarı doğru sonucu döndürmek yerine Z ve M'yi tamamen bırakmaya karar verdik.

Nasıl yapılacağını tam olarak biliyorsanız bunları daha sonra atayabilirsiniz. Alternatif olarak, nesnelerinizi girişte geçerli olacak şekilde oluşturma şeklinizi değiştirebilir veya nesnelerinizin biri geçerli diğeri de tüm özelliklerinizi koruyan iki sürümünü saklayabilirsiniz. Senaryoyu daha iyi açıklar ve nesnelerle ne yaparsanız, belki size ek çözümler verebiliriz.

Ayrıca, daha önce gördüğünüz gibi, noktaların sırasını değiştirmek, bir MULTILINESTRING döndürmek ve hatta bir POINT nesnesi döndürmek gibi MakeValidbeklenmedik şeyler de yapabilirsiniz .


Karşılaştığım bir fikir onları MULTIPOINT nesnesi olarak saklamaktı :

Sorun, linestring'inizin daha önce çizgi tarafından izlenen iki nokta arasındaki sürekli bir çizgi bölümünü geri çekmesidir. Tanım olarak, mevcut noktaları geri çekiyorsanız, linestring artık bu puan kümesini temsil edebilecek en basit geometri değildir ve MakeValid () size bunun yerine çok satırlı bir görünüm verecektir (ve Z / M değerlerinizi kaybedecektir).

Ne yazık ki, GPS verileri veya benzeri ile çalışıyorsanız, rotanızın bir noktasında yolunuzu geri çekmiş olmanız muhtemeldir, bu nedenle linestring'ler bu senaryolarda her zaman yararlı değildir :( Muhtemelen, bu tür veriler verileriniz düzenli noktalarda örneklenen bir nesnenin ayrı konumunu temsil ettiğinden, yine de çok noktalı.

Sizin durumunuzda gayet iyi geçer:

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

24400: Valid

Bunları kesinlikle LINESTRINGS olarak tutmanız gerekiyorsa, Z'yi MakeValidkorurken (ve diğer çılgın şeyleri yapmazken) kaynak X veya Y noktalarının bazılarını küçük bir değere hafifçe ayarlayan kendi versiyonunuzu yazmanız gerekecektir. diğer nesne türlerine dönüştür).

Hala bazı kodlar üzerinde çalışıyorum, ancak burada bazı başlangıç ​​fikirlerine bir göz atın:


EDIT Tamam, test ederken bulduğum birkaç şey:

  • Geometri nesnesi geçersizse, onunla fazla bir şey yapamazsınız. Sen okuyamaz STGeometryType, sen alamayan STNumPointsveya kullanımını STPointNonlara yineleme yapmak için. Eğer kullanamıyorsanız MakeValid, coğrafi nesnenin metin gösterim uzerinde çalışan temelde sıkışmış konum.
  • Kullanılması STAsText(), geçersiz bir nesnenin bile metin temsilini döndürür, ancak Z veya M değerlerini döndürmez. Bunun yerine, bizim istediğimiz AsTextZM()ya ToString().
  • Bir oluşturamazsınız işlevi çağrıları o RAND()ben sadece art arda daha büyük ve daha büyük değerlere göre dürtmek yapılan böylece, (fonksiyonları deterministik olması gerekir). Verilerinizin doğruluğu veya küçük değişikliklere ne kadar toleranslı olduğu hakkında hiçbir fikrim yok, bu nedenle bu işlevi kendi takdirinize bağlı olarak kullanın veya değiştirin.

Bu döngü sonsuza kadar devam edecek olası girişler varsa hiçbir fikrim yok. Uyarıldın.

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

Dizeyi ayrıştırmak yerine MultiPoint, aynı nokta kümesini kullanarak yeni bir nesne oluşturmayı seçtim; Test etmek için bazı kodlar var, bu değerlerden 3 tanesi (örneğiniz dahil) geçersiz başlıyor, ancak düzeltildi:

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

Harika cevap, teşekkürler BradC. Bunu soruma dahil etmedim, ancak gerçek verilerim yüzlerce / binlerce puan içeriyor, bu yüzden "@tinynum * 2" sürdürülebilir değildi. Bunun yerine "@tinynum" u tamamen düşürdüm ve 0 ile 0.000000003 arasında rastgele bir sayı kullandım. Bunu verilerle çalıştırıyorum ve şimdiye kadar 22k tamamlandı, hepsi LINESTRING olarak onaylandı.
CaptainSlock

3

Bu BradC en FixBadLineStringfonksiyonu , böylelikle için ölçek sağlayan, 0 ve 0.000000003 arasında rastgele bir sayı kullanmak tweaked LINESTRINGsnoktaları ile çok sayıda ve aynı zamanda koordinatlara değişiklik minimize eder:

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
Gerçekten iyi görünüyor, PWDENCRYPTişlevi bilmiyordum . Dışarıda bırakmış olabilirsiniz ABSve pozitif veya negatif bir sayı
döndürürdü
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.