#!/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")