Merge pull request #69 from Derpthemeus/automatic-base-eto

Add baseline ETo endpoint
This commit is contained in:
Samer Albahra
2019-07-02 15:09:27 -07:00
committed by GitHub
10 changed files with 642 additions and 2 deletions

4
.gitignore vendored
View File

@@ -6,3 +6,7 @@ npm-debug.log
.idea .idea
js js
weather.zip weather.zip
baselineEToData/*.bin
baselineEToData/*.png
baselineEToData/*.tif
baselineEToData/dataPreparer[.exe]

View File

@@ -0,0 +1,4 @@
# Don't send images to the Docker daemon since they are very large.
*.bin
*.tif
*.png

View File

@@ -0,0 +1,12 @@
FROM alpine:3.10
VOLUME /output/
ENTRYPOINT ["/entrypoint.sh"]
# Default to 20 passes.
CMD ["20"]
COPY dataPreparer.c /dataPreparer.c
COPY prepareData.sh /prepareData.sh
COPY entrypoint.sh /entrypoint.sh
RUN apk --update add imagemagick gcc libc-dev && chmod +x /entrypoint.sh /prepareData.sh

57
baselineEToData/README.md Normal file
View File

@@ -0,0 +1,57 @@
# Baseline ETo Data
The baseline ETo endpoint determines the baseline ETo for a location by reading a file generated using data from [MOD16](https://www.ntsg.umt.edu/project/modis/mod16.php).
The data is stored in a binary file that has 4 key differences from the GeoTIFF provided by MOD16:
* The bit depth is decreased from 16 bits to 8 bits to reduce the file size.
* Missing data is interpolated using the values of surrounding pixels.
The MOD16 dataset does not contain data for locations that don't have vegetated land cover (such as urban environments), which can be problematic since many users may set their location to nearby cities.
* The data is stored in an uncompressed format so that geographic coordinates can be mapped to the offset of the corresponding pixel in the file.
This means the file can be stored on disk instead of memory, and the pixel for a specified location can be quickly accessed by seeking to the calculated offset in the file.
* A metadata header that contains parameters about the data used to create the file (such as the image dimensions and instructions on how to map a pixel value to an annual ETo value) is added to the beginning of the file.
This header enables the weather server to use datafiles generated from future versions of the MOD16 dataset (even if these versions modify some of these parameters).
The datafile is to be stored as `baselineEToData/Baseline_ETo_Data.bin`.
The datafile is not included in the repo because it is very large (62 MB zipped, 710 MB uncompressed), but it [can be downloaded separately](http://www.mediafire.com/file/n7z32dbdvgyupk3/Baseline_ETo_Data.zip/file).
This file was generated by making 20 [passes](#passes) over the data from 2000-2013 in the MOD16A3 dataset.
Alternatively, it can be generated by running the data preparer program yourself.
## Preparing the Datafile
Since TIFF files do not support streaming, directly using the GeoTIFF images from MOD16 would require loading the entire image into memory.
To avoid this, the file must first be converted to a binary format so the pixels in the image can be read row-by-row.
Running `./prepareData.sh <PASSES>` will download the required image files using [wget](https://www.gnu.org/software/wget/), convert them to a binary format using [ImageMagick](https://imagemagick.org/index.php), compile the program with [gcc](https://gcc.gnu.org/), and run it .
This process can be simplified by using the included Dockerfile that will perform all of these steps inside a container.
The Dockerfile can be used by running `docker build -t baseline-eto-data-preparer . && docker run --rm -v $(pwd):/output baseline-eto-data-preparer <PASSES>`.
The `<PASSES>` argument is used to control how much the program should attempt to fill in missing data.
(#passes)
### Passes
The program fills in missing data by making several successive passes over the entire image, attempting to fill in each missing pixel on each pass.
The value for each missing pixel is interpolated using the values of pixels in the surrounding 5x5 square, and missing pixels that don't have enough data available will be skipped.
However, these pixels may be filled in on a later pass if future passes are able to fill in the surrounding pixels.
Running the program with a higher number of passes will fill in more missing data, but the program will take longer to run and each subsequent pass becomes less accurate (since the interpolations will be based on interpolated data).
## File Format
The data will be saved in a binary format beginning with the a 32 byte big-endian header in the following format:
| Offset | Type | Description |
| --- | --- | --- |
| 0 | uint8 | File format version |
| 1-4 | uint32 | Image width (in pixels) |
| 5-8 | uint32 | Image height (in pixels) |
| 9 | uint8 | Pixel bit depth (the only bit depth currently supported is 8) |
| 10-13 | float | Minimum ETo |
| 14-17 | float | Scaling factor |
| 18-32 | N/A | May be used in future versions |
The header is immediately followed by a `IMAGE_WIDTH * IMAGE_HEIGHT` bytes of data corresponding to the pixels in the image in row-major order.
Each pixel is interpreted as an 8 bit unsigned integer, and the average annual potential ETo at that location is `PIXEL * SCALING_FACTOR + MINIMUM_ETO` inches/year.
A value of `255` is special and indicates that no data is available for that location.
## Notes
* Although the [MOD16 documentation]((http://files.ntsg.umt.edu/data/NTSG_Products/MOD16/MOD16UsersGuide_V1.6_2018Aug.docx)) states that several pixel values are used to indicate the land cover type for locations that are missing data, the image actually only uses the value `65535`.
The program handles this by using a [mask image of water bodies](https://static1.squarespace.com/static/58586fa5ebbd1a60e7d76d3e/t/59394abb37c58179160775fa/1496926933082/Ocean_Mask.png) so it can fill in pixels for urban environments without also filling in data for oceans.
* The map uses an [equirectangular projection](https://en.wikipedia.org/wiki/Equirectangular_projection) with the northernmost 10 degrees and southernmost 30 degrees cropped off.

View File

@@ -0,0 +1,354 @@
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <time.h>
#define IMAGE_WIDTH 43200
#define IMAGE_HEIGHT 16800
#define MASK_WIDTH 10800
#define MASK_HEIGHT 5400
#define OUTPUT_FILE_TEMPLATE "./Baseline_ETo_Data-Pass_%d.bin"
#define FILENAME_MAX_LENGTH 40
#define HEADER_SIZE 32
long unsigned CROPPED_TOP_PIXELS = (MASK_WIDTH * MASK_HEIGHT * 10 / 180);
// These will be set by findPixelRange().
uint16_t minPixelValue = 0;
uint16_t maxPixelValue = 0xFFFF;
double bitReductionFactor = 256;
/** Copies the big-endian byte representation of the specified value into the specified buffer. */
void copyBytes(void* input, uint8_t* output, int unsigned length) {
int unsigned isBigEndian = 1;
isBigEndian = *((uint8_t*)(&isBigEndian)) == 0;
for (int unsigned i = 0; i < length; i++) {
int unsigned index = isBigEndian ? i : length - i - 1;
output[i] = *((uint8_t*) input + index);
}
}
/**
* Write file header to the specified buffer. The header format is documented in the README.
*/
void setHeader(uint8_t *header) {
for (int unsigned i = 0; i < HEADER_SIZE; i++) {
header[i] = 0;
}
uint32_t width = IMAGE_WIDTH;
uint32_t height = IMAGE_HEIGHT;
// originally 0.1, then multiplied by a value to compensate for the bit depth reduction and divided by 25.4 to convert to inches.
float scalingFactor = 0.1 * bitReductionFactor / 25.4;
float minimumETo = minPixelValue * 0.1 / 25.4;
// Version
header[0] = 1;
// Width
copyBytes(&width, &(header[1]), 4);
// Height
copyBytes(&height, &(header[5]), 4);
// Bit depth
header[9] = 8;
// Minimum ETo
copyBytes(&minimumETo, &(header[10]), 4);
// Scaling factor
copyBytes(&scalingFactor, &(header[14]), 4);
}
/**
* Calculates the minimum and maximum pixel values used in the image. These values can be used to optimally reduce the
* bit depth by mapping the minimum value to 0 and the maximum value to 254 (reserving 255 for fill pixels) and linearly
* interpolating the rest of the values.
*/
void findPixelRange(uint16_t* minPtr, uint16_t* maxPtr, double* bitReductionFactorPtr) {
time_t startTime = clock();
uint16_t minValue = 0xFFFF;
uint16_t maxValue = 0;
FILE* inputFile = fopen("./MOD16A3_PET_2000_to_2013_mean.bin", "rb");
if (inputFile == NULL) {
printf("An error occurred opening image file while finding min/max value.\n");
exit(1);
}
uint16_t buffer[IMAGE_WIDTH];
for (int unsigned y = 0; y < IMAGE_HEIGHT; y++) {
if (y % 1000 == 0) {
printf("Finding pixel range on row %d...\n", y);
}
fread(buffer, 2, IMAGE_WIDTH, inputFile);
if (ferror(inputFile)) {
printf("An error occurred reading image row %d while finding min/max values.\n", y);
exit(1);
}
if (feof(inputFile)) {
printf("Encountered EOF reading image row %d while finding min/max values.\n", y);
exit(1);
}
for (unsigned int x = 0; x < IMAGE_WIDTH; x++) {
uint16_t pixel = buffer[x];
// Skip fill pixels.
if (pixel > 65528) {
continue;
}
minValue = pixel < minValue ? pixel : minValue;
maxValue = pixel > maxValue ? pixel : maxValue;
}
}
*minPtr = minValue;
*maxPtr = maxValue;
*bitReductionFactorPtr = (maxValue - minValue + 1) / (float) 256;
fclose(inputFile);
printf("Found pixel range in %.1f seconds. Min value: %d\t Max value: %d\t Bit reduction factor:%f.\n", (clock() - startTime) / (float) CLOCKS_PER_SEC, minValue, maxValue, *bitReductionFactorPtr);
}
/** Reduces the image bit depth from 16 bits to 8 bits. */
void reduceBitDepth() {
clock_t startTime = clock();
FILE* originalFile = fopen("./MOD16A3_PET_2000_to_2013_mean.bin", "rb");
if (originalFile == NULL) {
printf("An error occurred opening input image file while reducing bit depth.\n");
exit(1);
}
char* reducedFileName = malloc(FILENAME_MAX_LENGTH);
snprintf(reducedFileName, FILENAME_MAX_LENGTH, OUTPUT_FILE_TEMPLATE, 0);
FILE* reducedFile = fopen(reducedFileName, "wb");
if (reducedFile == NULL) {
printf("An error occurred opening output image file while reducing bit depth.\n");
exit(1);
}
// Write the file header.
uint8_t header[32];
setHeader(header);
fwrite(header, 1, 32, reducedFile);
if (ferror(reducedFile)) {
printf("An error occurred writing file header while reducing bit depth.\n");
exit(1);
}
uint16_t inputBuffer[IMAGE_WIDTH];
uint8_t outputBuffer[IMAGE_WIDTH];
for (int unsigned y = 0; y < IMAGE_HEIGHT; y++) {
if (y % 1000 == 0) {
printf("Reducing bit depth on row %d...\n", y);
}
fread(inputBuffer, 2, IMAGE_WIDTH, originalFile);
if (ferror(originalFile)) {
printf("An error occurred reading row %d while reducing bit depth.\n", y);
exit(1);
}
if (feof(originalFile)) {
printf("Encountered EOF reading row %d while reducing bit depth.\n", y);
exit(1);
}
for (unsigned int x = 0; x < IMAGE_WIDTH; x++) {
uint16_t originalPixel = inputBuffer[x];
uint8_t reducedPixel = originalPixel > 65528 ? 255 : (uint8_t) ((originalPixel - minPixelValue) / bitReductionFactor);
outputBuffer[x] = reducedPixel;
}
fwrite(outputBuffer, 1, IMAGE_WIDTH, reducedFile);
if (ferror(reducedFile)) {
printf("An error occurred writing row %d while reducing bit depth.\n", y);
exit(1);
}
}
fclose(reducedFile);
fclose(originalFile);
printf("Finished reducing bit depth in %.1f seconds.\n", (clock() - startTime) / (double) CLOCKS_PER_SEC);
}
void fillMissingPixels(int unsigned pass) {
clock_t startTime = clock();
char* inputFileName = malloc(FILENAME_MAX_LENGTH);
snprintf(inputFileName, FILENAME_MAX_LENGTH, OUTPUT_FILE_TEMPLATE, pass - 1);
FILE* inputFile = fopen(inputFileName, "rb");
if (inputFile == NULL) {
printf("An error occurred opening input image file on pass %d.\n", pass);
exit(1);
}
char* outputFileName = malloc(FILENAME_MAX_LENGTH);
snprintf(outputFileName, FILENAME_MAX_LENGTH, OUTPUT_FILE_TEMPLATE, pass);
FILE* outputFile = fopen(outputFileName, "wb");
if (outputFile == NULL) {
printf("An error occurred opening output image file on pass %d.\n", pass);
exit(1);
}
FILE* maskFile = fopen("./Ocean_Mask.bin", "rb");
if (maskFile == NULL) {
printf("An error occurred opening mask image on pass %d.\n", pass);
exit(1);
}
uint8_t outputBuffer[IMAGE_WIDTH];
// Skip the header.
fseek(inputFile, 32, SEEK_SET);
if (ferror(inputFile)) {
printf("An error occurred reading header on pass %d.\n", pass);
exit(1);
}
if (feof(inputFile)) {
printf("Encountered EOF reading header on pass %d.\n", pass);
exit(1);
}
// Write the file header.
uint8_t header[32];
setHeader(header);
fwrite(header, 1, 32, outputFile);
if (ferror(outputFile)) {
printf("An error occurred writing file header on pass %d.\n", pass);
exit(1);
}
uint8_t* rows[5] = {0, 0, 0, 0, 0};
// Read the first 2 rows.
for (int unsigned rowIndex = 3; rowIndex < 5; rowIndex++) {
uint8_t* row = (uint8_t*) malloc(IMAGE_WIDTH);
fread(row, 1, IMAGE_WIDTH, inputFile);
if (ferror(inputFile)) {
printf("An error occurred reading image row %d on pass %d.\n", rowIndex - 3, pass);
exit(1);
}
if (feof(inputFile)) {
printf("Encountered EOF reading image row %d on pass %d.\n", rowIndex - 3, pass);
exit(1);
}
rows[rowIndex] = row;
}
long unsigned fixedPixels = 0;
long unsigned unfixablePixels = 0;
long unsigned waterPixels = 0;
for (int unsigned y = 0; y < IMAGE_HEIGHT; y++) {
if (y % 1000 == 0) {
printf("Filling missing pixels on pass %d row %d.\n", pass, y);
}
// Read a row from the mask.
uint8_t maskRow[MASK_WIDTH];
int unsigned maskOffset = y / (IMAGE_WIDTH / MASK_WIDTH) * MASK_WIDTH + CROPPED_TOP_PIXELS;
fseek(maskFile, maskOffset, SEEK_SET);
fread(maskRow, 1, MASK_WIDTH, maskFile);
if (ferror(maskFile)) {
printf("An error occurred reading mask at offset %d on pass %d.\n", maskOffset, pass);
exit(1);
}
if (feof(maskFile)) {
printf("Encountered EOF reading mask at offset %d on pass %d.\n", maskOffset, pass);
exit(1);
}
// Free the oldest row.
free(rows[0]);
// Shift the previous rows back.
for (int unsigned rowIndex = 1; rowIndex < 5; rowIndex++) {
rows[rowIndex - 1] = rows[rowIndex];
}
// Read the next row if one exists.
if (y < IMAGE_HEIGHT - 2) {
uint8_t* row = malloc(IMAGE_WIDTH);
fread(row, 1, IMAGE_WIDTH, inputFile);
if (ferror(inputFile)) {
printf("An error occurred reading image row %d on pass %d.\n", y + 2, pass);
exit(1);
}
if (feof(inputFile)) {
printf("Encountered EOF reading image row %d on pass %d,\n", y + 2, pass);
exit(1);
}
rows[4] = row;
}
for (unsigned int x = 0; x < IMAGE_WIDTH; x++) {
uint8_t pixel = *(rows[2] +x);
// Skip water pixels.
if (maskRow[x / (IMAGE_WIDTH / MASK_WIDTH)] > 128) {
if (pixel == 255) {
int unsigned totalWeight = 0;
float neighborTotal = 0;
for (int i = -2; i <= 2; i++) {
for (int j = -2; j <= 2; j++) {
int neighborX = x + i;
int neighborY = y + j;
if (neighborX < 0 || neighborX >= IMAGE_WIDTH || neighborY < 0 || neighborY >= IMAGE_HEIGHT) {
continue;
}
uint8_t neighbor = *(rows[2 + j] + neighborX);
if (neighbor == 255) {
continue;
}
int unsigned weight = 5 - (abs(i) + abs(j));
neighborTotal += weight * neighbor;
totalWeight += weight;
}
}
if (totalWeight > 11) {
pixel = (uint8_t) (neighborTotal / totalWeight);
fixedPixels++;
} else {
unfixablePixels++;
}
}
} else {
waterPixels++;
}
outputBuffer[x] = pixel;
}
fwrite(outputBuffer, 1, IMAGE_WIDTH, outputFile);
if (ferror(outputFile)) {
printf("An error occurred writing row %d on pass %d.\n", y, pass);
exit(1);
}
}
fclose(outputFile);
fclose(inputFile);
fclose(maskFile);
printf("Finished pass %d in %f seconds. Fixed pixels: %ld\t Unfixable pixels: %ld\t Water pixels: %ld.\n", pass, (clock() - startTime) / (double) CLOCKS_PER_SEC, fixedPixels, unfixablePixels, waterPixels);
}
int main(int argc, char* argv[]) {
if (argc != 2) {
printf("Proper usage: %s <passes>\n", argv[0]);
}
int unsigned passes = strtol(argv[1], NULL, 10);
if (passes <= 0) {
printf("passes argument must be a positive integer.\n");
exit(1);
}
findPixelRange(&minPixelValue, &maxPixelValue, &bitReductionFactor);
reduceBitDepth();
for (int unsigned i = 1; i <= passes; i++) {
fillMissingPixels(i);
}
return 0;
}

