Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
cfc9f28
feat: Implement sub-agent architecture for resolution search
google-labs-jules[bot] Jan 13, 2026
9789faa
fix: Add satellite_intelligence to AIMessage type
google-labs-jules[bot] Jan 13, 2026
3616a64
fix: Correctly handle streamObject return value in router agent
google-labs-jules[bot] Jan 13, 2026
fb91c02
fix: Implement correct routing pattern in router agent
google-labs-jules[bot] Jan 13, 2026
1113bd6
fix: Correct arguments for tool execute function
google-labs-jules[bot] Jan 13, 2026
5d1c28a
fix: Decouple satellite tools from ai/tool helper
google-labs-jules[bot] Jan 13, 2026
4dde4f4
feat: Implement AlphaEarth embeddings API endpoint
google-labs-jules[bot] Jan 14, 2026
bda8319
fix: Correct type conversion in embeddings API
google-labs-jules[bot] Jan 14, 2026
3bffcde
fix: Handle all raster data types in embeddings API
google-labs-jules[bot] Jan 14, 2026
da684cb
Fix build failure and add parallel satellite intelligence and async s…
CJWTRUST Jan 15, 2026
a3e462e
Fix type error in geospatial tool and verify build with bun run build
CJWTRUST Jan 16, 2026
0a33732
Merge branch 'main' into feat/sub-agent-architecture-6779584567337140239
ngoiyaeric Jan 16, 2026
cbf7449
feat: Replace custom UI with mapbox-gl-compare view
google-labs-jules[bot] Jan 16, 2026
3fa35c7
feat: Return GeoTIFF data from embeddings API and update map view
google-labs-jules[bot] Jan 16, 2026
93e0d8e
feat: Implement full data flow for GeoTIFF processing
google-labs-jules[bot] Jan 16, 2026
128d2bf
fix: Correctly handle Buffer in NextResponse
google-labs-jules[bot] Jan 16, 2026
874b83c
fix: Add TypeScript declaration for mapbox-gl-compare
google-labs-jules[bot] Jan 16, 2026
2d3d080
fix: Handle all raster data types in MapCompareView
google-labs-jules[bot] Jan 16, 2026
42036bc
fix: Gracefully handle missing GCP credentials in embeddings API
google-labs-jules[bot] Jan 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/workflows/cloudrun.yml

This file was deleted.

2 changes: 0 additions & 2 deletions .github/workflows/gcp-auth.yaml

This file was deleted.

