sábado, 28 de junho de 2014

Modelando entidades geográficas lineares: a classe Track

Uma vez que já foi modelada a classe Position, que armazena as coordenadas de um ponto no espaço geográfico, um próximo passo é modelar alguma das entidades que já vimos na análise dos formatos GPX e KML.

As duas entidades mais marcantes são as entidades pontuais (Waypoints, ou simplesmente Points), e também as entidades lineares (LineString, Track, TrackSegment, Polyline, etc.).

Optei por usar o termo Track porque, além de ser um termo comumente usado (GPS Trackmaker, Google My Tracks, objeto gx:Track do KML), a palavra track em inglês representa "pista", ou "rastro", e se presta bem para uma linha que pode ter sido tanto o caminho que algo ou alguém percorreu - ou pretende percorrer - quanto algum elemento linear no espaço terrestre, como um rio, uma estrada, uma ferrovia ou uma linha de transmissão.

Matematicamente, qualquer linha no espaço geográfico é uma sequência contínua de pontos, que pode ser definida por uma função paramétrica, onde a posição varia em função de um parâmetro (tempo, distância ao longo da linha, etc.). Como em geral as entidades têm complexidade arbitrária, com ângulos e reentrâncias, é conveniente decompor uma linha em uma sequência de segmentos. Assim, a linha é descrita pela sequência de segmentos, onde cada segmento por sua vez é descrito por seus pontos de controle.

Os segmentos podem ser de vários tipos: retas, arcos, geodésicas, seções cônicas, curvas de Bézier, splines ou NURBS. Assim, uma ferrovia provavelmente terá muitos trechos retos longos e curvas suaves, enquanto uma estrada na montanha terá uma alternância entre curvas abertas e fechadas, e um rio terá formas mais orgânicas e complexas, por exemplo.

Uma característica importante desses segmentos é que, em sua definição, eles contém um número finito de pontos de controle. Um segmento de reta, por exemplo, contém um ponto inicial e final. Uma curva de Bézier possui, além dos pontos inicial e final, também dois pontos de controle. Qualquer que seja o tipo de segmento, deve ser possível interpolar uma posição intermediária, de acordo com alguma fórmula que depende do tipo de segmento. No segmento de reta, que é o mais simples, o método usado é a interpolação linear.

Interpolação linear (à esquerda) e interpolação por curvas (à direita). A representação por segmentos de reta provoca uma perda de informação que é dependente da quantidade de pontos de controle, bem como do posicionamento dos mesmos ao longo da linha.
Tanto o formato KML quanto o GPX representam as linhas geográficas como "LineStrings", ou seja, linhas compostas por segmentos de reta concatenados, em que a posição final de um segmento é sempre a posição inicial do segmento seguinte. Portanto, e por uma questão de simplicidade, é essa a estrutura que adotaremos na classe Track.

É importante lembrar que, do ponto de vista conceitual, essa simplificação faz com que haja uma incerteza sobre a linha, já que não há informação nesses segmentos. Para isso, parte-se do pressuposto que a amostragem da linha foi feita com uma resolução adequada, e que portanto os detalhes e reentrâncias da linha estão mantidos na representação.

Por exemplo, para representar uma ferrovia com longos trechos retos, pode ser que seja adequada uma resolução de um quilômetro ou mais entre os pontos. Já para representar uma trilha sinuosa que sobe a encosta de um morro, uma resolução de 5 metros ou menos parece mais adequada. O fato é que, entre um ponto e outro, não há certeza sobre a posição real da linha, exceto pela convenção de que ela não sofra variações abruptas. Portanto, calculam-se os pontos intermediários via interpolação linear, com a suposição de que o erro será pouco significativo.

Já de posse de uma boa representação para os dados da classe, vejamos quais são as operações desejáveis, baseado nos usos frequentes das Tracks em aplicativos. São elas:
  • Comprimento dos Segmentos: é uma sequência que mapeia cada posição da linha para a respectiva distância com relação à posição anterior, ou seja, é uma sequência dos comprimentos de cada segmento da linha. Por convenção, o primeiro valor é sempre zero;
  • Comprimento Total: é a soma dos Comprimentos dos Segmentos;
  • Perfil Altimétrico: é uma sequência de pares numéricos, onde cada par associa uma distância a uma elevação correspondente;

Por ora, temos uma modelagem inicial dessa classe, a ser implementada em postagens futuras. Os passos seguintes de desenvolvimento serão a implementação de leitores de arquivo para que se extraiam objetos "Track" a partir dos formatos KML e GPX, e possivelmente alguma forma simples de visualização (plotagem).


segunda-feira, 16 de junho de 2014

Refactoring das classes Position em Python e C#

Na postagem anterior, foram criadas as classes Position nas linguagens Python e C# (que são as linguagens que eu costumo trabalhar), como forma de testar o quanto nossa modelagem é independente de plataforma.

Pretendo manter a interface pública das classes o mais parecida possível entre todas as linguagens, a não ser que isso viole de forma muito óbvia alguma característica de cada linguagem.

Logo depois de publicar a postagem, me dei conta de alguns esquecimentos, algumas inadequações, e algums escolhas de design "suspeitas", de forma que, alguma reflexão e alguma pesquisa depois, venho apresentar as classes refatoradas.

Em primeiro lugar, movi o código para o GitHub, de forma que o leitor sempre terá onde encontrar o código atualizado, bem como o histórico de modificações. As postagens antigas não vão ser editadas para refletir as mudanças que forem ocorrendo, mas as mudanças mais significativas serão discutidas em postagens específicas, se for o caso.