View File

@@ -0,0 +1,4 @@
#!/bin/sh
/prepareData.sh $1
# Move the last pass to the output directory.
mv $(ls Baseline_ETo_Data-Pass_*.bin | tail -n1) /output/Baseline_ETo_Data.bin

View File

@@ -0,0 +1,18 @@
#!/bin/sh
echo "Compiling dataPreparer.c..."
gcc -std=c99 -o dataPreparer dataPreparer.c
echo "Downloading ocean mask image..."
wget http://static1.squarespace.com/static/58586fa5ebbd1a60e7d76d3e/t/59394abb37c58179160775fa/1496926933082/Ocean_Mask.png
echo "Converting ocean mask image to binary format..."
magick Ocean_Mask.png -depth 8 gray:Ocean_Mask.bin
echo "Downloading MOD16 GeoTIFF..."
wget http://files.ntsg.umt.edu/data/NTSG_Products/MOD16/MOD16A3.105_MERRAGMAO/Geotiff/MOD16A3_PET_2000_to_2013_mean.tif
echo "Converting MOD16 GeoTIFF to binary format..."
magick MOD16A3_PET_2000_to_2013_mean.tif -depth 16 gray:MOD16A3_PET_2000_to_2013_mean.bin
echo "Preparing data..."
./dataPreparer $1