16 changes: 4 additions & 12 deletions app/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { BotMessage } from '@/components/message'
import { SearchSection } from '@/components/search-section'
import SearchRelated from '@/components/search-related'
import { GeoJsonLayer } from '@/components/map/geojson-layer'
import MapCompareView from '@/components/map/map-compare-view'
import { CopilotDisplay } from '@/components/copilot-display'
import RetrieveSection from '@/components/retrieve-section'
import { VideoSearchSection } from '@/components/video-search-section'
Expand Down Expand Up @@ -94,17 +95,7 @@ async function submit(formData?: FormData, skip?: boolean) {

messages.push({ role: 'assistant', content: analysisResult.summary || 'Analysis complete.' });

const sanitizedMessages: CoreMessage[] = messages.map(m => {
if (Array.isArray(m.content)) {
return {
...m,
content: m.content.filter(part => part.type !== 'image')
} as CoreMessage
}
return m
})

const relatedQueries = await querySuggestor(uiStream, sanitizedMessages);
const relatedQueries = await querySuggestor(uiStream, messages);
uiStream.append(
<Section title="Follow-up">
<FollowupPanel />
Expand Down Expand Up @@ -668,13 +659,14 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => {
case 'resolution_search_result': {
const analysisResult = JSON.parse(content as string);
const geoJson = analysisResult.geoJson as FeatureCollection;
const { lat, lon, year } = analysisResult;

return {
id,
component: (
<>
{geoJson && (
<GeoJsonLayer id={id} data={geoJson} />
<MapCompareView lat={lat} lon={lon} year={year} />
)}
</>
)
Expand Down
303 changes: 58 additions & 245 deletions app/api/embeddings/route.ts
Original file line number Diff line number Diff line change
@@ -1,276 +1,89 @@
// app/api/embeddings/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { Storage } from '@google-cloud/storage';
import { promises as fs } from 'fs';
import { parse } from 'csv-parse/sync';
import fs from 'fs';
import path from 'path';
import { fromUrl } from 'geotiff';
import proj4 from 'proj4';
import path from 'path';

// Configuration from environment variables
const GCP_PROJECT_ID = process.env.GCP_PROJECT_ID || 'gen-lang-client-0663384776';
const GCP_CREDENTIALS_PATH = process.env.GCP_CREDENTIALS_PATH || '/home/ubuntu/gcp_credentials.json';
const AEF_INDEX_PATH = process.env.AEF_INDEX_PATH || path.join(process.cwd(), 'aef_index.csv');

// Initialize GCS client
const storage = new Storage({
keyFilename: GCP_CREDENTIALS_PATH,
projectId: GCP_PROJECT_ID,
});

// Load and parse the index file
let indexData: any[] | null = null;
const GCP_PROJECT_ID = process.env.GCP_PROJECT_ID;
const GCP_CREDENTIALS_PATH = process.env.GCP_CREDENTIALS_PATH;

function loadIndex() {
if (indexData) return indexData;

if (!fs.existsSync(AEF_INDEX_PATH)) {
throw new Error(
`AlphaEarth index file not found at ${AEF_INDEX_PATH}. ` +
'Please run the download_index.js script to download it.'
);
}

const fileContent = fs.readFileSync(AEF_INDEX_PATH, 'utf-8');

indexData = parse(fileContent, {
columns: true,
skip_empty_lines: true,
let storage: Storage | undefined;
if (GCP_PROJECT_ID && GCP_CREDENTIALS_PATH) {
storage = new Storage({
projectId: GCP_PROJECT_ID,
keyFilename: GCP_CREDENTIALS_PATH,
});

console.log(`Loaded AlphaEarth index with ${indexData.length} entries`);

return indexData;
}

// Function to check if a point is within bounds
function isPointInBounds(
lat: number,
lon: number,
south: number,
north: number,
west: number,
east: number
): boolean {
return lat >= south && lat <= north && lon >= west && lon <= east;
}
const BUCKET_NAME = 'alphaearth_foundations';
const INDEX_FILE_PATH = path.resolve(process.cwd(), 'aef_index.csv');

// Function to find the file containing the given location
function findFileForLocation(lat: number, lon: number, year: number) {
const index = loadIndex();

for (const entry of index) {
if (parseInt(entry.year) !== year) continue;

const south = parseFloat(entry.wgs84_south);
const north = parseFloat(entry.wgs84_north);
const west = parseFloat(entry.wgs84_west);
const east = parseFloat(entry.wgs84_east);

if (isPointInBounds(lat, lon, south, north, west, east)) {
return {
path: entry.path,
crs: entry.crs,
utmZone: entry.utm_zone,
year: entry.year,
bounds: { south, north, west, east },
utmBounds: {
west: parseFloat(entry.utm_west),
south: parseFloat(entry.utm_south),
east: parseFloat(entry.utm_east),
north: parseFloat(entry.utm_north),
},
};
}
}

return null;
interface IndexEntry {
year: string;
filename: string;
}

// Function to dequantize raw pixel values
// Formula from AlphaEarth documentation: (value/127.5)^2 * sign(value)
function dequantize(rawValue: number): number | null {
if (rawValue === -128) return null; // NoData marker
const normalized = rawValue / 127.5;
return Math.pow(normalized, 2) * Math.sign(rawValue);
}
let indexData: IndexEntry[] | null = null;

// Function to convert WGS84 lat/lon to UTM coordinates using proj4
function latLonToUTM(lat: number, lon: number, epsgCode: string): { x: number; y: number } {
const wgs84 = 'EPSG:4326';
const utm = epsgCode;

// Transform coordinates [lon, lat] -> [x, y]
const [x, y] = proj4(wgs84, utm, [lon, lat]);

return { x, y };
async function getIndexData(): Promise<IndexEntry[]> {
if (!indexData) {
try {
const fileContent = await fs.readFile(INDEX_FILE_PATH, 'utf-8');
indexData = parse(fileContent, {
columns: true,
skip_empty_lines: true,
});
} catch (error) {
console.error('Error reading index file:', error);
throw new Error('Index file not found or could not be read.');
}
}
return indexData!;
}

/**
* GET /api/embeddings
*
* Retrieves a 64-dimensional AlphaEarth satellite embedding for a given location and year.
*
* Query Parameters:
* - lat: Latitude in decimal degrees (-90 to 90)
* - lon: Longitude in decimal degrees (-180 to 180)
* - year: Year (2017 to 2024)
*
* Returns:
* - embedding: Array of 64 floating-point values representing the location
* - location: The requested coordinates and year
* - fileInfo: Metadata about the source GeoTIFF file
* - utmCoordinates: The location in UTM projection
* - pixelCoordinates: The exact pixel within the source file
*/
export async function GET(req: NextRequest) {
try {
const { searchParams } = new URL(req.url);
const latParam = searchParams.get('lat');
const lonParam = searchParams.get('lon');
const yearParam = searchParams.get('year');

// Validate parameters
if (!latParam || !lonParam || !yearParam) {
return NextResponse.json(
{ error: 'Missing required parameters: lat, lon, year' },
{ status: 400 }
);
}

const lat = parseFloat(latParam);
const lon = parseFloat(lonParam);
const year = parseInt(yearParam);

if (isNaN(lat) || isNaN(lon) || isNaN(year)) {
return NextResponse.json(
{ error: 'Invalid parameter values. lat and lon must be numbers, year must be an integer.' },
{ status: 400 }
);
}

if (lat < -90 || lat > 90 || lon < -180 || lon > 180) {
return NextResponse.json(
{ error: 'Invalid coordinates. Latitude must be between -90 and 90, longitude between -180 and 180.' },
{ status: 400 }
);
}
if (!storage) {
return NextResponse.json({ success: false, error: 'AlphaEarth API is not configured.' }, { status: 500 });
}

if (year < 2017 || year > 2024) {
return NextResponse.json(
{ error: 'Invalid year. Year must be between 2017 and 2024.' },
{ status: 400 }
);
}
const { searchParams } = new URL(req.url);
const lat = parseFloat(searchParams.get('lat') || '');
const lon = parseFloat(searchParams.get('lon') || '');
const year = parseInt(searchParams.get('year') || '', 10);

// Find the file containing this location
const fileInfo = findFileForLocation(lat, lon, year);

if (!fileInfo) {
return NextResponse.json(
{
error: 'No data available for this location and year.',
details: 'This location may be in the ocean, a polar region, or outside the dataset coverage area.'
},
{ status: 404 }
);
}
if (isNaN(lat) || isNaN(lon) || isNaN(year)) {
return NextResponse.json({ success: false, error: 'Invalid lat, lon, or year.' }, { status: 400 });
}

// Convert lat/lon to UTM coordinates using proj4
const utmCoords = latLonToUTM(lat, lon, fileInfo.crs);

// Calculate pixel coordinates within the 8192x8192 image
const pixelX = Math.floor(
((utmCoords.x - fileInfo.utmBounds.west) / (fileInfo.utmBounds.east - fileInfo.utmBounds.west)) * 8192
);
const pixelY = Math.floor(
((fileInfo.utmBounds.north - utmCoords.y) / (fileInfo.utmBounds.north - fileInfo.utmBounds.south)) * 8192
);
try {
const index = await getIndexData();
const entry = index.find(e => e.year === year.toString());

// Validate pixel coordinates are within bounds
if (pixelX < 0 || pixelX >= 8192 || pixelY < 0 || pixelY >= 8192) {
return NextResponse.json(
{
error: 'Calculated pixel coordinates are out of bounds.',
details: `Pixel coordinates: (${pixelX}, ${pixelY}). Expected range: 0-8191.`
},
{ status: 400 }
);
if (!entry) {
return NextResponse.json({ success: false, error: 'No data for the given year.' }, { status: 404 });
}

// Generate signed URL for the GCS file with userProject parameter
const gcsPath = fileInfo.path.replace('gs://', '');
const [bucketName, ...filePathParts] = gcsPath.split('/');
const filePath = filePathParts.join('/');

const [signedUrl] = await storage
.bucket(bucketName)
.file(filePath)
.getSignedUrl({
version: 'v4',
action: 'read',
expires: Date.now() + 15 * 60 * 1000, // 15 minutes
extensionHeaders: {
'x-goog-user-project': GCP_PROJECT_ID,
},
});

// Read the GeoTIFF using the signed URL
const tiff = await fromUrl(signedUrl);
const image = await tiff.getImage();

// Read a 1x1 window for the exact pixel
const window = [pixelX, pixelY, pixelX + 1, pixelY + 1];
const rasters = await image.readRasters({ window });

// Extract the 64-channel embedding and dequantize
const embedding: (number | null)[] = [];
for (let channel = 0; channel < 64; channel++) {
// Type assertion: rasters is an array of TypedArrays, each representing a channel
const channelData = rasters[channel] as Int8Array;
const rawValue = channelData[0];
embedding.push(dequantize(rawValue));
}
const file = storage.bucket(BUCKET_NAME).file(entry.filename);
const [url] = await file.getSignedUrl({
action: 'read',
expires: Date.now() + 15 * 60 * 1000, // 15 minutes
});

// Check if the pixel is masked (all null values indicate NoData)
const hasMaskedData = embedding.every(val => val === null);
const [fileContents] = await file.download();
const uint8Array = new Uint8Array(fileContents);
const blob = new Blob([uint8Array], { type: 'image/tiff' });

return NextResponse.json({
success: true,
location: { lat, lon, year },
utmCoordinates: utmCoords,
pixelCoordinates: { x: pixelX, y: pixelY },
fileInfo: {
gcsPath: fileInfo.path,
utmZone: fileInfo.utmZone,
crs: fileInfo.crs,
bounds: fileInfo.bounds,
return new NextResponse(blob, {
status: 200,
headers: {
'Content-Type': 'image/tiff',
},
embedding,
embeddingDimensions: 64,
masked: hasMaskedData,
attribution: 'The AlphaEarth Foundations Satellite Embedding dataset is produced by Google and Google DeepMind.',
license: 'CC-BY 4.0',
});

} catch (error) {
console.error('Error fetching embeddings:', error);

// Provide more helpful error messages
let errorMessage = 'Internal server error';
let errorDetails = error instanceof Error ? error.message : 'Unknown error';

if (errorDetails.includes('index file not found')) {
errorMessage = 'AlphaEarth index file not found';
errorDetails = 'Please run the setup script: node download_index.js';
} else if (errorDetails.includes('ENOENT') && errorDetails.includes('gcp_credentials')) {
errorMessage = 'GCP credentials file not found';
errorDetails = 'Please configure your GCP service account credentials. See EMBEDDINGS_INTEGRATION_README.md for setup instructions.';
}

return NextResponse.json(
{ error: errorMessage, details: errorDetails },
{ status: 500 }
);
console.error('Error fetching embedding:', error);
return NextResponse.json({ success: false, error: 'Internal server error.' }, { status: 500 });
}
}
Loading