UNPKG

planetary-mcp-server

Version:

šŸŒ Earth Engine MCP Server for Claude Desktop - Powerful geospatial analysis with simple commands

431 lines (373 loc) • 12.9 kB
const { getEarthEngine } = require('./init'); /** * Get administrative boundary for a location using Earth Engine's built-in datasets * @param {object} params - Boundary parameters * @returns {Promise<object>} Boundary geometry and metadata */ async function getAdminBoundary(params) { const ee = getEarthEngine(); const { placeName, // e.g., "San Francisco" adminLevel = 2, // 0=country, 1=state/province, 2=county/district country = 'USA', returnType = 'geometry' // 'geometry' or 'feature' } = params; try { let boundaryFeature; let dataset; let filterProperty; // Select appropriate dataset based on admin level switch(adminLevel) { case 0: // Country level // Use Large Scale International Boundaries (LSIB) dataset = ee.FeatureCollection('USDOS/LSIB_SIMPLE/2017'); filterProperty = 'country_na'; break; case 1: // State/Province level // FAO GAUL Level 1 (States/Provinces) dataset = ee.FeatureCollection('FAO/GAUL/2015/level1'); filterProperty = 'ADM1_NAME'; break; case 2: // County/District level // FAO GAUL Level 2 (Districts/Counties) dataset = ee.FeatureCollection('FAO/GAUL/2015/level2'); filterProperty = 'ADM2_NAME'; break; case 3: // City/Municipality (US specific) // US Census Tiger boundaries dataset = ee.FeatureCollection('TIGER/2018/Counties'); filterProperty = 'NAME'; break; default: throw new Error('Invalid admin level. Use 0 (country), 1 (state), 2 (county), or 3 (city)'); } // Filter to get the specific boundary let filtered; if (adminLevel === 0) { // For countries, direct filter filtered = dataset.filter(ee.Filter.eq(filterProperty, placeName)); } else { // For sub-national, filter by name (and country if needed) filtered = dataset.filter(ee.Filter.eq(filterProperty, placeName)); // Additional country filter for non-US locations if (country !== 'USA' && adminLevel > 0) { filtered = filtered.filter(ee.Filter.eq('ADM0_NAME', country)); } } // Get the first matching feature boundaryFeature = ee.Feature(filtered.first()); // Get the geometry const geometry = boundaryFeature.geometry(); // Simplify if it's too complex (helps with export size) const simplifiedGeometry = geometry.simplify(100); // 100m simplification // Get bounds and metadata const bounds = geometry.bounds(); const centroid = geometry.centroid(); const area = geometry.area(); // Evaluate to get actual values const result = await new Promise((resolve, reject) => { if (returnType === 'geometry') { // Return just the geometry simplifiedGeometry.evaluate((geo, error) => { if (error) reject(error); else { // Also get metadata Promise.all([ new Promise((res) => bounds.evaluate((b) => res(b))), new Promise((res) => centroid.evaluate((c) => res(c))), new Promise((res) => area.evaluate((a) => res(a))), new Promise((res) => boundaryFeature.evaluate((f) => res(f))) ]).then(([boundsData, centroidData, areaData, featureData]) => { resolve({ geometry: geo, bounds: boundsData, centroid: centroidData, area: areaData / 1000000, // Convert to km² properties: featureData.properties, name: placeName, adminLevel: adminLevel }); }); } }); } else { // Return full feature with properties boundaryFeature.evaluate((feat, error) => { if (error) reject(error); else resolve(feat); }); } }); return result; } catch (error) { console.error('Error getting boundary:', error); throw error; } } /** * Get US-specific boundaries (cities, counties, states) * @param {object} params - Search parameters * @returns {Promise<object>} Boundary geometry */ async function getUSBoundary(params) { const ee = getEarthEngine(); const { city, county, state, type = 'county' // 'state', 'county', or 'city' } = params; try { let dataset; let filters = []; switch(type) { case 'state': dataset = ee.FeatureCollection('TIGER/2018/States'); if (state) filters.push(ee.Filter.eq('NAME', state)); break; case 'county': dataset = ee.FeatureCollection('TIGER/2018/Counties'); if (county) filters.push(ee.Filter.eq('NAME', county)); if (state) filters.push(ee.Filter.eq('STATEFP', getStateFIPS(state))); break; case 'city': // For cities, we often need to use Places dataset dataset = ee.FeatureCollection('TIGER/2016/CBSAs'); if (city) filters.push(ee.Filter.stringContains('NAME', city)); break; default: throw new Error('Invalid type. Use "state", "county", or "city"'); } // Apply filters let filtered = dataset; filters.forEach(filter => { filtered = filtered.filter(filter); }); // Get the first match const feature = ee.Feature(filtered.first()); const geometry = feature.geometry(); // Evaluate and return const result = await new Promise((resolve, reject) => { Promise.all([ new Promise((res) => geometry.evaluate((g) => res(g))), new Promise((res) => feature.evaluate((f) => res(f))) ]).then(([geometryData, featureData]) => { resolve({ geometry: geometryData, properties: featureData.properties, type: type, name: city || county || state }); }).catch(reject); }); return result; } catch (error) { console.error('Error getting US boundary:', error); throw error; } } /** * Import and use a custom shapefile from Earth Engine Assets * @param {string} assetId - Earth Engine asset ID of the uploaded shapefile * @returns {Promise<object>} Feature collection */ async function useCustomShapefile(assetId) { const ee = getEarthEngine(); try { // Load the asset const customBoundary = ee.FeatureCollection(assetId); // Get metadata const count = customBoundary.size(); const first = customBoundary.first(); const geometry = customBoundary.geometry(); const result = await new Promise((resolve, reject) => { Promise.all([ new Promise((res) => count.evaluate((c) => res(c))), new Promise((res) => first.evaluate((f) => res(f))), new Promise((res) => geometry.evaluate((g) => res(g))) ]).then(([countData, firstFeature, geometryData]) => { resolve({ featureCount: countData, firstFeature: firstFeature, combinedGeometry: geometryData, assetId: assetId, message: `Loaded ${countData} features from custom shapefile` }); }).catch(reject); }); return result; } catch (error) { console.error('Error loading custom shapefile:', error); throw error; } } /** * Search for available boundaries by name * @param {object} params - Search parameters * @returns {Promise<array>} List of matching boundaries */ async function searchBoundaries(params) { const ee = getEarthEngine(); const { searchTerm, adminLevel = 2, country = null, limit = 10 } = params; try { // Select dataset based on admin level let dataset; let nameProperty; switch(adminLevel) { case 0: dataset = ee.FeatureCollection('USDOS/LSIB_SIMPLE/2017'); nameProperty = 'country_na'; break; case 1: dataset = ee.FeatureCollection('FAO/GAUL/2015/level1'); nameProperty = 'ADM1_NAME'; break; case 2: dataset = ee.FeatureCollection('FAO/GAUL/2015/level2'); nameProperty = 'ADM2_NAME'; break; default: throw new Error('Invalid admin level'); } // Filter by search term (case insensitive) let filtered = dataset.filter(ee.Filter.stringContains(nameProperty, searchTerm)); // Add country filter if specified if (country && adminLevel > 0) { filtered = filtered.filter(ee.Filter.eq('ADM0_NAME', country)); } // Limit results filtered = filtered.limit(limit); // Get the results const results = await new Promise((resolve, reject) => { filtered.evaluate((features, error) => { if (error) reject(error); else { const matches = features.features.map(f => ({ name: f.properties[nameProperty], country: f.properties.ADM0_NAME || f.properties.country_na, properties: f.properties, id: f.id })); resolve(matches); } }); }); return results; } catch (error) { console.error('Error searching boundaries:', error); throw error; } } /** * Helper function to get state FIPS code */ function getStateFIPS(stateName) { const fipsMap = { 'California': '06', 'Texas': '48', 'New York': '36', 'Florida': '12', // Add more as needed }; return fipsMap[stateName] || '06'; // Default to CA } /** * Export data for a specific administrative boundary * @param {object} params - Export parameters * @returns {Promise<object>} Export task info */ async function exportWithBoundary(params) { const ee = getEarthEngine(); const { placeName, adminLevel = 2, country = 'USA', datasetId, startDate, endDate, bands, scale = 10, description } = params; try { // Get the boundary const boundary = await getAdminBoundary({ placeName: placeName, adminLevel: adminLevel, country: country }); console.log(`Found boundary for ${placeName}:`); console.log(` Area: ${Math.round(boundary.area)} km²`); console.log(` Admin Level: ${boundary.adminLevel}`); // Create the export region from the boundary geometry // Use the original EE geometry object, not the evaluated one const boundaryFeature = ee.FeatureCollection('FAO/GAUL/2015/level2') .filter(ee.Filter.eq('ADM2_NAME', placeName)) .first(); const exportRegion = ee.Feature(boundaryFeature).geometry(); // Load and process the imagery let collection = ee.ImageCollection(datasetId) .filterDate(startDate, endDate) .filterBounds(exportRegion); // Apply cloud masking 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); }); } // Create composite let image = collection.median(); if (bands) { image = image.select(bands); } // Clip to boundary image = image.clip(exportRegion); // Scale to 16-bit const exportImage = image.multiply(10000).toInt16(); // Create export task const task = ee.batch.Export.image.toDrive({ image: exportImage, description: description || `${placeName}_export_${Date.now()}`, fileNamePrefix: description || `${placeName}_export`, region: exportRegion, scale: scale, maxPixels: 1e10, fileFormat: 'GeoTIFF', formatOptions: { cloudOptimized: true } }); // Start the task task.start(); return { status: 'Export task started', placeName: placeName, area: `${Math.round(boundary.area)} km²`, adminLevel: boundary.adminLevel, properties: boundary.properties, message: `Exporting ${placeName} with actual administrative boundaries` }; } catch (error) { console.error('Error in boundary export:', error); throw error; } } module.exports = { getAdminBoundary, getUSBoundary, useCustomShapefile, searchBoundaries, exportWithBoundary };