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, MakeValidtambé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 MakeValidque 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 STNumPointsou usar STPointNpara 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 MultiPointobjeto 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