Skip to content

Commit

Permalink
Merge pull request #169 from Plant-for-the-Planet-org/feature/create-…
Browse files Browse the repository at this point in the history
…GOES16Provider

Integrate GOES-16 Geostationary Satellite as a GeoEventProvider
  • Loading branch information
dhakalaashish authored Apr 23, 2024
2 parents e422638 + d8cb1de commit 57fd28c
Show file tree
Hide file tree
Showing 12 changed files with 665 additions and 96 deletions.
1 change: 1 addition & 0 deletions apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"db:generate": "dotenv -e ../../.env npx prisma generate"
},
"dependencies": {
"@google/earthengine": "^0.1.396",
"@logtail/node": "^0.4.0",
"@planet-sdk/common": "^0.1.11",
"@prisma/client": "^5.0.0",
Expand Down
4 changes: 3 additions & 1 deletion apps/server/src/Interfaces/GeoEventProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,17 @@ export enum GeoEventProviderClientId {
VIIRS_NOAA20_NRT = 'VIIRS_NOAA20_NRT',
VIIRS_SNPP_NRT = 'VIIRS_SNPP_NRT',
VIIRS_SNPP_SP = 'VIIRS_SNPP_SP',
GEOSTATIONARY = 'GEOSTATIONARY'
}

export enum GeoEventProviderClient {
FIRMS = 'FIRMS',
GOES16 = 'GOES-16'
}

export interface GeoEventProviderConfig {
bbox: string;
slice: string;
apiUrl: string;
client: GeoEventProviderClient; //'FIRMS'
}

Expand All @@ -28,6 +29,7 @@ export interface GeoEventProviderClass {
geoEventProviderId: string,
slice: string,
clientApiKey: string,
lastRun: Date | null
) => Promise<GeoEvent[]>;
}

Expand Down
10 changes: 7 additions & 3 deletions apps/server/src/Services/GeoEvent/GeoEventHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ import { AlertType } from "../../Interfaces/SiteAlert";
import { type geoEventInterface as GeoEvent } from "../../Interfaces/GeoEvent";
import { createXXHash3 } from "hash-wasm";
import { prisma } from '../../server/db';
import { logger } from "../../../src/server/logger";

