ArcGIS Maps SDK for JavaScript ile Web Uygulamaları – 11 OpenAI API Tarafından Alınan Bilgilerin Haritada Gösterimi

Bu blog yazımızda; sorduğumuz konum bazlı soruları OpenAI API ile cevaplayarak, ArcGIS Maps SDK for JavaScript ile harita üzerinde gösterimini sağlayacağız.

Servis tarafında Node.js kullanılmıştır ama farklı dil ve yöntemler ile servislerinizi oluşturabilirsiniz.

Başlamadan Önce Sahip Olunması Gerekenler;

Servisin Hazırlanması

Node.js üzerinde servisi oluştururken Express kullanılmıştır. Ayrıca Cors ve OpenAI kütüphaneleri de import edilmiş olmalıdır.

Node.js üzerinde açacağımız servis ile sorduğumuz soruyu OpenAI API ile cevaplayarak, servis sonucu olarak döneceğiz.  Aşağıda bulunan kod parçası tam olarak bu işlemi yapmaktadır. /ask servis ucuna gönderilen parametreler OpenAI tarafından cevaplanmaktadır. /ask servisi bu örnek için GET metodu ile yapılmıştır.

Kod parçasının en önemli kısmı; GPT kısmına kendi rolünü tanımladığımız kısımda. Burada ki tanım OpenAI’nin sorularımıza cevap verme şeklini belirleyecektir. Tanımlama örnekte anlaşılırlık için Türkçe yazılmış olsa da İngilizce yazımlarda daha iyi sonuçlar dönmesi muhtemeldir. Örnekte tanımlanan rol genel olarak şöyledir;

  • Sorulan soruyla ilgili bir konum bilgisi ve 1 cümlelik bilgi metni döndür
  • Cevap dönen konum bilgisini 4326 koordinat sistemine döndür
  • Konumu ve bilgiyi belli bir JSON formatında döndür (Aşağıda örneği verilmiştir)
  • Sorulan soruyla alakalı bir konum bilgisine erişemiyorsan, boş bir nesne döndür.
const express = require("express");
const app = express();
const OpenAI = require("openai");
const cors = require("cors");

const port = 3000; // Servisin çalışacağı port

const apiKey = "XXXX"; // TODO: OpenAI API Key buraya girilmelidir.
client = new OpenAI({ apiKey: apiKey });

app.use(
  cors({
    origin: "*",
  })
);

app.get("/ask/:q", async (req, res) => {
  let prompt = req.params.q;
  response = await client.chat.completions.create({
    model: "gpt-4",
    messages: [
      {
        role: "system",
        content: `Sana verilen bilgiyle alakalı bir konum bilgisi döndüreceksin.
    En doğru konumu döndüreceksin. Bu konum bilgisini 4326 koordinatlarında ve json olarak döndüreceksin.
    Ayrıca 1 cümlelikte konumla ilgili yazı yazacaksın. Döndüreceğin JSON formatı şu şekilde olacak 
    {location:{enlem:33.00,boylam:16.00},info:'Konumla ilgili metin'}. Eğer konumu bulamayacağın bir şey 
    istenirse boş nesne döndüreceksin { } gibi.  Her zaman JSON döndüreceksin.
    Bir servis gibi çalışacaksın. En güncel bilgiyi sunacaksın.`,
      },
      { role: "user", content: prompt },
    ],
  });
  res.send(response.choices[0].message.content);
});

app.listen(port, () => {
  console.log(`App listening on port ${port}`);
});

Kod parçasının içerisinde TODO ile belirtilen yere OpenAI API Key girildikten ve başlatıldıktan sonra servisimiz kullanıma hazır hale gelmiş olacaktır. Servise örnek olarak “Türkiye’nin en yüksek yeri” diye sorduğunuzda dönen cevap aşağıdadır:

{
  "location": {
    "enlem": 39.916668,
    "boylam": 42.786667
  },
  "info": "Türkiye'nin en yüksek yeri, 5137 metre ile Ağrı Dağı'dır."
}

Haritada Sonuçların Gösterimi