Para este refactoring, as mudanças de API (que impactam a implementação de ambas as linguagens) foram as seguintes:
  • Implementação (conforme previamente modelado) do valor nulo para elevação, permitindo que haja um construtor com dois argumentos (latitude e longitude, nessa ordem), e um argumento opcional elevação cujo valor padrão é nulo;
  • Eliminação do construtor que usava uma sequência como argumento, pois isso enfraquecia a semântica, e dava margem a erros lógicos (pois não havia garantia de que os itens da sequência viriam na ordem correta), e de tempo de execução. Agora, a classe possui apenas um construtor, e esse construtor recebe dois argumentos numéricos, ou três argumentos numéricos. Uma discussão sobre essa decisão pode ser vista nesta pergunta do StackOverflow;
  • Apenas a Elevação possui setter. Latitude e a Longitude são somente leitura (e portanto possuem apenas getter);


Versão atual em C#:

using System;
using System.Collections.Generic;
using System.Linq;

namespace GpsDataModel
{
    public struct Position {

        private readonly double _lat, _lon;
        private double? _elev;

        public Position(double lat, double lon, double? elev = null) : this() {
            
            _lat = Math.Abs(((lat-90) % 360) - 180) - 90;
            _lon = ((lon-180) % 360) - 180;

            _elev = validateElevation(elev);
        }
        

        public double Latitude { get { return _lat; } }
        public double Longitude { get { return _lon; } }

        public double? Elevation { 
            get { return _elev; }
            set { _elev = validateElevation(value); }
        }


        private double? validateElevation(double? elev) {
            if (elev < -WGS84.SEMI_MAJOR_AXIS)
                throw new ArgumentOutOfRangeException(
                    String.Format("Elevation {0:0.00} " +
                           "is deeper than the center of the Earth!", elev));
            else
                return elev;
        }        


        public override string ToString() {
            return String.Format("Position({0:0.000}, {1:0.000}, {2:0.0})",
                                      _lat, _lon, _elev);
        }
    }
}

Comentários específicos sobre a implementação C#:
  • O argumento opcional é apresentado com um valor padrão, representado sintaticamente pelo operador "=";
  • Os tipos numéricos nullable são sufixados por um ponto de interrogação - no exemplo, "double?";
  • A lógica de validação do setter da Elevação, que deve ser usada também no construtor, foi movida para a função validateElevation;

Versão atual em Python:

#!/usr/bin/env python
# coding: utf-8

import WGS84

