Skip to content

Commit bafeb2e

Browse files
authored
Merge pull request #29 from OpenGeoscience/gistemp
Processor for NASA GISTEMP temperature anomaly data
2 parents f235808 + aa7d96b commit bafeb2e

16 files changed

Lines changed: 286 additions & 4 deletions

File tree

ansible/roles/dataqs/tasks/main.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,12 @@
8686
command: "{{ app_code_dir }}/venvs/geonode/bin/python {{app_code_dir}}/venvs/geonode/src/dataqs/dataqs/gfms/gfms.py"
8787
ignore_errors: yes
8888

89+
- include: geoserver_permissions.yml
90+
91+
- name: Create the gistemp coverage store
92+
command: "{{ app_code_dir }}/venvs/geonode/bin/python {{app_code_dir}}/venvs/geonode/src/dataqs/dataqs/gistemp/gistemp.py"
93+
ignore_errors: yes
94+
8995
- name: create geoserver data directory
9096
file: path=/var/lib/tomcat7/webapps/geoserver/data/data/geonode/forecast_io_airtemp recurse=yes owner=tomcat7 group=tomcat7 state=directory mode=g+rws
9197
sudo: yes

ansible/roles/dataqs/templates/dataq_settings.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
'dataqs.usgs_quakes',
3535
'dataqs.wqp',
3636
'dataqs.hifld',
37+
'dataqs.gistemp',
3738
)
3839

3940
# CELERY SETTINGS
@@ -121,6 +122,11 @@
121122
'task': 'dataqs.hifld.tasks.hifld_task',
122123
'schedule': crontab(day_of_week='sunday', hour=12, minute=0),
123124
'args': ()
125+
},
126+
'gistemp': {
127+
'task': 'dataqs.gistemp.tasks.gistemp_task',
128+
'schedule': crontab(day_of_month=15, hour=12, minute=0),
129+
'args': ()
124130
}
125131
}
126132

dataqs/gdacs/tests.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ def setUp(self):
2424

2525
def tearDown(self):
2626
httpretty.disable()
27+
self.processor.cleanup()
2728

2829
def test_download(self):
2930
"""

dataqs/gfms/tests.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
def get_mock_image():
1414
"""
15-
Return a canned response with HTML for Boston
15+
Return a canned GFMS test image
1616
"""
1717
zf = zipfile.ZipFile(os.path.join(script_dir,
1818
'resources/test_gfms.zip'))
@@ -33,6 +33,7 @@ def setUp(self):
3333

3434
def tearDown(self):
3535
httpretty.disable()
36+
self.processor.cleanup()
3637

3738
def test_find_current(self):
3839
"""

dataqs/gistemp/__init__.py

Whitespace-only changes.

