Leveraging AI in your web apps to transform geospatial data
Last month, at the 2025 Esri Developer and Technology Summit I presented a speedgeeking session where I demoed how a simple text query can turn into dynamic map highlights using the Google Gemini natural language API and the ArcGIS Maps SDK for JavaScript. So, let’s explore exactly how I built that GIS web app — from prompt to map — in just a few steps and with minimal code.
Why Gemini + GIS?
I love finding fun ways to put together what I’m learning with what I already know. In this case, I wanted to let users type any question (“List the top 3 hottest countries,” “Show me countries with population > 100 million,” etc.) and instantly highlight the results on a world map. Gemini handles the natural-language parsing, while ArcGIS handles the spatial query and rendering.
1. Project Setup
1.1 Create your files
In VS Code, create a new folder and inside it create the following files:
- index.html
- scripts.js
- styles.css
1.2 HTML skeleton
Use the VSCode shortcut (! + Enter) to scaffold your HTML. Then, link your CSS and JS local files:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Gemini + ArcGIS Demo</title>
<link rel="stylesheet" href="styles.css" />
<script src="scripts.js" defer></script>
</head>
<body>
</body>
</html>
1.3 Include ArcGIS JS SDK & Calcite
Add these inside <head>
before your styles.css and scripts.js:
<!-- ArcGIS CSS & JS -->
<link rel="stylesheet" href="https://js.arcgis.com/4.32/esri/themes/light/main.css" />
<script src="https://js.arcgis.com/4.32/"></script>
<!-- Calcite Components (UI) -->
<link rel="stylesheet" href="https://js.arcgis.com/calcite-components/3.0.3/calcite.css" />
<script type="module" src="https://js.arcgis.com/calcite-components/3.0.3/calcite.esm.js"></script>
1.4 ArcGIS API key
Still in <head>
, set your key:
<script>
var esriConfig = { apiKey: "YOUR_ARCGIS_KEY" };
</script>
2. Build the UI
2.1 Layout the map and controls
In the <body>
, create two columns—map on left, controls on right:
<div class="main-container">
<div class="left-column">
<arcgis-map id="map" basemap="arcgis/community" center="20,0" zoom="1">
<arcgis-zoom position="top-left"></arcgis-zoom>
</arcgis-map>
</div>
<div class="right-column">
<calcite-input id="userInput" placeholder="e.g. List the top 3 hottest countries"></calcite-input>
<calcite-button id="submitQuery">Submit</calcite-button>
<ul id="countryList"></ul>
<div id="explanation"></div>
</div>
</div>
2.2 CSS for layout
Add minimal CSS in styles.css:
html, body, arcgis-map {
padding:0;
margin:0;
height:100%;
width:100%;
}
.main-container {
display:flex;
height:100%;
}
.left-column {
flex:1;
}
.right-column {
position:absolute;
top:10px;
right:10px;
background:rgba(255,255,255,0.9);
padding:1rem;
border-radius:4px;
max-width:250px;
}
#countryList {
margin:0.5rem 0;
padding-left:1rem;
}
3. Gemini Query Logic
Open scripts.js. First, store your Gemini key and feature layer URL:
const googleGeminiApiKey = "YOUR_GEMINI_KEY";
const featureLayerURL = "https://services.arcgis.com/P3ePLMYs2RVChkJx/arcgis/rest/services/World_Countries_(Generalized)/FeatureServer/0";
💡 Pro Tip: Put your keys in a separate config.js that’s git-ignored, then import them. That way you never accidentally commit secrets.
3.1 The fetchCountriesFromQuery function
This function:
- Sends
query (q)
+ instructions to Gemini - Splits the returned text by lines
- Strips numbering, extracts country names & explanation
async function fetchCountriesFromQuery(q) {
const res = await fetch(
`https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=${googleGeminiApiKey}`, {
method:'POST',
headers:{ 'Content-Type':'application/json' },
body: JSON.stringify({
contents:[{ parts:[{
text: q +
" only list the country names in a numbered list, then on a new line start with 'Explanation:' and provide a one-sentence explanation."
}]}]
})
}
);
const data = await res.json();
const lines = data.candidates[0].content.parts[0].text.split('\n');
const countries = [], explanationLines = [];
for (let line of lines) {
const cleaned = line.replace(/^\d+\.?\s*/,'').trim();
if (/^Explanation:/i.test(cleaned)) explanationLines.push(cleaned.replace(/Explanation:\s*/i,''));
else if (/^[A-Za-z\s]+$/.test(cleaned) && cleaned) countries.push(cleaned);
}
return { countries, explanation: explanationLines[0] || "" };
}
4. Highlighting on the Map
4.1 Initialize map reference
Still in scripts.js, after page load:
const mapEl = document.getElementById('map');
let currentHighlightLayer = null;
4.2 The highlightCountries function
This function:
- Removes any existing highlight layer
- Builds a SQL WHERE clause from the country list
- Queries the FeatureLayer
- Creates a GraphicsLayer with red fills
async function highlightCountries(countries) {
if (currentHighlightLayer) mapEl.map.remove(currentHighlightLayer);
const where = `COUNTRY IN (${countries.map(c=>`'${c.replace(/'/g,"''")}'`).join(',')})`;
const [FeatureLayer, Graphic, GraphicsLayer] = await Promise.all([
$arcgis.import('esri/layers/FeatureLayer'),
$arcgis.import('esri/Graphic'),
$arcgis.import('esri/layers/GraphicsLayer')
]);
const fl = new FeatureLayer({ url: featureLayerURL, token: esriConfig.apiKey });
const result = await fl.queryFeatures({ where, outFields:['*'], returnGeometry:true });
const hlLayer = new GraphicsLayer();
result.features.forEach(f => {
hlLayer.add(new Graphic({
geometry: f.geometry,
symbol: { type:'simple-fill', color:[255,0,0,0.5], outline:{color:'red',width:2} }
}));
});
currentHighlightLayer = hlLayer;
mapEl.map.add(hlLayer);
}
5. Wire Up User Interaction
Attach an event listener to the Submit button:
const listEl = document.getElementById('countryList');
const explEl = document.getElementById('explanation');
document.getElementById('submitQuery').addEventListener('click', async () => {
const q = document.getElementById('userInput').value.trim();
if (!q) return;
listEl.innerHTML = explEl.textContent = "";
const { countries, explanation } = await fetchCountriesFromQuery(q);
// display list & explanation
listEl.innerHTML = countries.map(c=>`<li>${c}</li>`).join('');
explEl.textContent = explanation;
// highlight on map
await highlightCountries(countries);
});
6. Test & Iterate
- Open index.html in your browser.
- Type a query like “Top 3 most populous countries”.
- Click Submit.
- Watch the list populate and the map highlight those countries.
💡 Pro Tip: Use your browser console to inspect countries and explanation arrays—this helps debug parsing logic.
Feel free to tweak the prompt instructions, styling, or symbol colors. You’ve now built a fully interactive GIS web app driven by natural language!
Conclusion
In just about 100 lines of code, you’ve combined Gemini’s NLP power with ArcGIS’s mapping capabilities to transform user text into geospatial insights. This pattern — natural language → structured data → spatial visualization — unlocks countless possibilities for dynamic, user-driven maps.
This article was written by Courtney Yatteau, a Developer Advocate at Esri. The opinions expressed in this article are solely Courtney’s and do not necessarily represent the views, strategies, or opinions of her employer. If you have any feedback, please feel free to like and/or comment. Additionally, if you have any questions or comments that you’d prefer to send privately, you can contact Courtney through LinkedIn, X, or Bluesky. If you’re considering a career switch, looking to get into tech, or curious about what it’s like to work at Esri, then please stay tuned for future posts! You can also check out Esri’s careers page or this video for more information.
Top comments (2)
Thanks for sharing this thread! AI-driven mapping data insights from Gemini can be a game-changer, especially in understanding patterns and improving decision-making. If anyone needs help with interpreting the data or integrating it into workflows, feel free to reach out
Good pro tip in section 3.0!