Skip to content

Commit

Permalink
feat: new Earth Engine format and functions
Browse files Browse the repository at this point in the history
Implement: https://dhis2.atlassian.net/browse/DHIS2-16097
Update to the EarthEngineWorker
- `format` now needs to be passed explicitly ("Image", "ImageCollection" and
"FeatureCollection" are supported)
- `periodReducer` can be specified to reduce an ImageCollection to a single
Image (eg. get a monthly image from a daily dataset)
- `cloudScore` can be specified to mask clouds on ImageCollection
- `style` now:
  - replaces `params` (object with min, max and palette - eg, elevation)
  - replaces `legend` (array of classification - eg. landcover)
  - supports FeatureCollection styling (object with color and strokeWidth -
eg. buildings)
- `unmaskAggregation` gives a default value to NULL pixels for aggregation
- `useCentroid` to only count pixels if their centroid is in the region, otherwise
most pixels are counted and weighted by the fraction of the pixel within the region
- `mask` is replaced by `maskOperator` (eg. gt, gte or false) and takes the min
style value or 0 if not specified
Upgrade to latest version for Earth Engine JavaScript API (custom build for web
worker usage)
Some code refactoring

BREAKING CHANGE: Update to the EarthEngineWorker
  • Loading branch information
turban authored Jul 31, 2024
1 parent 7325abf commit c97b9c6
Show file tree
Hide file tree
Showing 9 changed files with 1,651 additions and 1,337 deletions.
14 changes: 7 additions & 7 deletions src/earthengine/__tests__/ee_worker_utils.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { hasClasses, getHistogramStatistics } from '../ee_worker_utils'

const scale = 1000

