Integrando IA com Mapas para geração de Localizações
Tags: javascript, typescript, braziliandevs, nodebr
Published: 2023-10-09 15:41:28

Faaala pessoas, amáveis!

Meu nome é Kevin Uehara, Staff Frontend Engineer no IFood, palestrante e criador de conteúdo.

Nesse artigo quero apresentar uma aplicação pessoal desenvolvida, utilizando mapas e realizando integração com o openAPI para geração de localizações. Imagine uma aplicação que pode roterizar os seus destinhos ou lugares mais conhecidos em determinada localização.

Trabalhar com mapas no frontend era algo que nunca tinha trabalhado em particular e achei que era algo muito mais complexo (ainda penso assim, mas muito menos). Lidar com mapas, renderização de polígonos, dados espaciais sendo todos processados ​​no client era algo que eu ficava me perguntando: como isso foi feito? Magia? Não! É tecnologia...

Talvez valha a pena falar sobre os desafios que enfrentei para criar as soluções que temos no frontend do iFood, em outro artigo. Mas aqui quero ser muito mais prático e prático na construção de um aplicativo de demonstração.

A ideia principal é apresentar o aplicativo e oferecer uma visão geral das ferramentas para construção de um aplicativo utilizando mapas. Mostrarei como integrar um mapa ao frontend e com base no que o usuário escrever em um campo textual, seja uma dúvida geográfica, o openAI irá sugerir localizações e será possível interagir com o mapa.

O objetivo é trazer algumas das ferramentas que tratam de mapas e são utilizadas no mercado. A aplicação foi construída utilizando a biblioteca Vite + React como ferramenta de front-end, Typescript, MapLibre, Tailwind e o openAI.

## Introdução

Como mencionei anteriormente (spoilers) vou usar o React com Typescript como boilerplate frontend, além de usar o Vite como ferramenta para gerenciar pacotes/dependências e criar o projeto. Por si só, o Vite (usar rollup) já valeria outro artigo falando apenas sobre ele, mas para não fugir do objetivo deste artigo, ao final disponibilizarei links para cada documentação. Então, ao invés de usar o Create React App (CRA) estarei usando o Vite, que vai nos trazer tudo que precisamos, agilizar, estruturar sua arquitetura enxuta.

Para facilitar nossa vida, também utilizarei o Tailwind para estilizar nossa aplicação, trazendo nossos estilos de forma simples e fácil de aplicar.

Também usarei a biblioteca de open source Maplibre para renderização de mapas. O React Map GL que nos fornecerá diversos componentes React focados em interações no mapa. Além disso, usarei MapTiler como estilo de mapa. MapTiler nos proporcionará um mapa mais bonito e limpo, sendo gratuito até um limite de solicitações. Por se tratar de um aplicativo de demonstração e exemplo, não nos preocuparemos com isso, mas fique atento a esse ponto (lembrando que existem estilos de mapas de código aberto do Open Street Maps, comumente conhecido como OSM, que você pode usar).

Em resumo usaremos:

**Vite** (Boilerplate)
**React + Typescript**
**Tailwind** (framework CSS)
**MapLibre** (Lib para renderizar o mapa)
**OpenAI** - Ferramenta de IA


![Suando Frio](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tyrv388qvhtypis6qcsy.gif)

Basicamente o que iremos criar é uma applicação web:


![Filme Eu Robô](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f00qe1mulrrztyohcv71.png)

**Mais**


![Imagem mapa mundi](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vgwmnao3l3cahdn0adn1.png)

Uma aplicação que utiliza IA (sem o will smith infelizmente) com mapas.

## Tecnologias

### OpenAI

O OpenAI em si é um laboratório de pesquisas relacionadas à inteligência artificial no EUA. Sendo que possui uma série de produtos e API's, sendo o mais conhecido o ChatGPT.

Iremos utilizar uma de suas API's através de um modelo de IA. Possui uma cota gratuita de 5 doláres, porém para essa demos precisei utilizar e pagar um custo um pouco maior.


![Documentação do OpenAI](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1h1r4vbcalbkkyhbhgeh.gif)

Após a criação da sua conta, também poderá utilizar o playground do openAI:

