def test_set_colors(): mc = MapCanvas((400, 300)) colors = [('blue', (0, 0, 255)), ('red', (255, 0, 0))] mc.add_colors(colors) assert mc.fore_image.get_color_names() == mc.back_image.get_color_names() colors = mc.get_color_names() assert colors == ['transparent', 'black', 'white', 'blue', 'red']
class IceImageOutput(Outputter): ''' Class that outputs ice data as an image for each ice mover. The image is PNG encoded, then Base64 encoded to include in a JSON response. ''' _schema = IceImageSchema def __init__(self, ice_movers=None, image_size=(800, 600), projection=None, viewport=None, **kwargs): ''' :param ice_movers: ice_movers associated with this outputter. :type ice_movers: An ice_mover object or sequence of ice_mover objects. Use super to pass optional \*\*kwargs to base class __init__ method ''' # this is a place where we store our gradient color infomration self.gradient_lu = {} self.map_canvas = MapCanvas(image_size, projection=projection, viewport=viewport, preset_colors='transparent') self.map_canvas.add_colors([('black', (0, 0, 0))]) self.set_gradient_colors( 'thickness', color_range=( (0, 0, 0x7f, 0x7f), # dark blue (0, 0, 0x7f, 0x3f), # dark blue (0, 0, 0x7f, 0x00), # dark blue (0xff, 0, 0, 0x00)), # red scale=(0.0, 6.0), num_colors=64) self.set_gradient_colors( 'concentration', color_range=( (0x80, 0xc0, 0xd0, 0x7f), # sky blue (0, 0x40, 0x60, 0x00)), # dark blue scale=(0.0, 1.0), num_colors=64) super(IceImageOutput, self).__init__(**kwargs) if (isinstance(ice_movers, collections.Iterable) and not isinstance(ice_movers, str)): self.ice_movers = ice_movers elif ice_movers is not None: self.ice_movers = (ice_movers, ) else: self.ice_movers = tuple() def set_gradient_colors( self, gradient_name, color_range=( (0, 0, 0x7f), # dark blue (0, 0xff, 0xff)), # cyan scale=(0.0, 10.0), num_colors=16): ''' Add a color gradient to our palette representing the colors we will use for our ice thickness :param gradient_name: The name of the gradient. :type gradient_name: str :param color_range: The colors we will build our gradient with. :type color_range: A 2 element sequence of 3-tuples containing 8-bit RGB values. :param scale: A range of values representing the low and high end of our gradient. :type scale: A 2 element sequence of float :param num_colors: The number of colors to use for the gradient. :type num_colors: Number ''' color_names = self.add_gradient_to_canvas(color_range, gradient_name, num_colors) self.gradient_lu[gradient_name] = (scale, np.array(color_names)) def add_gradient_to_canvas(self, color_range, color_prefix, num_colors): ''' Add a color gradient to our palette NOTE: Probably not the most efficient way to do this. :param color_range: The colors that we would like to use to generate our gradient :type color_range: A sequence of 2 or more 3-tuples :param color_prefix: The prefix that will be used in the naming of the colors in the gradient :type color_prefix: str :param num_colors: The number of gradient colors to generate :type num_colors: Number ''' color_range_idx = range(len(color_range)) color_space = np.linspace(color_range_idx[0], color_range_idx[-1], num=num_colors) r_grad = np.interp(color_space, color_range_idx, [c[0] for c in color_range]) g_grad = np.interp(color_space, color_range_idx, [c[1] for c in color_range]) b_grad = np.interp(color_space, color_range_idx, [c[2] for c in color_range]) if all([len(c) >= 4 for c in color_range]): a_grad = np.interp(color_space, color_range_idx, [c[3] for c in color_range]) else: a_grad = np.array([0.] * num_colors) new_colors = [] for i, (r, g, b, a) in enumerate(zip(r_grad, g_grad, b_grad, a_grad)): new_colors.append(('{}{}'.format(color_prefix, i), (r, g, b, a))) self.map_canvas.add_colors(new_colors) return [c[0] for c in new_colors] def lookup_gradient_color(self, gradient_name, values): try: (low_val, high_val), color_names = self.gradient_lu[gradient_name] except IndexError: return None scale_range = high_val - low_val q_step_range = scale_range / len(color_names) idx = (np.floor(values / q_step_range).astype(int).clip( 0, len(color_names) - 1)) return color_names[idx] def write_output(self, step_num, islast_step=False): """ Generate image from data """ # I don't think we need this for this outputter: # - it does stuff with cache initialization super(IceImageOutput, self).write_output(step_num, islast_step) if (self.on is False or not self._write_step or len(self.ice_movers) == 0): return None # fixme -- doing all this cache stuff just to get the timestep.. # maybe timestep should be passed in. for sc in self.cache.load_timestep(step_num).items(): model_time = date_to_sec(sc.current_time_stamp) iso_time = sc.current_time_stamp.isoformat() thick_image, conc_image, bb = self.render_images(model_time) # web_mercator = 'EPSG:3857' equirectangular = 'EPSG:32662' # info to return to the caller output_dict = { 'step_num': step_num, 'time_stamp': iso_time, 'thickness_image': thick_image, 'concentration_image': conc_image, 'bounding_box': bb, 'projection': equirectangular, } return output_dict def get_sample_image(self): """ This returns a base 64 encoded PNG image for testing, just so we have something This should be removed when we have real functionality """ # hard-coding the base64 really confused my editor.. image_file_file_path = os.path.join( os.path.split(__file__)[0], 'sample.b64') return open(image_file_file_path).read() def render_images(self, model_time): """ render the actual images This uses the MapCanvas code to do the actual rendering returns: thickness_image, concentration_image """ canvas = self.map_canvas # We kinda need to figure our our bounding box before doing the # rendering. We will try to be efficient about it mainly by not # grabbing our grid data twice. mover_grid_bb = None mover_grids = [] for mover in self.ice_movers: mover_grids.append(mover.get_grid_data()) mover_grid_bb = mover.get_grid_bounding_box( mover_grids[-1], mover_grid_bb) canvas.viewport = mover_grid_bb canvas.clear_background() # Here is where we draw our grid data.... for mover, mover_grid in zip(self.ice_movers, mover_grids): mover_grid_bb = mover.get_grid_bounding_box( mover_grid, mover_grid_bb) concentration, thickness = mover.get_ice_fields(model_time) thickness_colors = self.lookup_gradient_color( 'thickness', thickness) concentration_colors = self.lookup_gradient_color( 'concentration', concentration) dtype = mover_grid.dtype.descr unstructured_type = dtype[0][1] new_shape = mover_grid.shape + (len(dtype), ) mover_grid = (mover_grid.view(dtype=unstructured_type).reshape( *new_shape)) for poly, tc, cc in zip(mover_grid, thickness_colors, concentration_colors): canvas.draw_polygon(poly, fill_color=tc) canvas.draw_polygon(poly, fill_color=cc, background=True) # py_gd does not currently have the capability to generate a .png # formatted buffer in memory. (libgd can be made to do this, but # the wrapper is yet to be written) # So we will just write to a tempfile and then read it back. # If we ever have to do this anywhere else, a context manger would be good. tempdir = tempfile.mkdtemp() tempfilename = os.path.join(tempdir, "gnome_temp_image_file.png") canvas.save_foreground(tempfilename) thickness_image = open(tempfilename, 'rb').read().encode('base64') canvas.save_background(tempfilename) coverage_image = open(tempfilename, 'rb').read().encode('base64') os.remove(tempfilename) os.rmdir(tempdir) return ("data:image/png;base64,{}".format(thickness_image), "data:image/png;base64,{}".format(coverage_image), mover_grid_bb) def ice_movers_to_dict(self): ''' a dict containing 'obj_type' and 'id' for each object in list/collection ''' return self._collection_to_dict(self.ice_movers)
class IceImageOutput(Outputter): ''' Class that outputs ice data as an image for each ice mover. The image is PNG encoded, then Base64 encoded to include in a JSON response. ''' _state = copy.deepcopy(Outputter._state) # need a schema and also need to override save so output_dir # is saved correctly - maybe point it to saveloc _state.add_field(Field('ice_movers', save=True, update=True, iscollection=True)) _schema = IceImageSchema def __init__(self, ice_movers=None, image_size=(800, 600), projection=None, viewport=None, **kwargs): ''' :param ice_movers: ice_movers associated with this outputter. :type ice_movers: An ice_mover object or sequence of ice_mover objects. Use super to pass optional \*\*kwargs to base class __init__ method ''' # this is a place where we store our gradient color infomration self.gradient_lu = {} self.map_canvas = MapCanvas(image_size, projection=projection, viewport=viewport, preset_colors='transparent') self.map_canvas.add_colors([('black', (0, 0, 0))]) self.set_gradient_colors('thickness', color_range=((0, 0, 0x7f, 0x7f), # dark blue (0, 0, 0x7f, 0x3f), # dark blue (0, 0, 0x7f, 0x00), # dark blue (0xff, 0, 0, 0x00)), # red scale=(0.0, 6.0), num_colors=64) self.set_gradient_colors('concentration', color_range=((0x80, 0xc0, 0xd0, 0x7f), # sky blue (0, 0x40, 0x60, 0x00)), # dark blue scale=(0.0, 1.0), num_colors=64) super(IceImageOutput, self).__init__(**kwargs) if (isinstance(ice_movers, collections.Iterable) and not isinstance(ice_movers, str)): self.ice_movers = ice_movers elif ice_movers is not None: self.ice_movers = (ice_movers,) else: self.ice_movers = tuple() def set_gradient_colors(self, gradient_name, color_range=((0, 0, 0x7f), # dark blue (0, 0xff, 0xff)), # cyan scale=(0.0, 10.0), num_colors=16): ''' Add a color gradient to our palette representing the colors we will use for our ice thickness :param gradient_name: The name of the gradient. :type gradient_name: str :param color_range: The colors we will build our gradient with. :type color_range: A 2 element sequence of 3-tuples containing 8-bit RGB values. :param scale: A range of values representing the low and high end of our gradient. :type scale: A 2 element sequence of float :param num_colors: The number of colors to use for the gradient. :type num_colors: Number ''' color_names = self.add_gradient_to_canvas(color_range, gradient_name, num_colors) self.gradient_lu[gradient_name] = (scale, np.array(color_names)) def add_gradient_to_canvas(self, color_range, color_prefix, num_colors): ''' Add a color gradient to our palette NOTE: Probably not the most efficient way to do this. :param color_range: The colors that we would like to use to generate our gradient :type color_range: A sequence of 2 or more 3-tuples :param color_prefix: The prefix that will be used in the naming of the colors in the gradient :type color_prefix: str :param num_colors: The number of gradient colors to generate :type num_colors: Number ''' color_range_idx = range(len(color_range)) color_space = np.linspace(color_range_idx[0], color_range_idx[-1], num=num_colors) r_grad = np.interp(color_space, color_range_idx, [c[0] for c in color_range]) g_grad = np.interp(color_space, color_range_idx, [c[1] for c in color_range]) b_grad = np.interp(color_space, color_range_idx, [c[2] for c in color_range]) if all([len(c) >= 4 for c in color_range]): a_grad = np.interp(color_space, color_range_idx, [c[3] for c in color_range]) else: a_grad = np.array([0.] * num_colors) new_colors = [] for i, (r, g, b, a) in enumerate(zip(r_grad, g_grad, b_grad, a_grad)): new_colors.append(('{}{}'.format(color_prefix, i), (r, g, b, a))) self.map_canvas.add_colors(new_colors) return [c[0] for c in new_colors] def lookup_gradient_color(self, gradient_name, values): try: (low_val, high_val), color_names = self.gradient_lu[gradient_name] except IndexError: return None scale_range = high_val - low_val q_step_range = scale_range / len(color_names) idx = (np.floor(values / q_step_range) .astype(int) .clip(0, len(color_names) - 1)) return color_names[idx] def write_output(self, step_num, islast_step=False): """ Generate image from data """ # I don't think we need this for this outputter: # - it does stuff with cache initialization super(IceImageOutput, self).write_output(step_num, islast_step) if (self.on is False or not self._write_step or len(self.ice_movers) == 0): return None # fixme -- doing all this cache stuff just to get the timestep.. # maybe timestep should be passed in. for sc in self.cache.load_timestep(step_num).items(): model_time = date_to_sec(sc.current_time_stamp) iso_time = sc.current_time_stamp.isoformat() thick_image, conc_image, bb = self.render_images(model_time) # info to return to the caller web_mercator = 'EPSG:3857' equirectangular = 'EPSG:32662' output_dict = {'step_num': step_num, 'time_stamp': iso_time, 'thickness_image': thick_image, 'concentration_image': conc_image, 'bounding_box': bb, 'projection': equirectangular, } return output_dict def get_sample_image(self): """ This returns a base 64 encoded PNG image for testing, just so we have something This should be removed when we have real functionality """ # hard-coding the base64 really confused my editor.. image_file_file_path = os.path.join(os.path.split(__file__)[0], 'sample.b64') return open(image_file_file_path).read() def render_images(self, model_time): """ render the actual images This uses the MapCanvas code to do the actual rendering returns: thickness_image, concentration_image """ canvas = self.map_canvas # We kinda need to figure our our bounding box before doing the # rendering. We will try to be efficient about it mainly by not # grabbing our grid data twice. mover_grid_bb = None mover_grids = [] for mover in self.ice_movers: mover_grids.append(mover.get_grid_data()) mover_grid_bb = mover.get_grid_bounding_box(mover_grids[-1], mover_grid_bb) canvas.viewport = mover_grid_bb canvas.clear_background() # Here is where we draw our grid data.... for mover, mover_grid in zip(self.ice_movers, mover_grids): mover_grid_bb = mover.get_grid_bounding_box(mover_grid, mover_grid_bb) concentration, thickness = mover.get_ice_fields(model_time) thickness_colors = self.lookup_gradient_color('thickness', thickness) concentration_colors = self.lookup_gradient_color('concentration', concentration) dtype = mover_grid.dtype.descr unstructured_type = dtype[0][1] new_shape = mover_grid.shape + (len(dtype),) mover_grid = (mover_grid .view(dtype=unstructured_type) .reshape(*new_shape)) for poly, tc, cc in zip(mover_grid, thickness_colors, concentration_colors): canvas.draw_polygon(poly, fill_color=tc) canvas.draw_polygon(poly, fill_color=cc, background=True) # diagnostic so we can see what we have rendered. # print '\ndrawing reference objects...' # canvas.draw_graticule(False) # canvas.draw_tags(False) # canvas.save_background('background.png') # canvas.save_foreground('foreground.png') # py_gd does not currently have the capability to generate a .png # formatted buffer in memory. (libgd can be made to do this, but # the wrapper is yet to be written) # So we will just write to a tempfile and then read it back. with NamedTemporaryFile() as fp: canvas.save_foreground(fp.name) fp.seek(0) thickness_image = fp.read().encode('base64') with NamedTemporaryFile() as fp: canvas.save_background(fp.name) fp.seek(0) coverage_image = fp.read().encode('base64') return ("data:image/png;base64,{}".format(thickness_image), "data:image/png;base64,{}".format(coverage_image), mover_grid_bb) def rewind(self): 'remove previously written files' super(IceImageOutput, self).rewind() def ice_movers_to_dict(self): ''' a dict containing 'obj_type' and 'id' for each object in list/collection ''' return self._collection_to_dict(self.ice_movers) @classmethod def deserialize(cls, json_): """ append correct schema for current mover """ schema = cls._schema() _to_dict = schema.deserialize(json_) if 'ice_movers' in json_: _to_dict['ice_movers'] = [] for i, cm in enumerate(json_['ice_movers']): cm_cls = class_from_objtype(cm['obj_type']) cm_dict = cm_cls.deserialize(json_['ice_movers'][i]) _to_dict['ice_movers'].append(cm_dict) return _to_dict