Alternativa ao MakeValid () para dados espaciais no SQL Server 2016

13

Eu tenho uma tabela muito grande de LINESTRINGdados geográficos que estou mudando do Oracle para o SQL Server. Existem várias avaliações que são executadas com base nesses dados no Oracle e também precisam ser executadas com os dados no SQL Server.

O problema: o SQL Server possui requisitos mais rígidos para um válido LINESTRINGque o Oracle; "A instância LineString não pode se sobrepor ao longo de um intervalo de dois ou mais pontos consecutivos". Acontece que uma porcentagem de nossos LINESTRINGs não atende a esse critério, o que significa que as funções necessárias para avaliar os dados falham. Preciso ajustar os dados para que possam ser validados com êxito no SQL Server.

Por exemplo:

Validando um método muito simples LINESTRINGque se dobra novamente:

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

Executando a MakeValidfunção contra ela:

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)

Infelizmente, a MakeValidfunção altera a ordem dos pontos e remove a terceira dimensão, o que a torna inutilizável para nós. Estou procurando outra abordagem que resolva esse problema sem reordenar ou remover a 3ª dimensão.

Alguma ideia?

Meus dados reais contêm centenas / milhares de pontos.

CaptainSlock
fonte

Respostas:

12

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
BradC
fonte
Ótima resposta, obrigado BradC. Não incluí isso na minha pergunta, mas meus dados reais contêm centenas / milhares de pontos; portanto, "@tinynum * 2" não era sustentável. Em vez disso, larguei "@tinynum" inteiramente e usei um número aleatório entre 0 e 0,000000003. Eu tenho executado isso com os dados e, até agora, dos 22k concluídos, todos foram validados como LINESTRINGs.
CaptainSlock
3

Essa é a FixBadLineStringfunção do BradC ajustada para usar um número aleatório entre 0 e 0,000000003, permitindo escalar LINESTRINGscom um grande número de pontos e também minimizar a alteração nas coordenadas:

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
CaptainSlock
fonte
1
Parece muito bom, eu não sabia sobre a PWDENCRYPTfunção. Você poderia ter deixado de fora o ABSe teria retornado seja positiva ou número negativo, por isso não estamos sempre adicionando a X e Y. subtraindo
BradC