Skip to content

Commit fc4e33c

Browse files
author
Matt Bertrand
committed
Processor for University of Delaware Air Temperature and Precipitation
1 parent f5ce843 commit fc4e33c

7 files changed

Lines changed: 372 additions & 3 deletions

File tree

dataqs/helpers.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from StringIO import StringIO
3333
import rasterio
3434
from osgeo import gdal, ogr
35+
from osr import SpatialReference
3536
from rasterio.warp import RESAMPLING
3637
from rasterio.warp import calculate_default_transform, reproject
3738
import unicodedata
@@ -88,9 +89,6 @@ def gdal_translate(src_filename, dst_filename, dst_format="GTiff", bands=None,
8889
Convert a raster image with the specified arguments
8990
(as if running from commandline)
9091
"""
91-
from osgeo import gdal
92-
from osr import SpatialReference
93-
9492
if not options:
9593
options = []
9694

dataqs/udatp/__init__.py

Whitespace-only changes.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?xml version="1.0" encoding="UTF-8"?><sld:StyledLayerDescriptor xmlns="http://www.opengis.net/sld" xmlns:sld="http://www.opengis.net/sld" xmlns:ogc="http://www.opengis.net/ogc" xmlns:gml="http://www.opengis.net/gml" version="1.0.0">
2+
<sld:NamedLayer>
3+
<sld:Name>uod_air_mean_401</sld:Name>
4+
<sld:UserStyle>
5+
<sld:Name>uod_air_mean_401</sld:Name>
6+
<sld:Title>uod_air_mean_401</sld:Title>
7+
<sld:FeatureTypeStyle>
8+
<sld:Name>name</sld:Name>
9+
<sld:Rule>
10+
<sld:RasterSymbolizer>
11+
<ColorMap extended="true">
12+
<sld:ColorMapEntry color="#FFFFFF" label="" opacity="0.0" quantity="-999"/>
13+
<sld:ColorMapEntry color="#2b83ba" label="&lt;= -30 °C" opacity="1.0" quantity="-30"/>
14+
<sld:ColorMapEntry color="#6bb0af" label="-20.0 °C" opacity="1.0" quantity="-20"/>
15+
<sld:ColorMapEntry color="#abdda4" label="-10.0 °C" opacity="1.0" quantity="-10"/>
16+
<sld:ColorMapEntry color="#d5eeb1" label="-5.0 °C" opacity="1.0" quantity="-5"/>
17+
<sld:ColorMapEntry color="#ffffbf" label="0 °C" opacity="1.0" quantity="0"/>
18+
<sld:ColorMapEntry color="#fed690" label="5.0 °C" opacity="1.0" quantity="5"/>
19+
<sld:ColorMapEntry color="#fdae61" label="10.0 °C" opacity="1.0" quantity="10"/>
20+
<sld:ColorMapEntry color="#ea633e" label="20.0 °C" opacity="1.0" quantity="20"/>
21+
<sld:ColorMapEntry color="#d7191c" label="&gt;= 30 °C" opacity="1.0" quantity="30"/>
22+
</ColorMap>
23+
</sld:RasterSymbolizer>
24+
</sld:Rule>
25+
</sld:FeatureTypeStyle>
26+
</sld:UserStyle>
27+
</sld:NamedLayer>
28+
</sld:StyledLayerDescriptor>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?xml version="1.0" encoding="UTF-8"?><sld:StyledLayerDescriptor xmlns="http://www.opengis.net/sld" xmlns:sld="http://www.opengis.net/sld" xmlns:ogc="http://www.opengis.net/ogc" xmlns:gml="http://www.opengis.net/gml" version="1.0.0">
2+
<sld:NamedLayer>
3+
<sld:Name>uod_precip_total_401</sld:Name>
4+
<sld:UserStyle>
5+
<sld:Name>uod_precip_total_401</sld:Name>
6+
<sld:Title>uod_precip_total_401</sld:Title>
7+
<sld:FeatureTypeStyle>
8+
<sld:Name>name</sld:Name>
9+
<sld:Rule>
10+
<sld:RasterSymbolizer>
11+
<Opacity>1.0</Opacity>
12+
<ColorMap extended="true">
13+
<sld:ColorMapEntry color="#FFFFFF" label="" opacity="0.0" quantity="-1"/>
14+
<sld:ColorMapEntry color="#2b83ba" label="&lt; 5 mm" opacity="1.0" quantity="0"/>
15+
<sld:ColorMapEntry color="#6bb0af" label="20 mm" opacity="1.0" quantity="20"/>
16+
<sld:ColorMapEntry color="#abdda4" label="50 mm" opacity="1.0" quantity="50"/>
17+
<sld:ColorMapEntry color="#d5eeb1" label="100 mm" opacity="1.0" quantity="100"/>
18+
<sld:ColorMapEntry color="#ffffbf" label="150 mm" opacity="1.0" quantity="150"/>
19+
<sld:ColorMapEntry color="#fed690" label="200 mm" opacity="1.0" quantity="200"/>
20+
<sld:ColorMapEntry color="#fdae61" label="250 mm" opacity="1.0" quantity="250"/>
21+
<sld:ColorMapEntry color="#ea633e" label="300 mm" opacity="1.0" quantity="300"/>
22+
<sld:ColorMapEntry color="#d7191c" label="&gt;= 350 mm" opacity="1.0" quantity="350"/>
23+
</ColorMap>
24+
</sld:RasterSymbolizer>
25+
</sld:Rule>
26+
</sld:FeatureTypeStyle>
27+
</sld:UserStyle>
28+
</sld:NamedLayer>
29+
</sld:StyledLayerDescriptor>

dataqs/udatp/tasks.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
4+
###############################################################################
5+
# Copyright Kitware Inc. and Epidemico Inc.
6+
#
7+
# Licensed under the Apache License, Version 2.0 ( the "License" );
8+
# you may not use this file except in compliance with the License.
9+
# You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing, software
14+
# distributed under the License is distributed on an "AS IS" BASIS,
15+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
# See the License for the specific language governing permissions and
17+
# limitations under the License.
18+
###############################################################################
19+
20+
from __future__ import absolute_import
21+
22+
from celery import shared_task
23+
from dataqs.udatp.udatp import UoDAirTempPrecipProcessor
24+
25+
26+
@shared_task
27+
def udatp_task():
28+
processor = UoDAirTempPrecipProcessor()
29+
processor.run()

dataqs/udatp/tests.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
4+
###############################################################################
5+
# Copyright Kitware Inc. and Epidemico Inc.
6+
#
7+
# Licensed under the Apache License, Version 2.0 ( the "License" );
8+
# you may not use this file except in compliance with the License.
9+
# You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing, software
14+
# distributed under the License is distributed on an "AS IS" BASIS,
15+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
# See the License for the specific language governing permissions and
17+
# limitations under the License.
18+
###############################################################################
19+
20+
import glob
21+
import os
22+
from datetime import date
23+
from django.test import TestCase
24+
from dataqs.udatp.udatp import UoDAirTempPrecipProcessor
25+
from mock import patch
26+
27+
script_dir = os.path.dirname(os.path.realpath(__file__))
28+
29+
30+
def mock_retrbinary_nc(self, name, writer):
31+
"""
32+
Mocks the ftplib.FTP.retrbinary method, writes test image to disk.
33+
"""
34+
with open(os.path.join(script_dir, 'resources/uodtest.nc'), 'rb') as inf:
35+
writer(inf.read())
36+
return None
37+
38+
39+
def mock_retrbinary_tif(self, name, writer):
40+
"""
41+
Mocks the ftplib.FTP.retrbinary method, writes test image to disk.
42+
"""
43+
with open(os.path.join(script_dir, 'resources/uodtest.tif'), 'rb') as inf:
44+
writer(inf.read())
45+
return None
46+
47+
48+
def mock_none(self, *args):
49+
"""
50+
For mocking various FTP methods that should return nothing for tests.
51+
"""
52+
return None
53+
54+
55+
class UoDAirTempPrecipTest(TestCase):
56+
"""
57+
Tests the dataqs.gistemp module. Since each processor is highly
58+
dependent on a running GeoNode instance for most functions, only
59+
independent functions are tested here.
60+
"""
61+
62+
def setUp(self):
63+
self.processor = UoDAirTempPrecipProcessor()
64+
65+
def tearDown(self):
66+
self.processor.cleanup()
67+
68+
@patch('ftplib.FTP', autospec=True)
69+
@patch('ftplib.FTP.retrbinary', mock_retrbinary_nc)
70+
@patch('ftplib.FTP.connect', mock_none)
71+
@patch('ftplib.FTP.login', mock_none)
72+
@patch('ftplib.FTP.cwd', mock_none)
73+
def test_download(self, ftp_mock):
74+
"""
75+
Verify that a file is downloaded
76+
"""
77+
cdf_files = self.processor.download()
78+
for cdf in cdf_files:
79+
self.assertTrue(os.path.exists(cdf))
80+
81+
@patch('ftplib.FTP', autospec=True)
82+
@patch('ftplib.FTP.retrbinary', mock_retrbinary_nc)
83+
@patch('ftplib.FTP.connect', mock_none)
84+
@patch('ftplib.FTP.login', mock_none)
85+
@patch('ftplib.FTP.cwd', mock_none)
86+
def test_cleanup(self, ftp_mock):
87+
self.processor.download()
88+
self.assertNotEqual([], glob.glob(os.path.join(
89+
self.processor.tmp_dir, self.processor.prefix + '*')))
90+
self.processor.cleanup()
91+
self.assertEquals([], glob.glob(os.path.join(
92+
self.processor.tmp_dir, self.processor.prefix + '*')))
93+
94+
def test_date(self):
95+
last_date = self.processor.get_date(1380)
96+
self.assertEquals(last_date, date(2015, 12, 1))
97+
98+
@patch('ftplib.FTP', autospec=True)
99+
@patch('ftplib.FTP.retrbinary', mock_retrbinary_nc)
100+
@patch('ftplib.FTP.connect', mock_none)
101+
@patch('ftplib.FTP.login', mock_none)
102+
@patch('ftplib.FTP.cwd', mock_none)
103+
def test_convert(self, ftp_mock):
104+
cdf_files = self.processor.download()
105+
for cdf in cdf_files:
106+
self.processor.convert(cdf)
107+
self.assertNotEqual([], glob.glob(cdf.replace(
108+
'.nc', '.classic.lng.nc')))
109+
110+
@patch('ftplib.FTP', autospec=True)
111+
@patch('ftplib.FTP.retrbinary', mock_retrbinary_tif)
112+
@patch('ftplib.FTP.connect', mock_none)
113+
@patch('ftplib.FTP.login', mock_none)
114+
@patch('ftplib.FTP.cwd', mock_none)
115+
def test_extract_band(self, ftp_mock):
116+
cdf = self.processor.download()[0]
117+
tif = cdf.replace('.nc', '.tif')
118+
self.processor.extract_band(cdf, 1, tif)
119+
self.assertTrue(os.path.isfile(tif))

dataqs/udatp/udatp.py

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
4+
###############################################################################
5+
# Copyright Kitware Inc. and Epidemico Inc.
6+
#
7+
# Licensed under the Apache License, Version 2.0 ( the "License" );
8+
# you may not use this file except in compliance with the License.
9+
# You may obtain a copy of the License at
10+
#
11+
# http://www.apache.org/licenses/LICENSE-2.0
12+
#
13+
# Unless required by applicable law or agreed to in writing, software
14+
# distributed under the License is distributed on an "AS IS" BASIS,
15+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
# See the License for the specific language governing permissions and
17+
# limitations under the License.
18+
###############################################################################
19+
20+
from __future__ import absolute_import
21+
import logging
22+
import os
23+
import re
24+
import shutil
25+
from datetime import date
26+
from ftplib import FTP
27+
28+
from dateutil.relativedelta import relativedelta
29+
from dataqs.processor_base import GeoDataMosaicProcessor, GS_DATA_DIR, \
30+
GS_TMP_DIR
31+
from dataqs.helpers import get_band_count, gdal_translate, \
32+
nc_convert, style_exists, cdo_fixlng
33+
34+
logger = logging.getLogger("dataqs.processors")
35+
script_dir = os.path.dirname(os.path.realpath(__file__))
36+
37+
38+
class UoDAirTempPrecipProcessor(GeoDataMosaicProcessor):
39+
"""
40+
Processor for Land-Ocean Temperature Index, ERSSTv4, 1200km smoothing
41+
from the NASA Goddard Institute for Space Studies' Surface Temperature
42+
Analysis (GISTEMP).
43+
More info at http://data.giss.nasa.gov/gistemp/
44+
"""
45+
prefix = "uod_"
46+
base_url = "ftp.cdc.noaa.gov"
47+
48+
layers = {
49+
'air.mon.mean.v401.nc': {
50+
'title': 'U. Delaware Monthly Mean Air Temperature 1901 - 2015',
51+
'name': 'uod_air_mean_401'
52+
},
53+
'precip.mon.total.v401.nc': {
54+
'title': 'U. Delaware Monthly Total Precipitation 1901 - 2015',
55+
'name': 'uod_precip_total_401'
56+
}
57+
58+
}
59+
abstract = """Cort Willmott & Kenji Matsuura of the University of Delaware
60+
have put data together from a large number of stations, both from the GHCN2
61+
(Global Historical Climate Network) and, more extensively, from the archive
62+
of Legates & Willmott. More details can be found here for temperature and
63+
here for precipitation. The result is a monthly climatology of precipitation
64+
and air temperature, both at the surface, and a time series, spanning 1900
65+
to 2010, of monthly mean surface air temperatures, and monthly total
66+
precipitation. It is land-only in coverage, and complements the ICOADS
67+
(International Comprehensive Ocean-Atmosphere Data Set) data set well. For a
68+
complete description of the data as given by the providers, related
69+
datasets and references to relevant papers please see their web pages at the
70+
University of Delaware..
71+
72+
Source: http://www.esrl.noaa.gov/psd/data/gridded/data.UDel_AirT_Precip.html
73+
74+
The displayed image is based on the most current month.
75+
76+
Citations:
77+
- Willmott, C. J. and K. Matsuura (2001) Terrestrial Air Temperature and
78+
Precipitation: Monthly and Annual Time Series (1950 - 1999),
79+
http://climate.geog.udel.edu/~climate/html_pages/README.ghcn_ts2.html.
80+
- UDel_AirT_Precip data provided by the NOAA/OAR/ESRL PSD, Boulder,
81+
Colorado, USA, from their Web site at http://www.esrl.noaa.gov/psd/
82+
"""
83+
84+
def download(self, tmp_dir=GS_TMP_DIR):
85+
"""
86+
Retrieve NetCDF files via FTP
87+
:param tmp_dir: Temp directory to store files
88+
:return: list of saved output files
89+
"""
90+
ftp = FTP(self.base_url)
91+
ftp.login('anonymous', 'anonymous')
92+
ftp.cwd('/Datasets/udel.airt.precip/')
93+
outfiles = []
94+
for file in self.layers.keys():
95+
outfile = os.path.join(tmp_dir, '{}{}'.format(self.prefix, file))
96+
with open(outfile, 'wb') as output:
97+
ftp.retrbinary('RETR %s' % file, output.write)
98+
outfiles.append(outfile)
99+
return outfiles
100+
101+
def convert(self, nc_file):
102+
nc_transform = nc_convert(nc_file)
103+
cdo_transform = cdo_fixlng(nc_transform)
104+
return cdo_transform
105+
106+
def extract_band(self, tif, band, outname):
107+
outfile = os.path.join(self.tmp_dir, outname)
108+
gdal_translate(tif, outfile, bands=[band],
109+
projection='EPSG:4326',
110+
options=['TILED=YES', 'COMPRESS=LZW'])
111+
return outfile
112+
113+
def get_date(self, months):
114+
start_month = date(1901, 1, 1)
115+
return start_month + relativedelta(months=months - 1)
116+
117+
def run(self):
118+
"""
119+
Retrieve and process the latest NetCDF file.
120+
"""
121+
cdf_files = self.download()
122+
for cdf in cdf_files:
123+
cdf_file = self.convert(cdf)
124+
bands = get_band_count(cdf_file)
125+
key = os.path.basename(cdf).lstrip(self.prefix)
126+
print(key)
127+
layer_name = self.layers[key]['name']
128+
img_list = self.get_mosaic_filenames(layer_name)
129+
for band in range(1, bands + 1):
130+
band_date = re.sub('[\-\.]+', '',
131+
self.get_date(band).isoformat())
132+
img_name = '{}_{}T000000000Z.tif'.format(layer_name, band_date)
133+
if img_name not in img_list:
134+
band_tif = self.extract_band(cdf_file, band, img_name)
135+
dst_file = self.data_dir.format(gsd=GS_DATA_DIR,
136+
ws=self.workspace,
137+
layer=layer_name,
138+
file=img_name)
139+
dst_dir = os.path.dirname(dst_file)
140+
if not os.path.exists(dst_dir):
141+
os.makedirs(dst_dir)
142+
if dst_file.endswith('.tif'):
143+
shutil.move(os.path.join(self.tmp_dir, band_tif),
144+
dst_file)
145+
self.post_geoserver(dst_file, layer_name)
146+
147+
if not style_exists(layer_name):
148+
with open(os.path.join(script_dir, 'resources/{}.sld'.format(
149+
layer_name))) as sld:
150+
print(layer_name,
151+
os.path.join(script_dir,
152+
'resources/{}.sld'.format(layer_name)))
153+
self.set_default_style(layer_name, layer_name, sld.read())
154+
self.update_geonode(layer_name,
155+
title=self.layers[key]['title'],
156+
description=self.abstract,
157+
store=layer_name,
158+
bounds=('-180.0', '180.0', '-90.0', '90.0',
159+
'EPSG:4326'))
160+
self.truncate_gs_cache(layer_name)
161+
self.cleanup()
162+
163+
164+
if __name__ == '__main__':
165+
processor = UoDAirTempPrecipProcessor()
166+
processor.run()

0 commit comments

Comments
 (0)