dataqs/gistemp/gistemp.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
from __future__ import absolute_import
2+
import logging
3+
import os
4+
import re
5+
import shutil
6+
from datetime import date
7+
from dateutil.relativedelta import relativedelta
8+
from dataqs.processor_base import GeoDataMosaicProcessor, GS_DATA_DIR
9+
from dataqs.helpers import get_band_count, gdal_translate, \
10+
nc_convert, style_exists, cdo_fixlng, gunzip
11+
12+
logger = logging.getLogger("dataqs.processors")
13+
script_dir = os.path.dirname(os.path.realpath(__file__))
14+
15+
16+
class GISTEMPProcessor(GeoDataMosaicProcessor):
17+
"""
18+
Processor for Land-Ocean Temperature Index, ERSSTv4, 1200km smoothing
19+
from the NASA Goddard Institute for Space Studies' Surface Temperature
20+
Analysis (GISTEMP).
21+
More info at http://data.giss.nasa.gov/gistemp/
22+
"""
23+
prefix = "gistemp"
24+
base_url = "http://data.giss.nasa.gov/pub/gistemp/gistemp1200_ERSSTv4.nc.gz"
25+
layer_name = 'gistemp1200_ERSSTv4'
26+
title = 'Global Monthly Air Temperature Anomalies, 1880/01/01 - {}'
27+
abstract = """The GISTEMP analysis recalculates consistent temperature
28+
anomaly series from 1880 to the present for a regularly spaced array of virtual
29+
stations covering the whole globe. Those data are used to investigate regional
30+
and global patterns and trends. Graphs and tables are updated around the
31+
middle of every month using current data files from NOAA GHCN v3 (meteorological
32+
stations), ERSST v4 (ocean areas), and SCAR (Antarctic stations).
33+
34+
The displayed image is based on the most current month.
35+
36+
Citations:
37+
- GISTEMP Team, 2016: GISS Surface Temperature Analysis (GISTEMP).
38+
NASA Goddard Institute for Space Studies. Dataset accessed monthly
39+
since 8/2016 at http://data.giss.nasa.gov/gistemp/.
40+
- Hansen, J., R. Ruedy, M. Sato, and K. Lo, 2010: Global surface
41+
temperature change, Rev. Geophys., 48, RG4004, doi:10.1029/2010RG000345.
42+
43+
"""
44+
45+
def convert(self, nc_file):
46+
nc_transform = nc_convert(nc_file)
47+
cdo_transform = cdo_fixlng(nc_transform)
48+
return cdo_transform
49+
50+
def extract_band(self, tif, band, outname):
51+
outfile = os.path.join(self.tmp_dir, outname)
52+
gdal_translate(tif, outfile, bands=[band],
53+
projection='EPSG:4326',
54+
options=['TILED=YES', 'COMPRESS=LZW'])
55+
return outfile
56+
57+
def get_date(self, months):
58+
start_month = date(1880, 1, 1)
59+
return start_month + relativedelta(months=months-1)
60+
61+
def get_title(self, months):
62+
end_month = self.get_date(months)
63+
return self.title.format(end_month.strftime('%Y/%m/%d'))
64+
65+
def run(self):
66+
"""
67+
Retrieve and process the latest NetCDF file.
68+
"""
69+
gzfile = self.download(
70+
self.base_url, '{}.nc.gz'.format(self.layer_name))
71+
ncfile = gunzip(os.path.join(self.tmp_dir, gzfile))
72+
cdf_file = self.convert(ncfile)
73+
bands = get_band_count(cdf_file)
74+
img_list = self.get_mosaic_filenames(self.layer_name)
75+
for band in range(1, bands+1):
76+
band_date = re.sub('[\-\.]+', '', self.get_date(band).isoformat())
77+
img_name = '{}_{}T000000000Z.tif'.format(self.layer_name, band_date)
78+
if img_name not in img_list:
79+
band_tif = self.extract_band(cdf_file, band, img_name)
80+
dst_file = self.data_dir.format(gsd=GS_DATA_DIR,
81+
ws=self.workspace,
82+
layer=self.layer_name,
83+
file=img_name)
84+
dst_dir = os.path.dirname(dst_file)
85+
if not os.path.exists(dst_dir):
86+
os.makedirs(dst_dir)
87+
if dst_file.endswith('.tif'):
88+
shutil.move(os.path.join(self.tmp_dir, band_tif), dst_file)
89+
self.post_geoserver(dst_file, self.layer_name)
90+
91+
if not style_exists(self.layer_name):
92+
with open(os.path.join(script_dir,
93+
'resources/gistemp.sld')) as sld:
94+
self.set_default_style(self.layer_name, self.layer_name,
95+
sld.read().format(latest_band=bands))
96+
self.update_geonode(self.layer_name, title=self.get_title(bands),
97+
description=self.abstract,
98+
store=self.layer_name,
99+
bounds=('-180.0', '180.0', '-90.0', '90.0',
100+
'EPSG:4326'))
101+
self.truncate_gs_cache(self.layer_name)
102+
self.cleanup()
103+
104+
105+
if __name__ == '__main__':
106+
processor = GISTEMPProcessor()
107+
processor.run()
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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>gistemp1200_ersstv4</sld:Name>
4+
<sld:UserStyle>
5+
<sld:Name>gistemp1200_ersstv4</sld:Name>
6+
<sld:Title>gistemp1200_ersstv4</sld:Title>
7+
<sld:FeatureTypeStyle>
8+
<sld:Name>name</sld:Name>
9+
<sld:Rule>
10+
<sld:RasterSymbolizer>
11+
<Opacity>1.0</Opacity>
12+
<ChannelSelection>
13+
<GrayChannel>
14+
<SourceChannelName>{latest_band}</SourceChannelName>
15+
</GrayChannel>
16+
</ChannelSelection>
17+
<ColorMap extended="true">
18+
<sld:ColorMapEntry color="#2b83ba" label="&lt;= -7.5 °C" opacity="1.0" quantity="-750"/>
19+
<sld:ColorMapEntry color="#6bb0af" label="-5.0 °C" opacity="1.0" quantity="-500"/>
20+
<sld:ColorMapEntry color="#abdda4" label="-3.75 °C" opacity="1.0" quantity="-375"/>
21+
<sld:ColorMapEntry color="#d5eeb1" label="-1.5 °C" opacity="1.0" quantity="-150"/>
22+
<sld:ColorMapEntry color="#ffffbf" label="0 °C" opacity="1.0" quantity="0"/>
23+
<sld:ColorMapEntry color="#fed690" label="1.5 °C" opacity="1.0" quantity="150"/>
24+
<sld:ColorMapEntry color="#fdae61" label="3.75 °C" opacity="1.0" quantity="375"/>
25+
<sld:ColorMapEntry color="#ea633e" label="5.0 °C" opacity="1.0" quantity="500"/>
26+
<sld:ColorMapEntry color="#d7191c" label="&gt;= 75 °C" opacity="1.0" quantity="750"/>
27+
<sld:ColorMapEntry color="#FFFFFF" label="" opacity="0.0" quantity="32765"/>
28+
</ColorMap>
29+
</sld:RasterSymbolizer>
30+
</sld:Rule>
31+
</sld:FeatureTypeStyle>
32+
</sld:UserStyle>
33+
</sld:NamedLayer>
34+
</sld:StyledLayerDescriptor>
34.7 KB
Binary file not shown.

