Connect AI to mapping libraries. Teach large language models to generate markers, polygons, and spatial analysis on demand.
Yesterday you built an AI chat interface: the user types a question, the LLM generates text. Today we take the next step: making the AI think spatially.
Imagine typing this into your app:
"Show me deforestation hotspots in the Amazon basin
from the last 6 months, and highlight any areas
where NDVI dropped below 0.2"
And the AI responds by:
Planet Labs has pioneered what they call Agentic Geospatial AI: an AI system that can autonomously search, retrieve, analyze, and visualize satellite imagery in response to natural language queries.
Our approach today is simpler but follows the same core architecture: an LLM that understands geospatial concepts and can call functions to interact with a map. We are building the minimum viable version of this pattern.
The entire pipeline flows through four stages. Your browser orchestrates each handoff.
functionCall object (with name + arguments)functionResponseFunction calling (also called "tool use") is the mechanism that transforms an LLM from a text generator into an agent that can take actions. Instead of outputting text, the model outputs a structured request to invoke a specific function.1 This paradigm has been extended to geospatial contexts, where LLMs generate GeoJSON and invoke mapping functions via structured tool calls.8
functionCall object instead of plain textfunctionResponseaddMapMarker({lat: 48.856, lng: 2.352, label: "Paris"})When you call the Gemini API, you include a tools array that describes each function the AI is allowed to invoke. Think of it as handing the AI a toolbox and a manual for each tool.
// Define tools that Gemini can call
const tools = [{
functionDeclarations: [{
name: 'addMapMarker',
description: 'Add a marker to the map at specified coordinates',
parameters: {
type: 'OBJECT',
properties: {
latitude: { type: 'NUMBER', description: 'Latitude in decimal degrees' },
longitude: { type: 'NUMBER', description: 'Longitude in decimal degrees' },
label: { type: 'STRING', description: 'Marker popup label' }
},
required: ['latitude', 'longitude', 'label']
}
}]
}];
| Field | Purpose | Tips |
|---|---|---|
name | Unique identifier for the function | Use camelCase, be descriptive |
description | Tells the AI when to use this tool | Be specific! This is the AI's "manual" |
parameters | JSON Schema defining input | Describe each param clearly |
required | Which parameters are mandatory | Mark only truly required fields |
description field is critical. The AI reads it to decide when to call this function. A vague description leads to incorrect or missed function calls. Write it as if you were explaining the tool to a new colleague.You need to define both the function declarations (for Gemini) and the JavaScript implementations (for Leaflet). Here are the essential functions for a geospatial AI assistant:
Here is the full cycle: declaring tools, sending a request, and dispatching the function call to Leaflet.
// 1. Define the tool declarations
const tools = [{
functionDeclarations: [
{
name: 'addMapMarker',
description: 'Add a labeled marker to the Leaflet map',
parameters: {
type: 'OBJECT',
properties: {
latitude: { type: 'NUMBER', description: 'Latitude (WGS84)' },
longitude: { type: 'NUMBER', description: 'Longitude (WGS84)' },
label: { type: 'STRING', description: 'Popup text' }
},
required: ['latitude', 'longitude', 'label']
}
}
]
}];
// 2. Send user query to Gemini with tools
const response = await model.generateContent({
contents: [{ role: 'user', parts: [{ text: userQuery }] }],
tools: tools
});
// 3. Check if the response contains a function call
const part = response.response.candidates[0].content.parts[0];
if (part.functionCall) {
const { name, args } = part.functionCall;
// 4. Dispatch to Leaflet
if (name === 'addMapMarker') {
L.marker([args.latitude, args.longitude])
.addTo(map)
.bindPopup(args.label)
.openPopup();
}
}
functionResponse so it can generate a human-readable summary (e.g., "I've placed a marker on Paris for you").functionCall object (name + arguments) and your code is responsible for actually running the function. This ensures you maintain full control over what happens.
For an AI to be useful in Earth observation, it needs domain knowledge about how satellite data works. The most fundamental concept is the spectral index, especially NDVI.5 Rolf et al. (2024) argue that satellite data constitutes a distinct modality requiring specialized ML approaches.2
| NDVI Range | Interpretation | Visual |
|---|---|---|
< 0 | Water bodies, snow, clouds | 🔵 |
0 - 0.1 | Bare soil, rock, sand | 🟤 |
0.1 - 0.3 | Sparse vegetation, scrubland | 🟡 |
0.3 - 0.6 | Moderate vegetation, grassland | 🟢 |
> 0.6 | Dense, healthy vegetation | 🌲 |
| Band | Name | Wavelength |
|---|---|---|
B2 | Blue | 490 nm |
B3 | Green | 560 nm |
B4 | Red | 665 nm |
B8 | NIR | 842 nm |
B11 | SWIR | 1610 nm |
A system prompt is a special instruction block sent to the AI at the start of every conversation. It defines the AI's persona, knowledge, and behavioral rules. For geospatial AI, this is where you inject EO expertise.
const systemPrompt = `You are an Earth Observation AI assistant integrated
with a Leaflet web map. You understand:
SPECTRAL INDICES:
- NDVI = (NIR - Red) / (NIR + Red)
- Values: < 0 = water/snow, 0-0.1 = bare soil, 0.1-0.3 = sparse vegetation,
0.3-0.6 = moderate vegetation, > 0.6 = dense healthy vegetation
- NDWI = (Green - NIR) / (Green + NIR) for water bodies
SENTINEL-2 BANDS:
- B2=Blue(490nm), B3=Green(560nm), B4=Red(665nm)
- B8=NIR(842nm), B11=SWIR(1610nm), B12=SWIR(2190nm)
COORDINATE SYSTEM:
- Always use WGS84 (EPSG:4326) for latitude/longitude
- Latitude range: -90 to 90
- Longitude range: -180 to 180
RULES:
- When asked about a location, use function calling to display it
- Always validate coordinates before calling map functions
- If a location is ambiguous, ask the user to clarify
- Provide numerical context alongside visualizations`;
// Initialize the model with the system prompt
const model = genAI.getGenerativeModel({
model: 'gemini-2.0-flash',
systemInstruction: systemPrompt,
tools: tools
});
Gemini is a multimodal model, meaning it can process text, images, audio, and video in a single request. For Earth observation, this means you can send a satellite image to Gemini and ask it to interpret what it sees. Recent work has explored foundation models for geospatial reasoning, assessing LLM capabilities in understanding geometries and spatial relations.6
// Send a satellite image + question to Gemini
const imagePart = {
inlineData: {
mimeType: 'image/png',
data: base64EncodedImage // base64 string of the tile
}
};
const result = await model.generateContent([
imagePart,
{ text: 'Analyze this Sentinel-2 image. Identify land cover types and estimate NDVI ranges for each visible zone.' }
]);
GeoJSON is an open standard format (IETF RFC 7946) for encoding geographic features using JSON.4 It is natively supported by Leaflet, Mapbox, Google Maps, and virtually every web mapping library. It is also the format we will teach our AI to generate.
[longitude, latitude] (note the order!){
"type": "Feature",
"properties": {
"name": "ISU Strasbourg",
"type": "university"
},
"geometry": {
"type": "Point",
"coordinates": [
7.7676,
48.5734
]
}
}
[longitude, latitude] order, but Leaflet's L.latLng() uses (latitude, longitude). This mismatch is the #1 source of "my data is in the ocean" bugs. Fortunately, L.geoJSON() handles the conversion automatically.One of the most powerful applications of function calling is having the AI generate valid GeoJSON from natural language descriptions. The user describes a geographic feature in plain English, and the AI produces the structured data.
For more complex shapes, you can define a addGeoJSON function that accepts raw GeoJSON:
{
name: 'addGeoJSON',
description: 'Add a GeoJSON FeatureCollection to the map. Use for complex shapes like country borders, river paths, or multi-polygon regions.',
parameters: {
type: 'OBJECT',
properties: {
geojson: { type: 'STRING', description: 'Valid GeoJSON string' },
style: { type: 'OBJECT', description: 'Leaflet path style options',
properties: {
color: { type: 'STRING' },
fillColor: { type: 'STRING' },
fillOpacity: { type: 'NUMBER' }
}
}
},
required: ['geojson']
}
}
// Dispatcher: handle all function calls from Gemini
function executeFunctionCall(functionCall) {
const { name, args } = functionCall;
switch (name) {
case 'addMapMarker':
return L.marker([args.latitude, args.longitude])
.addTo(map).bindPopup(args.label).openPopup();
case 'drawCircle':
return L.circle([args.latitude, args.longitude], {
radius: args.radius,
color: args.color || '#10b981',
fillOpacity: 0.15
}).addTo(map);
case 'addGeoJSON':
try {
const geojsonData = JSON.parse(args.geojson);
return L.geoJSON(geojsonData, {
style: args.style || { color: '#10b981' }
}).addTo(map);
} catch (e) {
return { error: 'Invalid GeoJSON: ' + e.message };
}
case 'setMapView':
map.setView([args.latitude, args.longitude], args.zoom || 10);
return { success: true };
case 'clearMap':
map.eachLayer(layer => {
if (layer !== tileLayer) map.removeLayer(layer);
});
return { success: true };
default:
return { error: `Unknown function: ${name}` };
}
}
try/catch. AI-generated JSON can contain syntax errors. Return meaningful error messages so Gemini can self-correct and try again.AI-generated spatial data can contain errors. You need a robust validation layer between Gemini's output and your map.
| Error | Example | Fix |
|---|---|---|
| Lat/Lng swapped | [48.85, 2.35] as [2.35, 48.85] |
Check if lng is in lat range |
| Out-of-range coords | latitude: 200 |
Clamp to [-90, 90] / [-180, 180] |
| Unclosed polygon | First point ≠ last point | Append first point to close ring |
| Invalid JSON | Trailing comma, missing bracket | Parse in try/catch, report to AI |
| Wrong winding order | Clockwise instead of counter-clockwise | Use turf.js rewind() |
function validateCoordinates(lat, lng) {
if (lat < -90 || lat > 90) return { valid: false, error: 'Latitude must be between -90 and 90' };
if (lng < -180 || lng > 180) return { valid: false, error: 'Longitude must be between -180 and 180' };
return { valid: true };
}
functionResponse to Gemini. The model can read the error, understand what went wrong, and retry with corrected parameters. This is the foundation of agentic behavior.An agentic AI goes beyond single-turn question answering. It can plan, execute, observe results, and adjust its approach over multiple steps. The foundational framework is the ReAct pattern (Reasoning + Acting).3 This paradigm is now being applied to Earth observation, with emerging work on LLM agents that autonomously query and analyze satellite data.7
The cycle repeats until the AI determines the task is complete. This is fundamentally different from a single prompt-response interaction.
Consider the user prompt: "Compare vegetation density in Strasbourg's urban center vs. the surrounding countryside."
An agentic AI would decompose this into multiple steps:
setMapView(48.573, 7.752, 12) → Map pans to StrasbourgdrawCircle(48.573, 7.752, 3000, "red") → Urban zone markeddrawCircle(48.60, 7.85, 5000, "green") → Rural zone markedfunctionResponse results back and allowing the model to make additional function calls in subsequent turns, all within the same chat session.Oceanographic Cartographer
She created the first scientific map of the Atlantic Ocean floor, proving the theory of continental drift.
Earth Observation provides a macroscopic view of environmental trends, but its true power lies in downscaling this data to affect local policy and design, such as urban planning and sustainable workplaces.
Your startup needs to establish a new hybrid work hub. You must balance employee commute times, environmental impact (using the IPAT equation), and existing green infrastructure.
Interact with the live map below to explore the urban growth of Austin, Texas.
What was your biggest takeaway from this session, and how does it apply to the TERRA project? Write your response below. Your instructor will review this to track your progress.