Blog

Cache Maps

Por Victória Almeida e Miguel Teixeira

O mapa é uma ferramenta essencial em uma aplicação com funcionalidade de mapeamento geográfico e, tendo em vista áreas com acesso precário à internet, essa funcionalidade pode ser comprometida já que as imagens estão sendo constantemente carregadas de um servidor online. Visando resolver essa dificuldade, um cacheamento seria uma ótima solução.

O caso de uso que motivou a construção deste artigo gira em torno de um aplicativo mobile que usa o Expo, uma plataforma de código aberto para criar aplicativos nativos universais para Android, iOS e web com JavaScript e React.

O desenvolvimento de aplicativos com o Expo é tipicamente facilitado em muitos aspectos,
desde a criação, que pode acontecer através de um único e simples comando, à visualização de mudanças – geradas pelo código javascript que está sendo escrito – em tempo real, no aplicativo da própria plataforma.

Porém, a plataforma possui algumas peculiaridades. Por exemplo, os desenvolvedores só podem utilizar um conjunto de bibliotecas específico, determinado pela plataforma.

Até o momento da publicação deste artigo, a única biblioteca disponível no “ecossistema Expo” para lidar com mapas é o react-native-maps. Por padrão, o uso do react-native-maps envolve a utilização dos provedores de mapas nativos de cada plataforma, o que nos leva ao Google Maps para o Android, e o Apple Maps para o iOS, porém, felizmente, de forma alternativa, também existe a opção “custom tiles overlay”, que permite a utilização de outros métodos para construção de mapas como com o projeto OpenStreetMaps, que acabou sendo o que implementamos inicialmente na nossa aplicação.

A supremacia do OpenStreetMaps

Open Street Maps é um projeto open-source mantido como se pode imaginar, por uma comunidade global. A ideia se iniciou em 2004, quando dados de mapeamento eram controlados por organizações comerciais e governamentais, difíceis de serem usadas e caras. Como uma solução, o OpenStreetMaps propunha a criação grátis e edição de mapas do mundo feitas inteiramente por esforços de voluntários online. Essa estratégia funcionou tão bem que o Google precisou correr atrás de se atualizar introduzindo o Google Map Maker que tinha uma interface parecida com a do OSM, permitindo pessoas ao redor do mundo a contribuírem.

Porém o que faz Open Street Maps único é que quando alguém contribui no OSM, esses dados pertencem a pessoa e a comunidade do OSM, e permanecem grátis e aberto a todos, sob licença Creative Commons. Quando alguém contribui ao Google Maps, todas as mudanças pertencem ao Google e é a empresa que decide quem e como outros podem usar os dados.

Além disso, toda mudança feita no OpenStreetMap fica imediatamente visível aos outros, enquanto no caso do Google demora por conta de processos longos de moderação e análise.

Apesar do Google possuir uma interface melhor e ser mais polido, OpenStreetMap é uma base perfeita para projetos inovadores emergentes com dados de mapeamento de todo o planeta podendo ser baixadas e usadas de forma completamente offline, enquanto com seu “concorrente” só podemos cachear uma pequena região e geralmente não funciona ser acesso a internet.

Tiles

Tiles revolucionaram a ideia de mapeamento da web e nos deram acesso rápido e fácil a grandes conjuntos de dados. O esquema de tiles consiste em dividir o mundo em pequenos mosaicos (normalmente 256 x 256 pixels) para cada nível de zoom e pré-renderizar conjuntos de dados para esses mosaicos. Dessa forma, apenas uma pequena fração de um grande conjunto de dados é fornecida ao usuário a qualquer momento, resultando em um mapa que pode ser ampliado ou deslocado com facilidade.

Slippy Map, por sua vez, é um termo geral usado para referenciar web mapas modernos que utilizam tiles. Podemos ver abaixo um exemplo deste tipo de mapa, cada tile é representado por um quadrante:

Os Slippy Maps possuem uma estrutura convencional de armazenamento de tiles na forma do URL:

/zoom/x/y.png

Cada nível de zoom é um diretório, cada coluna é um subdiretório e cada tile nessa coluna é um arquivo. Assim, se fossemos organizar os nossos tiles do mapa acima por exemplo, teríamos:

