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

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

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.