planetary-mcp-server
Version:
🌍 Earth Engine MCP Server for Claude Desktop - Powerful geospatial analysis with simple commands
389 lines (339 loc) • 13 kB
JavaScript
const { getEarthEngine } = require('./init');
/**
* Export large areas with multiple options
* @param {object} params - Export parameters
* @returns {Promise<object>} Export options and instructions
*/
async function exportLargeArea(params) {
const ee = getEarthEngine();
const {
datasetId,
startDate,
endDate,
region,
scale = 10,
crs = 'EPSG:4326',
bands = null,
fileName = `large_export_${Date.now()}`,
maxPixelsForDirect = 1e8, // 100 million pixels for direct download
maxPixelsForDrive = 1e10 // 10 billion pixels for Drive export
} = params;
return new Promise((resolve, reject) => {
try {
// Create the image/composite
let collection = ee.ImageCollection(datasetId)
.filterDate(startDate, endDate);
let exportRegion;
if (region) {
exportRegion = region.type === 'Point'
? ee.Geometry.Point(region.coordinates).buffer(region.buffer || 5000)
: ee.Geometry.Polygon(region.coordinates);
collection = collection.filterBounds(exportRegion);
}
// Apply processing for Sentinel-2
if (datasetId.includes('S2')) {
collection = collection.map(function(img) {
const qa = img.select('QA60');
const cloudBitMask = 1 << 10;
const cirrusBitMask = 1 << 11;
const mask = qa.bitwiseAnd(cloudBitMask).eq(0)
.and(qa.bitwiseAnd(cirrusBitMask).eq(0));
return img.updateMask(mask)
.select('B.*')
.divide(10000)
.copyProperties(img, ['system:time_start']);
});
}
let image = collection.median();
if (bands) {
image = image.select(bands);
}
// Scale to 16-bit for export
const exportImage = image.multiply(10000).toInt16();
// Calculate area and pixel count
const bounds = exportRegion.bounds();
const coords = bounds.coordinates().getInfo()[0];
const minLon = Math.min(...coords.map(c => c[0]));
const maxLon = Math.max(...coords.map(c => c[0]));
const minLat = Math.min(...coords.map(c => c[1]));
const maxLat = Math.max(...coords.map(c => c[1]));
const widthDegrees = maxLon - minLon;
const heightDegrees = maxLat - minLat;
const widthMeters = widthDegrees * 111320;
const heightMeters = heightDegrees * 110540;
const pixelWidth = Math.ceil(widthMeters / scale);
const pixelHeight = Math.ceil(heightMeters / scale);
const totalPixels = pixelWidth * pixelHeight;
// Determine best export method based on size
const exportMethods = [];
// Method 1: Direct download (for smaller areas)
if (totalPixels <= maxPixelsForDirect) {
try {
const downloadUrl = exportImage.getDownloadURL({
name: fileName,
region: exportRegion,
scale: scale,
crs: crs,
format: 'GEO_TIFF',
maxPixels: maxPixelsForDirect
});
exportMethods.push({
method: 'Direct Download',
available: true,
url: downloadUrl,
description: 'Download directly to your computer',
estimatedSizeMB: Math.round((totalPixels * 2 * (bands ? bands.length : 12)) / (1024 * 1024)),
limitations: 'Best for areas under 100 million pixels'
});
} catch (error) {
exportMethods.push({
method: 'Direct Download',
available: false,
reason: 'Area too large for direct download',
error: error.message,
description: 'Direct download not available for this area size'
});
}
} else {
exportMethods.push({
method: 'Direct Download',
available: false,
reason: `Area has ${totalPixels.toLocaleString()} pixels, exceeds limit of ${maxPixelsForDirect.toLocaleString()}`,
description: 'Use Code Editor or Python API for export to Google Drive'
});
}
// Method 2: Tiled download (for medium areas)
if (totalPixels > maxPixelsForDirect && totalPixels <= 1e9) { // Limit tiling to 1 billion pixels
// Calculate tiles needed
const tileSize = Math.sqrt(maxPixelsForDirect) * scale; // meters per tile
const tilesX = Math.ceil(widthMeters / tileSize);
const tilesY = Math.ceil(heightMeters / tileSize);
const totalTiles = tilesX * tilesY;
const tiles = [];
for (let x = 0; x < tilesX; x++) {
for (let y = 0; y < tilesY; y++) {
const tileMinLon = minLon + (x * tileSize / 111320);
const tileMaxLon = Math.min(minLon + ((x + 1) * tileSize / 111320), maxLon);
const tileMinLat = minLat + (y * tileSize / 110540);
const tileMaxLat = Math.min(minLat + ((y + 1) * tileSize / 110540), maxLat);
const tileRegion = ee.Geometry.Rectangle([tileMinLon, tileMinLat, tileMaxLon, tileMaxLat]);
const tileUrl = exportImage.getDownloadURL({
name: `${fileName}_tile_${x}_${y}`,
region: tileRegion,
scale: scale,
crs: crs,
format: 'GEO_TIFF',
maxPixels: maxPixelsForDirect
});
tiles.push({
tile: `${x}_${y}`,
url: tileUrl,
bounds: [tileMinLon, tileMinLat, tileMaxLon, tileMaxLat]
});
}
}
exportMethods.push({
method: 'Tiled Download',
available: true,
totalTiles: totalTiles,
tilesX: tilesX,
tilesY: tilesY,
tiles: tiles.slice(0, 5), // Show first 5 tiles as example
description: `Download in ${totalTiles} tiles, then merge in GIS software`,
instructions: 'Download each tile and use GDAL or QGIS to merge them',
mergeCommand: `gdal_merge.py -o ${fileName}_merged.tif tile_*.tif`
});
}
// Method 3: Generate Code Editor script
const codeEditorScript = generateExportScript({
datasetId: datasetId,
startDate: startDate,
endDate: endDate,
region: region,
bands: bands,
scale: scale,
crs: crs,
fileName: fileName
});
exportMethods.push({
method: 'Earth Engine Code Editor',
available: true,
description: 'Export to your personal Google Drive using Code Editor',
script: codeEditorScript,
instructions: [
'1. Copy the script below',
'2. Go to https://code.earthengine.google.com/',
'3. Paste and run the script',
'4. Click "Run" button in Tasks tab',
'5. File will appear in your Google Drive'
]
});
// Method 4: Python API script
const pythonScript = generatePythonScript({
datasetId: datasetId,
startDate: startDate,
endDate: endDate,
region: region,
bands: bands,
scale: scale,
crs: crs,
fileName: fileName
});
exportMethods.push({
method: 'Python API',
available: true,
description: 'Use Earth Engine Python API for batch export',
script: pythonScript,
instructions: [
'1. Install: pip install earthengine-api',
'2. Authenticate: earthengine authenticate',
'3. Run the Python script',
'4. File will export to your Google Drive'
]
});
resolve({
areaInfo: {
bounds: [minLon, minLat, maxLon, maxLat],
widthKm: Math.round(widthMeters / 1000),
heightKm: Math.round(heightMeters / 1000),
pixelDimensions: `${pixelWidth} x ${pixelHeight}`,
totalPixels: totalPixels.toLocaleString(),
estimatedSizeMB: Math.round((totalPixels * 2 * (bands ? bands.length : 12)) / (1024 * 1024))
},
exportMethods: exportMethods,
recommendations: getRecommendations(totalPixels),
parameters: {
scale: scale,
crs: crs,
bands: bands || 'All bands',
format: 'GeoTIFF (16-bit)'
}
});
} catch (error) {
reject(error);
}
});
}
/**
* Generate Earth Engine Code Editor script
*/
function generateExportScript(params) {
const { datasetId, startDate, endDate, region, bands, scale, crs, fileName } = params;
const regionStr = region.type === 'Point'
? `ee.Geometry.Point([${region.coordinates.join(', ')}]).buffer(${region.buffer || 5000})`
: `ee.Geometry.Polygon([${JSON.stringify(region.coordinates)}])`;
const bandsStr = bands ? `.select(${JSON.stringify(bands)})` : '';
return `// Earth Engine Code Editor Script
// Export large area to Google Drive
// Define the area of interest
var region = ${regionStr};
// Load and process the image collection
var collection = ee.ImageCollection('${datasetId}')
.filterDate('${startDate}', '${endDate}')
.filterBounds(region);
// Apply cloud masking for Sentinel-2
${datasetId.includes('S2') ? `collection = collection.map(function(image) {
var qa = image.select('QA60');
var cloudBitMask = 1 << 10;
var cirrusBitMask = 1 << 11;
var mask = qa.bitwiseAnd(cloudBitMask).eq(0)
.and(qa.bitwiseAnd(cirrusBitMask).eq(0));
return image.updateMask(mask)
.select('B.*')
.divide(10000);
});` : ''}
// Create median composite
var image = collection.median()${bandsStr};
// Scale to 16-bit
var exportImage = image.multiply(10000).toInt16();
// Export to Drive
Export.image.toDrive({
image: exportImage,
description: '${fileName}',
folder: 'EarthEngine_Exports',
fileNamePrefix: '${fileName}',
region: region,
scale: ${scale},
crs: '${crs}',
maxPixels: 1e10,
fileFormat: 'GeoTIFF',
formatOptions: {
cloudOptimized: true
}
});
print('Export task created. Click Run in the Tasks tab to start the export.');
Map.centerObject(region, 10);
Map.addLayer(region, {color: 'red'}, 'Export Region');
Map.addLayer(image.select(['B4', 'B3', 'B2']), {min: 0, max: 0.3}, 'Image');`;
}
/**
* Generate Python script for export
*/
function generatePythonScript(params) {
const { datasetId, startDate, endDate, region, bands, scale, crs, fileName } = params;
const regionStr = region.type === 'Point'
? `ee.Geometry.Point([${region.coordinates.join(', ')}]).buffer(${region.buffer || 5000})`
: `ee.Geometry.Polygon([${JSON.stringify(region.coordinates)}])`;
const bandsStr = bands ? `.select(${JSON.stringify(bands)})` : '';
return `#!/usr/bin/env python
# Earth Engine Python API Script
# Export large area to Google Drive
import ee
import time
# Initialize Earth Engine
ee.Initialize()
# Define the area of interest
region = ${regionStr}
# Load and process the image collection
collection = ee.ImageCollection('${datasetId}') \\
.filterDate('${startDate}', '${endDate}') \\
.filterBounds(region)
${datasetId.includes('S2') ? `# Apply cloud masking for Sentinel-2
def mask_clouds(image):
qa = image.select('QA60')
cloud_bit_mask = 1 << 10
cirrus_bit_mask = 1 << 11
mask = qa.bitwiseAnd(cloud_bit_mask).eq(0) \\
.And(qa.bitwiseAnd(cirrus_bit_mask).eq(0))
return image.updateMask(mask) \\
.select('B.*') \\
.divide(10000)
collection = collection.map(mask_clouds)` : ''}
# Create median composite
image = collection.median()${bandsStr}
# Scale to 16-bit
export_image = image.multiply(10000).toInt16()
# Export to Drive
task = ee.batch.Export.image.toDrive(
image=export_image,
description='${fileName}',
folder='EarthEngine_Exports',
fileNamePrefix='${fileName}',
region=region,
scale=${scale},
crs='${crs}',
maxPixels=1e10,
fileFormat='GeoTIFF',
formatOptions={'cloudOptimized': True}
)
# Start the export
task.start()
print(f'Export task started: {task.id}')
print('Check https://code.earthengine.google.com/tasks for progress')
print('File will appear in your Google Drive in the EarthEngine_Exports folder')`;
}
/**
* Get recommendations based on pixel count
*/
function getRecommendations(pixelCount) {
if (pixelCount < 1e8) {
return 'Small area - Use direct download for immediate access';
} else if (pixelCount < 1e9) {
return 'Medium area - Use tiled download or Code Editor export';
} else {
return 'Large area - Use Code Editor or Python API for Drive export';
}
}
module.exports = {
exportLargeArea
};