![OpenAI Playground](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/356h14g9b3c1yj5eowfo.gif)


### MapLibre

Biblioteca open source para publicar mapas em sua aplicação. A exibição otimizada é possível graças à renderização de bloco vetorial acelerado por GPU e WebGL.


![Imagem da documentação do MapLibre](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kfbf27wfb1oxpy9io5nx.png)

### MapTiler

O MapTiler é um provedor de mapas com estilos e customizável. Podemos criar uma conta gratuita, porém possui cotas nas requisições, se tornando pago para sua utilização.
O MapTiler não é obrigatório para essa aplicação, sendo outra opção é utilizar o OpenStreetMaps (OSM) que é um provedor de mapeamento Open Source.


![Mapas do MapTiler](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hz5nwm3fhcqkm3nl6ewc.png)

### React Map GL

Biblioteca de componentes integrados ao MapLibre/Mapbox para se utilizar na sua aplicação. Permite utilizar hooks próprios para se integrar aos componentes e ao mapa. Facilitando assim, nossa vida como desenvolvedores :)


![Componentes do React Map GL](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zeemfe7qpbm1ei5qj4bz.png)

## Show me the code

Vamos criar nossa aplicação utilizando o vite, com react e typescript com o comando:

```sh
yarn create vite openai-maps --template react-ts
```

Entrando no projeto criado e instalando as dependências:

```sh
yarn
```

E rodando nossa aplicação:

```sh
yarn dev
```

Temos nossa tela inicial e projeto iniciado:

![Tela inicial do Vite](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ffspozija5hr8j9elql7.png)


Logo após, vamos configurar o tailwind, seguindo a própria documentação:

![Imagem de configuração do Tailwind](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d87kzkfd7hp6uqj1ikg7.png)


O próximo passo é criar e mapear as API Key's em um arquivo .env


![Imagem do arquivo env](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lvgaz92xzn5oxsj5b5pq.png)

Lembrando que você irá precisar criar uma conta no OpenAI e no MapTiler para obters as api keys. 

Em seguida, iremos instalar as dependências necessárias:

```sh
yarn add maplibre-gl react-map-gl openai
```


Alterando nosso `App.tsx` para utilizar o Mapa do MapLibre e os estilos do MapTiler:


![Imagem arquivo App.tsx](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iluatqs57lld0wlozgxi.png)


JÁ TEMOS UMA MAPA NA NOSSA APLICAÇÃO:


![Aplicação com um mapa rodando](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j3woy1nd6revxehf77jd.png)


Agora, iremos criar 3 componentes: LoadingSpinner, NavigationLabel e Sideber:

Irei criar dentro do diretório `src/components`

### LoadingSpinner

Apenas um componente Loading:

```tsx
export const LoadingSpinner = () => {
  return (
    <div className="flex flex-col items-center">
      <div
        className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-blue-600 border-solid border-current border-r-transparent align-[-0.125em] text-info motion-reduce:animate-[spin_1.5s_linear_infinite]"
        role="status"
      ></div>
      <label className="mt-2 text-base text-blue-700">Carregando...</label>
    </div>
  );
};
```

### SideBar

Componente responsável por ser um sidebar lateral onde vai conter o campo de busca e os resultados obtidos do openAI:

```tsx
import { PropsWithChildren } from "react";

interface SideBarProps {
  isOpen: boolean;
  handleOpen: (isOpen: boolean) => void;
}

export const SideBar: React.FC<PropsWithChildren<SideBarProps>> = ({
  children,
  isOpen,
  handleOpen,
}) => {
  return (
    <div className="absolute z-10">
      <div className="bg-white p-6 relative m-[14px] rounded-sm">
        <button
          onClick={() => handleOpen(!isOpen)}
          className="bg-hamburger w-[28px] h-[28px] absolute text-4xl left-2 top-1"
          id="button_aside"
        >
          ≡
        </button>
      </div>

      {isOpen && (
        <aside
          className="grid fixed top-0 bg-[#F6F9FE] w-3/12 h-full left-0 ease-out delay-150 duration-300 rounded-r-[25px] rounded-bl-[25px]"
          id="aside"
        >
          <div className="flex justify-between mt-8">
            <h1 className="text-[#6164E8] font-bold text-[13px] text-center text-xl ml-5">
              OpenAI
            </h1>
            <i
              className="mr-8 hover:text-red-600 hover:cursor-pointer text-xl"
              onClick={() => handleOpen(false)}
            >
              X
            </i>
          </div>
          <div className="flex flex-col h-screen ml-5 mt-3">{children}</div>
        </aside>
      )}
    </div>
  );
};
```