const legend = [
const style = [
{
id: 9,
value: 9,
name: 'Savannas',
color: '#d99125',
},
{
id: 13,
value: 13,
name: 'Urban and built-up',
color: '#cc0202',
},
Expand Down Expand Up @@ -76,14 +76,14 @@ describe('earthengine', () => {
data,
scale,
aggregationType,
legend,
style,
})

const itemA = result['O6uvpzGd5pu']
const itemB = result['fdc6uOvgoji']

expect(ids).toEqual(Object.keys(result))
expect(Object.keys(itemA).length).toBe(legend.length)
expect(Object.keys(itemA).length).toBe(style.length)
expect(itemA['9']).toBe(toPercent(0))
expect(itemA['13']).toBe(toPercent(7))
expect(itemB['9']).toBe(toPercent(5))
Expand All @@ -97,7 +97,7 @@ describe('earthengine', () => {
data,
scale,
aggregationType,
legend,
style,
})

const itemA = result['O6uvpzGd5pu']
Expand All @@ -116,7 +116,7 @@ describe('earthengine', () => {
data,
scale,
aggregationType,
legend,
style,
})

const itemA = result['O6uvpzGd5pu']
Expand Down
205 changes: 136 additions & 69 deletions src/earthengine/ee_worker.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expose } from 'comlink'
import ee from './ee_api_js_worker' // https://github.com/google/earthengine-api/pull/173
// import { ee } from '@google/earthengine/build/ee_api_js_debug' // Run "yarn add @google/earthengine"
import { getBufferGeometry } from '../utils/buffers.js'
import ee from './ee_api_js_worker.js' // https://github.com/google/earthengine-api/pull/173
import {
getInfo,
getScale,
Expand All @@ -9,15 +9,26 @@ import {
getClassifiedImage,
getHistogramStatistics,
getFeatureCollectionProperties,
} from './ee_worker_utils'
import { getBufferGeometry } from '../utils/buffers'

// Why we need to "hack" the '@google/earthengine bundle:
// https://groups.google.com/g/google-earth-engine-developers/c/nvlbqxrnzDk/m/QuyWxGt9AQAJ

const FEATURE_STYLE = { color: 'FFA500', strokeWidth: 2 }
applyFilter,
applyMethods,
applyCloudMask,
} from './ee_worker_utils.js'

const IMAGE = 'Image'
const IMAGE_COLLECTION = 'ImageCollection'
const FEATURE_COLLECTION = 'FeatureCollection'

// Options are defined here:
// https://developers.google.com/earth-engine/apidocs/ee-featurecollection-draw
const DEFAULT_FEATURE_STYLE = {
color: '#FFA500',
strokeWidth: 2,
pointRadius: 5,
}
const DEFAULT_TILE_SCALE = 1

const DEFAULT_UNMASK_VALUE = 0

class EarthEngineWorker {
constructor(options = {}) {
this.options = options
Expand Down Expand Up @@ -102,12 +113,21 @@ class EarthEngineWorker {
return this.eeImage
}

const { datasetId, filter, mosaic, band, bandReducer, mask, methods } =
this.options
const {
datasetId,
format,
filter,
periodReducer,
mosaic,
band,
bandReducer,
methods,
cloudScore,
} = this.options

let eeImage

if (!filter) {
if (format === IMAGE) {
// Single image
eeImage = ee.Image(datasetId)
this.eeScale = getScale(eeImage)
Expand All @@ -119,18 +139,26 @@ class EarthEngineWorker {
this.eeScale = getScale(collection.first())

// Apply array of filters (e.g. period)
filter.forEach(f => {
collection = collection.filter(
ee.Filter[f.type].apply(this, f.arguments)
)
})
collection = applyFilter(collection, filter)

// Mask out clouds from satellite images
if (cloudScore) {
collection = applyCloudMask(collection, cloudScore)
}

eeImage = mosaic
? collection.mosaic() // Composite all images inn a collection (e.g. per country)
: ee.Image(collection.first()) // There should only be one image after applying the filters
if (periodReducer) {
// Apply period reducer (e.g. going from daily to monthly)
eeImage = collection[periodReducer]()
} else if (mosaic) {
// Composite all images inn a collection (e.g. per country)
eeImage = collection.mosaic()
} else {
// There should only be one image after applying the filters
eeImage = ee.Image(collection.first())
}
}

// // Select band (e.g. age group)
// Select band (e.g. age group)
if (band) {
eeImage = eeImage.select(band)

Expand All @@ -143,19 +171,8 @@ class EarthEngineWorker {
}
}

// Mask out 0-values
if (mask) {
eeImage = eeImage.updateMask(eeImage.gt(0))
}

// Run methods on image
if (methods) {
Object.keys(methods).forEach(method => {
if (eeImage[method]) {
eeImage = eeImage[method].apply(eeImage, methods[method])
}
})
}
eeImage = applyMethods(eeImage, methods)

this.eeImage = eeImage

Expand All @@ -164,38 +181,52 @@ class EarthEngineWorker {

// Returns raster tile url for a classified image
getTileUrl() {
const { format, data } = this.options
const { datasetId, format, data, filter, style } = this.options

return new Promise(resolve => {
if (format === 'FeatureCollection') {
const { datasetId } = this.options
return new Promise((resolve, reject) => {
switch (format) {
case FEATURE_COLLECTION: {
let dataset = ee.FeatureCollection(datasetId)

let dataset = ee
.FeatureCollection(datasetId)
.draw(FEATURE_STYLE)
dataset = applyFilter(dataset, filter).draw({
...DEFAULT_FEATURE_STYLE,
...style,
})

if (data) {
dataset = dataset.clipToCollection(
this.getFeatureCollection()
)
}

if (data) {
dataset = dataset.clipToCollection(
this.getFeatureCollection()
dataset.getMap(null, response =>
resolve(response.urlFormat)
)

break
}
case IMAGE:
case IMAGE_COLLECTION: {
// eslint-disable-next-line prefer-const
let { eeImage, params } = getClassifiedImage(
this.getImage(),
this.options
)

dataset.getMap(null, response => resolve(response.urlFormat))
} else {
let { eeImage, params } = getClassifiedImage(
this.getImage(),
this.options
)
if (data) {
eeImage = eeImage.clipToCollection(
this.getFeatureCollection()
)
}

if (data) {
eeImage = eeImage.clipToCollection(
this.getFeatureCollection()
)
}
eeImage
.visualize(params)
.getMap(null, response => resolve(response.urlFormat))

eeImage
.visualize(params)
.getMap(null, response => resolve(response.urlFormat))
break
}
default:
reject(new Error('Unknown format'))
}
})
}
Expand All @@ -219,11 +250,26 @@ class EarthEngineWorker {

const featureCollection = ee
.FeatureCollection(imageCollection)
.select(['system:time_start', 'system:time_end'], null, false)
.select(
['system:time_start', 'system:time_end', 'year'],
null,
false
)

return getInfo(featureCollection)
}

// Returns min and max timestamp for an image collection
getTimeRange(eeId) {
const collection = ee.ImageCollection(eeId)

const range = collection.reduceColumns(ee.Reducer.minMax(), [
'system:time_start',
])

return getInfo(range)
}

// Returns aggregated values for org unit features
async getAggregations(config) {
if (config) {
Expand All @@ -233,20 +279,36 @@ class EarthEngineWorker {
format,
aggregationType,
band,
legend,
useCentroid,
style,
tileScale = DEFAULT_TILE_SCALE,
unmaskAggregation,
} = this.options
const singleAggregation = !Array.isArray(aggregationType)
const useHistogram =
singleAggregation && hasClasses(aggregationType) && legend
const image = await this.getImage()
singleAggregation &&
hasClasses(aggregationType) &&
Array.isArray(style)
const scale = this.eeScale
const collection = this.getFeatureCollection() // TODO: Throw error if no feature collection
const collection = this.getFeatureCollection()
let image = await this.getImage()

// Used for "constrained" WorldPop layers
// We need to unmask the image to get the correct population density
if (unmaskAggregation || typeof unmaskAggregation === 'number') {
image = image.unmask(
typeof unmaskAggregation === 'number'
? unmaskAggregation
: DEFAULT_UNMASK_VALUE
)
}

if (collection) {
if (format === 'FeatureCollection') {
const { datasetId } = this.options
const dataset = ee.FeatureCollection(datasetId)
if (format === FEATURE_COLLECTION) {
const { datasetId, filter } = this.options
let dataset = ee.FeatureCollection(datasetId)

dataset = applyFilter(dataset, filter)

const aggFeatures = collection
.map(feature => {
Expand Down Expand Up @@ -279,11 +341,11 @@ class EarthEngineWorker {
data,
scale: scaleValue,
aggregationType,
legend,
style,
})
)
} else if (!singleAggregation && aggregationType.length) {
const reducer = combineReducers(ee)(aggregationType)
const reducer = combineReducers(aggregationType, useCentroid)
const props = [...aggregationType]

let aggFeatures = image.reduceRegions({
Expand Down Expand Up @@ -315,13 +377,18 @@ class EarthEngineWorker {
aggFeatures = aggFeatures.select(props, null, false)

return getInfo(aggFeatures).then(getFeatureCollectionProperties)
} else throw new Error('Aggregation type is not valid')
} else throw new Error('Missing org unit features')
} else {
throw new Error('Aggregation type is not valid')
}
} else {
throw new Error('Missing org unit features')
}
}
}

// Service Worker not supported in Safari
if (typeof onconnect !== 'undefined') {
// eslint-disable-next-line no-undef
onconnect = evt => expose(EarthEngineWorker, evt.ports[0])
} else {
expose(EarthEngineWorker)
Expand Down
Loading

0 comments on commit c97b9c6

Please sign in to comment.