segunda-feira, 28 de março de 2016

Issues do GitHub - um recurso que vale mais do que parece.

Faz pouco tempo que comecei a usar o GitHub no trabalho. Depois de alguns anos sofrendo com o SVN hospedado no servidor local da empresa, um novo desenvolvedor conseguiu "nos convencer" a migrar para o Git, por ser um sistema melhor, e para a nuvem, devido a vários problemas de disponibilidade que começaram a interferir terrivelmente no nosso pair-programming remoto.

Eu fui o mais fácil de convencer a fazer isso, porque já tinha lido bastante, e porque já tinha "upado" algumas coisas pra lá, dos meus projetos de hobby. O fato é que eu nunca tinha tido usado "seriamente" o GitHub para os projetos pessoais, um pouco por falta de insistência, falta de disciplina minha, e por não haver uma real necessidade. O projeto acabava virando uma bagunça de qualquer forma, e o Dropbox acabava sendo a melhor forma de eu ter "sempre a última versão" entre diferentes máquinas.

Pois o fato é que desde que começamos a usar o GitHub como uma equipe, para desenvolvimento "sério", e em especial por estarmos pagando - e portanto querendo espremer todo o suco da laranja - resolvi ir um pouco mais além e explorar todas as outras coisas que a plataforma GitHub (e não necessariamente o Git) oferece. E, por haver uma necessidade de gestão, as Issues se mostraram surpreendentemente úteis.

Tão úteis que resolvi começar a usar também para os projetos pessoais, e a surpresa é que, mesmo sem ser "desenvolvimento sério", mesmo sem ser trabalho em equipe (ao menos não por enquanto), eu percebi que, simplesmente por estar usando as issues, o desenvolvimento teve um ganho de qualidade e produtividade absurdo!

O que notei de mais marcante é que, juntamente com a facilidade do branching, as issues são a forma ideal de registrar uma direção desejada para o desenvolvimento no momento e no contexto em que é adequado fazer isso. E esse momento definitivamente não é enquanto se está escrevendo código.

O hábito que adquiri rapidamente, e que está dominando a forma de desenvolver agora é:

  1. Assim que eu tenho um momento "ahá!", e me dou conta de qual é o principal obstáculo ou feature faltando, eu corro pra abrir a issue e escrever em detalhes qual é a natureza do problema: qual a situação desejada, qual a situação atual, quais os passos necessários para transformar a situação atual na situação desejada, e quais os prováveis obstáculos para isso;
  2. Esse issue deve quase que obrigatoriamente se transformar em uma branch;
  3. Enquanto eu estiver nessa branch, eu não posso mexer em outra coisa! Manter um histórico "limpo" e evitar a diluição do esforço requer que, numa branch, eu só trabalhe no tema da branch (que deve estar apropriadamente capturado pelo nome escolhido para ela);
  4. O maior sonho de quem está trabalhando numa branch que não seja dev ou master deve ser terminar a implementação o mais rápido possível, para que a branch possa ser apagada imediatamente após sua conclusão.
Fazendo isso, temos vários elementos positivos, que somados têm um impacto muito relevante na velocidade e qualidade:
  • O fato de um Issue ser criado via browser, com uma interface afastada do código-fonte, leva o desenvolvedor a pensar de forma global e abstrata sobre o problema, em um momento em que o "emocional" não está comprometido com as dificuldades de criação ou implementação. Essa dissociação entre o pensar e o agir fazem com que cada uma dessas etapas seja desacoplada da outra, tornando-se comparativamente mais fácil;
  • Ter um par, formado por um Issue e sua Branch correspondente, leva o desenvolvedor a manter o foco naquela funcionalidade específica, e ter um critério definido de "pronto" gera um incentivo palpável e uma sensação de progresso que são muito motivantes;
  • A necessidade de isolar as modificações para evitar regressão durante o merge faz com que se privilegie soluções coesas e desacopladas.
Da próxima vez que o leitor der uma visitada no seu repositório, experimente brincar com as Issues - e em especial brinque também com as Labels e Milestones. Garanto que vai se surpreender com o retorno!

domingo, 10 de agosto de 2014

Usando Graphviz para Visualização e Gerenciamento de Dependências

O Gerenciamento de Dependências (Dependency Management) é uma atividade muito útil não apenas em programação e projeto de software, mas em qualquer atividade minimamente complexa em que se deva construir ou executar alguma coisa.

Especificamente em se tratando de software, inúmeras características de qualidade em arquitetura, como modularidade, arquitetura em camadas, information hiding e gerenciamento da complexidade em geral são, em sua essência, atividades de detecção e organização de dependências.

