Alternativa a MakeValid () per i dati spaziali in SQL Server 2016


13

Ho una grande tabella di LINESTRINGdati geografici che sto passando da Oracle a SQL Server. Esistono diverse valutazioni eseguite su questi dati in Oracle e dovranno essere eseguite anche su dati in SQL Server.

Il problema: SQL Server ha requisiti più severi per un valido LINESTRINGdi Oracle; "L'istanza LineString non può sovrapporsi su un intervallo di due o più punti consecutivi". Accade solo che una percentuale della nostra LINESTRINGs non rispetti quel criterio, il che significa che le funzioni di cui abbiamo bisogno per valutare i dati falliscono. Ho bisogno di regolare i dati in modo che possano essere validati con successo in SQL Server.

Per esempio:

Convalida di un metodo molto semplice LINESTRINGche si raddoppia su se stesso:

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

Esecuzione della MakeValidfunzione contro di essa:

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)

Sfortunatamente la MakeValidfunzione cambia l'ordine dei punti e rimuove la terza dimensione, il che la rende inutilizzabile per noi. Sto cercando un altro approccio che risolva questo problema senza riordinare o rimuovere la terza dimensione.

Qualche idea?

I miei dati effettivi contengono centinaia / migliaia di punti.

Risposte:


12

Lasciatemi avvertire che sto giocando con i dati spaziali nel server SQL per la prima volta (quindi probabilmente conoscete già questa prima parte), ma mi ci è voluto un po 'di tempo per capire che SQL Server non sta trattando le coordinate (xyz) come vere Valori 3D, li sta trattando come (latitudine longitudine) con un valore "elevazione" opzionale, Z, che viene ignorato dalla convalida e da altre funzioni.

Prova:

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

Il tuo primo esempio mi è sembrato strano perché (0 0 1), (0 1 2) e (0 -1 3) non sono collineari nello spazio 3D (sono un matematico, quindi pensavo in questi termini). IsValidDetailed(e MakeValid) li sta trattando come (0 0), (0 1) e (0, -1), che crea una linea sovrapposta.

Per dimostrarlo, basta scambiare la X e la Z e convalida:

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

24400: Valid

Questo in realtà ha senso se pensiamo a queste come regioni o percorsi tracciati sulla superficie del nostro globo, anziché come punti nello spazio matematico 3D.


La seconda parte del problema è che i valori dei punti Z (e M) non vengono conservati da SQL attraverso le funzioni :

Le coordinate Z non vengono utilizzate nei calcoli effettuati dalla libreria e non vengono eseguite attraverso i calcoli della libreria.

Questo è purtroppo di progettazione. Questo è stato segnalato a Microsoft nel 2010 , la richiesta è stata chiusa come "Won't Fix". Potresti trovare rilevante questa discussione, il loro ragionamento è:

L'assegnazione di Z e M è ambigua, poiché MakeValid divide e fonde elementi spaziali. I punti vengono spesso creati, rimossi o spostati durante questo processo. Pertanto MakeValid (e altre costruzioni) elimina i valori Z e M.

Per esempio:

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

I valori Z e M sono ambigui per il punto (0 0). Abbiamo deciso di eliminare completamente Z e M invece di restituire un risultato semi-corretto.

Puoi assegnarli in seguito se sai esattamente come. In alternativa puoi cambiare il modo in cui generi i tuoi oggetti in modo che siano validi sull'input, oppure mantenere due versioni dei tuoi oggetti, una valida e un'altra che conserva tutte le tue funzionalità. Se spieghi meglio il tuo scenario e cosa fai con gli oggetti forse potremmo essere in grado di darti soluzioni alternative.

Inoltre, come hai già visto, MakeValidpuoi anche fare altre cose inaspettate , come cambiare l'ordine dei punti, restituire un MULTILINESTRING o persino restituire un oggetto POINT.


Un'idea che mi è venuta in mente è stata quella di archiviarli come oggetto MULTIPOINT :

Il problema è quando il tuo linestring in realtà ripercorre una sezione continua di linea tra due punti precedentemente tracciati dalla linea. Per definizione, se stai ripercorrendo i punti esistenti, la stringa lineare non è più la geometria più semplice che può rappresentare questo set di punti e MakeValid () ti darà invece una multistringa (e perderai i tuoi valori Z / M).

Sfortunatamente, se stai lavorando con dati GPS o simili, è molto probabile che potresti aver rintracciato il tuo percorso in qualche punto del percorso, quindi i linestring non sono sempre così utili in questi scenari :( Probabilmente, tali dati dovrebbero essere archiviati come un multipunto comunque poiché i tuoi dati rappresentano la posizione discreta di un oggetto campionato in punti regolari nel tempo.

Nel tuo caso convalida bene:

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

24400: Valid

Se hai assolutamente bisogno di mantenerli come LINESTRINGS, dovrai scrivere la tua versione MakeValidche regola leggermente alcuni dei punti X o Y di origine con un valore minuscolo, preservando comunque Z (e non fare altre cose folli come convertirlo in altri tipi di oggetti).

Sto ancora lavorando su un po 'di codice, ma dai un'occhiata ad alcune delle idee di partenza qui:


EDIT Ok, alcune cose che ho trovato durante il test:

  • Se l'oggetto geometria non è valido, non puoi farci molto. Non è possibile leggere il STGeometryType, non è possibile ottenere STNumPointso utilizzare STPointNper scorrere attraverso di essi. Se non puoi usarlo MakeValid, sei praticamente bloccato nell'operare sulla rappresentazione testuale dell'oggetto geografico.
  • L'uso STAsText()restituirà la rappresentazione testuale anche di un oggetto non valido, ma non restituirà i valori Z o M. Invece, vogliamo AsTextZM()o ToString().
  • Non è possibile creare una funzione che chiama RAND()(le funzioni devono essere deterministiche), quindi l'ho appena spinta da valori sempre più grandi. Non ho davvero idea di quale sia la precisione dei tuoi dati, o quanto sia tollerante nei confronti di piccoli cambiamenti, quindi usa o modifica questa funzione a tua discrezione.

Non ho idea se ci sono possibili input che faranno andare avanti questo loop per sempre. Sei stato avvertito.

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

Invece di analizzare la stringa, ho scelto di creare un nuovo MultiPointoggetto utilizzando lo stesso insieme di punti, in modo da poter scorrere attraverso di essi e spostarli, quindi riassemblare un nuovo LineString. Ecco del codice per testarlo, 3 di questi valori (incluso il tuo esempio) iniziano non validi ma sono stati corretti:

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

Ottima risposta, grazie BradC. Non ho incluso questo nella mia domanda, ma i miei dati effettivi contengono centinaia / migliaia di punti, quindi "@tinynum * 2" non era sostenibile. Invece ho lasciato cadere "@tinynum" interamente e ho usato un numero casuale compreso tra 0 e 0,000000003. Ho eseguito questo con i dati e finora, di 22k completati, tutti sono stati convalidati come LINESTRING.
CaptainSlock

3

Questa è la FixBadLineStringfunzione di BradC ottimizzata per utilizzare un numero casuale compreso tra 0 e 0,000000003, consentendogli così di scalare LINESTRINGscon un gran numero di punti e minimizzando anche la modifica delle coordinate:

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
Sembra davvero buono, non sapevo della PWDENCRYPTfunzione. Avresti potuto tralasciare ABSe avrebbe restituito un numero positivo o negativo, quindi non sempre aggiungiamo a X e sottraggiamo da Y.
BradC,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.