def support_epsg_code(self, crs): """Returns a list of supported EPSG codes. """ try: get_projection_params(crs) except ValueError: return False return True
def changeMapSection(self, index=0, only_kwargs=False): """ Change the current map section to one of the predefined regions. """ # Get the initial projection parameters from the tables in mss_settings. current_map_key = self.cbChangeMapSection.currentText() predefined_map_sections = config_loader( dataset="predefined_map_sections") current_map = predefined_map_sections.get( current_map_key, {"CRS": current_map_key, "map": {}}) proj_params = get_projection_params(current_map["CRS"]) # Create a keyword arguments dictionary for basemap that contains # the projection parameters. kwargs = current_map["map"] kwargs.update({"CRS": current_map["CRS"], "BBOX_UNITS": proj_params["bbox"], "PROJECT_NAME": self.waypoints_model.name}) kwargs.update(proj_params["basemap"]) if only_kwargs: # Return kwargs dictionary and do NOT redraw the map. return kwargs logging.debug("switching to map section '%s' - '%s'", current_map_key, kwargs) self.mpl.canvas.redraw_map(kwargs) self.mpl.navbar.clear_history()
def test_get_projection_params(self): assert utils.get_projection_params("epsg:4839") == {'basemap': {'epsg': '4839'}, 'bbox': 'meter(10.5,51)'} with pytest.raises(ValueError): utils.get_projection_params('auto2:42005') with pytest.raises(ValueError): utils.get_projection_params('auto:42001') with pytest.raises(ValueError): utils.get_projection_params('crs:84')
def produce_plot(self, query, mode): """ Handler for a GetMap and GetVSec requests. Produces a plot with the parameters specified in the URL. # TODO: Handle multiple layers. (mr, 2010-06-09) # TODO: Cache the produced images: Check whether an image with the given # parameters has already been produced. (mr, 2010-08-18) """ logging.debug("GetMap/GetVSec request. Interpreting parameters..") # Evaluate query parameters: # ============================= version = query.get("VERSION", "1.1.1") # Image size. width = query.get('WIDTH', 900) height = query.get('HEIGHT', 600) figsize = float(width if width != "" else 900), float( height if height != "" else 600) logging.debug(" requested image size = %sx%s", figsize[0], figsize[1]) # Requested layers. layers = [ layer for layer in query.get('LAYERS', '').strip().split(',') if layer ] layer = layers[0] if len(layers) > 0 else '' if layer.find(".") > 0: dataset, layer = layer.split(".") else: dataset = None logging.debug(" requested dataset = '%s', layer = '%s'", dataset, layer) # Requested style(s). styles = [ style for style in query.get('STYLES', 'default').strip().split(',') if style ] style = styles[0] if len(styles) > 0 else None logging.debug(" requested style = '%s'", style) # Forecast initialisation time. init_time = query.get('DIM_INIT_TIME') if init_time is not None: try: init_time = parse_iso_datetime(init_time) except ValueError: return self.create_service_exception( code="InvalidDimensionValue", text= "DIM_INIT_TIME has wrong format (needs to be 2005-08-29T13:00:00Z)", version=version) logging.debug(" requested initialisation time = '%s'", init_time) # Forecast valid time. valid_time = query.get('TIME') if valid_time is not None: try: valid_time = parse_iso_datetime(valid_time) except ValueError: return self.create_service_exception( code="InvalidDimensionValue", text= "TIME has wrong format (needs to be 2005-08-29T13:00:00Z)", version=version) logging.debug(" requested (valid) time = '%s'", valid_time) # Coordinate reference system. crs = query.get("CRS" if version == "1.3.0" else "SRS", 'EPSG:4326').lower() is_yx = version == "1.3.0" and crs.startswith("epsg") and int( crs[5:]) in axisorder_yx # Allow to request vertical sections via GetMap, if the specified CRS is of type "VERT:??". msg = None if crs.startswith('vert:logp'): mode = "getvsec" else: try: get_projection_params(crs) except ValueError: return self.create_service_exception( code="InvalidSRS", text=f"The requested CRS '{crs}' is not supported.", version=version) logging.debug(" requested coordinate reference system = '%s'", crs) # Create a frameless figure (WMS) or one with title and legend # (MSS specific)? Default is WMS mode (frameless). noframe = query.get('FRAME', 'off').lower() == 'off' # Transparency. transparent = query.get('TRANSPARENT', 'false').lower() == 'true' if transparent: logging.debug(" requested transparent image") # Return format (image/png, text/xml, etc.). return_format = query.get('FORMAT', 'image/png').lower() logging.debug(" requested return format = '%s'", return_format) if return_format not in ["image/png", "text/xml"]: return self.create_service_exception( code="InvalidFORMAT", text=f"unsupported FORMAT: '{return_format}'", version=version) # 3) Check GetMap/GetVSec-specific parameters and produce # the image with the corresponding section driver. # ======================================================= if mode == "getmap": # Check requested layer. if (dataset not in self.hsec_layer_registry) or ( layer not in self.hsec_layer_registry[dataset]): return self.create_service_exception( code="LayerNotDefined", text=f"Invalid LAYER '{dataset}.{layer}' requested", version=version) # Check if the layer requires time information and if they are given. if self.hsec_layer_registry[dataset][ layer].uses_inittime_dimension() and init_time is None: return self.create_service_exception( code="MissingDimensionValue", text= "INIT_TIME not specified (use the DIM_INIT_TIME keyword)", version=version) if self.hsec_layer_registry[dataset][ layer].uses_validtime_dimension() and valid_time is None: return self.create_service_exception( code="MissingDimensionValue", text="TIME not specified", version=version) # Check if the requested coordinate system is supported. if not self.hsec_layer_registry[dataset][layer].support_epsg_code( crs): return self.create_service_exception( code="InvalidSRS", text=f"The requested CRS '{crs}' is not supported.", version=version) # Bounding box. try: if is_yx: bbox = [ float(v) for v in query.get( 'BBOX', '-90,-180,90,180').split(',') ] bbox = (bbox[1], bbox[0], bbox[3], bbox[2]) else: bbox = [ float(v) for v in query.get( 'BBOX', '-180,-90,180,90').split(',') ] except ValueError: return self.create_service_exception( text=f"Invalid BBOX: {query.get('BBOX')}", version=version) # Vertical level, if applicable. level = query.get('ELEVATION') level = float(level) if level is not None else None layer_datatypes = self.hsec_layer_registry[dataset][ layer].required_datatypes() if any(_x in layer_datatypes for _x in ["pl", "al", "ml", "tl", "pv"]) and level is None: # Use the default value. level = -1 elif ("sfc" in layer_datatypes) and \ all(_x not in layer_datatypes for _x in ["pl", "al", "ml", "tl", "pv"]) and \ level is not None: return self.create_service_exception( text= f"ELEVATION argument not applicable for layer '{layer}'. Please omit this argument.", version=version) plot_driver = self.hsec_drivers[dataset] try: plot_driver.set_plot_parameters( self.hsec_layer_registry[dataset][layer], bbox=bbox, level=level, crs=crs, init_time=init_time, valid_time=valid_time, style=style, figsize=figsize, noframe=noframe, transparent=transparent, return_format=return_format) image = plot_driver.plot() except (IOError, ValueError) as ex: logging.error("ERROR: %s %s", type(ex), ex) logging.debug("%s", traceback.format_exc()) msg = "The data corresponding to your request is not available. Please check the " \ "times and/or levels you have specified.\n\n" \ f"Error message: '{ex}'" return self.create_service_exception(text=msg, version=version) elif mode == "getvsec": # Vertical secton path. path = query.get("PATH") if path is None: return self.create_service_exception(text="PATH not specified", version=version) try: path = [float(v) for v in path.split(',')] path = [[lat, lon] for lat, lon in zip(path[0::2], path[1::2])] except ValueError: return self.create_service_exception( text=f"Invalid PATH: {path}", version=version) logging.debug("VSEC PATH: %s", path) # Check requested layers. if (dataset not in self.vsec_layer_registry) or ( layer not in self.vsec_layer_registry[dataset]): return self.create_service_exception( code="LayerNotDefined", text=f"Invalid LAYER '{dataset}.{layer}' requested", version=version) # Check if the layer requires time information and if they are given. if self.vsec_layer_registry[dataset][ layer].uses_inittime_dimension(): if init_time is None: return self.create_service_exception( code="MissingDimensionValue", text= "INIT_TIME not specified (use the DIM_INIT_TIME keyword)", version=version) if valid_time is None: return self.create_service_exception( code="MissingDimensionValue", text="TIME not specified", version=version) # Bounding box (num interp. points, p_bot, num labels, p_top). try: bbox = [ float(v) for v in query.get("BBOX", "101,1050,10,180").split(",") ] except ValueError: return self.create_service_exception( text=f"Invalid BBOX: {query.get('BBOX')}", version=version) plot_driver = self.vsec_drivers[dataset] try: plot_driver.set_plot_parameters( plot_object=self.vsec_layer_registry[dataset][layer], vsec_path=path, vsec_numpoints=bbox[0], vsec_path_connection="greatcircle", vsec_numlabels=bbox[2], init_time=init_time, valid_time=valid_time, style=style, bbox=bbox, figsize=figsize, noframe=noframe, transparent=transparent, return_format=return_format) image = plot_driver.plot() except (IOError, ValueError) as ex: logging.error("ERROR: %s %s", type(ex), ex) msg = "The data corresponding to your request is not available. Please check the " \ "times and/or path you have specified.\n\n" \ f"Error message: {ex}" return self.create_service_exception(text=msg, version=version) # 4) Return the produced image. # ============================= return image, return_format
def plot_hsection(self, data, lats, lons, bbox=(-180, -90, 180, 90), level=None, figsize=(960, 640), crs=None, proj_params=None, valid_time=None, init_time=None, style=None, resolution=-1, noframe=False, show=False, transparent=False): """ EPSG overrides proj_params! """ if proj_params is None: proj_params = {"projection": "cyl"} bbox_units = "latlon" # Projection parameters from EPSG code. if crs is not None: proj_params, bbox_units = [ get_projection_params(crs)[_x] for _x in ("basemap", "bbox") ] logging.debug("plotting data..") # Check if required data is available. self.data_units = self.driver.data_units.copy() for datatype, dataitem, dataunit in self.required_datafields: if dataitem not in data: raise KeyError(f"required data field '{dataitem}' not found") origunit = self.driver.data_units[dataitem] if dataunit is not None: data[dataitem] = convert_to(data[dataitem], origunit, dataunit) self.data_units[dataitem] = dataunit else: logging.debug("Please add units to plot variables") # Copy parameters to properties. self.data = data self.lats = lats self.lons = lons self.level = level self.valid_time = valid_time self.init_time = init_time self.style = style self.resolution = resolution self.noframe = noframe self.crs = crs # Derive additional data fields and make the plot. logging.debug("preparing additional data fields..") self._prepare_datafields() logging.debug("creating figure..") dpi = 80 figsize = (figsize[0] / dpi), (figsize[1] / dpi) facecolor = "white" fig = mpl.figure.Figure(figsize=figsize, dpi=dpi, facecolor=facecolor) logging.debug( "\twith frame and legends" if not noframe else "\twithout frame") if noframe: ax = fig.add_axes([0.0, 0.0, 1.0, 1.0]) else: ax = fig.add_axes([0.05, 0.05, 0.9, 0.88]) # The basemap instance is created with a fixed aspect ratio for framed # plots; the aspect ratio is not fixed for frameless plots (standard # WMS). This means that for WMS plots, the map will always fill the # entire image area, no matter of whether this stretches or shears the # map. This is the behaviour specified by the WMS standard (WMS Spec # 1.1.1, Section 7.2.3.8): # "The returned picture, regardless of its return format, shall have # exactly the specified width and height in pixels. In the case where # the aspect ratio of the BBOX and the ratio width/height are different, # the WMS shall stretch the returned map so that the resulting pixels # could themselves be rendered in the aspect ratio of the BBOX. In # other words, it should be possible using this definition to request a # map for a device whose output pixels are themselves non-square, or to # stretch a map into an image area of a different aspect ratio." # NOTE: While the MSUI always requests image sizes that match the aspect # ratio, for instance the Metview 4 client does not (mr, 2011Dec16). # Some additional code to store the last 20 coastlines in memory for quicker # access. key = repr((proj_params, bbox, bbox_units)) basemap_use_cache = getattr(mss_wms_settings, "basemap_use_cache", False) basemap_request_size = getattr(mss_wms_settings, "basemap_request_size ", 200) basemap_cache_size = getattr(mss_wms_settings, "basemap_cache_size", 20) bm_params = { "area_thresh": 1000., "ax": ax, "fix_aspect": (not noframe) } bm_params.update(proj_params) if bbox_units == "degree": bm_params.update({ "llcrnrlon": bbox[0], "llcrnrlat": bbox[1], "urcrnrlon": bbox[2], "urcrnrlat": bbox[3] }) elif bbox_units.startswith("meter"): # convert meters to degrees try: bm_p = basemap.Basemap(resolution=None, **bm_params) except ValueError: # projection requires some extent bm_p = basemap.Basemap(resolution=None, width=1e7, height=1e7, **bm_params) bm_center = [float(_x) for _x in bbox_units[6:-1].split(",")] center_x, center_y = bm_p(*bm_center) bbox_0, bbox_1 = bm_p(bbox[0] + center_x, bbox[1] + center_y, inverse=True) bbox_2, bbox_3 = bm_p(bbox[2] + center_x, bbox[3] + center_y, inverse=True) bm_params.update({ "llcrnrlon": bbox_0, "llcrnrlat": bbox_1, "urcrnrlon": bbox_2, "urcrnrlat": bbox_3 }) elif bbox_units == "no": pass else: raise ValueError(f"bbox_units '{bbox_units}' not known.") if basemap_use_cache and key in BASEMAP_CACHE: bm = basemap.Basemap(resolution=None, **bm_params) (bm.resolution, bm.coastsegs, bm.coastpolygontypes, bm.coastpolygons, bm.coastsegs, bm.landpolygons, bm.lakepolygons, bm.cntrysegs) = BASEMAP_CACHE[key] logging.debug("Loaded '%s' from basemap cache", key) else: bm = basemap.Basemap(resolution='l', **bm_params) # read in countries manually, as those are laoded only on demand bm.cntrysegs, _ = bm._readboundarydata("countries") if basemap_use_cache: BASEMAP_CACHE[key] = (bm.resolution, bm.coastsegs, bm.coastpolygontypes, bm.coastpolygons, bm.coastsegs, bm.landpolygons, bm.lakepolygons, bm.cntrysegs) if basemap_use_cache: BASEMAP_REQUESTS.append(key) BASEMAP_REQUESTS[:] = BASEMAP_REQUESTS[-basemap_request_size:] if len(BASEMAP_CACHE) > basemap_cache_size: useful = {} for idx, key in enumerate(BASEMAP_REQUESTS): useful[key] = useful.get(key, 0) + idx least_useful = sorted([ (value, key) for key, value in useful.items() ])[:-basemap_cache_size] for _, key in least_useful: del BASEMAP_CACHE[key] BASEMAP_REQUESTS[:] = [ _x for _x in BASEMAP_REQUESTS if key != _x ] # Set up the map appearance. bm.drawcoastlines(color='0.25') bm.drawcountries(color='0.5') bm.drawmapboundary(fill_color='white') # zorder = 0 is necessary to paint over the filled continents with # scatter() for drawing the flight tracks and trajectories. # Curiously, plot() works fine without this setting, but scatter() # doesn't. bm.fillcontinents(color='0.98', lake_color='white', zorder=0) self._draw_auto_graticule(bm) if noframe: ax.axis('off') self.bm = bm # !! BETTER PASS EVERYTHING AS PARAMETERS? self.fig = fig self.shift_data() self.mask_data() self._plot_style() # Set transparency for the output image. if transparent: fig.patch.set_alpha(0.) # Return the image as png embedded in a StringIO stream. canvas = FigureCanvas(fig) output = io.BytesIO() canvas.print_png(output) if show: logging.debug("saving figure to mpl_hsec.png ..") canvas.print_png("mpl_hsec.png") # Convert the image to an 8bit palette image with a significantly # smaller file size (~factor 4, from RGBA to one 8bit value, plus the # space to store the palette colours). # NOTE: PIL at the current time can only create an adaptive palette for # RGB images, hence alpha values are lost here. If transparency is # requested, the figure face colour is stored as the "transparent" # colour in the image. This works in most cases, but might lead to # visible artefacts in some cases. logging.debug("converting image to indexed palette.") # Read the above stored png into a PIL image and create an adaptive # colour palette. output.seek(0) # necessary for PIL.Image.open() palette_img = PIL.Image.open(output).convert(mode="RGB").convert( "P", palette=PIL.Image.ADAPTIVE) output = io.BytesIO() if not transparent: logging.debug("saving figure as non-transparent PNG.") palette_img.save( output, format="PNG") # using optimize=True doesn't change much else: # If the image has a transparent background, we need to find the # index of the background colour in the palette. See the # documentation for PIL's ImagePalette module # (http://www.pythonware.com/library/pil/handbook/imagepalette.htm). The # idea is to create a 256 pixel image with the same colour palette # as the original image and use it as a lookup-table. Converting the # lut image back to RGB gives us a list of all colours in the # palette. (Why doesn't PIL provide a method to directly access the # colours in a palette??) lut = palette_img.resize((256, 1)) lut.putdata(list(range(256))) lut = [c[1] for c in lut.convert("RGB").getcolors()] facecolor_rgb = list( mpl.colors.hex2color(mpl.colors.cnames[facecolor])) for i in [0, 1, 2]: facecolor_rgb[i] = int(facecolor_rgb[i] * 255) facecolor_index = lut.index(tuple(facecolor_rgb)) logging.debug( "saving figure as transparent PNG with transparency index %s.", facecolor_index) palette_img.save(output, format="PNG", transparency=facecolor_index) logging.debug("returning figure..") return output.getvalue()