Para que a discussão que se segue seja baseada em um exemplo concreto, vou considerar uma situação real que me aconteceu hoje:

Sou cadastrado no site Coursera, que é um site agregador de cursos online para educação à distância (a ideia me agrada, embora ainda não tenha me engajado em acompanhar de fato algum curso, até agora). Sendo assim, recebi de forma inesperada um email oferecendo vários cursos, e me interessei pelo curso chamado "Data Scientist's Toolbox", da Universidade Johns Hopkins.

Rapidamente descobri que existem cursos relacionados a esse, mas logo em seguida descobri que os cursos não só eram relacionados, como faziam parte de uma série de cursos, e que havia relações de dependência entre eles. Ou seja, era sugerido, ou mesmo requerido, que para cursar determinados cursos, os pré-requisitos já tivessem sido cumpridos.

As dependências são apresentadas em um PDF, que não é exatamente uma forma legível de apresentar uma lista exaustiva de dependências:

Cada curso pode ter dependências "hard"
ou "soft" com relação a outros cursos.


Impaciente para fazer o malabarismo mental - e receoso de esquecer alguma dependência por "erro de leitura" - decidi fazer um gráfico das dependências.

A forma mais fácil de fazer um gráfico é desenhar o gráfico, mas essa é a forma que eu menos gosto de usar, evitando fazer qualquer coisa à mão sempre que possível. Em especial, não só o melhor layout é difícil de aceitar, como qualquer modificação posterior pode envolver redesenhar tudo novamente. Como um bom nerd, me recuso a fazer isso, e depois de muito pesquisar descobri a ferramenta Graphviz, que transforma a descrição de um grafo, sob a forma de um arquivo de texto simples em alguma linguagem de descrição de grafos (a mais comum é a linguagem DOT), na imagem de um grafo propriamente dito, com layout automático configurável.

Bom, o gráfico gerado pelo Graphviz ficou assim:

Dependências "hard" mostradas em preto;
Dependências "soft" mostradas em cinza;
A seta se origina na dependência em direção ao elemento dependente

Se o leitor quiser saber como esse gráfico foi gerado (com qual programa, e a partir de qual arquivo-fonte), isso será explicado no final da postagem. Agora vou comentar sobre como a visualização desse gráfico ajuda na análise do problema, ou seja, no gerenciamento de dependências propriamente dito.

A primeira coisa que chama atenção é que TODOS os elementos dependem de "Data Scientist's Toolbox", e também de "R Programming". Isso polui muito o gráfico, dificultando ver as outras dependências entre os módulos. Antes de começar a reorganizar graficamente essas dependências, convém fazer duas considerações:
  1. Muitas vezes, se A depende de B e B depende de C, não necessariamente precisamos concluir que A depende diretamente de C. No gráfico, não é possível afirmar que os módulos que dependem de "R Programming" dependem direta ou indiretamente de "Data Scientist's Toolbox";
  2. No caso da dependência entre "R Programming" e "Data Scientist's Toolbox", ela é qualitativamente diferente das outras (porque é "soft"), portanto temos mais um motivo para não colapsar inconsequentemente as dependências entre " ' Toolbox" e o restante dos módulos.
Ainda assim, como casualmente TODOS os módulos que dependem de um também dependem de outro, vamos colapsá-los em um sub-grafo chamado "Basics":

Com a criação do elemento "Basics", o gráfico fica muito mais limpo e legível.

Bom, agora ficou bem mais fácil perceber as "dependências herdadas" mencionadas acima. Essas dependências são da forma "A depende de B que depende de C, mas A também depende de C", conforme visualizado no gráfico por uma seta "pulando" diretamente de um elemento a outro, mas também "passando" por um elemento intermediário. A conclusão correta é que, se A depende de B que depende de C, não é necessário afirmar de forma explícita - e redundante - que A depende de C. Portanto essas dependências redundantes podem ser e serão removidas:


 Agora sim, podemos dizer que o gráfico está em sua forma "canônica", com todas as redundâncias agrupadas, colapsadas ou removidas. Podemos então, tirar algumas conclusões diretas a partir da visualização do gráfico:
  • O módulo mais avançado é "Practical Machine Learning", requerendo no mínimo outros três módulos sequenciais ("R Programming", "Statistical Inference" e "Regression Models"), mas preferivelmente outros cinco módulos (os já citados, mais " ' Toolbox" que é soft e "Exploratory..." que além de soft pode ser cursado em paralelo com "Statistical..." ou "Regression...");
  • "Getting and Cleaning Data" não depende nem é pré-requisito dos outros módulos exceto o básico. Portanto, se meu interesse maior for esse, eu posso ver que vai dar pouco trabalho e que não preciso me preocupar em cursar ou não os outros módulos;
  • Não existe uma ordem pré-definida para cursar os módulos que dependem apenas do módulo básico. Essa é uma característica benéfica de descrever um processo em forma de dependências, ao invés de "etapas" (este, depois este, depois este, etc.).