![Componente SideBar](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z2j6vw9w6rvtwwavmo1v.gif)


### NavigationLabel

Componente responsável por pegar a localização gerada pelo openAI e após selecionado, utiliza o hook do react-map-gl para ir até o local (utilizando a função flyTo())

```tsx
import { useMap } from "react-map-gl/maplibre";
import { MessageResult } from "../../App";

interface NavigateLabelProps extends MessageResult {
  index: number;
  handleLocation: (location: number[]) => void;
}

export const NavigateLabel = ({
  location,
  name,
  index,
  handleLocation,
}: NavigateLabelProps) => {
  const { current: map } = useMap();

  const onClick = () => {
    if (location) {
      handleLocation(location);
      const [lat, lng] = location;
      map?.flyTo({ center: [lng, lat], zoom: 14 });
      return;
    }

    map?.flyTo({ center: [-46.6388, -23.5489], zoom: 14 });
  };

  return (
    <label
      onClick={onClick}
      key={index}
      className="text-blue-700 hover:cursor-pointer hover:text-blue-500"
    >
      {index + 1} - {name}
    </label>
  );
};
```

Agora vamos criar nossa integração com o OpenAI, criando um arquivo Service.

```ts
import { Configuration, OpenAIApi } from "openai";

const configuration = new Configuration({
  apiKey: import.meta.env.VITE_OPENAI_API_KEY,
});

const MESSAGE_COMPLEMENT =
  "formatado em json array com o campo name, representando o nome do lugar e a localização. E o campo location sendo um array com as coordenadas";

export class OpenAi {
  static async getLocations(message: string) {
    const OpenAi = new OpenAIApi(configuration);
    try {
      const response = await OpenAi.createCompletion({
        model: "text-davinci-003",
        prompt: `${message} ${MESSAGE_COMPLEMENT}`,
        max_tokens: 500,
        temperature: 0,
      });
      return response;
    } catch (error) {
      console.error(error);
    }
  }
}
```

E A MAGIA ESTÁ AÍ!
Eu simulo a resposta do openAI como se fosse uma chamada à uma API formatada em JSON.

Graças à constante:

```tsx
const MESSAGE_COMPLEMENT =
  "formatado em json array com o campo name, representando o nome do lugar e a localização. E o campo location sendo um array com as coordenadas";
```

Assim eu concateno o que o usuário digitou com o formato que eu espero, para assim, renderizar de forma correta os componentes.

Visualizando no playground do openAI:

![OpenAI playground com a constante](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j01bmoi925zrdpgrggvq.gif)

Agora refatorando nosso `App.tsx` por todos os componentes e chamando nosso Service.

