ทางเลือกในการ MakeValid () สำหรับข้อมูลเชิงพื้นที่ใน SQL Server 2016


13

ฉันมีตารางLINESTRINGข้อมูลทางภูมิศาสตร์จำนวนมากที่ฉันย้ายจาก Oracle ไปยัง SQL Server มีการประเมินจำนวนหนึ่งที่ดำเนินการกับข้อมูลนี้ใน Oracle และพวกเขาจะต้องดำเนินการกับข้อมูลใน SQL Server ด้วย

ปัญหา: SQL Server มีข้อกำหนดที่เข้มงวดยิ่งขึ้นสำหรับการใช้ที่ถูกต้องLINESTRINGกว่า Oracle; "อินสแตนซ์ 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ฟังก์ชั่นเปลี่ยนลำดับของคะแนนและลบส่วนที่สามซึ่งทำให้เราใช้ไม่ได้ ฉันกำลังมองหาวิธีการอื่นที่สามารถแก้ปัญหานี้ได้โดยไม่ต้องจัดเรียงใหม่หรือลบส่วนที่สามออก

ความคิดใด ๆ

ข้อมูลจริงของฉันมีหลายร้อย / หลายพันคะแนน

คำตอบ:


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) ไม่ใช่ collinear ในอวกาศ 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

สิ่งนี้สมเหตุสมผลถ้าเราคิดว่าสิ่งเหล่านี้เป็นภูมิภาคหรือเส้นทางที่ติดตามบนพื้นผิวโลกของเราแทนที่จะเป็นจุดในพื้นที่ 3 มิติทางคณิตศาสตร์


ส่วนที่สองของปัญหาของคุณคือค่าจุด Z (และ M) จะไม่ถูกสงวนไว้โดย SQL ผ่านฟังก์ชัน :

พิกัด Z ไม่ได้ใช้ในการคำนวณใด ๆ ที่ทำโดยห้องสมุดและไม่ได้ดำเนินการผ่านการคำนวณห้องสมุดใด ๆ

นี่คือการออกแบบที่น่าเสียดาย สิ่งนี้ถูกรายงานไปยัง Microsoft ในปี 2010คำขอถูกปิดเป็น "ไม่สามารถแก้ไขได้" คุณอาจพบว่าการสนทนาที่เกี่ยวข้องกับเหตุผลของพวกเขาคือ:

การกำหนด 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()

ค่า Z และ M ไม่ชัดเจนสำหรับจุด (0 0) เราตัดสินใจที่จะวาง Z และ M โดยสมบูรณ์แทนที่จะกลับผลลัพธ์ที่ถูกต้องครึ่งหนึ่ง

คุณสามารถกำหนดพวกเขาในภายหลังหากคุณรู้วิธี หรือคุณสามารถเปลี่ยนวิธีที่คุณสร้างวัตถุของคุณให้ถูกต้องในอินพุตหรือเก็บวัตถุสองเวอร์ชันของคุณหนึ่งรุ่นที่ถูกต้องและอีกรุ่นหนึ่งที่รักษาคุณลักษณะทั้งหมดของคุณ หากคุณอธิบายสถานการณ์ของคุณให้ดีขึ้นและสิ่งที่คุณทำกับวัตถุบางทีเราอาจสามารถแก้ไขปัญหาเพิ่มเติมให้คุณได้

นอกจากนี้ตามที่คุณได้เห็นแล้วMakeValidยังสามารถทำสิ่งที่ไม่คาดคิดอื่น ๆเช่นเปลี่ยนลำดับของคะแนนส่งคืน MULTILINESTRING หรือส่งคืนวัตถุ POINT


หนึ่งความคิดที่ฉันเจอคือการเก็บไว้เป็นวัตถุ MULTIPOINT แทน :

ปัญหาคือเมื่อการคืนค่าของคุณดึงส่วนต่อเนื่องของเส้นตรงระหว่างจุดสองจุดที่ติดตามโดยสายก่อนหน้านี้ ตามคำจำกัดความถ้าคุณกำลังย้อนกลับจุดที่มีอยู่แล้ว linestring จะไม่ใช่เรขาคณิตที่ง่ายที่สุดที่สามารถเป็นตัวแทนของชุดนี้ได้แล้ว MakeValid () จะให้ multilinestring แทน (และสูญเสียค่า 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 ที่ผ่านมาทั้งหมดได้รับการตรวจสอบว่าเป็น LINESTRING
CaptainSlock

3

นี่คือฟังก์ชั่นของ 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
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.