Uma coisa fundamental que deve ser dita, e que diferencia entre a visualização de dependências e o verdadeiro gerenciamento de dependências, é que, ao contrário deste caso em que as dependências já vieram prontas, a maior utilidade do método é quando estamos analisando as dependências de algo que nós estamos desenvolvendo: ao detectarmos dependências espúrias, podemos intervir para eliminá-las, melhorando a qualidade do desenvolvimento.

Como gerar os gráficos

O primeiro passo é criar um arquivo na linguagem DOT. O arquivo que gerou o penúltimo gráfico encontra-se abaixo, com comentários no formato // destacando os pontos chave:
 
digraph g {
rankdir = "LR"   // faz com que as dependências ocorram "left to right"
label = "Data Science Course Series\nJohns Hopkins University"

node [shape="rectangle"]  // faz com que o formato padrão dos nós seja um retângulo

// definição de variáveis do tipo "node", com as respectivas labels
toolbox     [label="Data Scientist's\nToolbox"]
r           [label="R Programming"]
getting     [label="Getting and\nCleaning Data"]
explore     [label="Exploratory\nData Analysis"]
reproduce   [label="Reproducible\nResearch"]
statistical [label="Statistical\nInference"]
regression  [label="Regression\nModels"]
machine     [label="Practical\nMachine Learning"]
products    [label="Developing\nData Products"]


// clusters são definidos com subgrafos cujo nome têm o prefixo "cluster"
subgraph cluster_basics {
    style=filled
    fillcolor="#ffffff"
    
    label="Basics"
    
    toolbox -> r [color="#cccccc"]   // cor cinza simbolizando "soft"
}

explore -> machine [color="#cccccc"]  // "soft"
explore -> products [color="#cccccc"]  // "soft"


// hard dependencies
subgraph hard {  

    // "ltail" (logic tail) posiciona explicitamente a origem da seta

    r -> getting [ltail=cluster_basics]
    r -> explore [ltail=cluster_basics]
    r -> reproduce [ltail=cluster_basics]
    r -> statistical [ltail=cluster_basics]
    
    r -> regression [ltail=cluster_basics]
    statistical -> regression
    
    r -> machine [ltail=cluster_basics]
    regression -> machine
    
    r -> products [ltail=cluster_basics]
    reproduce -> products
}


}

Esse arquivo precisa ser interpretado por algum programa que vai gerar a imagem propriamente dita. No site do Graphviz há instaladores para todas as plataformas. No Linux, usei a linha de comando da seguinte forma:

dot -T svg -o "graph.svg" "graph.gv" 

No Windows, tenho usado a interface gráfica GVEdit. Tudo isso baixado e instalado a partir do site de forma bastante tranquila.

WYSIWYG com três truques malandros

Evidentemente a construção de um arquivo desses é muito melhor quando é interativa: a cada elemento adicionado, geramos um novo grafo e vemos se ele está crescendo da forma como desejamos. Que eu saiba, não há um editor que faça isso de forma contínua, tipo uma "IDE" para a linguagem DOT, mas é possível fazer algo bem parecido da seguinte forma:
  1. Configurar alguma mini-IDE (editor de texto que tenha o comando "Run" para executar scripts, eu uso o Geany) para rodar a linha de comando acima com um atalho de teclado;
  2. Gerar uma saída em formato .SVG, para que a qualidade seja independente de zoom;
  3. Criar um HTML estático que mostre o SVG em um elemento de imagem (largura e altura proporcionais ao tamanho da janela), e que se auto-atualize usando uma tag
    <meta http-equiv="refresh" content="1">
     
Assim, posso trabalhar com a tela dividida ao meio, e cada vez que aperto F5 o arquivo DOT é salvo, o SVG é gerado, e sua exibição é, em no máximo um segundo, atualizada na janela do navegador:

Se isso não é WYSIWYG, então nada é, não é mesmo?

sábado, 12 de julho de 2014

