analysis/webservice/algorithms/MapFetchHandler.py (277 lines of code) (raw):

# Licensed to the Apache Software Foundation (ASF) under one or more # contributor license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright ownership. # The ASF licenses this file to You under the Apache License, Version 2.0 # (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import calendar import errno import io import json import math import os import time from subprocess import call import boto3 import numpy as np from PIL import Image from PIL import ImageDraw from PIL import ImageFont from dateutil.relativedelta import * from . import colortables import webservice.GenerateImageMRF as MRF from webservice.algorithms.NexusCalcHandler import NexusCalcHandler as BaseHandler from webservice.NexusHandler import nexus_handler @nexus_handler class MapFetchCalcHandler(BaseHandler): name = "MapFetchHandler" path = "/map" description = "Creates a map image" params = { "ds": { "name": "Dataset", "type": "string", "description": "A supported dataset shortname identifier" }, "t": { "name": "Time", "type": "int", "description": "Data observation date" }, "output": { "name": "Output Format", "type": "string", "description": "Output format. Use 'PNG' for this endpoint" }, "min": { "name": "Minimum Value", "type": "float", "description": "Minimum value to use when computing color scales" }, "max": { "name": "Maximum Value", "type": "float", "description": "Maximum value to use when computing color scales" }, "ct": { "name": "Color Table", "type": "string", "description": "Identifier of a supported color table" }, "interp": { "name": "Interpolation filter", "type": "string", "description": "Interpolation filter to use when rescaling image data. Can be 'nearest', 'lanczos', 'bilinear', or 'bicubic'." }, "width": { "name": "Width", "type": "int", "description": "Output image width (max: 8192)" }, "height": { "name": "Height", "type": "int", "description": "Output image height (max: 8192)" } } singleton = True NO_DATA_IMAGE = None def __init__(self, tile_service_factory): BaseHandler.__init__(self, tile_service_factory) @staticmethod def __tile_to_image(img_data, tile, min, max, table, x_res, y_res): width = len(tile.longitudes) height = len(tile.latitudes) d = np.ma.filled(tile.data[0], np.nan) for y in range(0, height): for x in range(0, width): value = d[y][x] if not np.isnan(value) and value != 0: lat = tile.latitudes[y] lon = tile.longitudes[x] pixel_y = int(math.floor(180.0 - ((lat + 90.0) * y_res))) pixel_x = int(math.floor((lon + 180.0) * x_res)) value = np.max((min, value)) value = np.min((max, value)) value255 = int(round((value - min) / (max - min) * 255.0)) rgba = MapFetchCalcHandler.__get_color(value255, table) img_data.putpixel((pixel_x, pixel_y), (rgba[0], rgba[1], rgba[2], 255)) @staticmethod def __translate_interpolation(interp): if interp.upper() == "LANCZOS": return Image.LANCZOS elif interp.upper() == "BILINEAR": return Image.BILINEAR elif interp.upper() == "BICUBIC": return Image.BICUBIC else: return Image.NEAREST @staticmethod def __make_tile_img(tile): width = len(tile.longitudes) height = len(tile.latitudes) img = Image.new("RGBA", (width, height), (0, 0, 0, 0)) return img @staticmethod def __get_xy_resolution(tile): x_res = abs(tile.longitudes[0] - tile.longitudes[1]) y_res = abs(tile.latitudes[0] - tile.latitudes[1]) return x_res, y_res @staticmethod def __create_global(nexus_tiles, stats, width=2048, height=1024, force_min=np.nan, force_max=np.nan, table=colortables.grayscale, interpolation="nearest"): data_min = stats["minValue"] if np.isnan(force_min) else force_min data_max = stats["maxValue"] if np.isnan(force_max) else force_max x_res, y_res = MapFetchCalcHandler.__get_xy_resolution(nexus_tiles[0]) x_res = 1 y_res = 1 canvas_width = int(360.0 / x_res) canvas_height = int(180.0 / y_res) img = Image.new("RGBA", (canvas_width, canvas_height), (0, 0, 0, 0)) img_data = img.getdata() for tile in nexus_tiles: MapFetchCalcHandler.__tile_to_image(img_data, tile, data_min, data_max, table, x_res, y_res) final_image = img.resize((width, height), MapFetchCalcHandler.__translate_interpolation(interpolation)) return final_image @staticmethod def __get_color(value, table): index = (float(value) / float(255)) * (len(table) - 1) prev = int(math.floor(index)) next = int(math.ceil(index)) f = index - prev prevColor = table[prev] nextColor = table[next] r = int(round(nextColor[0] * f + (prevColor[0] * (1.0 - f)))) g = int(round(nextColor[1] * f + (prevColor[1] * (1.0 - f)))) b = int(round(nextColor[2] * f + (prevColor[2] * (1.0 - f)))) return (r, g, b, 255) @staticmethod def __colorize(img, table): data = img.getdata() for x in range(0, img.width): for y in range(0, img.height): if data[x + (y * img.width)][3] == 255: value = data[x + (y * img.width)][0] rgba = MapFetchCalcHandler.__get_color(value, table) data.putpixel((x, y), (rgba[0], rgba[1], rgba[2], 255)) @staticmethod def __create_no_data(width, height): if MapFetchCalcHandler.NO_DATA_IMAGE is None: img = Image.new("RGBA", (width, height), (0, 0, 0, 0)) draw = ImageDraw.Draw(img) fnt = ImageFont.load_default() for x in range(10, width, 100): for y in range(10, height, 100): draw.text((x, y), "NO DATA", (180, 180, 180), font=fnt) MapFetchCalcHandler.NO_DATA_IMAGE = img return MapFetchCalcHandler.NO_DATA_IMAGE def calc(self, computeOptions, **args): ds = computeOptions.get_argument("ds", None) dataTimeEnd = computeOptions.get_datetime_arg("t", None) if dataTimeEnd is None: raise Exception("Missing 't' option for time") dataTimeEnd = time.mktime(dataTimeEnd.timetuple()) dataTimeStart = dataTimeEnd - 86400.0 color_table_name = computeOptions.get_argument("ct", "smap") color_table = colortables.__dict__[color_table_name] interpolation = computeOptions.get_argument("interp", "nearest") force_min = computeOptions.get_float_arg("min", np.nan) force_max = computeOptions.get_float_arg("max", np.nan) width = np.min([8192, computeOptions.get_int_arg("width", 1024)]) height = np.min([8192, computeOptions.get_int_arg("height", 512)]) stats = self._get_tile_service().get_dataset_overall_stats(ds) daysinrange = self._get_tile_service().find_days_in_range_asc(-90.0, 90.0, -180.0, 180.0, ds, dataTimeStart, dataTimeEnd) if len(daysinrange) > 0: ds1_nexus_tiles = self._get_tile_service().get_tiles_bounded_by_box_at_time(-90.0, 90.0, -180.0, 180.0, ds, daysinrange[0]) img = self.__create_global(ds1_nexus_tiles, stats, width, height, force_min, force_max, color_table, interpolation) else: img = self.__create_no_data(width, height) imgByteArr = io.BytesIO() img.save(imgByteArr, format='PNG') imgByteArr = imgByteArr.getvalue() class SimpleResult(object): def toJson(self): return json.dumps({"status": "Please specify output type as PNG."}) def toImage(self): return imgByteArr return SimpleResult() def generate(self, ds, granule_name, prefix, ct, interp, _min, _max, width, height, time_interval): color_table_name = ct if ct is None: color_table_name = "smap" color_table = colortables.__dict__[color_table_name] interpolation = interp if interp is None: interpolation = "near" force_min = _min force_max = _max if _min is None: force_min = np.nan if _max is None: force_max = np.nan temp_width = width temp_height = height if width is None: temp_width = 1024 if height is None: temp_height = 512 width = np.min([8192, temp_width]) height = np.min([8192, temp_height]) if time_interval == 'day': time_interval = relativedelta(days=+1) else: time_interval = relativedelta(months=+1) stats = self._get_tile_service().get_dataset_overall_stats(ds) start_time, end_time = self._get_tile_service().get_min_max_time_by_granule(ds, granule_name) MRF.create_all(ds, prefix) # Make a temporary directory for storing the .png and .tif files temp_dir = '/tmp/tmp/' try: os.makedirs(temp_dir) except OSError as e: if e.errno != errno.EEXIST: raise while start_time <= end_time: one_interval_later = start_time + time_interval temp_end_time = one_interval_later - relativedelta(minutes=+1) # prevent getting tiles for 2 intervals ds1_nexus_tiles = self._get_tile_service().find_tiles_in_box(-90.0, 90.0, -180.0, 180.0, ds, start_time, temp_end_time) if ds1_nexus_tiles is not None: img = self.__create_global(ds1_nexus_tiles, stats, width, height, force_min, force_max, color_table, interpolation) else: img = self.__create_no_data(width, height) imgByteArr = io.BytesIO() img.save(imgByteArr, format='PNG') imgByteArr = imgByteArr.getvalue() arr = str(start_time).split() # arr[0] should contain a string of the date in format 'YYYY-MM-DD' fulldate = arr[0] temp_png = temp_dir + fulldate + '.png' temp_tif = temp_dir + fulldate + '.tif' open(temp_png, 'wb').write(imgByteArr) arr = fulldate.split('-') year = arr[0] month = calendar.month_abbr[int(arr[1])] dt = month + '_' + year retcode = MRF.png_to_tif(temp_png, temp_tif) if retcode == 0: retcode = MRF.geo_to_mrf(temp_tif, prefix, year, dt, ds) if retcode == 0: retcode = MRF.geo_to_arctic_mrf(temp_tif, prefix, year, dt, ds) if retcode == 0: retcode = MRF.geo_to_antarctic_mrf(temp_tif, prefix, year, dt, ds, interp) if retcode != 0: break start_time = one_interval_later tar_file = ds + '.tar.gz' retcode = call(["tar", "-zcvf", tar_file, ds]) if retcode == 0: # Delete temporary files/folders if tar.gz is created successfully call(["rm", "-rf", ds]) call(["rm", "-rf", temp_dir]) else: print("Error creating tar.gz") # Upload the tar.gz file to the sea-level-mrf S3 bucket s3bucket = 'sea-level-mrf' s3client = boto3.client('s3') try: with open(tar_file, 'rb') as data: s3client.upload_fileobj(data, s3bucket, tar_file) except Exception as e: print(("Unable to add tar.gz to S3: \n" + str(e))) call(["rm", "-rf", tar_file]) # Delete the tar.gz from local storage