Nesse post eu mostro a solução que propus para resolver um problema: converter coordenadas de uma projeção e datum qualquer para SIRGAS2000. Como não encontrei um bom suporte para a conversão no R, tive que recorrer à calculadora geográfica do INPE, criando uma automação para interagir com ela e realizar esta tarefa.
Há algumas semanas uma das pessoas com quem trabalho trouxe a necessidade de fazer uma conversão de coordenadas, a fim de que pudéssemos seguir com algumas análises que estávamos fazendo. Essa tarefa parecia ser coisa simples, pois deveríamos apenas passar as coordenadas da projeçãosf
no R quanto na lib geopandas
no Python. Assim, não havia com o que se preocupar…certo?
De uma forma surpreendente, não consegui encontrar um jeito confiável de fazer a conversão das coordenadas nem no R e nem no Python. A primeira tentativa que fiz foi no R, e acabei esbarrando com a falta de suporte ao datum SIRGAS2000: apesar do código EPSGsf
não parecem ter suporte para ela - falhando na conversão logo de cara. Com isso, fiz minha segunda tentativa usando o Python, mas fiquei meio desconfiado do output: na maior parte dos casos, parecia que o geopandas
fazia a mudança de projeção e datum do arquivo shapefile sem que, no entanto, os valores das coordenadas em si fossem alteradas. Assim, acabamos esbarrando nesse bloqueio para avançar.
Como não eram muitos pontos que deveriam ter as coordenadas convertidas - mais ou menos uns 50 -, surgiu a ideia de usar uma aplicação como QGIS para realizar as conversões. Apesar da ideia ser boa para o momento, ela traria muitos problemas no curto ou médio prazo: (1) precisávamos colocar as informações de latitude e longitude dentro de um dataframe
geográfico (i.e., um geodataframe
ou um sf
) e setar o seu datum e projeção, (2) a partir daí precisaríamos salvar o arquivo para o disco para abrir no QGIS, (3) onde precisaríamos executar manualmente muitos passos para converter as coordenadas e exportar um novo shapefile que, (4) finalmente, poderíamos abrir no R/Python para usar. Além disso, em algum momento receberíamos mais um batch de dados, e precisaríamos repetir o procedimento todo de novo. Logo, resolvemos usar essa solução para sair do lugar naquele momento, mas precisávamos de outra estratégia para tornar essa etapa do pipeline de dados mais robusta e reprodutível.
Uma solução que propus para isso foi o uso da calculadora geográfica do INPE. Eu já havia usado ela para desenvolver um trabalho que no passado, e sabia que ali teríamos um resultado bastante confiável. Naquela época, eu havia feito a conversão das coordenadas toda de forma manualSelenium
para interagir com uma página dinâmica. Nesse contexto, acabei criando essa automação usando o Python, mas reproduzi os mesmos passos com o R e, neste post, aproveito o reticulate
para contar sobre a solução usando tanto o Python quanto o R.
A calculadora geográfica do INPE está disponível através de uma página web, neste link. A página tem uma estrutura bastante simples e intuitiva, através da qual podemos fornecer os inputs para três tarefas principais: (1) converter coordenadas (o box de cima), (2) calcular a distância entre dois pontos (o box do meio) e (3) calcular o meridiano central a partir de uma coordenada (o box de baixo). Nosso foco neste post será o box de cima.
A animação abaixo ilustra de que forma funciona a conversão de coordenadas através da página. A sequência de etapas é a seguinte:
Beleza. Como já podemos reparar, parece haver algum tipo de dinamismo na página: conforme vamos preenchendo o formulário e clicando nas coisas, novas opções e boxes vão surgindo. Para ficar mais claro o que está acontecendo, vamos olhar o que acontece com o código HTML da página conforme vamos interagindo com ela. Para isso, vamos conferir a animação abaixo, que demonstra o que acontece depois da primeira vez que clicamos no avançar (i.e., etapa 4 na lista anterior). Como podemos ver, o formulário do primeiro box é uma requisição do tipo POST, que poderíamos passar normalmente usando o R ou o Python. Todavia, uma vez que enviamos o formulário preenchido para o site, vemos que o conteúdo do box seguinte é gerado de forma dinâmica - esse é o momento em que àquela tag frameset
brilha, e o conteúdo em seu interior é populado. Analisando esse conteúdo, podemos ver que ele abre a possibilidade de fazermos uma nova requisição do tipo POST, desta vez para enviar os dados associados ao passo 5 da lista acima. Ou seja, parece que dois POSTs são necessários para obter o resultado, e que eles vão aparecendo na página de forma dinâmica.
Vamos à última etapa do que rola por baixo dos panos no código HTML, olhando a próxima animação abaixo. Novamente, a tag frameset
brilha uma vez que o formulário do segundo box é enviado, e a seleção é toda recolhida; se a expandirmos de novo, podemos ver que o seu conteúdo mudou um bocado e, agora, temos acesso à tabela com os dados das coordenadas já convertidas. De uma forma ou de outra, parece que esta tabela também é gerada de forma dinâmica a partir das informações passadas nos formulários anteriores. Além disso, não sei se você notou, mas existe uma chamada para um javascript
em diversas partes do código. Isso acende a luz amarela.
Tendo em vista o funcionamento da página, resolvi tentar primeiro o R
para fazer as requisições POST e converter as coordenadas. Fui no httr
e não tive sucesso: nada do que tentei me permitia obter o conteúdo para que eu fizesse o segundo POST. Daí então resolvi tentar o rvest
e a possibilidade de usar uma sessão para poder passar os dois POSTs. Aqui eu já tive um pouquinho mais de sucesso, mas sem conseguir chegar onde queria: obtive o resultado do primeiro POST e fazer o segundo, mas o resultado foi uma página em branco. Depois dessa frustraçãodesse aprendizado, resolvi tentar a lib requests do Python, mas também não tive muito sucesso. Assim, busquei alguma informação na internet sobre como lidar com iframes
e requisições POST associados à páginas com conteúdo dinâmico gerado em javascript
e PHP
. Entretanto, tudo o que encontrei foram recomendações para usar o Selenium
para a tarefa. Como eu já estava cansado de buscar uma solução mais simples, resolvi então dar o braço a torcer e partir para o Selenium.
Eu já havia usado o Selenium
no Python no passado, e tinha sido bem simples: foi só baixar o driver para o navegador, criar a instância dele e pronto, a navegação e automação estão na mão. No R, entretanto, a coisa é muito mais complexa: a instalação do RSelenium
não segue os padrões convencionais dos pacotes do R, existe a necessidade de definir algumas coisas através do terminal e, além disso, existe muito pouco material para resolução de problemas sobre o esse pacote online. Eu até cheguei à superar as duas primeiras complexidades, mas esbarrei na última algumas vezes ao longo das minhas tentativas de criar a automação. Por conta disso, resolvi começar o desenvolvimento da solução através do Python, de forma que assim que eu chegasse ao resultado que precisava, traduziria o código para o R.
Com isto em mente, a primeira coisa que precisamos fazer é importar o pacote reticulate
e apontar o path onde está o arquivo binário do Python na minha máquina.
library(reticulate)
use_python(python = Sys.which('python'))
O segundo passo é carregar as libs e funções que vamos precisar - tanto o próprio Selenium
quanto àquelas que nos ajudarão a plotar algumas imagens do avanço da automação.
from selenium import webdriver
from selenium.webdriver.support.ui import Select
from selenium.webdriver.chrome.options import Options
from matplotlib.pyplot import imshow
from PIL import Image
import numpy as np
Começamos a automação per se definindo a forma como vamos executar o Selenium
. Neste post eu defini que o utilizaremos no modo headless
a fim de que o navegador não seja aberto durante a execução do código. Se você definir esse argumento como False
, então verá a execução toda através do navegador em tempo real.
# setando as opcoes para rodar headless
options = Options()
options.headless = True
Todas as operações que faremos com o Selenium
farão uso de uma instância do Chrome. Para tal, você precisa baixar um executável do Chromedriver que seja compatível com o seu navegador Google Chrome (i.e., geralmente àquela com a mesma versão do seu navegador) e colocá-lo em algum diretório. Uma vez que você tenha feito isso, basta especificar o path até o executável, e passar a instância com as opções de execução do driver que definimos anteriormente para a função webdriver.Chrome
. O código abaixo faz isso e, então, imprime uma mensagem para avisar se conseguimos inicializar a sessão do webdriver ou não.
# abrindo o driver do Chrome
driver = webdriver.Chrome(
executable_path='/Users/Nicholas/Downloads/chromedriver',
options = options
)
# printando o status do driver
try:
if driver.session_id:
print('Inicialização do driver feita com sucesso!')
except:
print('Inicialização do driver falhou!')
Inicialização do driver feita com sucesso!
Uma vez que tenhamos a nossa instância do webdriver, podemos usá-la para navegar até a página que queremos. Para isso, basta usarmos o método .get
, e passar a url
da calculadora geográfica do INPE. Uma vez que a página começa a ser carregada, aproveito para passar o método implicitly_wait
de forma a dizer para o webdriver esperar alguns segundos entre as chamadas de todas as funções do webdriver - isso é útil para evitar de ficarmos enviando ações para o navegador sem que os objetos que queiramos interagir tenham sido carregados. Finalmente, aproveito para tirar um screenshot da tela do webdriver, só para ver se estamos no caminho certo.
# entrando no site da calculadora geografica do INPE
driver.get(url = 'http://www.dpi.inpe.br/calcula/')
# aguardando o site carregar
driver.implicitly_wait(10)
# salvando imagem da pagina
scr = driver.save_screenshot(filename='images/screenshot_1.png')
# abrindo imagem da pagina
imshow(np.asarray(Image.open('images/screenshot_1.png', 'r')))
Tudo parece certo! A próxima coisa que precisaremos fazer é preencher o formulário do primeiro box. Para isso, vamos imitar o nosso comportamento através do Selenium
, fazendo com que:
contents
;Select
para interagir com o menu dropdown que recebe as informações da projeção de entrada. Essa função recebe como argumento o ID do elemento que contém o menu dropdown - este, por sua vez, pode ser encontrado através de um xpath específico (descrito no pedaço de código abaixo);select_by_value
, especificando a string que representa a projeção dos dados de entrada - essa string pode ser encontrada dentro do código HTML da página. Eu optei por usar um select_by_value
aqui, mas também seria possível usar um select_by_visible_text
e passar o texto conforme está escrito no menu dropdown - só não fiz isso por uma questão de escolha mesmo;Select
e passar o ID do elemento que contém este menu dropdown;select_by_value
o valor que representa o datum de entrada (novamente, este valor pode ser encontrado dentro do código HTML da página, e usar o select_by_visible_text
seria uma opção viável);Avançar
e clicamos nele.# passando para o primeiro frame para preencher
driver.switch_to.frame(frame_reference = 'contents')
# selecionando a projecao de entrada
seletor_projecao_entrada = Select(driver.find_element(by = 'xpath', value='//body//div//center//table//tbody//tr[3]//select'))
seletor_projecao_entrada.select_by_value(value = 'latlong_gd')
## imputando o valor da longitude
input_X = driver.find_element(by = 'xpath', value='//body//div//center//table//tbody//tr[5]//td//input')
input_X.send_keys('-43.1034')
## imputando o valor da latitude
input_Y = driver.find_element(by = 'xpath', value='//body//div//center//table//tbody//tr[7]//td//input')
input_Y.send_keys('-22.8822')
# selecionando o datum de entrada
seletor_datum_entrada = Select(driver.find_element(by = 'xpath', value='//body//div//center//table//tbody//tr[9]//select'))
seletor_datum_entrada.select_by_value(value = '1')
## clicando no avancar
driver.find_element(by = 'xpath', value='//body//div//center//table//tbody//tr[10]//td').click()
# salvando imagem da pagina
scr = driver.save_screenshot(filename='images/screenshot_2.png')
# abrindo imagem da pagina
imshow(np.asarray(Image.open('images/screenshot_2.png', 'r')))
Podemos ver que a automação funcionou até aqui! Como resposta aos inputs anteriores, precisamos agora é especificar as informações para os dados de saída da conversão. Para isso, continuaremos reproduzindo o nosso comportamento:
switch_to.parent_frame
para o primeiro e o método switch_to.frame
para o segundo. No caso do segundo box, é importante resaltar que precisamos passar como argumento o ID do elemento que contém o segundo box. Neste caso, este elemento pode ser selecionar através do nome deste box: mainp
;Select
, passando o ID do elemento que contém o menu dropdown e, então, definindo o valor desejado;Avançar
deste box e clicamos nele.# passando para o segundo frame
driver.switch_to.parent_frame()
driver.switch_to.frame(frame_reference = driver.find_element_by_name('mainp'))
# selecionando a projecao de saida
seletor_projecao_saida = Select(driver.find_element(by = 'xpath', value='//html//body//table//tbody//tr[2]//select'))
seletor_projecao_saida.select_by_value(value = 'latlong')
# selecionando o datum de saida
seletor_datum_entrada = Select(driver.find_element(by = 'xpath', value='//html//body//table//tbody//tr[4]//td//select'))
seletor_datum_entrada.select_by_value(value = '5')
## clicando no avancar
driver.find_element(by = 'xpath', value='//html//body//table//tbody//tr[5]//td').click()
# salvando imagem da pagina
scr = driver.save_screenshot(filename='images/screenshot_3.png')
# abrindo imagem da pagina
imshow(np.asarray(Image.open('images/screenshot_3.png', 'r')))
Pronto! Chegamos onde precisávamos: o último box com a tabelinha contendo as informações das coordenadas já convertidas. Nosso foco a seguir será conseguir pegar o código HTML que contém essa tabela e guardar essa informação em um objeto. A partir daí, vamos passar esse objeto do Python para o R, a fim de fazer com que o parsear fique mais fácil de ser feito. Mas, antes, vamos por partes:
canvas
;# passando para o ultimo frame
driver.switch_to.parent_frame()
driver.switch_to.frame(frame_reference = driver.find_element_by_name('canvas'))
# pegando a tabela
tabela = driver.find_elements(by = 'xpath', value='/html/body')
# pegando o HTML da tabela
html_da_tabela = tabela[0].get_attribute('innerHTML')
# salvando imagem da pagina
scr = driver.save_screenshot(filename='images/screenshot_4.png')
# abrindo imagem da pagina
imshow(np.asarray(Image.open('images/screenshot_4.png', 'r')))
Agora que temos o código HTML da tabela, fica fácil parsear os resultados da conversão. Vamos pegar o código HTML que está dentro do objeto do Python, ler ele como um html no R, e transformá-lo em um dataframe
através da função html_table
do pacote rvest
. Depois que isto está feito, basta apenas fazer mais uma arrumação dos dados e a tarefa está concluída!
# carregando pacotes
library(dplyr) # para manipulacao de dados
library(tidyr) # para manipular dados tambem
library(purrr) # para manipular listas
library(rvest) # para ajudar a parsear o HTML
# pegando o HTML do ultimo frame a partir do python
output <- py$html_da_tabela |>
# lendo o html dele
read_html() |>
# parseando a tabela
html_table() |>
# tirando a tabela de dentro da lista
pluck(1) |>
# eliminando o que é lixo
filter(!X1 %in% c('Resultado', 'Resultado da conversao:', '--'), X2 != '--') |>
# passando a tabela para o formato largo
pivot_wider(names_from = X1, values_from = X2) |>
# tratando o nome das colunas
janitor::clean_names()
# printando a tabela
rmarkdown::paged_table(x = output)
datum_entrada <chr> | datum_saida <chr> | longitude_em_gms <chr> | longitude_em_gd <chr> | |
---|---|---|---|---|
SAD69 | SIRGAS2000 | O 43 6 13.755 | -43.103820896775 |
Coordenadas convertidas e direto dentro do código! Como não temos mais nenhuma outra coordenada para converter aqui, podemos voltar para o Python e fechar a sessão do driver.
# fecha o driver
driver.close()
Neste post eu tentei mostrar uma solução que propus para resolver um problema pontual: converter um conjunto de coordenadas para o datum SIRGAS2000 de forma reprodutível, uma vez que não encontrei muito suporte para isso no R, e a conversão através do Python me pareceu meio estranha. A implementação da solução é baseada na automação da operação de conversão de coordenadas através do site da calculadora geográfica do INPE, usando o Selenium
. Assim, toda a conversão ocorre através de um navegador e múltiplas requisições para um servidor, e não através do processamento na sua máquina per se. Isto quer dizer que esta solução não é muito escalável se tivermos que converter um grande número de coordenadas, e deve ser usada com moderação.
O desenvolvimento dessa solução veio muito no sentido de automar uma etapa de um pipeline de dados, que não usa um volume tão grande de observações assim. Além disso, essa solução também nasceu da necessidade de resolver uma questão onde não encontramos uma alternativa mais simples e pré-existente. Com isso, essa solução está OK para endereçar problemas que envolvam aqueles mesmos tipos de contextos, mas está longe de ser o melhor caminho caso eles sejam diferentes. Um exemplo disto seria caso a conversão de datum fosse entre SAD69 e WGS84, onde as funções do pacote sf
do R parecem funcionar bem - além de ser uma alternativa muito mais simples do que a que mostro aqui! E, por sinal, acho que no fim do dia é isso que realmente importa: resolver o problema de forma rápida, simples, objetiva e reprodutível.
Dúvidas, sugestões ou críticas? É só me procurar que a gente conversa!
Eu usei um misto de R e Python para fins de demonstração neste post. Todavia, você pode encontrar essa automação escrita toda em R neste link aqui, e toda em Python neste outro aqui. Uma coisa que seria interessante fazer também é criar uma classe no Python, que pudesse ser usada de forma mais reprodutível para fazer essa conversão. Eu nunca havia chegado a construir uma, e acabei tentando fazer isso aqui pela primeira vez. A ideia é que ela fosse útil para converter não só como mostrei aqui, mas também outras que eu julguei serem comuns em aplicações do nosso dia a dia (e.g., UTM para grau decimal). Eu não cheguei a testar essa classe de forma exaustiva, então sugiro cuidado ao usar ela. De forma similar, sei que ela está longe de estar bem escrita - e estou muito aberto à sugestões de como melhorá-la e oportunidades de aprender mais!
Finalmente, uma outra extensão que vejo seria de pegar o código PHP que faz a conversão das coordenadas e portá-lo para R e Python. Esse código está disponível para download no próprio site da calculadora geográfica do INPE, e seria uma alternativa muito viável para escalar essa conversão.
If you see mistakes or want to suggest changes, please create an issue on the source repository.
Text and figures are licensed under Creative Commons Attribution CC BY 4.0. Source code is available at https://github.com/nacmarino/codex/, unless otherwise noted. The figures that have been reused from other sources don't fall under this license and can be recognized by a note in their caption: "Figure from ...".
For attribution, please cite this work as
Marino (2021, Dec. 23). Codex: Convertendo coordenadas através da calculadora geográfica do INPE. Retrieved from https://nacmarino.github.io/codex/posts/2021-12-23-convertendo-coordenadas-atraves-da-calculadora-geografica-do-inpe/
BibTeX citation
@misc{marino2021convertendo, author = {Marino, Nicholas}, title = {Codex: Convertendo coordenadas através da calculadora geográfica do INPE}, url = {https://nacmarino.github.io/codex/posts/2021-12-23-convertendo-coordenadas-atraves-da-calculadora-geografica-do-inpe/}, year = {2021} }