const processGeoEvents = async (breadcrumbPrefix: string, geoEventProviderClientId: GeoEventProviderClientId, geoEventProviderId: string, slice: string, geoEvents: GeoEvent[]) => {
const processGeoEvents = async (geoEventProviderClientId: GeoEventProviderClientId, geoEventProviderId: string, slice: string, geoEvents: GeoEvent[]) => {
const hasher = await createXXHash3(); // Create the hasher outside the function
const buildChecksum = (geoEvent: GeoEvent): string => {
hasher.init(); // Reset the hasher
Expand Down Expand Up @@ -46,6 +45,11 @@ const processGeoEvents = async (breadcrumbPrefix: string, geoEventProviderClient
// having the provided providerKey
const geoEvents = await prisma.geoEvent.findMany({
select: { id: true },
// IMPROVEMENT: this code does not identify duplicate between providers,
// It only identifies duplicate within a provider
// To identify duplicates between providers, remove geoEventProviderId from the where clause
// However, that would increase memory usage, and possibly freeze the process
// Identify ways to test for duplication against the entire database.
where: { geoEventProviderId: geoEventProviderId, eventDate: { gt: new Date(Date.now() - 30 * 60 * 60 * 1000) } },
});
// Only compare with data from last 26 hrs
Expand Down Expand Up @@ -85,7 +89,7 @@ const processGeoEvents = async (breadcrumbPrefix: string, geoEventProviderClient
geoEventProviderClientId: geoEventProviderClientId,
geoEventProviderId: geoEventProviderId,
radius: geoEvent.radius ? geoEvent.radius : 0,
slice: slice,
slice: geoEventProviderClientId === 'GEOSTATIONARY' ? geoEvent.slice : slice,
data: geoEvent.data,
}))

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {type GeoEventProviderClass} from '../../Interfaces/GeoEventProvider';
import NasaGeoEventProvider from './ProviderClass/NasaGeoEventProviderClass';
import GOES16GeoEventProvider from './ProviderClass/GOES16GeoEventProviderClass';
// import additional GeoEvent provider implementations below

const createGeoEventProviderClassRegistry = function (
Expand Down Expand Up @@ -31,6 +32,7 @@ const createGeoEventProviderClassRegistry = function (

const GeoEventProviderClassRegistry = createGeoEventProviderClassRegistry([
new NasaGeoEventProvider(),
new GOES16GeoEventProvider()
// add new alert providers here
]);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import {
type GeoEventProviderConfig,
type GeoEventProviderClientId,
type GeoEventProviderConfigGeneral,
type GeoEventProviderClass
} from '../../../Interfaces/GeoEventProvider';
import { type geoEventInterface as GeoEvent } from "../../../Interfaces/GeoEvent"
import {Confidence} from '../../../Interfaces/GeoEvent';
import {determineSlice} from "../../../utils/geometry"
import ee from '@google/earthengine'

type FireDataEntry = [number, number, Date];
type AllFireData = FireDataEntry[];
interface PrivateKeyJson {
"type": string;
"project_id": string;
"private_key_id": string;
"private_key": string;
"client_email": string;
"client_id": string;
"auth_uri": string;
"token_uri": string;
"auth_provider_x509_cert_url": string;
"client_x509_cert_url": string;
"universe_domain": string;
}

interface GOES16GeoEventProviderConfig extends GeoEventProviderConfig {
// Add any additional config properties if needed
privateKey: PrivateKeyJson;
}

class GOES16GeoEventProviderClass implements GeoEventProviderClass {
private config: GeoEventProviderConfigGeneral | undefined;

constructor() {
this.getLatestGeoEvents = this.getLatestGeoEvents.bind(this);
}

getKey(): string {
return 'GOES-16';
}

initialize(config?: GeoEventProviderConfigGeneral): void {
this.config = config;
}

async authenticateEarthEngine(): Promise<void> {
const {privateKey} = this.getConfig()
const private_key = JSON.parse(JSON.stringify(privateKey))
return new Promise<void>((resolve, reject) => {
ee.data.authenticateViaPrivateKey(
private_key,
() => {
ee.initialize(
null,
null,
() => {
console.log('Google Earth Engine authentication successful');
resolve();
},
(err) => {
console.error('Google Earth Engine initialization error', err);
reject(err);
}
);
},
(err) => {
console.error('Google Earth Engine authentication error', err);
reject(err);
}
);
});
}

async getLatestGeoEvents(geoEventProviderClientId: string, geoEventProviderId: string, slice: string, clientApiKey: string, lastRun: Date | null): Promise<GeoEvent[]> {
return new Promise<GeoEvent[]>(async (resolve, reject) => {
try {
// Ensure Earth Engine is authenticated and initialized before fetching data
await this.authenticateEarthEngine();
let allFireData: AllFireData = [];

// If lastRun is more than 2 hours ago or null, then start from 2 hours ago, else start from lastRunDate
const currentDateTime = new Date();
const lastRunDate = lastRun ? new Date(lastRun) : null;
const twoHoursAgo = new Date(currentDateTime.getTime() - 2 * 3600 * 1000);
const fromDateTime = (!lastRunDate || (currentDateTime.getTime() - lastRunDate.getTime()) > 2 * 3600 * 1000) ? twoHoursAgo : lastRunDate;

const images = ee.ImageCollection("NOAA/GOES/16/FDCF").filterDate(fromDateTime, currentDateTime);

// Fetch and process images here...
// The process includes fetching image IDs, processing them to extract fire data, etc.
// This is a simplified outline; integrate the logic from your initial example here.
const getImagesId = () => {
return new Promise((resolve, reject) => {
images.evaluate((imageCollection) => {
if (imageCollection && imageCollection.features) {
const imagesData = imageCollection.features.map(feature => feature.id);
resolve(imagesData);
} else {
reject(new Error("No features found"));
}
});
});
};
try {
const array_imagesId = await getImagesId() as string[];
for (const imageId of array_imagesId) {
const image = ee.Image(`${imageId}`)
// Get the datetime information from the image metadata
const datetimeInfo = await ee.Date(image.get('system:time_start')).getInfo();
const datetime = new Date(datetimeInfo.value);


const temperatureImage = image.select('Temp');
const xMin = -142; // On station as GOES-E
const xMax = xMin + 135;
const geometry = ee.Geometry.Rectangle([xMin, -65, xMax, 65], null, true);
var temperatureVector = temperatureImage.reduceToVectors({
geometry: geometry,
scale: 2000,
geometryType: 'centroid',
labelProperty: 'temp',
maxPixels: 1e10,
});
const fireData = await new Promise((resolve, reject) => {
temperatureVector.evaluate((featureCollection) => {
if (featureCollection && featureCollection.features) {
// Map each feature to include datetime in its data
// [long, lat, eventDate]
const fireDataWithTime = featureCollection.features.map(feature => [...feature.geometry.coordinates, datetime]);
resolve(fireDataWithTime);
} else {
reject(new Error("No features found"));
}
});
}) as FireDataEntry;

// Concatenate the current image's fire data with the master array
allFireData = allFireData.concat(fireData);
};
} catch (error) {
console.error("Error fetching fire data:", error);
}

// Normalize the fire data into GeoEvent format
const geoEventsData: GeoEvent[] = allFireData.map((fireData: FireDataEntry) => ({
type: 'fire',
latitude: fireData[1],
longitude: fireData[0],
eventDate: new Date(fireData[2]),
confidence: Confidence.High,
isProcessed: false,
geoEventProviderClientId: geoEventProviderClientId as GeoEventProviderClientId,
geoEventProviderId: geoEventProviderId,
slice: determineSlice(fireData[1], fireData[0]),
data: {'satellite': clientApiKey, 'slice': slice}
}));

resolve(geoEventsData);
} catch (error) {
console.error('Failed to fetch or process GOES-16 data', error);
reject(error);
}
});
}

getConfig(): GOES16GeoEventProviderConfig {
if (typeof this.config === 'undefined') {
throw new Error(`Invalid or incomplete GOES-16 event provider configuration`);
}
const config = this.config
if (typeof config.client === "undefined") {
throw new Error(`Missing property 'client' in alert provider configuration`);
}
if (typeof config.bbox === "undefined") {
throw new Error(`Missing property 'bbox' in alert provider configuration`);
}
if (typeof config.slice === "undefined") {
throw new Error(`Missing property 'slice' in alert provider configuration`);
}
if (typeof config.privateKey === "undefined") {
throw new Error(`Missing property 'satelliteType' in alert provider configuration`);
}
return {
client: config.client,
bbox: config.bbox,
slice: config.slice,
privateKey: config.privateKey
};
}
}

export default GOES16GeoEventProviderClass;
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type DataRecord from '../../../Interfaces/DataRecord';
import { Confidence } from '../../../Interfaces/GeoEvent';

interface NasaGeoEventProviderConfig extends GeoEventProviderConfig {
apiUrl: string;
}

class NasaGeoEventProviderClass implements GeoEventProviderClass {
Expand Down
Loading

0 comments on commit 57fd28c

Please sign in to comment.