Source code for merplot.mermaid_plot

#!/usr/bin/env python
# -*- coding: utf-8 -*-
""" Utilities to create a simple map of the Mermaid float locations.

:author:
    Lucas Sawade (lsawade@princeton.edu), 2019

:license:
    GNU Lesser General Public License, Version 3
    (http://www.gnu.org/copyleft/lgpl.html):

Last Update:
    September 2019

"""

import re
import os
# I think there is probably a matplotlib function with date time, if I can
# find out about that, I can write a simple vincenty formula for
# locations2degree. If I just prepend it to the code and call it the same the
# code will not even change.
# Then it's only basic python code + matplotlib + numpy + cartopy
from obspy import UTCDateTime
from obspy.geodetics.base import locations2degrees
import codecs
import numpy as np

import cartopy.crs as ccrs
import cartopy.feature as cfeature
import matplotlib.ticker as mticker
from cartopy.mpl.ticker import LongitudeFormatter, LatitudeFormatter
from matplotlib.collections import LineCollection
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import yaml



#--------- This is necessary for round caps in the line collection-------------
"""
This might sound arbitrary, but the LineCollection plots very sharps lines 
that end at the actually point. This means that if there is a turn in 
Mermaids trajectory, the line becomes super-jagged; we don't want that. The 
work around is the following, which enables `round` line caps.
"""
import types
from matplotlib.backend_bases import GraphicsContextBase, RendererBase