182
routes/baselineETo.ts Normal file
View File

@@ -0,0 +1,182 @@
/* This script requires the file Baseline_ETo_Data.bin file to be created in the baselineEToData directory. More
* information about this is available in /baselineEToData/README.md.
*/
import * as express from "express";
import * as fs from "fs";
import { GeoCoordinates } from "../types";
import { getParameter, resolveCoordinates } from "./weather";
const DATA_FILE = __dirname + "/../../baselineEToData/Baseline_ETo_Data.bin";
let FILE_META: FileMeta;
readFileHeader().then( ( fileMeta ) => {
FILE_META = fileMeta;
console.log( "Loaded baseline ETo data." );
} ).catch( ( err ) => {
console.error( "An error occurred while reading the annual ETo data file header. Baseline ETo endpoint will be unavailable.", err );
} );
export const getBaselineETo = async function( req: express.Request, res: express.Response ) {
const location: string = getParameter( req.query.loc );
// Error if the file meta was not read (either the file is still being read or an error occurred and it could not be read).
if ( !FILE_META ) {
res.status( 503 ).send( "Baseline ETo calculation is currently unavailable." );
return;
}
// Attempt to resolve provided location to GPS coordinates.
let coordinates: GeoCoordinates;
try {
coordinates = await resolveCoordinates( location );
} catch (err) {
res.status( 404 ).send( `Error: Unable to resolve coordinates for location (${ err })` );
return;
}
let eto: number;
try {
eto = await calculateAverageDailyETo( coordinates );
} catch ( err ) {
/* Use a 500 error code if a more appropriate error code is not specified, and prefer the error message over the
full error object if a message is defined. */
res.status( err.code || 500 ).send( err.message || err );
return;
}
res.status( 200 ).json( {
eto: Math.round( eto * 1000 ) / 1000
} );
};
/**
* Retrieves the average daily potential ETo for the specified location.
* @param coordinates The location to retrieve the ETo for.
* @return A Promise that will be resolved with the average potential ETo (in inches per day), or rejected with an error
* (which may include a message and the appropriate HTTP status code to send the user) if the ETo cannot be retrieved.
*/
async function calculateAverageDailyETo( coordinates: GeoCoordinates ): Promise< number > {
// Convert geographic coordinates into image coordinates.
const x = Math.floor( FILE_META.origin.x + FILE_META.width * coordinates[ 1 ] / 360 );
// Account for the 30+10 cropped degrees.
const y = Math.floor( FILE_META.origin.y - FILE_META.height * coordinates[ 0 ] / ( 180 - 30 - 10 ) );
// The offset (from the start of the data block) of the relevant pixel.
const offset = y * FILE_META.width + x;
/* Check if the specified coordinates were invalid or correspond to a part of the map that was cropped. */
if ( offset < 0 || offset > FILE_META.width * FILE_META.height ) {
throw { message: "Specified location is out of bounds.", code: 404 };
}
let byte: number;
try {
// Skip the 32 byte header.
byte = await getByteAtOffset( offset + 32 );
} catch ( err ) {
console.error( `An error occurred while reading the baseline ETo data file for coordinates ${ coordinates }:`, err );
throw { message: "An unexpected error occurred while retrieving the baseline ETo for this location.", code: 500 }
}
// The maximum value indicates that no data is available for this point.
if ( ( byte === ( 1 << FILE_META.bitDepth ) - 1 ) ) {
throw { message: "ETo data is not available for this location.", code: 404 };
}
return ( byte * FILE_META.scalingFactor + FILE_META.minimumETo ) / 365;
}
/**
* Returns the byte at the specified offset in the baseline ETo data file.
* @param offset The offset from the start of the file (the start of the header, not the start of the data block).
* @return A Promise that will be resolved with the unsigned representation of the byte at the specified offset, or
* rejected with an Error if an error occurs.
*/
function getByteAtOffset( offset: number ): Promise< number > {
return new Promise( ( resolve, reject ) => {
const stream = fs.createReadStream( DATA_FILE, { start: offset, end: offset } );
stream.on( "error", ( err ) => {
reject( err );
} );
// There's no need to wait for the "end" event since the "data" event will contain the single byte being read.
stream.on( "data", ( data ) => {
resolve( data[ 0 ] );
} );
} );
}
/**
* Parses information from the baseline ETo data file from the file header. The header format is documented in the README.
* @return A Promise that will be resolved with the parsed header information, or rejected with an error if the header
* is invalid or cannot be read.
*/
function readFileHeader(): Promise< FileMeta > {
return new Promise( ( resolve, reject) => {
const stream = fs.createReadStream( DATA_FILE, { start: 0, end: 32 } );
const headerArray: number[] = [];
stream.on( "error", ( err ) => {
reject( err );
} );
stream.on( "data", ( data: number[] ) => {
headerArray.push( ...data );
} );
stream.on( "end", () => {
const buffer = Buffer.from( headerArray );
const version = buffer.readUInt8( 0 );
if ( version !== 1 ) {
reject( `Unsupported data file version ${ version }. The maximum supported version is 1.` );
return;
}
const width = buffer.readUInt32BE( 1 );
const height = buffer.readUInt32BE( 5 );
const fileMeta: FileMeta = {
version: version,
width: width,
height: height,
bitDepth: buffer.readUInt8( 9 ),
minimumETo: buffer.readFloatBE( 10 ),
scalingFactor: buffer.readFloatBE( 14 ),
origin: {
x: Math.floor( width / 2 ),
// Account for the 30+10 cropped degrees.
y: Math.floor( height / ( 180 - 10 - 30) * ( 90 - 10 ) )
}
};
if ( fileMeta.bitDepth === 8 ) {
resolve( fileMeta );
} else {
reject( "Bit depths other than 8 are not currently supported." );
}
} );
} );
}
/** Information about the data file parsed from the file header. */
interface FileMeta {
version: number;
/** The width of the image (in pixels). */
width: number;
/** The height of the image (in pixels). */
height: number;
/** The number of bits used for each pixel. */
bitDepth: number;
/** The ETo that a pixel value of 0 represents (in inches/year). */
minimumETo: number;
/** The ratio of an increase in pixel value to an increase in ETo (in inches/year). */
scalingFactor: number;
/**
* The pixel coordinates of the geographic coordinates origin. These coordinates are off-center because the original
* image excludes the northernmost 10 degrees and the southernmost 30 degrees.
*/
origin: {
x: number;
y: number;
};
}