Purismo vs. Pragmatismo: A definição de Testes como melhor forma de especificação de um sistema

Hoje li (pela segunda vez) esse trecho do livro Object Oriented Software Construction, de Bertrand Meyer (leitura altamente recomendável):

"Em Tecnologia Orientada a Objetos, os objetos descritos por uma classe são definidos apenas pelo que é possível fazer com eles: operações, e propriedades formais dessas operações (os contratos).
(...)
A tradição na modelagem dos sistemas de informação geralmente pressupõe uma "realidade externa" que antecede qualquer programa que a utilize; para um desenvolvedor orientado a objetos, essa noção não tem sentido, pois a realidade não existe independentemente daquilo que se pretende fazer com ela. (Mais precisamente, é irrelevante se ela existe ou não, pois nós só conhecemos aquilo que podemos usar, e o que sabemos a respeito de alguma coisa é definido inteiramente pela forma como podemos usá-la)."

Essa ideia reflete bem duas diferentes posturas com relação à modelagem e ao desenvolvimento de software: uma purista, e a outra pragmática.

A visão purista busca modelar os objetos de forma platônica, em que as propriedades e operações características derivam de sua própria natureza. São os tipos abstratos de que fala Meyer. Objetos que existem na realidade tenderiam a ser modelados em sua completude, e de forma que cada propriedade ou operação computacional fosse a contrapartida de uma propriedade ou operação real.

A outra visão, pragmática, é aquela proposta pelo último parágrafo citado: a definição de uma classe é a definição de suas operações, ou seja, deve ser representado aquilo que é conveniente representar, pois essa é a realidade relevante. Essa visão se mostra aparentemente mais saudável para orientar o desenvolvimento de software, especialmente após algumas leituras sobre Test Drivem Development.

São perspectivas opostas sobre o mesmo tema, como duas faces da mesma moeda: na primeira, aquilo que se pode fazer com o objeto é consequência da definição de sua natureza canônica, que vem primeiro e é tratada como sendo mais importante; na segunda, a declaração das operações possíveis é que de fato acaba por definir o objeto.

A tentativa de esgotar a definição de um objeto a partir de sua natureza intrínseca, independente de suas potenciais aplicações, pode facilmente levar à paralisia por análise (analysis paralysis), que foi um sintoma já observado para algumas classes e propriedades do Modelo de Dados de GPS, tratado em postagens anteriores.

Portanto, o plano daqui por diante, no desenvolvimento do Modelo de Dados de GPS, é inverter - mesmo que temporariamente -  essa abordagem bottom-up na definição de um modelo de domínio e modelo de dados, e passar a utilizar algumas práticas da metodologia agile, as quais seriam principalmente:
  • Desenvolvimento Orientado a Testes (test-driven development, TDD). Nessa metodologia, o teste é definido antes, e a implementação é levada adiante apenas para satisfazer o teste. Assim, temos a descrição do teste como a especificação definitiva do sistema (pois descreve como ele deve se comportar), e o código-fonte como a descrição definitiva do sistema propriamente dito.
  • Desenvolvimento Orientado por Comportamento (behavior-driven development, BDD), uma vertente do TDD, que pode ser equiparado (de forma generosamente relaxada) ao Design Dirigido por Objetivos (goal-directed design, Alan Cooper). Ambas as propostas buscam orientar qualquer desenvolvimento de software à satisfação dos objetivos do usuário. Assim sendo, cada linha de código, cada método, cada classe deve ter sua existência à satisfação de alguma necessidade real do usuário. Para isso, o software deve ter um conjunto de funções, que deriva de um conjunto (lista) de requisitos, que por sua vez deriva (ou deveria derivar) de um conjunto de casos de uso. Esses casos de uso (associados a personas e cenários) são justamente a definição dos testes do BDD.
  • YAGNI: you ain't gonna need it. Implementar somente o que é necessário. Isso se opõe ao anti-pattern "big design up-front", que é o que eu (inspirado, é verdade, na ISO) vinha tentando fazer. Nas palavras de Ron Jeffries: "implemente funcionalidade apenas quando você realmente precisar, e não quando você apenas pensa que vai precisar".
Portanto, o plano é deixar um pouco de lado a super-especificação de implementação, e abordar o lado oposto do desenvolvimento, que são os usuários e as aplicações pretendidas, e ao invés de ter um modelo de implementação exaustivo, ter um sistema que surja simples porém operacional (atendendo a alguma necessidade inicial básica), podendo a partir de então evoluir, ser refatorado, e ser expandido.

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.