class Position(object):

    __slots__ = ('_lat', '_lon', '_elev')

    def __init__ (self, lat, lon, elev=None):

        self._lat = abs(((float(lat)-90) % 360) - 180) - 90
        self._lon = ((float(lon)-180) % 360) - 180

        self.elevation = elev

           
    
    @property
    def latitude(self):
        return self._lat
        
    @property
    def longitude(self):
        return self._lon
        
    @property
    def elevation(self):
        return self._elev

    @elevation.setter
    def elevation(self, elev):
        if elev > (-WGS84.SEMI_MAJOR_AXIS):
            self._elev = float(elev)
        else:
            raise ValueError ("Elevation %.2f " + "
                is deeper than the center of the Earth!" % elev)
 
 

    def __str__ (self):
        return ("Position(%.3f, %.3f, %.1f)"
                          % (self._lat, self._lon, self._elev))

quarta-feira, 11 de junho de 2014

Posição geográfica com a classe Position: Implementações em Python e C#

Anteriormente, foi discutido como representar simbolicamente a posição geográfica, dentro do sistema de coordenadas WGS84, em um contexto independente de plataforma, constituindo-se em uma classe com três propriedades numéricas (uma para cada coordenada tridimensional), todas somente leitura, respeitando certos limites:
  • Latitude, em graus, no intervalo { -90 <= Latitude <= 90 };
  • Longitude, em graus, no intervalo { -180 <= Longitude < 180 };
  • Elevação, em metros, no intervalo { -6378137 < Elevação };
 Daí decorre o seguinte modelo de design:
  • Cada instância de Position deve ser os três valores inicializados, contando para isso com um construtor com três parâmetros numéricos, e também um construtor com um parâmetro que seja uma sequência de três valores numéricos, na ordem "Latitude, Longitude, Elevação";
  • Considerando que Latitude e Longitude, ao longo da superfície da Terra, são circulares, entende-se que valores além dos limites devem ser "continuados", das seguintes formas:
    • Longitudes crescentes passam de 180 para -180, e decrescentes passam de -180 para 180, lembrando que uma posição exatamente sobre o meridiano 180, de acordo com a ISO TC 211, deve ser representada com valor negativo (ou seja, -180);
    • Latitudes crescentes, chegando a 90, começam a decrescer, conforme a sequência de exemplo [87, 88, 89, 90, 89, 88, 87...], enquanto decrescentes se comportam ao contrário: [-87, -88, -89, -90, -89, -88 ...]. Isso representaria alguém atravessando o pólo de um lado a outro.
    • Elevações podem ser negativas, desde que não ultrapassem o centro da Terra. Não se considera a continuação dessa linha em direção ao outro lado porque isso mudaria a latitude e a longitude, violando o pressuposto de independência entre as coordenadas.
A seguir, implementações comentadas em C# e Python. O código contém comentários numerados que são discutidos abaixo.

Classe Position em C#

using System;
using System.Collections.Generic;
using System.Linq;

namespace GpsDataModel
{

    public static class WGS84 {
        public static double SEMI_MAJOR_AXIS { get { return 6378137; } }
    }

    public struct Position {    // (1)

        private readonly double _lat, _lon, _elev;    // (2)


        public Position(double lat, double lon, double elev) {
            
            // (3) //
            _lat = Math.Abs(((lat-90) % 360) - 180) - 90;
            _lon = ((lon-180) % 360) - 180;
            
            
            if (elev > -WGS84.SEMI_MAJOR_AXIS)
                _elev = elev;
            else
                // (4) //
                throw new ArgumentOutOfRangeException(
                    String.Format("Elevation {0:0.00} is deeper than the center of the Earth!", elev));    
        }

        public Position(IEnumerable coords) : this(    // (5)
            coords.ElementAt(0),
            coords.ElementAt(1),
            coords.ElementAt(2)
        ) {}    // (6)
        
        
        // (7) //
        public double Latitude { get { return _lat; } }
        public double Longitude { get { return _lat; } }
        public double Elevation { get { return _lat; } }



        // (8) //
        public override string ToString() {
            return String.Format("Position({0:0.000}, {1:0.000}, {2:0.0})", _lat, _lon, _elev);
        }

    }


    // (9) //
    class Program {
        static void Main(string[] args) {
            Console.WriteLine(new Position(91, 180, 100));
            Console.WriteLine(new Position(-91, -180, 100));
            Console.WriteLine(new Position(0, -190, 100));
            Console.WriteLine(new Position(new double[]{91, 180, 100}));
            
            // (10) //
            //try {
                //new Position(0,0,0).Elevation = 20;
            //} catch (ArgumentOutOfRangeException e) {
                //Console.WriteLine(e.Message);
            //}
        }
    }

}


  1. Uso de struct ao invés de class: de acordo com a documentação do C#/.NET, structs são recomendadas para objetos pequenos que representam valor e que não serão modificados. A própria documentação sugere "pontos em um sistema de coordenadas" são bons exemplos de struct;
  2. Uso de readonly: variáveis readonly não podem ser modificadas após sua inicialização. A inicialização ocorre no construtor, obrigatoriamente;
  3. Uso de "fórmulas de validação": como foi dito, desejamos que os valores de latitude e longitude sejam "contínuos", portanto foi atribuída a fórmula da onda triangular para a latitude, e a fórmula da onda dente-de-serra para a longitude. Assim, pode-se fornecer valores de entrada fora do intervalo, e a fórmula vai convertê-los para valores válidos dentro do intervalo de cada coordenada;
  4. Tratamento de erro na validação da Elevação: Como foi dito, valores de elevação que estão abaixo do centro da Terra não são válidos. Em C#, a forma recomendável de tratar esse erro é gerar manualmente uma exceção (no caso, do tipo ArgumentOutOfRangeException), com a sentença throw;
  5. Construtor alternativo: desejamos que, além de usar no construtor os três argumentos numéricos separados, seja possível usar como argumento uma sequência com os três valores. Assim, qualquer argumento do tipo "IEnumerable" pode ser usado, o qual vai ser repassado ao construtor padrão por encadeamento, usando a sintaxe ": this";
  6. Bloco vazio após o construtor: a sintaxe do C# exige que haja um bloco após um construtor, mesmo que esse bloco esteja vazio;
  7. Propriedades públicas somente com getter: recomenda-se expor os campos privados através de propriedades públicas que tenham somente getter. No caso da classe Position, isso é (mais) uma forma de garantir a imutabilidade dos valores após criados;
  8. Representação amigável (human-readable) com o método ToString: de acordo com o livro Effective C#, recomenda-se dar um override no método ToString (herdado de Object), para que qualquer tentativa de conversão do objeto em string retorne uma string legível, que traga alguma informação sobre os valores daquela instância da classe;
  9. Programa simples de teste: para o propósito desta postagem, incluí um programa que demonstra o uso dos construtores e do método ToString. O recomendável seria ter um teste unitário para a classe, mas isso fica para uma próxima oportunidade.

Classe Position em Python

#!/usr/bin/env python
# coding: utf-8

class WGS84(object):
    SEMI_MAJOR_AXIS = 6378137

class Position(object):    # (1)

    __slots__ = ('_lat', '_lon', '_elev')    # (2)

    def __init__ (self, *args):    # (3)

        if len(args) == 1:
            lat, lon, elev = args[0]
        elif len(args) == 3:
            lat, lon, elev = args
        else:    # (4)
            raise ValueError ("Position constructor takes 3 arguments or sequence with length = 3")

        self.latitude = lat
        self.longitude = lon
        self.elevation = elev



    ## (5) ##
    @property
    def latitude(self):
        return self._lat

    @latitude.setter
    def latitude(self, value):
        self._lat = abs(((float(value)-90) % 360) - 180) - 90



    ## (6) ##
    def get_longitude(self):
        return self._lon

    def set_longitude(self, value):
        self._lon = ((float(value)-180) % 360) - 180

    longitude = property(get_longitude, set_longitude)



    @property
    def elevation(self):
        return self._elev

    @elevation.setter
    def elevation(self, value):
        if value > (-WGS84.SEMI_MAJOR_AXIS):
            self._elev = float(value)
        else:
            raise ValueError ("Elevation %.2f is deeper than the center of the Earth!" % value)


    ## (7) ##
    def __str__ (self):
        return "Position(%.3f, %.3f, %.1f)" % (self._lat, self._lon, self._elev)


## (8) ##
if __name__ == "__main__":

    print Position(0,0,0)
    print Position(90, 180, 10)
    print Position(-90, -180, 20)
    print Position(91, 181, 30)
    print Position(-91, -181, 40)
    print Position ((-30, -51, 50)) 
     
  1. Herdando de object: em Python "tudo são objects", e uma recomendação é que todas as classes herdem do tipo object;
  2. Usando __slots__ para diminuir uso de memória e evitar o uso de setattr: como nossa classe é para ser leve - o que justificou o uso de struct em C# - estamos usando a propriedade __slots__ para definir que a classe Position vai ter somente três atributos. Caso isso não fosse feito, vale a situação padrão em Python, que é a existência da propridade __dict__, que é um dicionário dinâmico existente em cada instância. Isso permite adicionar e remover atributos, e é um comportamento que também não desejamos (ao menos não nesta classe);
  3. Uso de *args como parâmetro: em Python, o asterisco antes de um parâmetro significa que os argumentos passados serão transformados em uma sequência, disponível no corpo da função como a variável args. Assim, podemos passar os três argumentos numéricos, ou uma sequência de comprimento três, tratando condicionalmente cada caso no corpo do construtor;
  4. Tratamento de erro na validação da Elevação: assim como em C#, geramos manualmente uma exceção, neste caso com a sentença raise;
  5. Getters e setters com a função property: a função property é usada para que atributos sejam acessados através de funções, que podem incluir validação, cálculo, etc.
    1. Neste caso, a classe apresentará a propriedade "latitude", por exemplo, mas não será possível atribuir um valor a ela, pois ela não possui setter, apenas getter;
    2. Os nomes das funções devem ser os mesmos dos decoradores;
    3. No caso da latitude, é usada a sintaxe "@", e a funçao property é aplicada como um decorador;
    4. Nada impede o usuário de setar diretamente a propriedade "_lat", mas a convenção de nomenclatura usa o underline no início do nome para indicar uma variável interna que não deve ser diretamente setada;
  6. Função property declarada diretamente: Neste caso, a função property é usada de forma explícita, ao invés de usar decoradores, e podemos escolher nomes diferentes para as funções se quisermos;
  7. Representação amigável com o método __str__: em Python, a conversão para string chama automaticamente o método __str__, que deve pode ser substituído conforme a conveniência;
  8. Auto-teste: por convenção, cada módulo (ou seja, cada arquivo) contém um bloco condicional que roda quando o script é chamado diretamente, mas não roda quando o módulo é importado por outro script. Isso é detectado porque a variável __name__ assume o valor "__main__" rodando diretamente. Isso costuma ser aproveitado para inserir alguns testes simples, ou algum código que permita executar uma função específica, mas também permita o reuso das funções declaradas no arquivo, através de importação.

Muito provavelmente, ambas as implementações são passíveis de melhorias. Quando e se isso for feito, haverá postagens com discussão apropriada.

Nas postagens que virão, pretendo subir na escala de abstração, entrando no mundo das geometrias definidas por posições, como Pontos, Linhas e Superfícies.

sexta-feira, 6 de junho de 2014

Implementando a posição geográfica com a classe Position

 As primeiras postagens deste blog se mantiveram bastante abstratas, mas algumas modelagens prévias foram necessárias para construir um modelo composto por elementos bem fundamentados.

Já foi argumentado que, para aplicações de uso pessoal (não profissionais), o datum WGS84, usado pelo sistema GPS, é a escolha óbvia, e que no caso de visualização em mapas, a Projeção Esférica de Mercator (usada pelo Google Maps e similares) é o padrão de facto.

Ambos os sistemas de referência (datum 3D e projeção 2D) descrevem o espaço em que os fenômenos geográficos são posicionados, e portanto a partir daqui podemos nos ocupar da definição da posição propriamente dita.

A importância da posição na informação geográfica é muito bem discutida no documento "Abstract Specification, Topic 5 - Features", onde lemos o seguinte:
  • "As coordenadas de uma entidade geográfica de interesse (feature) são o conjunto de pontos suficientes para a construção geométrica da extensão geo-espacial dessa entidade";
  • "O Mundo Dimensional (aquele definido por coordenadas) é o último nível de abstração geo-espacial "genérica" do Mundo Real. O próximo nível é chamado Mundo do Domínio, já dependente de um domínio de implementação";
  • "Cada instância de uma entidade pode ser considerada a materialização de um modelo (classe) da entidade, tendo seus atributos preenchidos com os valores específicos daquela instância".
  • "Recomenda-se que entidades com extensão espacial sejam modeladas por formas geométricas primitivas simples".
  • "Entidades geográficas são definidas por seus atributos, um dos quais consiste em alguma Geometria. Por sua vez, geometrias são constituídas por um conjunto de Pontos dispostos e conectados de determinada forma".
Vemos então que o Ponto, na forma de uma coordenada em um sistema de referência, é a unidade elementar de descrição das propriedades espaciais de qualquer entidade geográfica.

Em outro documento, o "Abstract Specification, Topic 1 - Feature Geometry", temos um extenso modelo UML que trata da representação geométrica de entidades. Pretendo abordar maiores detalhes sobre outras entidades no futuro, mas no contexto de posições representadas por pontos, o esquema geométrico define três classes importantes:
  1. A classe DirectPosition: tem a responsabilidade de armazenar os valores de coordenadas de uma posição dentro de determinado sistema de coordenadas. Apresenta três propriedades: coordinate, que é uma sequência ordenada de valores numéricos; dimensionque retorna o número de dimensões do sistema de coordenadas; e coordinate_system, que retorna o sistema de coordenadas ao qual a posição pertence;
  2. A classe GM_Point: é uma sub-classe de GM_Object, contendo uma instância de DirectPosition;
  3. A classe GM_PointRef: é uma referência a algum outro ponto pré-existente, no sentido de permitir que um mesmo GM_Point possa participar de mais de uma feature, evitando redundâncias.

Coordenada: um tipo diferente de "par ordenado"

Uma consideração importante na definição de uma "posição geográfica" é a forma como os valores que compõem a coordenada são definidos. Um exemplo pode ser extraído dos próprios formatos KML e GPX, vistos anteriormente: a diferença com que a informação é armazenada em cada um dos formatos:
  • No formato KML, a coordenada é uma string de caracteres, sem espaços, onde cada valor é separado por vírgula, especificamente na ordem "longitude,latitude,[elevação]", onde a elevação é opcional;
  • No formato GPX, não há "ordenada", pois cada Trackpoint possui um atributo chamado Latitude, e outro atributo chamado Longitude, não importante a ordem com que eles são declarados. Além disso, a elevação é um elemento adicional, que pode ou não estar presente.
Assim sendo, temos um caso (KML) em que os valores de cada eixo do sistema de coordenadas são definidos somente pela ordem. O que chama a atenção é que, no formato KML, a longitude vem antes da latitude (GML Coordinate Reference System, LonLat84_5773), o que é o contrário do que costuma ocorrer em vários outros esquemas (e aliás, cria dificuldades enormes para "copiar e colar" strings representando coordenadas...).

Quanto a isso, diversas normas ISO estabelecem a forma recomendada de uso de uma coordenada:
  • Entende-se por coordenada uma sequência de n valores escalares representando um único ponto em um espaço n-dimensional;
  • A coordenada é equivalente ao que na matemática e também na informática recebe a denominação vetor: um elemento que não é escalar, mas sim formado por componentes, e portanto tem propriedades e operações algébricas, aritméticas e geométricas vetoriais, que são qualitativamente diferentes das mesmas propriedades e operações escalares.
  • Os valores devem ser mutuamente independentes entre si;
  • O número de valores deve ser igual ao número de dimensões do Sistema de Referência;
  • A ordem dos valores é definida pelo Sistema de Referência utilizado;
Vejam que no caso do GPX a posição não é exatamente uma coordenada, pois latitude e longitude são definidos nominalmente, sem uma ordem especial.

Latitude e Longitude são qualitativamente diferentes

Quando pensamos em espaço tridimensional, geralmente imaginamos coordenadas XYZ de um sistema retangular onde cada eixo pode assumir valores que variam de -infinito a +infinito, e onde as unidades de medida em qualquer eixo são simétricas.

Já no caso das coordenadas esféricas, isso não ocorre, pelos seguintes motivos:
  • Os valores de Latitude não são infinitos, eles variam entre a latitude do pólo norte e a latitude do pólo sul, respectivamente -90 e 90 graus;
  • Os valores de Longitude não são "exatamente" infinitos, mas sim cíclicos ao redor do eixo terrestre. Se um objeto se deslocar continuamente ao longo do Equador, em algum momento ele ultrapassará o limite e "aparecerá" do outro lado de um intervalo que varia entre -180 e 180 graus;
  • Os valores de elevação podem ser positivamente infinitos (embora isso signifique praticamente trocar a geografia pela astronomia), mas se tivermos uma elevação negativa com relação ao nível do mar, e o valor dessa elevação for excedendo o raio terrestre, estaremos já criando uma elevação crescente em direção ao outro lado do globo.
Assim sendo, há restrições naturais ao intervalo válido para cada um dos valores de uma coordenada geográfica, que devem ser considerados ao definir uma classe Position.

Elevação: definir ou não definir?

Outro "tema polêmico" diz respeito à elevação. Até muito recentemente, os softwares e sites de mapas (TrackMaker, Google Maps) apresentavam a informação em 2D. Trajetos desenhados com o mouse envolviam clicar no mapa, definindo uma sequência de coordenadas 2D que constituíam uma Track. A informação tridimensional só era disponível se tivéssemos um arquivo GPS, do qual era possível plotar a altimetria em um gráfico de altitude vs distância, por exemplo.

Com a chegada do Google Earth, um imenso ganho foi obtido em termos de "sentir" o terreno durante a visualização, com a possibilidade de imaginar a dificuldade de passar por um ou por outro caminho, e desenhar o caminho preferido na forma de um Track. Para a imensa decepção, entretanto, mesmo os arquivos KML salvos no Google Earth continham "0.000" no valor de altitude...

Atualmente, existem várias fontes de dados de elevação online, derivadas do projeto Shuttle Radar Topography Mission (SRTM), e diversos aplicativos e sites já exibem informação de altimetria automaticamente, sob demanda, enquanto o usuário desenha sua rota sobre o mapa. Como em geral a elevação do terreno é função da posição geográfica, visto que cada ponto da superfície da Terra tem uma única altitude (exceto nos casos raros de penhascos verticais ou com inclinação negativa), é conceitualmente possível extrair a altitude de uma coordenada do tipo latitude,longitude, usando para isso um Modelo de Elevação Digital (Digital Elevation Model - DEM).

É importante salientar que a premissa acima - elevação como função de latitude e longitude - vale apenas para a superfície terrestre, não sendo suficiente para descrever trajetórias de objetos voadores, marinhos ou subterrâneos.

Se considerarmos que o espaço geográfico é tridimensional, e que o sistema de coordenadas que escolhemos (WGS84) é também tridimensional, sendo os dados de mapa apenas o resultado de uma projeção (ou seja, uma visualização bidimensional de entidades cuja natureza é tridimensional), então somos obrigados a afirmar que o valor de elevação faz parte de qualquer instância de uma classe Position, mesmo quando não é conhecido.

O Google Earth opta por inserir o valor "0,000" (zero) em pontos cuja elevação não é conhecida.

O KML define como opcional o terceiro valor de coordenada, assim como o GPX trata a elevação como algo que é "medido", ou seja, pode ou não estar presente. Isso contempla bem o caso em que um TrackSegment apresenta elevação na maioria dos pontos, mas por razões quaisquer possa conter pontos onde o valor de elevação é ausente.

Assim, me parece (e a discussão a esse respeito é muito bem-vinda) que inserir o valor "0,000" no lugar de "nulo" é um erro, pois zero é um valor numericamente válido, porque ele pode de fato ser zero (por exemplo no nível do mar), e é impossível distinguir, em uma aplicação se uma instância com elevação zero possui ou não possui alguma elevação. Em especial, no KML, o elemento <altitudeMode> determina como uma feature deve ser renderizada, possibilitanto ao próprio Google Earth (ou qualquer outra aplicação compatível) renderizar corretamente as entidades junto ao solo quando a elevação for zero e o modo for clampToGround.

A definição da classe Position

Depois de tudo que foi exposto, chegamos na hora de propor uma classe para a representação da posição geográfica. A seguir, as características dessa classe:
  • A classe se chamará Position. O idioma inglês foi escolhido porque é a linguagem da tecnologia e de todos os padrões utilizados até agora (ISO, KML, GPX) e das linguagens em que o sistema possa vir a ser implementado. Outros nomes seriam possíveis. A ISO propõe DirectPosition, mas não apresenta nenhum outro tipo Position que não seja "direct", e não justifica o uso de "direct" neste contexto. Aplicações web (Google, Strava, etc.) usam latlon ou latlng, mas esses nomes são pouco específicos. Também, o nome Point não me parece ser o mais adequado, pois refere-se à primitiva geométrica e/ou topológica, que contém a posição como um de seus atributos. Também foi evitado o prefixo "geo", como em "GeoPoint" ou "GeoPosition", devido à confusão entre as disciplinas Geometria e Geografia. Dado que a classe Position pertencerá a algum namespace situado em um pacote voltado à informação geográfica, o contexto de Position como uma classe geográfica fica implícito, e deverá também estar explícito em alguma documentação;
  • O sistema de coordenadas é o WGS84. Qualquer projeção 2D deverá utilizar a latitude e a longitude nesse sistema, simplesmente ignorando a elevação;
  • As propriedades (atributos) da classe se chamam latitude, longitude e elevation. O termo elevação é preferível para representar valores absolutos em relação ao geóide ou elipsóide de referência, enquanto altitude se refere à altura com relação ao solo - por exemplo, de um avião durante o vôo;
  • As propriedades podem ter seu nome iniciando com letra maiúscula ou minúscula, de acordo com a convenção da linguagem em que a classe for implementada;
  • A propriedade latitude deve respeitar a restrição $\begin{aligned}\{ -90 < latitude < 90 \}\end{aligned}$, a propriedade longitude deve respeitar a restrição $\begin{aligned}\{ -180 < longitude < 180 \}\end{aligned}$, e a propriedade elevation deve respeitar a restrição $\begin{aligned}\{ elevation < -EARTH\_RADIUS \}\end{aligned}$, onde EARTH_RADIUS deve ser uma constante definida em um namespace acessível à classe Position;
  • Nenhum construtor da classe Position deve ser capaz de gerar uma instância de Position com valores inválidos (fora do intervalo);
  • Uma posição não é mutável. Se algum código cliente quiser alterar uma posição, deve criar uma nova instância de Position com os novos valores desejados;
  • O valor de elevação, quando não conhecido, deve receber o valor nulo (NaN, None, null, ou o que for mais apropriado na linguagem de implementação).
Um comportamento que definitivamente merece debate é como validar os valores de latitude, longitude e elevação passados como argumentos ao construtor, quando esses valores excedem os limites. Como já foi argumentado, o valor de longitude muda subidamente de 180 para -180 se girarmos ao redor do equador. Por outro lado, se formos seguindo um meridiano, a latitude aumenta até 90 e passa imediatamente a diminuir (pelo meridiano oposto) até -90, e então passa a aumentar novamente. Um gráfico do primeiro deslocamento mostraria a longitude variando como um dente de serra, enquanto o gráfico do segundo seria um formato triangular (desculpem pela ausência da figura, ainda não domino a arte de produzi-las para as postagens). Se o construtor deveria limitar os valores ao máximo, ou calcular sua correspondência "podando" o excesso (provavelmente usando o operador de divisão em módulo "%"), ou simplesmente gerar um erro, ainda não tenho certeza, mas me parece que a segunda opção é matematicamente válida.

No momento, a classe não possui nenhum método, já que sua responsabilidade é somente definir uma posição geográfica válida.

Na próxima postagem, pretendo propor implementações em alguma(s) linguagem(ns), considerando que as linguagens alvo que pretendo trabalhar são Python, C# e Javascript.

terça-feira, 3 de junho de 2014

Representando a posição geográfica: Sistemas de Coordenadas, Datums e Projeções

A modelagem de entidades geográficas, assim como sua representação computacional, têm como necessidade elementar a representação de sua posição.

Em termos abstratos, a posição de um ponto, por exemplo, é determinada por um conjunto de coordenadas, devendo haver uma coordenada para cada dimensão do espaço em que o ponto existe.

No caso da geografia, temos dois espaços principais onde as posições são definidas:
  1. O espaço real, tridimensional, habitado pelos seres.
  2. O espaço cartográfico, em geral bidimensional, voltado à elaboração de mapas planos (em papel ou meio eletrônico).
A correspondência entre o mesmo ponto definido nos dois espaços é feita através de uma projeção cartográfica, que consiste em uma função matemática que transforma a posição tridimensional em posição bidimensional.
O espaço real é o espaço em que habitamos, e inclui todo o universo. No contexto da representação geográfica, temos o planeta Terra como principal objeto de referência, e em geral a Terra serve como origem para um sistema de coordenadas.

Sistema de coordenadas esférico (latitude, longitude e elevação),
com origem no centro da Terra

Na ilustração acima, vemos um sistema de coordenadas esférico, onde a letra Φ representa a latitude em graus a partir da linha do equador, e a letra λ representa a longitude em graus a partir de um meridiano de referência.

Caso a Terra fosse uma esfera perfeita, qualquer ponto em sua superfície teria a mesma coordenada de elevação, cujo valor seria igual ao raio da Terra, representado pelo segmento vermelho na figura.

O fato de vivermos em um espaço tridimensional requer obrigatoriamente que uma posição seja representada por três coordenadas, seja qual for o sistema utilizado (retangular, cilíndrico ou esférico, por exemplo), mas o sistema de coordenadas esférico apresenta a grande vantagem de termos uma coordenada de elevação que é ortogonal às outras, ou seja, cujo eixo é coincidente com o campo gravitacional.

Já o fato de representarmos informação geográfica frequentemente na forma de mapas planos (folhas de papel, telas de computador) também torna benéfico que o eixo que representa a elevação seja ortogonal aos eixos de posição horizontal, já que a gravidade faz com que o terreno natural e mesmo os espaços artificiais tenham uma estrutura predominantemente "plana".

Escolhendo um Sistema de Referência

Existem inúmeros sistemas de referência por coordenadas que permitem localizar com precisão cada posição geográfica na terra. Em geral esses sistemas foram criados em épocas e países diferentes para suprir demandas específicas relacionadas à cartografia, ou seja, à representação do espaço geográfico através de mapas de papel.

Atualmente, com a consolidação da cartografia eletrônica e da criação de informação geográfica nativamente digital, há uma facilidade muito grande de converter a posição entre dois sistemas de coordenadas quaisquer, desde que os parâmetros do datum de cada sistema de referência estejam bem documentados.

Quando falamos em datum, estamos nos referindo à descrição matemática da forma aproximada da terra, que é achatada nos pólos, dessa forma possuindo um elipsóide de referência. Esse elipsóide é complementado por um geóide de referência, que considera as variações locais na distribuição do campo gravitacional da terra (causada pela assimetria das massas dos continentes) e serve para determinar a altitude com relação ao nível do mar. Mais informações e ilustrações podem ser encontradas nesta página, de onde veio esta figura:

À esquerda, cálculo da elevação de uma montanha com relação ao elipsóide de referência, definido matematicamente. À direita, cálculo da elevação da mesma montanha com relação ao geóide, que acompanha variações locais do campo gravitacional terrestre.
A figura ajuda a explicar a razão de existirem tantos datums locais para diferentes partes do mundo: embora exista um elipsóide que se adequa melhor ao geóide global, quando a intenção é mapear uma região restrita é mais conveniente usar um elipsóide que se adeque bem à região do geóide restrita à vizinhança local (em geral, o território de algum país). Nas cartas topográficas do Exército Brasileiro, por exemplo, o datum utilizado é o de Córrego Alegre.

No caso do usuário doméstico, todos os dados oriundos de GPS utilizam como datum o datum WGS84 (World Geodetic System).

Projeções

Até agora, a discussão se manteve ao espaço tridimensional, e às definições de sistemas de coordenadas tridimensionais, sua origem e seus eixos. Mesmo que consideremos apenas a superfície terrestre, essa superfície ocupa um espaço tridimensional.

A representação visual das entidades geográficas, entretanto, é feita de forma geralmente bidimensional. Quando calculamos a distância entre pontos, ou ao longo de um caminho, ou a forma e o tamanho relativos de superfícies em um mapa, não fazemos isso em um modelo sólido, em 3D de verdade, mas sim em um mapa plano, de papel ou "de pixels", referenciado por uma grade em geral retangular.

É muito bem conhecido o problema de representar a superfície terrestre em um meio plano, visto que não podemos simplesmente "achatar" uma superfície esférica de forma que ela fique plana sem provocar algum tipo de distorção. Por isso, inúmeras projeções foram inventadas ao longo do tempo para que a distorção de cada mapa fosse minimizada para a aplicação pretendida.

Papel versus Pixels

Até há pouco tempo atrás, os mapas de papel eram usados como o próprio meio onde se realizavam medições e cálculos, pois não havia ferramentas computacionais como as de hoje. Algumas projeções distorcem menos as distâncias, outras distorcem menos os ângulos, outras preservam as áreas relativas, etc., mas todas envolvem algum tipo de distorção que precisa ser considerada caso se deseje realizar medições diretamente no mapa.

Com a migração massiva da cartografia em papel para a cartografia digital, duas características passam a ser relevantes para a definição de uma projeção preferível ao usuário não-profissional:
  • A praticidade computacional em converter coordenadas tridimensionais em coordenadas de mapa;
  • A facilidade de calcular automaticamente superfícies, distâncias, ângulos, sem necessidade de fazer cálculos e medições visuais no mapa.
Assim, por exemplo, quando o usuário clica dois pontos no Google Maps para saber a distância entre eles ao longo de uma estrada, a distorção cartográfica é irrelevante, pois o cálculo da distância é feito usando o sistema de coordenadas tridimensional (que considera a distância geodésica entre os pontos).

Ou seja, o software "sabe" que as distâncias e os tamanhos aparentes das coisas, na projeção do mapa, não são proporcionais.

Mais que isso: o mapa que o usuário vê na tela é apenas uma representação, uma visualização bidimensional de um modelo que é intrinsecamente tridimensional, e é sobre esse modelo que os cálculos são de fato realizados. Isso faz com que a projeção possa ser escolhida com muito mais flexibilidade, e de acordo com a conveniência computacional.

Qual a projeção usada nos mapas da internet?

A principal entidade a organizar e sistematizar as diferentes projeções foi o Grupo Europeu de Prospecção de Petróleo (EPSG - European Petrol Survey Group).

Uma forma computacionalmente natural de planificar o globo é simplesmente atribuir longitude e latitude aos eixos X e Y do mapa. Essa é a projeção chamada equi-retangular, e no caso em que a latitude principal é o equador, é chamada de plate carré (EPSG:32663):

Projeção equi-retangular com gradícula de 10° (fonte: NASA)
Essa projeção preserva a uniformidade de distâncias ao longo dos meridianos, mas distorce os paralelos, fazendo com que estes fiquem muito mais longos do que quanto maior a latitude. Regiões como Antártida, Groenlândia, Rússia e Canadá apresentam grande deformação de formas e ângulos.

Uma outra projeção bastante comum que busca minimizar esse problema é a Projeção de Mercator. Nessa projeção, a "dilatação" dos paralelos é compensada por uma "dilatação" correspondente dos meridianos, fazendo com que as regiões perto dos pólos tenham uma área aparentemente maior, porém preservando as proporções geométricas nas vizinhanças de qualquer ponto do mapa. Além disso, a linha mais curta entre dois pontos quaisquer aparece no mapa como uma linha reta, o que é importante para a navegação. Abaixo, a projeção de Mercator (EPSG:3857):

Essa projeção foi escolhida pelo Google para o Google Maps, e foi na época denominada "EPSG:900913" (a palavra "google" escrita em números...), mas foi depois renumerada pelo EPSG para 3857. Outros nomes são "Spherical Mercator" ou "Web Mercator", mas todas são a mesma projeção.

Praticamente todos os serviços de mapas na internet (Google, Bing, Yahoo, Nokia, OpenStreetMap, ArcGIS) utilizam essa projeção para exibir mapas e também para calcular distâncias, por isso é importante salientar que existe um pequeno erro conhecido no cálculo das distâncias, porque nesse sistema os pontos são projetados sobre um esferóide de referência, e não sobre um elipsóide, que daria resultados mais precisos (porém levemente mais complexos de serem calculados). O esferóide considera o raio terrestre no Equador como se fosse o raio da Terra inteira. Isso causa um aumento das distâncias calculadas em latitudes maiores, mas com erros tipicamente menores que 0,3%.

Conclusão

Para representar e comunicar corretamente a posição geográfica, é necessária a definição de um sistema de referência por coordenadas. Como o espaço é tridimensional, o sistema contém três coordenadas.

Devido à forma esférica da Terra, cujo campo gravitacional é orientado radialmente, torna-se prático o uso de um sistema de coordenadas esférico, composto por latitude, longitude e elevação.

A elevação necessita de uma referência de "zero", e a referência natural é o nível do mar. Devido a variações na gravidade terrestre devido à distribuição assimétrica dos continentes, esse nível do mar descreve um geóide que é irregular. Esse geóide é aproximado por um elipsóide, que considera dois raios e uma excentricidade, devido ao achatamento dos pólos. Por fim, em aplicações cartográficas onde a precisão absoluta não é necessária, é possível optar por um esferóide, que representa a Terra como sendo esférica.

Além da representação tridimensional, que pode e deve ser usada para cálculos, a visualização em geral é feita em 2D, e para isso deve ser escolhida uma projeção, que consiste em uma fórmula que mapeia (projeta) determinado ponto terrestre em um determinado ponto no mapa.

Devido à proliferação dos mapas na internet, que tornou acessível a informação geográfica para o usuário entusiasta, qualquer aplicação de mapas se beneficia de usar o mesmo sistema de coordenadas e a mesma projeção dos serviços de mapa online.

Esse sistema de coordenadas é o WGS84, e essa projeção é a Esférica de Mercator.

Daqui por diante, todas as discussões a respeito de posição geográfica e sua projeção feitas neste blog considerarão por padrão esses sistemas, exceto quando explicitamente afirmado o contrário.

A partir desta discussão, podemos também começar a tratar da modelagem e da representação computacional da posição geográfica, coisa que pretendo fazer na próxima postagem.