Haritada sonuç gösterimi için basit bir arayüz yeterli olacaktır. Text Input ve bir Submit butonu ile servisimize istek yapıp dönen cevap içerisindeki konum bilgilerini GraphicLayer altına bir Graphic ekleyerek haritamızda göstereceğiz. Ayrıca servisten aldığımız 1 cümlelik bilgi metnini Popup ile göstereceğiz.

Aşağıda paylaşılan kod ile basit bir arayüz tasarlamış olacağız. Bu kod Türkiye’yi merkeze alan bir harita, haritanın üzerinde input ve bir button içermektedir. Aşağıdaki kodu arayüz için kullanabilirsiniz. Diğer paylaşılacak kodlar için şablon niteliği de taşımaktadır.

Diğer paylaşılacak kod parçacıkları burada yorum satırı ile belirtilen yere eklenerek devam edilmelidir.

<html lang="tr">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
    <title>Open AI - ArcGIS</title>
    <style>
        body{
            margin: 0px;
            padding: 0px;
        }
        #viewDiv {
            position: absolute;
            height: calc(100vh - 100px);
            width: 100%;
            bottom: 0px;
            left: 0px;
            right: 0px;
            z-index: 0;
        }

        .nav {
            position: absolute;
            height: 100px;
            top: 0px;
            left: 0px;
            right: 0px;
            background-color: #F1F7ED;
            display: flex;
            flex-direction: row;
            justify-content: space-between;
            z-index: 1;
            box-shadow: rgba(0, 0, 0, 0.16) 0px 10px 36px 0px, rgba(0, 0, 0, 0.06) 0px 0px 0px 1px;
        }

        #submit {
            position: fixed;
            height: 40px;
            width: 50px;
            border-width: 0px;
            margin-top: 30px;
            margin-left: -35px;
            background-color: rgb(255, 81, 0);
            color: #54494B;
            font-weight: bold;
            cursor: pointer;
        }

        #prompt {
            background-color: #54494B;
            color: #F1F7ED;
            height: 85px;
            width: 500px;
            font-size: 25px;
            border-width: 0px;
            margin: 7px;
            padding: 7px;
        }

        #head {
            font-weight: bold;
            color: #54494B;
            padding-right: 1%;
        }

        #loading {
            background-color: #00000018;
            position: absolute;
            z-index: 2;
            margin: 0px;
            padding: 0px;
            width: 100%;
            height: 100vh;

        }

        .footer {
            max-height: 200px;
            position: absolute;
            bottom: 0px;
            left: 0px;
            z-index: 1;
        }
    </style>
    <script type="module" src="https://js.arcgis.com/calcite-components/1.11.0/calcite.esm.js"></script>
    <link rel="stylesheet" type="text/css" href="https://js.arcgis.com/calcite-components/1.11.0/calcite.css" />
    <link rel="stylesheet" href="https://js.arcgis.com/4.28/esri/themes/light/main.css" />
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
    <script src="https://js.arcgis.com/4.28/"></script>

</head>

<body>
    <div id='loading' style="display: none;">
        <div style="position: relative;top: 40%;font-size: 46px;text-align: centers;">
            <calcite-loader scale="l"></calcite-loader>
        </div>

    </div>

    <div class="nav">
        <div>
            <input id='prompt' type="text" value="Dünyanın en uzun ağacı nerede?"></input>
            <button id='submit'>Bul</button>
        </div>

        <h1 id='head'>ArcGIS Maps For JavaScript SDK - Open AI API</h1>

    </div>

    <div id="viewDiv"></div>

    <div class="footer">
        <img src='https://images.crunchbase.com/image/upload/c_lpad,f_auto,q_auto:eco,dpr_1/cag3xw87zxdbeagl1b33'
            width="148px" height="148px" style='margin:5px;'></img>
        <img src='https://cdn.iconscout.com/icon/free/png-256/free-openai-7601069-6138535.png?f=webp' width="152px"
            height="152px" style='margin:5px;'></img>
    </div>

</body>
<script>
    require(["esri/Map", "esri/views/MapView", "esri/Graphic", "esri/layers/GraphicsLayer"],
(Map, MapView, Graphic, GraphicsLayer) => {

        const map = new Map({
            basemap: "topo-vector"
        });

        const view = new MapView({
            container: "viewDiv",
            map: map,
            zoom: 3,
            center: [34.4387, 38.9637]
        });

        // [!] Kodlar buraya yazılmalıdır.

    });
 