- 2/
    - 0/
        - 0.png
        - 1.png
        - 2.png
        - 3.png
    - 1/
        - 0.png
        - 1.png
        - 2.png
        - 3.png
    - 2/
        - 0.png
        - 1.png
        - 2.png
        - 3.png
    - 3/
        - 0.png
        - 1.png
        - 2.png
        - 3.png

Cacheando o mapa

As etapas que seguimos foram:

  1. Obtenção das tiles
  2. Desenvolvimento de um script
  3. Utilização das tiles com a biblioteca react-native-maps

Vamos descrever abaixo cada uma destas etapas:

Obtenção das tiles

Baixamos o mapa (do OpenStreetMaps) a ser cacheado com o auxílio do software Maperitive.

Desenvolvimento de um script

Para que o mapa cacheado funcione com a biblioteca react-native-maps, é preciso que estejam organizadas em um endereço url dentro do celular do usuário (um endereço no sistema de arquivos do celular) que possa ser acessado a partir de uma url template no formato:

"algum_endereco_base_no_sistema_de_arquivos/nossas_tiles/${z}/${x}/${y}.png"

Quando o bundle do app é gerado, o formato de endereço requerido para a URL do mapa cacheado (que corresponde à estrutura de pastas/arquivos do mapa que foram colocados na pasta assets) não fica mais disponível, e isso tem a ver com a forma que os assets são organizados dentro do app na geração do bundle.

Para obter uma URL template para os arquivos cacheados do mapa, criamos um script que recria a estrutura de arquivos necessária (a mesma que está presente na pasta assets antes da geração do bundle) no sistema de arquivos do celular.


O script que escrevemos pode ser encontrado no repositório aberto do projeto:
https://gitlab.com/LabIS-UFRJ/urbe-latam/to-no-preve/app-tonopreve/-/blob/master/assets/tiles/script.js

Primeiro escrevemos o header do arquivo com suas devidas importações:

fs.writeFile(
    "../../tiles.js",
    'import { Asset } from "expo-asset"\nimport * as FileSystem from "expo-file-system"\nexport const tilesAddresses = [',
    (err) => {
        if (err) {
            throw err
        }
    }
)

Pegamos todos os tiles, restringindo a busca para os tipos de arquivos que queremos:

const allFiles = getFiles(".").filter(
    (file) => file.includes(".png") && !file.includes("Zone")
)

Então percorremos toda lista escrevendo no arquivo com sua devida formatação:

for (let file = 0; file < allFiles.length; file++) {
    const address = allFiles[file].substring(1, allFiles[file].length)
    let targetString =
        "[() => Asset.loadAsync(require('./assets/tiles" +
        address +
        "')), `${FileSystem.cacheDirectory}tiles" +
        address +
        "`"
    if (!(file === allFiles.length - 1)) {
        targetString += "],\n"
    } else {
        targetString += "]]\n"
    }

    fs.appendFile("../../tiles.js", targetString, (err) => {
        if (err) {
            throw err
        }
    })
}

Por fim avisamos no terminal que o script já fez o trabalho!

console.log("Done! :)")

No final teremos um arquivo tiles.js no formato a seguir:

Utilização das tiles com a biblioteca react-native-maps

O script gera uma lista com a localização atual do asset dentro do bundle do app e a localização desejada após o processo. A localização final desejada é no sistema de arquivos do celular, com a estrutura de pastas/arquivos seguindo o padrão que gera a URL template que usaremos posteriormente com o react-native-maps.

Para fazer uma cópia dos arquivos de dentro do bundle para o sistema de arquivos do celular foi utilizada a função copyAsync da biblioteca expo-file-system. Essa implementação pode ser encontrado em:
https://gitlab.com/LabIS-UFRJ/urbe-latam/to-no-preve/app-tonopreve/-/blob/master/App.js#L70

Após a execução da função setTilesCache presente no link acima, o mapa cacheado já fica disponível a partir da URL template ${FileSystem.cacheDirectory}tiles/${z}/${y}/${x}.png.
A URL foi utilizada na propriedade urlTemplate do component UrlTile da biblioteca react-native-maps, como pode ser visto no seguinte arquivo do repositório:
https://gitlab.com/LabIS-UFRJ/urbe-latam/to-no-preve/app-tonopreve/-/blob/master/screens/MapaScreen.js