dataqs/gistemp/tasks.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from __future__ import absolute_import
2+
3+
from celery import shared_task
4+
from dataqs.gistemp.gistemp import GISTEMPProcessor
5+
6+
7+
@shared_task
8+
def gistemp_task():
9+
processor = GISTEMPProcessor()
10+
processor.run()

dataqs/gistemp/tests.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import glob
2+
import os
3+
import httpretty
4+
from django.test import TestCase
5+
from dataqs.gistemp.gistemp import GISTEMPProcessor
6+
7+
script_dir = os.path.dirname(os.path.realpath(__file__))
8+
9+
10+
def get_mock_image():
11+
"""
12+
Return a canned test image (1 band of original NetCDF raster)
13+
"""
14+
zf = os.path.join(script_dir, 'resources/gistemp1200_ERSSTv4.nc')
15+
with open(zf, 'rb') as gzfile:
16+
return gzfile.read()
17+
18+
19+
class GISTEMPTest(TestCase):
20+
"""
21+
Tests the dataqs.gistemp module. Since each processor is highly
22+
dependent on a running GeoNode instance for most functions, only
23+
independent functions are tested here.
24+
"""
25+
26+
def setUp(self):
27+
self.processor = GISTEMPProcessor()
28+
httpretty.enable()
29+
30+
def tearDown(self):
31+
httpretty.disable()
32+
self.processor.cleanup()
33+
34+
def test_download(self):
35+
"""
36+
Verify that a file is downloaded
37+
"""
38+
httpretty.register_uri(httpretty.GET,
39+
self.processor.base_url,
40+
body=get_mock_image())
41+
imgfile = self.processor.download(
42+
self.processor.base_url,
43+
'{}.nc'.format(self.processor.layer_name))
44+
self.assertTrue(os.path.exists(
45+
os.path.join(self.processor.tmp_dir, imgfile)))
46+
47+
def test_cleanup(self):
48+
httpretty.register_uri(httpretty.GET,
49+
self.processor.base_url,
50+
body=get_mock_image())
51+
self.processor.download(self.processor.base_url,
52+
'{}.nc'.format(self.processor.layer_name))
53+
self.assertNotEqual([], glob.glob(os.path.join(
54+
self.processor.tmp_dir, self.processor.prefix + '*')))
55+
self.processor.cleanup()
56+
self.assertEquals([], glob.glob(os.path.join(
57+
self.processor.tmp_dir, self.processor.prefix + '*')))

0 commit comments

Comments
 (0)