Mostrando postagens com marcador classe. Mostrar todas as postagens
Mostrando postagens com marcador classe. Mostrar todas as postagens

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.