[docs]class GC(GraphicsContextBase): """This class is unrelated to Mermaid, but fixes the issue of jagged Lines when creating a LineCollection by defining the capstyle as round. """ def __init__(self): super().__init__() self._capstyle = 'round'
[docs]def custom_new_gc(self): """Returns new rendering class.""" return GC()
# Activating the renderer. RendererBase.new_gc = types.MethodType(custom_new_gc, RendererBase) #------------------------------------------------------------------------------ # For reading the filter file into a dictionary
[docs]def read_yaml_file(filename): """Reads yaml file and returns dictionary with content. :param filename: String with yaml file location :type filename: str :returns: dict Usage: .. code-block:: python # Read the file d = read_yaml_file(<some_yaml_file_location_as_string>) value = d[key] """ with open(filename) as fh: return yaml.load(fh, Loader=yaml.FullLoader)
[docs]def max_UTC(UTC_list, ind=False): """ Get first, latest time in a list of UTC :param UTC_list: list of UTCDateTime stamps :param ind: boolean defining whether the index in list is supposed to be output. Default False :return: latest UTCDateTime stamp. If ind is True, tuple of (latest time stamp, index) is output. Usage: .. code-block:: python from mermaid_plot import max_UTC # Assuming you have a list of UTCDateTimes most_recent_UTC = max_UTC(<some_UTCDatetime_list>) """ counter = 0 index = 0 max_timestamp = UTC_list[0] for time in UTC_list: if time > max_timestamp: max_timestamp = time index = counter counter +=1 if ind: return max_timestamp, index else: return max_timestamp
[docs]def min_UTC(UTC_list, ind=False): """ Get first, earliest time in a list of UTC :param UTC_list: list of UTCDateTime stamps :param ind: boolean defining whether the index in list is supposed to be output. Default False :return: earliest UTCDateTime stamp. If ind is True, tuple of (earliest time stamp, index) is output. Usage: .. code-block:: python from mermaid_plot import max_UTC # Assuming you have a list of UTCDateTimes oldest_UTC = min_UTC(<some_UTCDatetime_list>) """ counter = 0 index = 0 min_timestamp = UTC_list[0] for time in UTC_list: if time < min_timestamp: min_timestamp = time index = counter counter +=1 if ind: return min_timestamp, index else: return min_timestamp
[docs]def get_coordinates_from_kml_path(kml_file): """Reads .kml file and returns corresponding latitude and longitude lists. WARNING!!! Only works for kmls with a single path! :param kml_file: path/to/kml_file :type kml_file: str :returns: tuple with list of latitudes and list longitudes Usage: .. code-block:: python # KML file kml_file = "<path/to/kml_file>" # Get lats, lons lat, lon = get_coordinates_from_kml_path(kml_file) """ # Read kml file kml = open(kml_file, 'r').read() # Catch coordinate coord_catch = re.findall("(.+)<coordinates>(.+)</coordinates>(" ".+)", kml, re.DOTALL | re.MULTILINE) coord_list = coord_catch[0][1].split(",") new_list = [] for coord in coord_list: coord_new = coord.strip() if "0 " == coord_new[:2]: new_list.append(coord_new[2:]) else: new_list.append(coord_new) if new_list[-1] == '0': new_list.pop(-1) if (len(new_list) % 2) != 0: raise ValueError("Uneven number of coordinates. .kml read failed.\n" "Coordinates: %d" % len(new_list)) latitudes = [] longitudes = [] for _i, coord in enumerate(new_list): # print(coord) # print(_i % 2) if (_i % 2) == 0: longitudes.append(float(coord)) else: latitudes.append(float(coord)) return latitudes, longitudes
[docs]def get_positions(vital_file, filter_dict=False): """ Reads vital file and the gps coordinates in it. :param vital_file: path/to/your_float.vit :param filter_dict: This is a filter dictionary for the specific :return: tuple of three lists (dates, latitudes, longitudes) Usage: .. code-block:: python # Vital file vit_file = "<path/to/<your_vital_file>.vit>" # Get lats, lons mermaid_name, dates, latitudes, longitudes = get_positions(vit_file, filter_dict) """ # Read file with codecs.open(vital_file, encoding='utf-8') as f: content = f.read() # Find battery values gps_catch = re.findall( "(.+): (.{3,5})deg(.+)mn, (.+)deg(.+)mn", content) dates = [UTCDateTime(0).strptime(i[0], "%Y%m%d-%Hh%Mmn%S") for i in gps_catch] latitudes = [float(s[1].strip()[1:]) + float(s[2]) / 60 if s[1].strip()[0] == "N" else - float(s[1].strip()[1:]) - float(s[2]) / 60 for s in gps_catch] longitudes = [float(s[3].strip()[1:]) + float(s[4]) / 60 if s[3].strip()[0] == "E" else - float(s[3].strip()[1:]) - float(s[4]) / 60 for s in gps_catch] # Get the name of the Mermaid mermaid_name_tmp = os.path.basename(vital_file).split(".")[1] \ .split("-")[1:] mermaid_name = mermaid_name_tmp[1].lstrip("0") # Filter if filter_dictionary is given if filter_dict is not False: # Create empty lists latitudes_fixed = [] longitudes_fixed = [] dates_fixed = [] for _i, (date, latitude, longitude) in \ enumerate(zip(dates, latitudes, longitudes)): # Filter date if it is in any filter window if any([(UTCDateTime(window[0]) < date) and (date < UTCDateTime(window[1])) for window in filter_dict[int(mermaid_name)] ["time_filter"]]) is not True: dates_fixed.append(date) latitudes_fixed.append(latitude) longitudes_fixed.append(longitude) else: latitudes_fixed = latitudes longitudes_fixed = longitudes dates_fixed = dates return mermaid_name, dates_fixed, latitudes_fixed, longitudes_fixed
[docs]def get_last_positions(vital_file_list): """ Get all last positions of a list of vital files. :param vital_file_list: :return: prints list of last positions on the screen """ if type(vital_file_list) is not list: vital_file_list = [vital_file_list] # print descriptor print("label, time, latitude, longitude") for vit in vital_file_list: # Get last position: mermaid_number, t, lat, lon = get_positions(vit) # Check if there are no times for a Mermaid in the time window. if len(t) != 0: # Print shit print("%s, %s, %.5f, %.5f" % (mermaid_number, t[-1], lat[-1], lon[-1]))
[docs]def plot_path(lon, lat, **kwargs): """ Plots line on map. :param lat: list of latitudes :param lon: list of logitudes :param kwargs: keyword arguments for plotting function :return: path handle """ # Plot track path = plt.plot(lon, lat, transform=ccrs.Geodetic(), **kwargs) return path
[docs]def plot_point(lon, lat, size=2, **kwargs): """ Plots line on map. :param lat: one latitude degree :param lon: one longitude degreeĀ  :param kwargs: keyword arguments for plotting function :return: plot handle """ # Plot track pl = plt.plot(lon, lat, transform=ccrs.Geodetic(), **kwargs) return pl
[docs]def plot_text(text, lon, lat, **kwargs): """ Plots line on map. :param text: String with text input :param lat: one latitude degree :param lon: one longitude degree :param kwargs: keyword arguments for plotting function :return: text handle """ # Plot track txt = plt.text(lon, lat, text, transform=ccrs.Geodetic(), **kwargs) return txt
[docs]class MermaidLocations(object): """Class that handles plotting of MERMAIDS using the vital file input. The underlying mapping tool box is Cartopy which is very powerful, but not yet fully grown. As a result Only the PlateCarree projection can be used as of now; hence, no option to vary this parameter in terms of plotting. Usage: .. code-block:: python # Create ML plotting class ML = MermaidLocation.from_vit_file(vital_file_list) # Plot full map ML.plot() """ def __init__(self, latitudes, longitudes, times=None, mermaid_names=None, lon_ticks=[160.0, 180.0, -180.0, -160.0, -140.0, -120.0, -100.0], lat_ticks=[-60, -40.0, -20.0, 0.0, 20.0], minlon=160.0, maxlon=260.0, minlat=-42.5, maxlat=5.0, central_longitude=180.0, mermaid_markersize=30, markerfontsize=None, legend=True, legend_cols=1, legend_title=None, fontsize=14, plot_labels=True, label_offset=-0.05, trajectories=True, trajectory_width=8, trajectory_cmp="gist_heat", wms=False, wms_url=None, wms_layer=None, frames=100, frames_per_sec=24, animation_writer="ffmpeg", movie_dpi=150, figsize=(15, 8)): """This part initializes the class. :param latitudes: 2D list with 1 row for each mermaid :type latitudes: list :param longitudes: 2D list with 1 row for each mermaid :type longitudes: list :param times: 2D list with 1 row for each mermaid :type times: list :param mermaid_names: List with 1 name for each mermaid :type mermaid_names: list :param lon_ticks: List of map longitude ticks for plotting. Make sure you have one more index than necessary. #justpythonthings :type lon_ticks: list :param lat_ticks: List of map latitude ticks for plotting. Make sure you have one more index than necessary. #justpythonthings :type lat_ticks: list :param minlon: Minimum map longitude :type minlon: float or int :param maxlon: Maximum map longitude :type maxlon: float or int :param minlat: Minimum map latitude :type minlat: float or int :param maxlat: Maximum map latitude :type maxlat: float or int :param central_longitude: Set the central longitude of the map. Important for plotting of the pacific for example. :type central_longitude: float or int :param begin: Datetime of float operation :type begin: UTCDateTime :param end: Datetime stamp of float operation :type end: UTCDateTime :param mermaid_markersize: Markersize for the Mermaid markers :type mermaid_markersize: float or int :param legend: Plot legend of map content :type legend: bool :param legend_cols: Number of columns in the legend. Default 1 :type legend_cols: int :param legend_title: Some title. Default None :type legend_title: str :param fontsize: General fontsize for the figure :type fontsize: float or int :param plot_labels: Plot labels of the mermaid number onto the markers :type plot_labels: bool :param label_offset: Text will be offset by a some latitude. Important if you change the size of the marker since the center of the text is not on the point given in label location. :type label_offset: float :param markerfontsize: Fontsize for Label on Mermaid marker :type markerfontsize: float or int :param trajectories: Plot trajectories of the mermaids. Default `True`, but :attr:`times` has to be defined. :type trajectories: bool :param trajectory_width: Width of the trajectories' line plots :type trajectory_width: float or int :param trajectory_cmp: Colormap of the trajectories. :type trajectory_cmp: str :param wms: Get WMS map from a server. :type wms: bool :param wms_url: WMS request URL :type wms_url: str :param wms_layer: Name of the requested layer. :type wms_layer: str :param frames: If animation is wanted the frame number can be set with this kwarg. :type frames: int :param animation_writer: Choose what encoder to use to write the animation to file. Default is "ffmpeg", imagemagick requires a brew install on mac. :type animation_writer: str :param movie_dpi: The dpi with which the movie is exported :type movie_dpi: int :param figsize: Define the figure size (Width, Height) :type figsize: tuple :return: MermaidLocation object. """ # Main Data self.latitudes = latitudes self.longitudes = longitudes self.times = times self.mermaid_names = mermaid_names ### Plot paramaters # Figure size self.figsize = figsize # General Map Stuff self.legend = legend self.legend_cols = legend_cols self.legend_title = legend_title self.fontsize = fontsize # MapBorders self.central_longitude = central_longitude self.bounds = [minlon, maxlon, minlat, maxlat] self.lon_ticks = lon_ticks self.lat_ticks = lat_ticks # Markers self.mermaid_markersize = mermaid_markersize self.plot_labels = plot_labels self.label_offset = label_offset if markerfontsize == None: self.markerfontsize = 5/25 * self.mermaid_markersize else: self.markerfontsize = markerfontsize # Trajectories self.trajectories = trajectories self.trajectory_width = trajectory_width self.trajectory_cmap = trajectory_cmp # Animation settings self.frames = frames self.frames_per_sec = frames_per_sec self.writer = animation_writer self.movie_dpi = movie_dpi # WMS settings self.wms = wms self.wms_url = wms_url self.wms_layer = wms_layer # Empty data container for extra data such as ship tracks. self.auxiliary_data = None """Will be stored in form of dictionaries xdata:, ydata:, kwarg_dict"""
[docs] @classmethod def from_vit_file(cls, vital_file_list, filter_dict=False, **kwargs): """Gets the content of the vital and parses it to the class. Parameters are the same as for the `__init__` except the `latitude`, `longitude`, times, and mermaid_names """ # Create empty lists times = [] latitudes = [] longitudes = [] mermaid_names = [] # Get filter dictionary if filter_dict: filter_dict = read_yaml_file(filter_dict) for mermaid in vital_file_list: # Get locations and times mermaid_name, t, lat, lon = \ get_positions(mermaid, filter_dict=filter_dict) # Check if there are no times for a Mermaid in the time window. if len(t) != 0: # Add to lists mermaid_names.append(mermaid_name) times.append(t) latitudes.append(lat) longitudes.append(lon) return cls(latitudes, longitudes, times=times, mermaid_names=mermaid_names, **kwargs)
[docs] def compute_second_record(self): """Takes in all times and creates smallest and largest number from max and min UTCDatetimes.""" pass # Create empty lists. first_times = [] last_times = [] for t in self.times: # Get Mermaid start and endtimes first_times.append(t[0]) last_times.append(t[-1]) # Set oldest and youngest date self.first_time = min_UTC(first_times) self.last_time = max_UTC(last_times) # Get a history of the time in seconds. self.times_s = [] # Make seconds out of UTCDateTimes for _i, rows in enumerate(self.times): new_row = [] for _j, time in enumerate(rows): new_row.append(self.last_time - time) self.times_s.append(new_row)
[docs] def add_aux_data(self, lon, lat, **kwargs): """ Adding auxiliary data. :param lon: Longitudes :type lon: float or list of floats :param lat: Latitudes :type lon: float or list of floats :param kwargs: keyword arguments for matplotlib functions :return: """ if self.auxiliary_data is None: self.auxiliary_data = [] # Create empty dictionary data_dict = dict() data_dict["lon"] = lon data_dict["lat"] = lat data_dict["kwargs"] = kwargs # Add data to data list self.auxiliary_data.append(data_dict)
[docs] def plot(self, f=None, **kwargs): """Plots everything. :param f: Filename. No plot will be displayed, but a file will be generated. :param kwargs: Are parsed to `pyplot.savefig` if `f` is defined and to plt.show if `f` is `None` """ # Plot background map self.plot_map() # Plot Trajectories self.plot_trajectories() # Plot Mermaid markers self.plot_markers() # Plot auxiliary data self.plot_aux_data() # Plot colorbar self.activate_colorbar() # Plot legend if wanted self.plot_legend() if f is not None: # Save figure plt.savefig(f, bbox_inches='tight', **kwargs) else: # Show stuff plt.show(block=True)
[docs] def animate(self, f=None, writer=None, **kwargs): """Animates the trajectories of the Mermaids... :param f: output file name :type f: str :param writer: name of movie writer. Overwrites class config. :type writer: str """ # Plotting the basemap self.plot_map() # Plot auxiliary data self.plot_aux_data() # Plot first markers. # activate the colorbar # Will not work right now .... # self.activate_colorbar() # Plot legend if wanted self.plot_legend() # Plot animation ani = self.plot_animation() if f is None: # Show everything plt.show() else: if writer is None: # Set up formatting for the movie files if self.writer == "ffmpeg": Writer = animation.writers['ffmpeg'] writer = Writer(fps=self.frames_per_sec, metadata=dict(artist='Me'), bitrate=1800) else: Writer = animation.writers[writer] writer = Writer(fps=self.frames_per_sec, metadata=dict(artist='Me'), bitrate=1800) ani.save(f, writer=writer, dpi=self.movie_dpi,)
[docs] def plot_animation(self): """ Plots animation of the trajectories .. warning:: This method only works if you have defined the times! The way this method works is, first all full segments to be drawn at each timestep are computed into lists as well as the final marker positions. These are then accessed by the :func:`update` function, which changes the location of the marker and adds the trajectory. The update function is defined inside this function, as it has no meaning outside.""" if self.times is None: raise ValueError("You can only plot trajectories if the time " "stamps are defined! Trajectories without times " "make no sense, you see ... ?") else: self.compute_second_record() # As a function of time steps compute time chunks. time_chunks = self._get_time_chunks() # Create empty list for trajectories that are too long. Meaning a # list of End points for split trajectories. self.end_points = [] # Create list of indices for each frame line_collection_list = [] segments = [] times = [] last_times = [] # Necessary for marker plotting marker_location_list = [] marker_list = [] label_list = [] last_times = [] for _i in range(self.frames): # List of indices for each mermaid of one frame. frame_segments = [] frame_times = [] frame_last_locations = [] frame_last_times = [] # Create big line collection for every frame for _j, (lat, lon, t) in enumerate(zip(self.latitudes, self.longitudes, self.times_s)): # Get indices within time_chunks ind = np.where((time_chunks[_i][0] <= np.array(t)) & (np.array(t) <= time_chunks[_i][1]))[0] # Saving the latest time for display purposes! frame_last_times.append(time_chunks[_i][0]) if len(ind) > 0: # last location last_lat = np.array(lat)[ind][-1] last_lon = np.array(lon)[ind][-1] frame_last_locations.append([last_lon, last_lat]) else: # Append None if there is no updated location. frame_last_locations.append(None) if len(ind) > 1: # Get picked times picked_times = np.array(t)[ind] # Get segments and indices segs, inds = self._get_segments(_i, np.array(lat)[ind], np.array(lon)[ind], self.end_points) # Add to lists. frame_segments += segs frame_times += picked_times[inds].tolist() # Creating an emtpy list of markers outside the map # it wasn't possible to make the markers empty if _i == 0: marker, lab = self._plot_mermaid_marker( np.array([9999]), np.array([9999]), self.mermaid_names[_j], zorder=10000 + _j) # Add marker to marker list and label to label list marker_list.append(marker) label_list.append(lab) # Add all location for one frame to location list marker_location_list.append(frame_last_locations) # get last times for every timestep last_times.append(max_UTC([self.last_time - x for x in frame_last_times])) if len(frame_segments) > 0: # Create line collection and append to LC for each frame lc = self._make_line_collection(np.array([]), np.array([]), self.first_time, self.last_time, self.trajectory_cmap, self.trajectory_width) line_collection_list.append(self.ax.add_collection(lc)) segments.append(frame_segments) times.append(frame_times) else: line_collection_list.append(None) segments.append(None) times.append(None) # Set title time = last_times[-1].isoformat().split(".")[ 0].split("T") label = "Date: %s -- Time: %s" % (time[0], time[1]) tit = plt.title(label) tit.set_va("bottom") # Create Update function for the map def update(i): """ This is the function that updates the figure. Meaning, The mermaid marker locations are updated, additional trajectories are added, and title timestamp is changed. """ # Note the -1 index. has been set because the times are reversed # due to previous calculations... if line_collection_list[-i] is not None: line_collection_list[-i].set_segments(np.array(segments[-i])) line_collection_list[-i].set_array(np.array(times[-i])) z = line_collection_list[-i].get_zorder() line_collection_list[-i].set_zorder(z+i) # Updating the title time1 = last_times[-i].isoformat().split(".")[ 0].split("T") label = "Date: %s -- Time: %s" % (time1[0], time1[1]) tit.set_text(label) tit.set_backgroundcolor("w") # necessary because updating the # title doesnt overwrite the old one, but plots it on top # Updating the marker for _j, (marker, label, locs) in \ enumerate(zip(marker_list, label_list, marker_location_list[-i])): if locs is not None: # Setting marker[0] is necessary marker[0].set_xdata(locs[0]) marker[0].set_ydata(locs[1]) label.set_position((locs[0], locs[1] + self.label_offset)) # Only return none-None objects to be updated. return_tuple = [x for x in line_collection_list if x is not None] \ + [x[0] for x in marker_list if x is not None] \ + [x for x in label_list if x is not None] \ + [tit] return return_tuple # Create animation is activated outside this function via plt.show() ani = animation.FuncAnimation(self.fig, update, frames=np.arange(self.frames)+1, interval=1, repeat=False, blit=True, init_func=lambda: [x for x in line_collection_list if x is not None] + [x[0] for x in marker_list if x is not None] + [x for x in label_list if x is not None] ) return ani
def _get_time_chunks(self): """From the number of frames as well as min/max times the time chunks can be found. The function will return a list of timestamp pairs given in seconds after the minimum time.""" # Get dt from timespan/frames dt = (self.last_time - self.first_time)/self.frames time_chunks = [] for i in range(self.frames): chunk = [i*dt, (i+1)*dt] time_chunks.append(chunk) return time_chunks
[docs] def plot_legend(self): """Plots legend with the given parameters""" # Set up dictionary with legend setup properties legend_props = {"loc": 'lower right', "ncol": self.legend_cols, "edgecolor": "k", "fancybox": False, "framealpha": 1, "fontsize": self.fontsize-2} # Plot legend if wanted if self.legend: l = plt.legend(**legend_props) if self.legend_title is not None: font_dict = {"weight": "bold", "size": self.fontsize-2} l.set_title(self.legend_title, prop=font_dict)
[docs] def plot_map(self): """Plots the background map for float visualization and sets the axis and figure properties.""" # Set projection. The fix below is to solve the issu of a badly # resolved Great Circle paths # ------ GC plotting fix ------- class PC(ccrs.PlateCarree): @property def threshold(self): return 0.001 # ------------------------------ proj = PC(self.central_longitude) self.fig = plt.figure(figsize=self.figsize) self.ax = plt.axes(projection=proj) # ax.set_global() self.ax.frameon = False # ax.outline_patch.set_visible(False) # Set gridlines. NO LABELS HERE, there is a bug in the gridlines # function around 180deg gl = self.ax.gridlines(crs=ccrs.PlateCarree(), draw_labels=False, linewidth=1, color='lightgray', alpha=0.5, linestyle='-') gl.xlabels_top = False gl.ylabels_left = False gl.xlines = True gl.xlocator = mticker.FixedLocator(self.lon_ticks) gl.ylocator = mticker.FixedLocator(self.lat_ticks) # Set ticklabels self.ax.set_xticks(self.lon_ticks, crs=ccrs.PlateCarree()) self.ax.set_yticks(self.lat_ticks, crs=ccrs.PlateCarree()) # Change fontsize font_dict = {"fontsize": self.fontsize, "weight": "bold"} self.ax.set_xticklabels(self.ax.get_xticklabels(), fontdict=font_dict) self.ax.set_yticklabels(self.ax.get_yticklabels(), fontdict=font_dict) # Change format lon_formatter = LongitudeFormatter(zero_direction_label=True) lat_formatter = LatitudeFormatter() self.ax.xaxis.set_major_formatter(lon_formatter) self.ax.yaxis.set_major_formatter(lat_formatter) # Set Map boundary self.ax.set_extent(self.bounds, crs=ccrs.PlateCarree()) # Get WMS map if wanted if self.wms: self.ax.add_wms(self.wms_url, self.wms_layer, resample=True, interpolation='spline36') else: self.ax.stock_img() self.ax.add_feature(cfeature.LAND, zorder=10) self.ax.add_feature(cfeature.COASTLINE, zorder=10)
[docs] def plot_trajectories(self): """This function uses the the complete data of latitude, longitude and times to plot the trajectories. .. warning:: This method only works if you have defined the times! """ if self.times is None: raise ValueError("You can only plot trajectories if the time " "stamps are defined! Trajectories without times " "make no sense, you see ... ?") else: self.compute_second_record() # Create empty list for trajectories that are too long. Meaning a # list of End points for split trajectories. self.end_points = [] # Loop over mermaids for _i, (lat, lon, t) in enumerate(zip(self.latitudes, self.longitudes, self.times_s)): # Create line collection lc = self._plot_1_traj(_i, lat, lon, t, self.end_points) self.ax.add_collection(lc)
def _plot_1_traj(self, _i, lat, lon, t, end_points): """This function computes one trajectory for a given set of lats, lons. and times, as well as the first and last time of all trajectories in seconds, line width, cmap. :param _i: integer in list of mermaids :type _i: int :param lat: List of latitudes corresponfing to Mermaid track :type lat: list :param lon: List of longitudes corresponfing to Mermaid track :type lon: list :param t: List of times corresponfing to Mermaid track :param end_points: list that collects disconnecting points so that markers can be plotted. :type: list :return: LineCollection to be plotted """ # Transform arrays to numpy arrays for LineCollection lat = np.array(lat) lon = np.array(lon) t = np.array(t) # indices for times of each segment segments, indices = self._get_segments(_i, lat, lon, end_points) # Create LineCollection from points lc = self._make_line_collection(segments, t[indices], self.first_time, self.last_time, self.trajectory_cmap, self.trajectory_width) return lc @staticmethod def _get_segments(_i, lat, lon, end_points): """This function takes in lats and lons, and returns segments for a LineCollection. :param _i: index for end_point list :type _i: int :param lat: latitudes :type lat: list :param lon: longitudes :type lon: list :param end_points: end_points of trajectories (defining time jumps) :type end_points: list :return: tuple with (segments, indices) """ # Create empty lists indices = [] segments = [] # For each point pair in the trajectory of the Mermaid the loop # creates Line segements if the distance is smaller the 0.55deg for _j, (lat1, lon1, lat2, lon2) in enumerate( zip(lat[:-1], lon[:-1], lat[1:], lon[1:])): # Compute distance between points dist = locations2degrees(lat1, lon1, lat2, lon2) # Only create segment if # Segment if distance is smaller than 5 degrees. if dist < 3: segments.append([(lon1, lat1), (lon2, lat2)]) indices.append(_j) else: end_points.append((_i, lat1, lon1, dist)) return segments, indices @staticmethod def _make_line_collection(segments, cvals, min_val, max_val, cmap, line_width, zorder=100): """Takes in parameters to make colorbased paths LineCollection. :param segments: linesegments to be plotted :type segments: 2D array or list :param cvals: list corresponding to the segments :type cvals: list :param min_val: min normalization value :type min_val: float :param max_val: max normalization value :type max_val: float :param cmap: colorbar :type cmap: str :param line_width: width of the lineplotted. :type line_width: float :param zorder: plotting priority :type zorder: int :return: """ lc = LineCollection(segments, cvals, cmap=plt.get_cmap(cmap), norm=plt.Normalize(0, max_val - min_val), zorder=zorder) lc.set_transform(ccrs.Geodetic()) lc.set_array(cvals) lc.set_linewidth(line_width) return lc
[docs] def plot_markers(self): """This function uses the data included in :class:`MermaidLocations` to plot the last positions of each Mermaid. """ # Plot Mermaid Locations for _i, (lat, lon) in enumerate(zip(self.latitudes, self.longitudes)): # Add label for one Mermaid. if _i == 0: self._plot_mermaid_marker(lon[-1], lat[-1], self.mermaid_names[_i], zorder=125 + _i, label="Mermaid") else: self._plot_mermaid_marker(lon[-1], lat[-1], self.mermaid_names[_i], zorder=125 + _i) for end_point in self.end_points: # Check if distance large enough. The value 6 was found by trial # and error. Lower values would have included the 24h tests as # previous posititions, and we don't want those. # The zorder here is set to a lower value than the main markers. # So that the main markers are always on top! if end_point[3] > 5.28: self._plot_mermaid_marker(end_point[2], end_point[1], self.mermaid_names[end_point[0]], zorder=75+_i)
def _plot_mermaid_marker(self, lon, lat, name, **kwargs): """ Plot mermaid marker. Only kwargs that work for both scatter plotting and text plotting are usable here! The external functions :func:`plot_point` and :func:`plot_text` is used to achieve that. :param lon: longitude :param lat: latitude :param name: name of mermaid. :param kwargs: for plt.text and plt.plot/scatter :return: tuple with (marker, label) handles """ # Mermaid Marker marker = plot_point(lon, lat, markersize=self.mermaid_markersize, marker=self._get_mermaid_vertices(), markeredgecolor='k', markerfacecolor='orange', clip_on=True, **kwargs) if self.plot_labels: lab = plot_text(name, lon, lat + self.label_offset, clip_on=True, horizontalalignment="center", verticalalignment='center', multialignment="center", fontsize=self.markerfontsize, fontweight="bold", **kwargs) else: lab = plot_point(lon, lat, marker="_", markeredgecolor='k', markersize=10 / 25 * self.mermaid_markersize, markerfacecolor='k', clip_on=True, **kwargs) return marker, lab
[docs] def plot_aux_data(self): """Plot data that is added to the class prior to plotting.""" if self.auxiliary_data is not None: for d in self.auxiliary_data: # Plot track plot_path(d["lon"], d["lat"], **d["kwargs"])
[docs] def activate_colorbar(self): """This activates the colorbar. """ # Calling function to plot one trajectory. lc = self._plot_1_traj(0, self.latitudes[0], self.longitudes[0], self.times_s[0], self.end_points) # Get max time extent maxt = self.last_time - self.first_time # Set marks at 5 positions ticks = [0] for tick in [1, 2, 3, 4]: # Get tick ticks.append(tick * maxt / 4) labels = [] for tick in ticks: # Get label time = (self.last_time - tick).isoformat().split(".")[0].split("T") label = "Date: %s\nTime: %s" % (time[0], time[1]) labels.append(label) # Create colorbar cb = self.fig.colorbar(lc, ticks=ticks, shrink=0.85, aspect=35) # Set labels # Change fontsize cb.set_ticklabels(labels) for l in cb.ax.yaxis.get_ticklabels(): l.set_fontsize(self.fontsize) cb.set_label(' UTC', rotation="horizontal", fontsize=self.fontsize, horizontalalignment="left", fontweight="bold")
@staticmethod def _get_mermaid_vertices(): """Returns vertices for the mermaid marker.""" return [(0.1, -.9), (0.1, -0.3), (0.25, -0.3), (0.4, 0), # right (0.25, 0.3), (0.1, 0.3), (0.1, 0.35), (0.05, 0.35), (0.05, 0.9), (-0.05, 0.9), (-0.05, 0.35), (-0.1, 0.35), # left (-0.1, 0.3), (-0.25, 0.3), (-0.4, 0), (-0.25, -0.3), (-0.1, -0.3), (-0.1, -1), (0.1, -.9)]
if __name__ == "__main__": print("This function is only called by python binaries or imported" "imported externally")