DEV Community

Cover image for Integrando IA com Mapas para geração de Localizações
Kevin Toshihiro Uehara for NodeBR

Posted on

Integrando IA com Mapas para geração de Localizações

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

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

Filme Eu Robô

Mais

Imagem mapa mundi

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

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

OpenAI Playground

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

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

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

Show me the code

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

yarn create vite openai-maps --template react-ts
Enter fullscreen mode Exit fullscreen mode

Entrando no projeto criado e instalando as dependências:

yarn
Enter fullscreen mode Exit fullscreen mode

E rodando nossa aplicação:

yarn dev
Enter fullscreen mode Exit fullscreen mode

Temos nossa tela inicial e projeto iniciado:

Tela inicial do Vite

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

Imagem de configuração do Tailwind

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

Imagem do arquivo env

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:

yarn add maplibre-gl react-map-gl openai
Enter fullscreen mode Exit fullscreen mode

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

Imagem arquivo App.tsx

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

Aplicação com um mapa rodando

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

Irei criar dentro do diretório src/components

LoadingSpinner

Apenas um componente Loading:

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

SideBar

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

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

Componente SideBar

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

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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

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);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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

Graças à constante:

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";
Enter fullscreen mode Exit fullscreen mode

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

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

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;
Enter fullscreen mode Exit fullscreen mode

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

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

Em resumo é isso

Image description

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

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

Top comments (3)

Collapse
 
cristuker profile image
Cristian Magalhães

Ficou muito bom cara, curti demais!

Collapse
 
ananeridev profile image
Ana Beatriz

Aeeeew demais! Primeiro artigo com estilo!

Collapse
 
nagref profile image
Fagner Lima

Show de bola Kevin! Muito bom!