View File

@@ -38,7 +38,7 @@ const ADJUSTMENT_METHOD: { [ key: number ] : AdjustmentMethod } = {
* @return A promise that will be resolved with the coordinates of the best match for the specified location, or * @return A promise that will be resolved with the coordinates of the best match for the specified location, or
* rejected with an error message if unable to resolve the location. * rejected with an error message if unable to resolve the location.
*/ */
async function resolveCoordinates( location: string ): Promise< GeoCoordinates > { export async function resolveCoordinates( location: string ): Promise< GeoCoordinates > {
if ( !location ) { if ( !location ) {
throw "No location specified"; throw "No location specified";
@@ -444,7 +444,7 @@ function ipToInt( ip: string ): number {
* @param parameter An array of parameters or a single parameter value. * @param parameter An array of parameters or a single parameter value.
* @return The first element in the array of parameter or the single parameter provided. * @return The first element in the array of parameter or the single parameter provided.
*/ */
function getParameter( parameter: string | string[] ): string { export function getParameter( parameter: string | string[] ): string {
if ( Array.isArray( parameter ) ) { if ( Array.isArray( parameter ) ) {
parameter = parameter[0]; parameter = parameter[0];
} }

View File

@@ -6,6 +6,7 @@ import * as cors from "cors";
import * as weather from "./routes/weather"; import * as weather from "./routes/weather";
import * as local from "./routes/weatherProviders/local"; import * as local from "./routes/weatherProviders/local";
import * as baselineETo from "./routes/baselineETo";
let host = process.env.HOST || "127.0.0.1", let host = process.env.HOST || "127.0.0.1",
port = parseInt( process.env.PORT ) || 3000; port = parseInt( process.env.PORT ) || 3000;
@@ -32,6 +33,10 @@ app.get( "/", function( req, res ) {
res.send( process.env.npm_package_description + " v" + process.env.npm_package_version ); res.send( process.env.npm_package_description + " v" + process.env.npm_package_version );
} ); } );
// Handle requests matching /baselineETo
app.options( /baselineETo/, cors() );
app.get( /baselineETo/, cors(), baselineETo.getBaselineETo );
// Handle 404 error // Handle 404 error
app.use( function( req, res ) { app.use( function( req, res ) {
res.status( 404 ); res.status( 404 );