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, MakeValid
puoi 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 MakeValid
che 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 STNumPoints
o utilizzare STPointN
per 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 MultiPoint
oggetto 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