</script>

</html>

Kod parçasıyla ile ekranımızın genel görünüşünü oluşturduk. Şimdi bunları çalışır hale getirelim.

Öncelikle alacağımız konum bilgilerini gösterebilmemiz için bir katman (GraphicsLayer) oluşturalım ve bu katmanı haritamıza ekleyelim.

const layer = new GraphicsLayer()

map.add(layer)

Daha sonra aşağıdaki kod parçası ile HTML elementlerimizin tanımlamasını yapalım.

const _loading = document.getElementById('loading') // İstek sırasında loading ekranı
const _prompt  = document.getElementById('prompt')  // Kullanıcın bilgi gireceği input
const _submit  = document.getElementById('submit')   // Inputun yanında yer alan Bul butonu

Servise istek yapıldıktan sonra kullanıcının beklemesini sağlamak için bir yükleme ekranına ihtiyacımız olacak. Bunu arayüz kodunda paylaşılan ve HTML tekil kimliği “loading” olarak belirlenen bir div’in görünürlüğünü açıp kapatarak gerçekleştireceğiz. Bu işlemi basit tutmak için aşağıdaki gibi bir fonksiyon tanımlaması yapıyoruz. Bu fonksiyona her true parametresini gönderdiğimizde ekranda yükleme ekranı görünür hale getirilecektir. Kapatmak için ise yine aynı fonksiyona false değerini göndermemiz yeterli olacaktır.

const setLoadingVisible = (bool) => {
    if(bool){
        _loading.style.display = 'block'
    } else {
        _loading.style.display = 'none'
    }
}

Ekranımızdaki HTML input tipindeki metin kutusuna metin girildiğinde ve “Bul” butonuna tıklandığında sonucu döndürmek için ‘onclick’ fonksiyonunun tanımlamasını yazıyoruz. Bu fonksiyon ile düğmemize tıklandığında, “http://localhost:3000/ask” adresindeki servisimize istek yapıp cevabını alıyoruz. Aldığımız cevabın içerisinde konum bilgisi dönmediyse (OpenAI’ya konum bulamadıysa boş nesne göndermesini söylemiştik) “Sonuç bulunamadı.” mesaj kutusu ile kullanıcıyı bilgilendirip fonksiyonu sonlandırıyoruz. Eğer sonuç döndüyse gelen konum ve metin bilgisi ile bir Graphic oluşturuyoruz ve GraphicsLayer‘a ekliyoruz. Bu şekilde servisten dönen konum bilgisi haritada görünebilir oluyor. Daha sonra harita nesnemize, noktayı merkeze aldırıp yakınlaşmasını sağlıyoruz. Aynı zamanda eklediğimiz noktanın Popup bilgisini de tıklanmayı beklemeden açıyoruz. Bu yöntem ile kullanıcı sadece arama yaparak hem noktayı hemde onunla ilgili metin yazısını tıklama ihtiyacı olmadan ekranında görebilmiş oluyor.

_submit.onclick = () => {
    view.popup.close() // Ekranda kalan popup varsa kapat
    
    let promptTxt = _prompt.value // Kullanıcının yazdığı metin bilgisini al
    
    setLoadingVisible(true) // Yükleme ekranını görünür hale getir.

    axios.get(`http://localhost:3000/ask/${promptTxt}`) // Servise isteğimizi gönderiyoruz.
    .then((res)=>{
        if(res.data.location == undefined){ // Servisten konum bilgisi dönmeme durumunu kontrol ediyoruz.
            alert('Sonuç bulunamadı.')
            return // Sonuç dönmediyse alert yazdırıp işlemi bitiriyoruz.
        }

        let pointGraphic = new Graphic({ // Aldığımız konum ve metin bilgisi ile Graphic oluşturuyoruz.
            geometry:  { // Konum bilgisi
                type: "point",
                longitude: res.data.location.boylam,            
                latitude: res.data.location.enlem,               
            },
            popupTemplate: { // Metin bilgisini gösterecek şekilde popup düzenlemesi yapıyoruz.
                title: "Bilgi",                     
                content: 'Bilgi : '+res.data.info    
            }
        });

        view.goTo({ // Bilgisini göstereceğimiz yeri haritada merkeze alıp yakınlaştırıyoruz.
            center: [res.data.location.boylam, res.data.location.enlem],
            zoom: 4
        });

        view.popup.open({ // Popup'ı tıklanmasını beklemeden açıyoruz.
            features: [pointGraphic],
            location: pointGraphic.geometry
        });

        layer.add(pointGraphic) // Katmanımıza bu nokta bilgisini ekleyip haritada görünür hale getiriyoruz.
        
    })
    .catch((err)=>{
        console.log(err)
    })
    .finally(()=>{ // Tüm süreç sonlandıktan sonra yükleme ekranımızı kapatıyoruz.
        setLoadingVisible(false)
    })
}