```tsx
import Map, { Marker } from "react-map-gl/maplibre";
import { OpenAi } from "./services/openai";
import "maplibre-gl/dist/maplibre-gl.css";
import { SideBar } from "./components/Sidebar";
import { useState } from "react";
import { PinIcon } from "./components/icons";
import { LoadingSpinner } from "./components/LoadingSpinner";
import { NavigateLabel } from "./components/NavigateLabel";

export interface MessageResult {
  name?: string;
  location?: number[];
}

const initialValueLocation = {
  longitude: -47.0616,
  latitude: -22.9064,
  zoom: 10,
};

function App() {
  const [isSideBarOpen, setIsSideBarOpen] = useState(false);
  const [inputValue, setInputValue] = useState("");
  const [messageResult, setMessageResult] = useState<MessageResult[]>([]);
  const [chosenLocation, setChosenLocation] = useState<number[]>([]);
  const [isLoading, setIsLoading] = useState(false);

  const handleMessage = async () => {
    setIsLoading(true);
    const response = await OpenAi.getLocations(inputValue);
    const message = response?.data.choices.length
      ? response?.data.choices[0].text
      : "";

    try {
      if (message) {
        setMessageResult(JSON.parse(message));
      }
    } catch (error) {
      console.error("Failed on JSON Parse");
    } finally {
      setIsLoading(false);
    }
  };

  const handleCleanResults = () => {
    setInputValue("");
    setMessageResult([]);
    setChosenLocation([]);
  };

  return (
    <>
      <div className="relative top-0 left-0">
        <Map
          initialViewState={initialValueLocation}
          style={{ width: "100vw", height: "100vh" }}
          mapStyle={`https://api.maptiler.com/maps/cadastre-satellite/style.json?key=${
            import.meta.env.VITE_MAP_TILER_API_KEY
          }`}
        >
          <SideBar
            isOpen={isSideBarOpen}
            handleOpen={() => setIsSideBarOpen(!isSideBarOpen)}
          >
            <label className="text-base mt-3 mb-2" htmlFor="message">
              O que você gostaria de saber?
            </label>
            <input
              type="text"
              name="message"
              id="message"
              value={inputValue}
              onChange={(evt) => setInputValue(evt.target.value)}
              className="w-11/12 pr-2 pl-2 rounded-sm h-14 border-blue-800 text-base border-2"
            />
            <span className="text-sm text-gray-500 mt-1">
              Ex: Me dê sugestões de praias no brasil
            </span>
            <button
              className="w-11/12 h-12 mt-3 bg-blue-800 hover:bg-blue-500 text-white rounded text-base"
              onClick={handleMessage}
            >
              Buscar
            </button>
            <button
              className="w-11/12 h-12 mt-2 bg-red-700 hover:bg-red-500 text-white rounded text-base"
              onClick={handleCleanResults}
            >
              Limpar Busca
            </button>

            {isLoading && (
              <div className="mt-3 flex justify-center">
                <LoadingSpinner />
              </div>
            )}

            <div className="mt-10 text-base flex flex-col">
              {!isLoading && messageResult.length > 0 && (
                <h2 className="text-xl text-blue-700 font-bold mb-2">
                  Resultados
                </h2>
              )}

              {!isLoading &&
                messageResult.map((result, index) => (
                  <NavigateLabel
                    {...result}
                    index={index}
                    handleLocation={(loc) => setChosenLocation(loc)}
                  />
                ))}
            </div>

            <div className="mt-20">
              <label className="text-sm text-gray-500">
                Created by Kevin Uehara
              </label>
            </div>
          </SideBar>

          {chosenLocation && chosenLocation.length && (
            <Marker latitude={chosenLocation[0]} longitude={chosenLocation[1]}>
              <PinIcon />
            </Marker>
          )}
        </Map>
      </div>
    </>
  );
}

export default App;
```

Lembrando que não estou utilizando nenhum gerenciador de estado como ContextAPI, Redux, Jotai, Zustand etc... Apenas estou utilizando o próprio state e passando via prop-drilling.

FINALMENTE TEMOS NOSSA APLICAÇÃO:

![Gif da aplicação](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e1tyombxetqnfcmdnul4.gif)

E finalmente finalizamos nossa aplicação, utilizando IA para gerar localizações em mapas. Incrível, não?

Em resumo é isso

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ak4j7e5b81rwfgr575xp.gif)


Essa mesma apresentação que se encontra nesse artigo foi feita através de uma live no canal da NodeBR:

[[▶️LIVE NODEBR] - Integrando IA com Mapas para geração de localizações](https://www.youtube.com/watch?v=Ap47z1cnhfY)

Então por hoje é isso galera!
Muito obrigado e fiquem bem sempre!

NodeBR Linktree: https://linktr.ee/nodebr

Contatos:
Youtube: https://www.youtube.com/@ueharakevin/
Linkedin: https://www.linkedin.com/in/kevin-uehara
Instagram: https://www.instagram.com/uehara_kevin/
Twitter: https://twitter.com/ueharaDev
Github: https://github.com/kevinuehara
dev.to: https://dev.to/kevin-uehara
Email: uehara.kevin@gmail.com