Deixe-me ressaltar que estou jogando com dados espaciais no SQL Server pela primeira vez (então você provavelmente já conhece esta primeira parte), mas demorei um pouco para descobrir que o SQL Server não está tratando as coordenadas (xyz) como verdadeiras Valores 3D, trata-os como (longitude de latitude) com um valor opcional de "elevação", Z, que é ignorado pela validação e outras funções.
Evidência:
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).
Seu primeiro exemplo me pareceu estranho porque (0 0 1), (0 1 2) e (0 -1 3) não são colineares no espaço 3D (sou matemático, então estava pensando nesses termos). IsValidDetailed
(e MakeValid
) os trata como (0 0), (0 1) e (0, -1), o que faz uma linha sobreposta.
Para provar isso, basta trocar o X e Z, e ele valida:
select geography::STGeomFromText('LINESTRING (1 0 0, 2 1 0, 3 -1 0)', 4326)
.IsValidDetailed()
24400: Valid
Isso realmente faz sentido se pensarmos nelas como regiões ou caminhos traçados na superfície do nosso globo, em vez de pontos no espaço 3D matemático.
A segunda parte do seu problema é que os valores dos pontos Z (e M) não são preservados pelo SQL por meio de funções :
As coordenadas Z não são usadas em nenhum cálculo feito pela biblioteca e não são realizadas em nenhum cálculo da biblioteca.
Infelizmente, isso é por design. Isso foi relatado à Microsoft em 2010 , a solicitação foi encerrada como "Não será corrigido". Você pode achar essa discussão relevante, o raciocínio deles é:
A atribuição de Z e M é ambígua, porque MakeValid divide e mescla elementos espaciais. Os pontos geralmente são criados, removidos ou movidos durante esse processo. Portanto, MakeValid (e outras construções) reduz os valores de Z e M.
Por exemplo:
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()
Os valores Z e M são ambíguos para o ponto (0 0). Decidimos descartar Z e M completamente, em vez de retornar o resultado meio correto.
Você pode atribuí-los mais tarde, se souber exatamente como. Como alternativa, você pode alterar a maneira como gera seus objetos para serem válidos na entrada ou manter duas versões dos seus objetos, uma válida e outra que preserva todos os seus recursos. Se você explicar melhor seu cenário e o que você faz com os objetos, talvez possamos fornecer soluções adicionais.
Além disso, como você já viu, MakeValid
também pode fazer outras coisas inesperadas , como alterar a ordem dos pontos, retornar uma MULTILINESTRING ou até retornar um objeto POINT.
Uma ideia que encontrei foi armazená-los como um objeto MULTIPOINT :
O problema é quando sua cadeia de linhas, na verdade, refaz uma seção contínua da linha entre dois pontos que foram rastreados anteriormente pela linha. Por definição, se você estiver refazendo pontos existentes, a cadeia de linhas não será mais a geometria mais simples que pode representar esse conjunto de pontos, e MakeValid () fornecerá uma cadeia de linhas múltiplas (e perderá seus valores Z / M).
Infelizmente, se você estiver trabalhando com dados de GPS ou algo semelhante, é bem provável que você tenha refazido seu caminho em algum momento da rota, portanto, as cadeias de linhas nem sempre são tão úteis nesses cenários :( Indiscutivelmente, esses dados devem ser armazenados como de qualquer maneira, pois seus dados representam a localização discreta de um objeto amostrado em pontos regulares no tempo.
No seu caso, valida perfeitamente:
select geometry::STGeomFromText('MULTIPOINT (0 0 1, 0 1 2, 0 -1 3)',4326)
.IsValidDetailed()
24400: Valid
Se você absolutamente precisar mantê-los como LINESTRINGS, precisará escrever sua própria versão MakeValid
que ajusta levemente alguns dos pontos X ou Y de origem por algum valor minúsculo, preservando Z (e não faz outras coisas malucas como convertê-lo em outros tipos de objetos).
Ainda estou trabalhando em algum código, mas dê uma olhada em algumas das idéias iniciais aqui:
EDIT Ok, algumas coisas que encontrei durante o teste:
- Se o objeto de geometria é inválido, você simplesmente não pode fazer muito com ele. Você não pode ler
STGeometryType
, não pode obter STNumPoints
ou usar STPointN
para iterar através deles. Se você não pode usar MakeValid
, está basicamente preso à operação na representação de texto do objeto geográfico.
- Usar
STAsText()
retornará a representação de texto mesmo de um objeto inválido, mas não retornará valores Z ou M. Em vez disso, queremos AsTextZM()
ou ToString()
.
- Você não pode criar uma função que chame
RAND()
(as funções precisam ser determinísticas); portanto, apenas a induzi a valores cada vez maiores. Realmente não tenho idéia de qual é a precisão dos seus dados ou de quão tolerantes são as pequenas alterações; portanto, use ou modifique essa função a seu critério.
Não faço ideia se existem entradas possíveis que farão com que esse loop continue para sempre. Você foi avisado.
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
Em vez de analisar a string, optei por criar um novo MultiPoint
objeto usando o mesmo conjunto de pontos, para que eu pudesse iterá-los e cutucá-los e, em seguida, remontar um novo LineString. Aqui está um código para testá-lo, três desses valores (incluindo sua amostra) começam inválidos, mas são corrigidos:
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