Bu işlemlerden sonra artık kullanıcı bir soru sorduğu zaman OpenAI API ile cevaplayıp, konumunu haritada gösterebilmiş oluyoruz. Son olarak aşağıdaki kod bloğunu da ekleyerek kullanıcı bir cümle yazarken uzunluğuna bağlı olarak yazı boyutumuzu küçülterek metnin sığmasının sağlayacak tasarımsal bir düzenleme daha yapmış oluyoruz.

_prompt.onkeydown = (e) => {
    if((e.target.value.length) >= 10 ){
        _prompt.style.fontSize = '28px'
    } else {
        _prompt.style.fontSize = (38-(e.target.value.length))+'px'
    }
}

Bu eklediğimiz kod ile beraber düzenlemelerimiz bitmiş oluyor. Tüm işlemlerden sonra örneğini gösterdiğimiz HTML sayfanın kodu aşağıdaki gibi görünmelidir.

<html lang="tr">
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
        <title>Open AI - ArcGIS</title>
        <style>
        body{
            margin: 0px;
            padding: 0px;
        }
        #viewDiv {
            position: absolute;
            height: calc(100vh - 100px);
            width: 100%;
            bottom: 0px;
            left: 0px;
            right: 0px;
            z-index: 0;
        }

        .nav{
            position: absolute;
            height: 100px;
            top: 0px;
            left: 0px;
            right: 0px;
            background-color:#F1F7ED;
            display: flex;
            flex-direction: row;
            justify-content: space-between;
            z-index: 1;
            box-shadow: rgba(0, 0, 0, 0.16) 0px 10px 36px 0px, rgba(0, 0, 0, 0.06) 0px 0px 0px 1px;
        }

        #submit{
            position: fixed;
            height: 40px;
            width: 50px;
            border-width: 0px;
            margin-top: 30px;
            margin-left: -35px;
            background-color: rgb(255, 81, 0);
            color: #54494B;
            font-weight: bold;
            cursor: pointer;
        }

        #prompt{
            background-color: #54494B;
            color: #F1F7ED;
            height: 85px;
            width: 500px;
            font-size: 25px;
            border-width: 0px;
            margin: 7px;
            padding: 7px;
        }

        #head{
            font-weight: bold;
            color: #54494B;
            padding-right: 1%;
        }

        #loading{
            background-color: #00000018;
            position: absolute;
            z-index: 2;
            margin: 0px;
            padding: 0px;
            width: 100%;
            height: 100vh;
            
        }

        .footer{
            max-height: 200px;
            position: absolute;
            bottom: 0px;
            left: 0px;
            z-index: 1;
        }

        </style>
        <script type="module" src="https://js.arcgis.com/calcite-components/1.11.0/calcite.esm.js"></script>
        <link   rel="stylesheet" type="text/css" href="https://js.arcgis.com/calcite-components/1.11.0/calcite.css" />
        <link   rel="stylesheet" href="https://js.arcgis.com/4.28/esri/themes/light/main.css" />
        <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
        <script src="https://js.arcgis.com/4.28/"></script>

    </head>

    <body>
        <div id='loading' style="display: none;" >
            <div style="position: relative;top: 40%;font-size: 46px;text-align: centers;" >
                <calcite-loader scale="l"></calcite-loader>
            </div>

        </div>

        <div class="nav">
            <div>
                <input  id='prompt' type="text" value="Dünyanın en uzun ağacı nerede?" ></input>
                <button id='submit' >Bul</button>
            </div>

            <h1 id='head' >ArcGIS Maps For JavaScript SDK - OpenAI API</h1>

        </div>

        <div id="viewDiv"></div>
        
        <div class="footer">
            <img src='https://images.crunchbase.com/image/upload/c_lpad,f_auto,q_auto:eco,dpr_1/cag3xw87zxdbeagl1b33' width="148px" height="148px" style='margin:5px;'></img>
            <img src='https://cdn.iconscout.com/icon/free/png-256/free-openai-7601069-6138535.png?f=webp'               width="152px" height="152px" style='margin:5px;' ></img>
        </div>

    </body>
    <script>
        require(["esri/Map", "esri/views/MapView","esri/Graphic","esri/layers/GraphicsLayer"], (Map, MapView,Graphic,GraphicsLayer) => {

            const map = new Map({
                basemap: "topo-vector"
            });

            const view = new MapView({
                container: "viewDiv",
                map: map,
                zoom: 3,
                center: [34.4387, 38.9637] // longitude, latitude
            });

            const layer = new GraphicsLayer()

            map.add(layer)

            const _loading = document.getElementById('loading') // İstek sırasında loading ekranı
            const _prompt  = document.getElementById('prompt')  // Kullanıcın bilgi gireceği input
            const _submit  = document.getElementById('submit')   // Inputun yanında yer alan Bul butonu

            const setLoadingVisible = (bool) => {
                if(bool){
                    _loading.style.display = 'block'
                } else {
                    _loading.style.display = 'none'
                }
            }

            _prompt.onkeydown = (e) => {
                if((e.target.value.length) >= 10 ){
                    _prompt.style.fontSize = '28px'
                } else {
                    _prompt.style.fontSize = (38-(e.target.value.length))+'px'
                }
            }

            _submit.onclick = () => {
                view.popup.close() // Ekranda kalan popup varsa kapat
                
                let promptTxt = _prompt.value // Kullanıcının yazdığı metin bilgisini al
                
                setLoadingVisible(true) // Yükleme ekranını görünür hale getir.

                axios.get(`http://localhost:3000/ask/${promptTxt}`) // Servise isteğimizi gönderiyoruz.
                .then((res)=>{
                    if(res.data.location == undefined){ // Servisten konum bilgisi dönmeme durumunu kontrol ediyoruz.
                        alert('Sonuç bulunamadı.')
                        return // Sonuç dönmediyse alert yazdırıp işlemi bitiriyoruz.
                    }

                    let pointGraphic = new Graphic({ // Aldığımız konum ve metin bilgisi ile Graphic oluşturuyoruz.
                        geometry:  { // Konum bilgisi
                            type: "point",
                            longitude: res.data.location.boylam,            
                            latitude: res.data.location.enlem,               
                        },
                        popupTemplate: { // Metin bilgisini gösterecek şekilde popup düzenlemesi yapıyoruz.
                            title: "Bilgi",                     
                            content: 'Bilgi : '+res.data.info    
                        }
                    });

                    view.goTo({ // Bilgisini göstereceğimiz yeri haritada merkeze alıp yakınlaştırıyoruz.
                        center: [res.data.location.boylam, res.data.location.enlem],
                        zoom: 4
                    });

                    view.popup.open({ // Popup'u tıklanmayı beklenmeden açıyoruz.
                        features: [pointGraphic],
                        location: pointGraphic.geometry
                    });

                    layer.add(pointGraphic) // Katmanımıza bu bilgiyi ekleyip haritada görünür hale getiriyoruz.
                    
                })
                .catch((err)=>{
                    console.log(err)
                })
                .finally(()=>{ // Tüm süreç sonlandıktan sonra yükleme ekranımızı kapatıyoruz.
                    setLoadingVisible(false)
                })
            }

        });

        </script>
    </html>

İleri düzey ArcGIS Developer eğitimleri, atölye çalışmaları ve yol gösterme hizmetleri için Esri Türkiye Profesyonel Hizmetler birimi ile irtibata geçiniz.

Esri Türkiye 2023

Önceki Yazı
ArcGIS Business Analyst ile Suitability Analizi Skor Hesaplamaları
Yazıyı görüntüle
Sonraki Yazı
ArcGIS Business Analyst ile Void Analizi
Yazıyı görüntüle