"""Tools for reconstructing and plotting geological features and feature data through time.

Methods in `plot.py` reconstruct geological features using 
[pyGPlates' `reconstruct` function](https://www.gplates.org/docs/pygplates/generated/pygplates.reconstruct.html),
turns them into plottable Shapely geometries, and plots them onto 
Cartopy GeoAxes using Shapely and GeoPandas.

Classes
-------
PlotTopologies
"""
import math
import re
import warnings

import cartopy.crs as ccrs
import geopandas as gpd
import matplotlib.pyplot as plt
import numpy as np
import pygplates
from shapely.geometry import (
    LineString,
    MultiLineString,
    MultiPolygon,
    Point,
    Polygon,
    box,
)
from shapely.geometry.base import BaseGeometry, BaseMultipartGeometry
from shapely.ops import linemerge, substring

from . import ptt
from .geometry import pygplates_to_shapely
from .gpml import _load_FeatureCollection
from .pygplates import FeatureCollection as _FeatureCollection
from .pygplates import _is_string
from .read_geometries import get_geometries as _get_geometries
from .read_geometries import (
    get_valid_geometries,
)  # included for backwards compatibility
from .reconstruction import PlateReconstruction as _PlateReconstruction
from .tools import EARTH_RADIUS


def plot_subduction_teeth(
    geometries,
    width,
    polarity=None,
    height=None,
    spacing=None,
    projection="auto",
    transform=None,
    ax=None,
    **kwargs,
):
    """Add subduction teeth to a plot.

    The subduction polarity used for subduction teeth can be specified
    manually or detected automatically if `geometries` is a
    `geopandas.GeoDataFrame` object with a `polarity` column.

    Parameters
    ----------
    geometries : geopandas.GeoDataFrame, sequence of shapely geometries, or str
        If a `geopandas.GeoDataFrame` is given, its geometry attribute
        will be used. If `geometries` is a string, it must be the path to
        a file, which will be loaded with `geopandas.read_file`. Otherwise,
        `geometries` must be a sequence of shapely geometry objects (instances
        of the `shapely.geometry.base.BaseGeometry` class).
    width : float
        The (approximate) width of the subduction teeth. If a projection is
        used, this value will be in projected units.
    polarity : {"left", "l", "right", "r", None}, default None
        The subduction polarity of the geometries. If no polarity is provided,
        and `geometries` is a `geopandas.GeoDataFrame`, this function will
        attempt to find a `polarity` column in the data frame and use the
        values given there. If `polarity` is not manually specified and no
        appropriate column can be found, an error will be raised.
    height : float, default None
        If provided, the height of the subduction teeth. As with `width`,
        this value should be given in projected units. If no value is given,
        the height of the teeth will be equal to 0.6 * `width`.
    spacing : float, default None
        If provided, the spacing between the subduction teeth. As with
        `width` and `height`, this value should be given in projected units.
        If no value is given, `spacing` will default to `width`, producing
        tightly packed subduction teeth.
    projection : cartopy.crs.Transform, "auto", or None, default "auto"
        The projection of the plot. If the plot has no projection, this value
        can be explicitly given as `None`. The default value is "auto", which
        will acquire the projection automatically from the plot axes.
    transform : cartopy.crs.Transform, or None, default None
        If the plot is projected, a `transform` value is usually needed.
        Frequently, the appropriate value is an instance of
        `cartopy.crs.PlateCarree`.
    ax : matplotlib.axes.Axes, or None, default None
        The axes on which the subduction teeth will be drawn. By default,
        the current axes will be acquired using `matplotlib.pyplot.gca`.
    **kwargs
        Any further keyword arguments will be passed to
        `matplotlib.patches.Polygon`.

    Raises
    ------
    ValueError
        If `width` <= 0, or if `polarity` is an invalid value or could not
        be determined.
    """
    if ax is None:
        ax = plt.gca()

    if projection == "auto":
        try:
            projection = ax.projection
        except AttributeError:
            projection = None
    elif isinstance(projection, str):
        raise ValueError("Invalid projection: {}".format(projection))

    if polarity is None:
        polarity_column = _find_polarity_column(geometries.columns.values)
        if polarity_column is None:
            raise ValueError(
                "Could not automatically determine polarity; "
                + "it must be defined manually instead."
            )
        triangles = []
        for p in geometries[polarity_column].unique():
            if p.lower() not in {"left", "l", "right", "r"}:
                continue
            gdf_polarity = geometries[geometries[polarity_column] == p]
            triangles.extend(
                _tessellate_triangles(
                    gdf_polarity,
                    width,
                    p,
                    height,
                    spacing,
                    projection,
                    transform,
                )
            )
    else:
        triangles = _tessellate_triangles(
            geometries,
            width,
            polarity,
            height,
            spacing,
            projection,
            transform,
        )

    if projection is not None:
        domain = projection.domain
        triangles = [domain.intersection(i) for i in triangles]

    if hasattr(ax, "add_geometries") and projection is not None:
        ax.add_geometries(triangles, crs=projection, **kwargs)
    else:
        for triangle in triangles:
            ax.fill(*triangle.exterior.xy, **kwargs)


def _tessellate_triangles(
    geometries,
    width,
    polarity="left",
    height=None,
    spacing=None,
    projection=None,
    transform=None,
):
    """Generate subduction teeth triangles for plotting.

    Forms continuous trench geometries and identifies their subduction polarities.
    Subduction teeth triangles can be customised with a given spacing and width.
    Their apexes point in the identified polarity directions.

    Parameters
    ----------
    geometries : geopandas.GeoDataFrame, sequence of shapely geometries, or str
        If a `geopandas.GeoDataFrame` is given, its geometry attribute
        will be used. If `geometries` is a string, it must be the path to
        a file, which will be loaded with `geopandas.read_file`. Otherwise,
        `geometries` must be a sequence of shapely geometry objects (instances
        of the `shapely.geometry.base.BaseGeometry` class).
    width : float
        The (approximate) width of the subduction teeth. If a projection is
        used, this value will be in projected units.
    polarity : {"left", "l", "right", "r", None}, default "left"
        The subduction polarity of the geometries. If no polarity is provided,
        and `geometries` is a `geopandas.GeoDataFrame`, this function will
        attempt to find a `polarity` column in the data frame and use the
        values given there. If `polarity` is not manually specified and no
        appropriate column can be found, an error will be raised.
    height : float, default None
        If provided, the height of the subduction teeth. As with `width`,
        this value should be given in projected units. If no value is given,
        the height of the teeth will be equal to 0.6 * `width`.
    spacing : float, default None
        If provided, the spacing between the subduction teeth. As with
        `width` and `height`, this value should be given in projected units.
        If no value is given, `spacing` will default to `width`, producing
        tightly packed subduction teeth.
    projection : cartopy.crs.Transform, "auto", or None, default None
        The projection of the plot. If the plot has no projection, this value
        can be explicitly given as `None`. The default value is "auto", which
        will acquire the projection automatically from the plot axes.
    transform : cartopy.crs.Transform, or None, default None
        If the plot is projected, a `transform` value is usually needed.
        Frequently, the appropriate value is an instance of
        `cartopy.crs.PlateCarree`.

    Returns
    -------
    results : list of shapely Polygon objects
        Subduction teeth generated for the given geometries.
    """
    if width <= 0.0:
        raise ValueError("Invalid `width` argument: {}".format(width))
    polarity = _parse_polarity(polarity)
    geometries = _parse_geometries(geometries)
    if height is None:
        height = width * 2.0 / 3.0
    if spacing is None:
        spacing = width

    if projection is not None:
        geometries_new = []
        for i in geometries:
            geometries_new.extend(_project_geometry(i, projection, transform))
        geometries = geometries_new
        del geometries_new
    geometries = linemerge(geometries)
    if isinstance(geometries, BaseMultipartGeometry):
        geometries = list(geometries.geoms)
    elif isinstance(geometries, BaseGeometry):
        geometries = [geometries]

    results = _calculate_triangle_vertices(
        geometries,
        width,
        spacing,
        height,
        polarity,
    )

    return results


def _project_geometry(geometry, projection, transform=None):
    """Project shapely geometries onto a certain Cartopy CRS map projection.

    Uses a coordinate system ("transform"), if given.

    Parameters
    ----------
    geometry : shapely.geometry.base.BaseGeometry
        An instance of a shapely geometry.
    projection : cartopy.crs.Transform,
        The projection of the plot.
    transform : cartopy.crs.Transform or None, default None
        If the plot is projected, a `transform` value is usually needed.
        Frequently, the appropriate value is an instance of
        `cartopy.crs.PlateCarree`.

    Returns
    -------
    projected : list
        The provided shapely geometries projected onto a Cartopy CRS map projection.
    """
    if transform is None:
        transform = ccrs.PlateCarree()
    result = [projection.project_geometry(geometry, transform)]
    projected = []
    for i in result:
        if isinstance(i, BaseMultipartGeometry):
            projected.extend(list(i.geoms))
        else:
            projected.append(i)
    return projected


def _calculate_triangle_vertices(
    geometries,
    width,
    spacing,
    height,
    polarity,
):
    """Generate vertices of subduction teeth triangles.

    Triangle bases are set on shapely BaseGeometry trench instances with their apexes
    pointing in directions of subduction polarity. Triangle dimensions are set by a
    specified width, spacing and height (either provided by the user or set as default
    values from _tessellate_triangles). The teeth are returned as shapely polygons.

    Parameters
    ----------
    geometries : list of shapely geometries (instances of the
        shapely.geometry.base.BaseGeometry or shapely.geometry.base.BaseMultipartGeometry
        class)
        Trench geometries projected onto a certain map projection (using a
        coordinate system if specified), each with identified subduction polarities.
        Teeth triangles will be generated only on the BaseGeometry instances.
    width : float
        The (approximate) width of the subduction teeth. If a projection is
        used, this value will be in projected units.
    spacing : float,
        The spacing between the subduction teeth. As with
        `width` and `height`, this value should be given in projected units.
    height : float, default None
        The height of the subduction teeth. This value should also be given in projected
        units.
    polarity : {"left", "right"}
        The subduction polarity of the shapely geometries.

    Returns
    -------
    triangles : list of shapely polygons
        The subduction teeth generated along the supplied trench geometries.
    """
    if isinstance(geometries, BaseGeometry):
        geometries = [geometries]
    triangles = []
    for geometry in geometries:
        if not isinstance(geometry, BaseGeometry):
            continue

        length = geometry.length
        tessellated_x = []
        tessellated_y = []

        for distance in np.arange(spacing, length, spacing):
            point = Point(geometry.interpolate(distance))
            tessellated_x.append(point.x)
            tessellated_y.append(point.y)
        tessellated_x = np.array(tessellated_x)
        tessellated_y = np.array(tessellated_y)

        for i in range(len(tessellated_x) - 1):
            normal_x = tessellated_y[i] - tessellated_y[i + 1]
            normal_y = tessellated_x[i + 1] - tessellated_x[i]
            normal = np.array((normal_x, normal_y))
            normal_mag = np.sqrt((normal**2).sum())
            if normal_mag == 0:
                continue
            normal *= height / normal_mag
            midpoint = np.array((tessellated_x[i], tessellated_y[i]))
            if polarity == "right":
                normal *= -1.0
            apex = midpoint + normal

            next_midpoint = np.array((tessellated_x[i + 1], tessellated_y[i + 1]))
            line_vector = np.array(next_midpoint - midpoint)
            line_vector_mag = np.sqrt((line_vector**2).sum())
            line_vector /= line_vector_mag
            triangle_point_a = midpoint + width * 0.5 * line_vector
            triangle_point_b = midpoint - width * 0.5 * line_vector
            triangle_points = np.array(
                (
                    triangle_point_a,
                    triangle_point_b,
                    apex,
                )
            )
            triangles.append(Polygon(triangle_points))
    return triangles


def _parse_polarity(polarity):
    """Ensure subduction polarities have valid strings as labels - either "left", "l", "right" or "r".

    The geometries' subduction polarities are either provided by the user in plot_subduction_teeth
    or found automatically in a geopandas.GeoDataFrame column by _find_polarity_column, if such a
    column exists.

    Parameters
    ----------
    polarity : {"left", "l", "right", "r"}
        The subduction polarity of the geometries (either set by the user or found automatically
        from the geometries' data frame).

    Returns
    -------
    polarity : {"left", "right"}
        Returned if the provided polarity string is one of {"left", "l", "right", "r"}. "l" and "r"
        are classified and returned as "left" and "right" respectively.

    Raises
    ------
    TypeError
        If the provided polarity is not a string type.
    ValueError
        If the provided polarity is not valid ("left", "l", "right" or "r").
    """
    if not isinstance(polarity, str):
        raise TypeError("Invalid `polarity` argument type: {}".format(type(polarity)))
    if polarity.lower() in {"left", "l"}:
        polarity = "left"
    elif polarity.lower() in {"right", "r"}:
        polarity = "right"
    else:
        valid_args = {"left", "l", "right", "r"}
        err_msg = "Invalid `polarity` argument: {}".format(
            polarity
        ) + "\n(must be one of: {})".format(valid_args)
        raise ValueError(err_msg)
    return polarity


def _find_polarity_column(columns):
    """Search for a 'polarity' column in a geopandas.GeoDataFrame to extract subduction
    polarity values.

    Subduction polarities can be used for tessellating subduction teeth.

    Parameters
    ----------
    columns : geopandas.GeoDataFrame.columns.values instance
        A list of geopandas.GeoDataFrame column header strings.

    Returns
    -------
    column : list
        If found, returns a list of all subduction polarities ascribed to the supplied
        geometry data frame.
    None
        if a 'polarity' column was not found in the data frame. In this case, subduction
        polarities will have to be manually provided to plot_subduction_teeth.

    """
    pattern = "polarity"
    for column in columns:
        if re.fullmatch(pattern, column) is not None:
            return column
    return None


def _parse_geometries(geometries):
    """Resolve a geopandas.GeoSeries object into shapely BaseGeometry and/or
    BaseMutipartGeometry instances.

    Parameters
    ----------
    geometries : geopandas.GeoDataFrame, sequence of shapely geometries, or str
        If a `geopandas.GeoDataFrame` is given, its geometry attribute
        will be used. If `geometries` is a string, it must be the path to
        a file, which will be loaded with `geopandas.read_file`. Otherwise,
        `geometries` must be a sequence of shapely geometry objects (instances
        of the `shapely.geometry.base.BaseGeometry` class).

    Returns
    -------
    out : list
        Resolved shapely BaseMutipartGeometry and/or BaseGeometry instances.
    """
    geometries = _get_geometries(geometries)
    if isinstance(geometries, gpd.GeoSeries):
        geometries = list(geometries)

    # Explode multi-part geometries
    # Weirdly the following seems to be faster than
    # the equivalent explode() method from GeoPandas:
    out = []
    for i in geometries:
        if isinstance(i, BaseMultipartGeometry):
            out.extend(list(i.geoms))
        else:
            out.append(i)
    return out


def _clean_polygons(data, projection):
    data = gpd.GeoDataFrame(data)
    data = data.explode(ignore_index=True)

    if data.crs is None:
        data.crs = ccrs.PlateCarree()

    if isinstance(
        projection,
        (
            ccrs._RectangularProjection,
            ccrs._WarpedRectangularProjection,
        ),
    ):
        central_longitude = _meridian_from_projection(projection)
        dx = 1.0e-3
        dy = 5.0e-2
        rects = (
            box(
                central_longitude - 180,
                -90,
                central_longitude - 180 + dx,
                90,
            ),
            box(
                central_longitude + 180 - dx,
                -90,
                central_longitude + 180,
                90,
            ),
            box(
                central_longitude - 180,
                -90 - dy * 0.5,
                central_longitude + 180,
                -90 + dy * 0.5,
            ),
            box(
                central_longitude - 180,
                90 - dy * 0.5,
                central_longitude + 180,
                90 + dy * 0.5,
            ),
        )
        rects = gpd.GeoDataFrame(
            {"geometry": rects},
            geometry="geometry",
            crs=ccrs.PlateCarree(),
        )
        data = data.overlay(rects, how="difference")

    projected = data.to_crs(projection)

    # If no [Multi]Polygons, return projected data
    for geom in projected.geometry:
        if isinstance(geom, (Polygon, MultiPolygon)):
            break
    else:
        return projected

    proj_width = np.abs(projection.x_limits[1] - projection.x_limits[0])
    proj_height = np.abs(projection.y_limits[1] - projection.y_limits[0])
    min_distance = np.mean((proj_width, proj_height)) * 1.0e-4

    boundary = projection.boundary
    if np.array(boundary.coords).shape[1] == 3:
        boundary = type(boundary)(np.array(boundary.coords)[:, :2])
    return _fill_all_edges(projected, boundary, min_distance=min_distance)


def _fill_all_edges(data, boundary, min_distance=None):
    data = gpd.GeoDataFrame(data).explode(ignore_index=True)

    def drop_func(geom):
        if hasattr(geom, "exterior"):
            geom = geom.exterior
        coords = np.array(geom.coords)
        return np.all(np.abs(coords) == np.inf)

    to_drop = data.geometry.apply(drop_func)
    data = (data[~to_drop]).copy()

    def filt_func(geom):
        if hasattr(geom, "exterior"):
            geom = geom.exterior
        coords = np.array(geom.coords)
        return np.any(np.abs(coords) == np.inf) or (
            min_distance is not None and geom.distance(boundary) < min_distance
        )

    to_fix = data.index[data.geometry.apply(filt_func)]
    for index in to_fix:
        fixed = _fill_edge_polygon(
            data.geometry.at[index],
            boundary,
            min_distance=min_distance,
        )
        data.geometry.at[index] = fixed
    return data


def _fill_edge_polygon(geometry, boundary, min_distance=None):
    if isinstance(geometry, BaseMultipartGeometry):
        return type(geometry)(
            [_fill_edge_polygon(i, boundary, min_distance) for i in geometry.geoms]
        )
    if not isinstance(geometry, Polygon):
        geometry = Polygon(geometry)
    coords = np.array(geometry.exterior.coords)

    segments_list = []
    segment = []
    for x, y in coords:
        if (np.abs(x) == np.inf or np.abs(y) == np.inf) or (
            min_distance is not None and boundary.distance(Point(x, y)) <= min_distance
        ):
            if len(segment) > 1:
                segments_list.append(segment)
                segment = []
            continue
        segment.append((x, y))
    if len(segments_list) == 0:
        return geometry
    segments_list = [LineString(i) for i in segments_list]

    out = []
    for i in range(-1, len(segments_list) - 1):
        segment_before = segments_list[i]
        point_before = Point(segment_before.coords[-1])

        segment_after = segments_list[i + 1]
        point_after = Point(segment_after.coords[0])

        d0 = boundary.project(point_before, normalized=True)
        d1 = boundary.project(point_after, normalized=True)
        boundary_segment = substring(boundary, d0, d1, normalized=True)

        if boundary_segment.length > 0.5 * boundary.length:
            if d1 > d0:
                seg0 = substring(boundary, d0, 0, normalized=True)
                seg1 = substring(boundary, 1, d1, normalized=True)
            else:
                seg0 = substring(boundary, d0, 1, normalized=True)
                seg1 = substring(boundary, 0, d1, normalized=True)

            if isinstance(seg0, Point) and isinstance(seg1, LineString):
                boundary_segment = seg1
            elif isinstance(seg1, Point) and isinstance(seg0, LineString):
                boundary_segment = seg0
            else:
                boundary_segment = linemerge([seg0, seg1])

        if i == -1:
            out.append(segment_before)
        out.append(boundary_segment)
        if i != len(segments_list) - 2:
            out.append(segment_after)

    return Polygon(np.vstack([i.coords for i in out]))


def _meridian_from_ax(ax):
    if hasattr(ax, "projection") and isinstance(ax.projection, ccrs.Projection):
        proj = ax.projection
        return _meridian_from_projection(projection=proj)
    return 0.0


def _meridian_from_projection(projection):
    x = np.mean(projection.x_limits)
    y = np.mean(projection.y_limits)
    return ccrs.PlateCarree().transform_point(x, y, projection)[0]


def shapelify_features(features, central_meridian=0.0, tessellate_degrees=None):
    """Generate Shapely `MultiPolygon` or `MultiLineString` geometries
    from reconstructed feature polygons.

    Notes
    -----
    Some Shapely polygons generated by `shapelify_features` cut longitudes of 180
    or -180 degrees. These features may appear unclosed at the dateline, so Shapely
    "closes" these polygons by connecting any of their open ends with lines. These
    lines may manifest on GeoAxes plots as horizontal lines that span the entire
    global extent. To prevent this, `shapelify_features` uses pyGPlates'
    [DateLineWrapper](https://www.gplates.org/docs/pygplates/generated/pygplates.datelinewrapper)
    to split a feature polygon into multiple closed polygons if it happens to cut the
    antimeridian.
    Another measure taken to ensure features are valid is to order exterior coordinates
    of Shapely polygons anti-clockwise.

    Parameters
    ----------
    features : iterable of <pygplates.Feature>, <ReconstructedFeatureGeometry> or <GeometryOnSphere>
        Iterable containing reconstructed polygon features.
    central_meridian : float
        Central meridian around which to perform wrapping; default: 0.0.
    tessellate_degrees : float or None
        If provided, geometries will be tessellated to this resolution prior
        to wrapping.

    Returns
    -------
    all_geometries : list of `shapely.geometry.BaseGeometry`
        Shapely geometries converted from the given reconstructed features. Any
        geometries at the dateline are split.

    See Also
    --------
    geometry.pygplates_to_shapely : convert PyGPlates geometry objects to
    Shapely geometries.
    """
    if isinstance(
        features,
        (
            pygplates.Feature,
            pygplates.ReconstructedFeatureGeometry,
            pygplates.GeometryOnSphere,
        ),
    ):
        features = [features]

    geometries = []
    for feature in features:
        if isinstance(feature, pygplates.Feature):
            geometries.extend(feature.get_all_geometries())
        elif isinstance(feature, pygplates.ReconstructedFeatureGeometry):
            geometries.append(feature.get_reconstructed_geometry())
        elif isinstance(feature, (pygplates.GeometryOnSphere, pygplates.LatLonPoint)):
            geometries.append(feature)
        elif isinstance(feature, pygplates.DateLineWrapper.LatLonMultiPoint):
            geometries.append(
                pygplates.MultiPointOnSphere(
                    [i.to_lat_lon() for i in feature.get_points()]
                )
            )
        elif isinstance(feature, pygplates.DateLineWrapper.LatLonPolyline):
            geometries.append(pygplates.PolylineOnSphere(feature.get_points()))
        elif isinstance(feature, pygplates.DateLineWrapper.LatLonPolygon):
            geometries.append(
                pygplates.PolygonOnSphere(
                    [i.to_lat_lon() for i in feature.get_exterior_points()]
                )
            )

    return [
        pygplates_to_shapely(
            i,
            force_ccw=True,
            validate=True,
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
            explode=False,
        )
        for i in geometries
    ]


shapelify_feature_lines = shapelify_features
shapelify_feature_polygons = shapelify_features


class PlotTopologies(object):
    """A class with tools to read, reconstruct and plot topology features at specific
    reconstruction times.

    `PlotTopologies` is a shorthand for PyGPlates and Shapely functionalities that:

    * Read features held in GPlates GPML (GPlates Markup Language) files and
    ESRI shapefiles;
    * Reconstruct the locations of these features as they migrate through
    geological time;
    * Turn these reconstructed features into Shapely geometries for plotting
    on `cartopy.mpl.geoaxes.GeoAxes` or `cartopy.mpl.geoaxes.GeoAxesSubplot` map
    Projections.

    To call the `PlotTopologies` object, supply:

    * an instance of the GPlately `plate_reconstruction` object

    and optionally,

    * a `coastline_filename`
    * a `continent_filename`
    * a `COB_filename`
    * a reconstruction `time`
    * an `anchor_plate_id`

    For example:

        # Calling the PlotTopologies object
        gplot = gplately.plot.PlotTopologies(plate_reconstruction,
                                            coastline_filename=None,
                                            continent_filename=None,
                                            COB_filename=None,
                                            time=None,
                                            anchor_plate_id=0,
                )

        # Setting a new reconstruction time
        gplot.time = 20 # Ma

    The `coastline_filename`, `continent_filename` and `COB_filename` can be single
    strings to GPML and/or shapefiles, as well as instances of `pygplates.FeatureCollection`.
    If using GPlately's `DataServer` object to source these files, they will be passed as
    `pygplates.FeatureCollection` items.

    Some features for plotting (like plate boundaries) are taken from the `PlateReconstruction`
    object's`topology_features` attribute. They have already been reconstructed to the given
    `time` using [Plate Tectonic Tools](https://github.com/EarthByte/PlateTectonicTools).
    Simply provide a new reconstruction time by changing the `time` attribute, e.g.

        gplot.time = 20 # Ma

    which will automatically reconstruct all topologies to the specified time.
    You __MUST__ set `gplot.time` before plotting anything.

    A variety of geological features can be plotted on GeoAxes/GeoAxesSubplot maps
    as Shapely `MultiLineString` or `MultiPolygon` geometries, including:

    * subduction boundaries & subduction polarity teeth
    * mid-ocean ridge boundaries
    * transform boundaries
    * miscellaneous boundaries
    * coastline polylines
    * continental polygons and
    * continent-ocean boundary polylines
    * topological plate velocity vector fields
    * netCDF4 MaskedArray or ndarray raster data:
        - seafloor age grids
        - paleo-age grids
        - global relief (topography and bathymetry)
    * assorted reconstructable feature data, for example:
        - seafloor fabric
        - large igneous provinces
        - volcanic provinces

    Attributes
    ----------
    plate_reconstruction : instance of <gplately.reconstruction.PlateReconstruction>
        The GPlately `PlateReconstruction` object will be used to access a plate
        `rotation_model` and a set of `topology_features` which contains plate boundary
        features like trenches, ridges and transforms.

    anchor_plate_id : int, default 0
        The anchor plate ID used for reconstruction.

    base_projection : instance of <cartopy.crs.{transform}>, default <cartopy.crs.PlateCarree> object
        where {transform} is the map Projection to use on the Cartopy GeoAxes.
        By default, the base projection is set to cartopy.crs.PlateCarree. See the
        [Cartopy projection list](https://scitools.org.uk/cartopy/docs/v0.15/crs/projections.html)
        for all supported Projection types.

    coastlines : str, or instance of <pygplates.FeatureCollection>
        The full string path to a coastline feature file. Coastline features can also
        be passed as instances of the `pygplates.FeatureCollection` object (this is
        the case if these features are sourced from the `DataServer` object).

    continents : str, or instance of <pygplates.FeatureCollection>
        The full string path to a continent feature file. Continent features can also
        be passed as instances of the `pygplates.FeatureCollection` object (this is
        the case if these features are sourced from the `DataServer` object).

    COBs : str, or instance of <pygplates.FeatureCollection>
        The full string path to a COB feature file. COB features can also be passed
        as instances of the `pygplates.FeatureCollection` object (this is the case
        if these features are sourced from the `DataServer` object).

    coastlines : iterable/list of <pygplates.ReconstructedFeatureGeometry>
        A list containing coastline features reconstructed to the specified `time` attribute.

    continents : iterable/list of <pygplates.ReconstructedFeatureGeometry>
        A list containing continent features reconstructed to the specified `time` attribute.

    COBs : iterable/list of <pygplates.ReconstructedFeatureGeometry>
        A list containing COB features reconstructed to the specified `time` attribute.

    time : float
        The time (Ma) to reconstruct and plot geological features to.

    topologies : iterable/list of <pygplates.Feature>
        A list containing assorted topologies like:

        - pygplates.FeatureType.gpml_topological_network
        - pygplates.FeatureType.gpml_oceanic_crust
        - pygplates.FeatureType.gpml_topological_slab_boundary
        - pygplates.FeatureType.gpml_topological_closed_plate_boundary

    ridge_transforms : iterable/list of <pygplates.Feature>
        A list containing ridge and transform boundary sections of type
        pygplates.FeatureType.gpml_mid_ocean_ridge

    ridges : iterable/list of <pygplates.Feature>
        A list containing ridge boundary sections of type pygplates.FeatureType.gpml_mid_ocean_ridge

    transforms : iterable/list of <pygplates.Feature>
        A list containing transform boundary sections of type pygplates.FeatureType.gpml_mid_ocean_ridge

    trenches : iterable/list of <pygplates.Feature>
        A list containing trench boundary sections of type pygplates.FeatureType.gpml_subduction_zone

    trench_left : iterable/list of <pygplates.Feature>
        A list containing left subduction boundary sections of type pygplates.FeatureType.gpml_subduction_zone

    trench_right : iterable/list of <pygplates.Feature>
        A list containing right subduction boundary sections of type pygplates.FeatureType.gpml_subduction_zone

    other : iterable/list of <pygplates.Feature>
        A list containing other geological features like unclassified features, extended continental crusts,
        continental rifts, faults, orogenic belts, fracture zones, inferred paleo boundaries, terrane
        boundaries and passive continental boundaries.

    """

    def __init__(
        self,
        plate_reconstruction,
        coastlines=None,
        continents=None,
        COBs=None,
        time=None,
        anchor_plate_id=0,
    ):
        self.plate_reconstruction = plate_reconstruction

        if self.plate_reconstruction.topology_features is None:
            raise ValueError("Plate model must have topology features.")

        self.base_projection = ccrs.PlateCarree()

        # store these for when time is updated
        # make sure these are initialised as FeatureCollection objects

        self._coastlines = _load_FeatureCollection(coastlines)
        self._continents = _load_FeatureCollection(continents)
        self._COBs = _load_FeatureCollection(COBs)

        self.coastlines = None
        self.continents = None
        self.COBs = None

        self._anchor_plate_id = self._check_anchor_plate_id(anchor_plate_id)

        # store topologies for easy access
        # setting time runs the update_time routine
        self._time = None
        if time is not None:
            self.time = time

    def __getstate__(self):
        filenames = self.plate_reconstruction.__getstate__()

        # add important variables from Points object
        if self._coastlines:
            filenames["coastlines"] = self._coastlines.filenames
        if self._continents:
            filenames["continents"] = self._continents.filenames
        if self._COBs:
            filenames["COBs"] = self._COBs.filenames
        filenames["time"] = self.time
        filenames["plate_id"] = self._anchor_plate_id

        return filenames

    def __setstate__(self, state):
        plate_reconstruction_args = [state["rotation_model"], None, None]
        if "topology_features" in state:
            plate_reconstruction_args[1] = state["topology_features"]
        if "static_polygons" in state:
            plate_reconstruction_args[2] = state["static_polygons"]

        self.plate_reconstruction = _PlateReconstruction(*plate_reconstruction_args)

        self._coastlines = None
        self._continents = None
        self._COBs = None
        self.coastlines = None
        self.continents = None
        self.COBs = None

        # reinstate unpicklable items
        if "coastlines" in state:
            self._coastlines = _FeatureCollection()
            for feature in state["coastlines"]:
                self._coastlines.add(_FeatureCollection(feature))

        if "continents" in state:
            self._continents = _FeatureCollection()
            for feature in state["continents"]:
                self._continents.add(_FeatureCollection(feature))

        if "COBs" in state:
            self._COBs = _FeatureCollection()
            for feature in state["COBs"]:
                self._COBs.add(_FeatureCollection(feature))

        self._anchor_plate_id = state["plate_id"]
        self.base_projection = ccrs.PlateCarree()
        self._time = None

    @property
    def time(self):
        """The reconstruction time."""
        return self._time

    @time.setter
    def time(self, var):
        """Allows the time attribute to be changed. Updates all instances of the time attribute in the object (e.g.
        reconstructions and resolving topologies will use this new time).

        Raises
        ------
        ValueError
            If the chosen reconstruction time is <0 Ma.
        """
        if var == self.time:
            pass
        elif var >= 0:
            self.update_time(var)
        else:
            raise ValueError("Enter a valid time >= 0")

    @property
    def anchor_plate_id(self):
        """Anchor plate ID for reconstruction. Must be an integer >= 0."""
        return self._anchor_plate_id

    @anchor_plate_id.setter
    def anchor_plate_id(self, anchor_plate):
        self._anchor_plate_id = self._check_anchor_plate_id(anchor_plate)
        self.update_time(self.time)

    @staticmethod
    def _check_anchor_plate_id(id):
        id = int(id)
        if id < 0:
            raise ValueError("Invalid anchor plate ID: {}".format(id))
        return id

    def update_time(self, time):
        """Re-reconstruct features and topologies to the time specified by the `PlotTopologies` `time` attribute
        whenever it or the anchor plate is updated.

        Notes
        -----
        The following `PlotTopologies` attributes are updated whenever a reconstruction `time` attribute is set:

        - resolved topology features (topological plates and networks)
        - ridge and transform boundary sections (resolved features)
        - ridge boundary sections (resolved features)
        - transform boundary sections (resolved features)
        - subduction boundary sections (resolved features)
        - left subduction boundary sections (resolved features)
        - right subduction boundary sections (resolved features)
        - other boundary sections (resolved features) that are not subduction zones or mid-ocean ridges
        (ridge/transform)

        Moreover, coastlines, continents and COBs are reconstructed to the new specified `time`.
        """
        self._time = float(time)
        resolved_topologies = ptt.resolve_topologies.resolve_topologies_into_features(
            self.plate_reconstruction.rotation_model,
            self.plate_reconstruction.topology_features,
            self.time,
        )

        (
            self.topologies,
            self.ridge_transforms,
            self.ridges,
            self.transforms,
            self.trenches,
            self.trench_left,
            self.trench_right,
            self.other,
        ) = resolved_topologies

        # miscellaneous boundaries
        self.continental_rifts = []
        self.faults = []
        self.fracture_zones = []
        self.inferred_paleo_boundaries = []
        self.terrane_boundaries = []
        self.transitional_crusts = []
        self.orogenic_belts = []
        self.sutures = []
        self.continental_crusts = []
        self.extended_continental_crusts = []
        self.passive_continental_boundaries = []
        self.slab_edges = []
        self.misc_transforms = []
        self.unclassified_features = []

        for topol in self.other:
            if topol.get_feature_type() == pygplates.FeatureType.gpml_continental_rift:
                self.continental_rifts.append(topol)

            elif topol.get_feature_type() == pygplates.FeatureType.gpml_fault:
                self.faults.append(topol)

            elif topol.get_feature_type() == pygplates.FeatureType.gpml_fracture_zone:
                self.fracture_zones.append(topol)

            elif (
                topol.get_feature_type()
                == pygplates.FeatureType.gpml_inferred_paleo_boundary
            ):
                self.inferred_paleo_boundaries.append(topol)

            elif (
                topol.get_feature_type() == pygplates.FeatureType.gpml_terrane_boundary
            ):
                self.terrane_boundaries.append(topol)

            elif (
                topol.get_feature_type()
                == pygplates.FeatureType.gpml_transitional_crust
            ):
                self.transitional_crusts.append(topol)

            elif topol.get_feature_type() == pygplates.FeatureType.gpml_orogenic_belt:
                self.orogenic_belts.append(topol)

            elif topol.get_feature_type() == pygplates.FeatureType.gpml_suture:
                self.sutures.append(topol)

            elif (
                topol.get_feature_type() == pygplates.FeatureType.gpml_continental_crust
            ):
                self.continental_crusts.append(topol)

            elif (
                topol.get_feature_type()
                == pygplates.FeatureType.gpml_extended_continental_crust
            ):
                self.extended_continental_crusts.append(topol)

            elif (
                topol.get_feature_type()
                == pygplates.FeatureType.gpml_passive_continental_boundary
            ):
                self.passive_continental_boundaries.append(topol)

            elif topol.get_feature_type() == pygplates.FeatureType.gpml_slab_edge:
                self.slab_edges.append(topol)

            elif topol.get_feature_type() == pygplates.FeatureType.gpml_transform:
                self.misc_transforms.append(topol)

            elif (
                topol.get_feature_type()
                == pygplates.FeatureType.gpml_unclassified_feature
            ):
                self.unclassified_features.append(topol)

        # reconstruct other important polygons and lines
        if self._coastlines:
            self.coastlines = self.plate_reconstruction.reconstruct(
                self._coastlines,
                self.time,
                from_time=0,
                anchor_plate_id=self.anchor_plate_id,
            )

        if self._continents:
            self.continents = self.plate_reconstruction.reconstruct(
                self._continents,
                self.time,
                from_time=0,
                anchor_plate_id=self.anchor_plate_id,
            )

        if self._COBs:
            self.COBs = self.plate_reconstruction.reconstruct(
                self._COBs, self.time, from_time=0, anchor_plate_id=self.anchor_plate_id
            )

    # subduction teeth
    def _tessellate_triangles(
        self, features, tesselation_radians, triangle_base_length, triangle_aspect=1.0
    ):
        """Places subduction teeth along subduction boundary line segments within a MultiLineString shapefile.

        Parameters
        ----------
        shapefilename  : str
            Path to shapefile containing the subduction boundary features

        tesselation_radians : float
            Parametrises subduction teeth density. Triangles are generated only along line segments with distances
            that exceed the given threshold tessellation_radians.

        triangle_base_length : float
            Length of teeth triangle base

        triangle_aspect : float, default=1.0
            Aspect ratio of teeth triangles. Ratio is 1.0 by default.

        Returns
        -------
        X_points : (n,3) array
            X points that define the teeth triangles
        Y_points : (n,3) array
            Y points that define the teeth triangles
        """

        tesselation_degrees = np.degrees(tesselation_radians)
        triangle_pointsX = []
        triangle_pointsY = []

        date_line_wrapper = pygplates.DateLineWrapper()

        for feature in features:
            cum_distance = 0.0

            for geometry in feature.get_geometries():
                wrapped_lines = date_line_wrapper.wrap(geometry)
                for line in wrapped_lines:
                    pts = np.array(
                        [
                            (p.get_longitude(), p.get_latitude())
                            for p in line.get_points()
                        ]
                    )

                    for p in range(0, len(pts) - 1):
                        A = pts[p]
                        B = pts[p + 1]

                        AB_dist = B - A
                        AB_norm = AB_dist / np.hypot(*AB_dist)
                        cum_distance += np.hypot(*AB_dist)

                        # create a new triangle if cumulative distance is exceeded.
                        if cum_distance >= tesselation_degrees:
                            C = A + triangle_base_length * AB_norm

                            # find normal vector
                            AD_dist = np.array([AB_norm[1], -AB_norm[0]])
                            AD_norm = AD_dist / np.linalg.norm(AD_dist)

                            C0 = A + 0.5 * triangle_base_length * AB_norm

                            # project point along normal vector
                            D = C0 + triangle_base_length * triangle_aspect * AD_norm

                            triangle_pointsX.append([A[0], C[0], D[0]])
                            triangle_pointsY.append([A[1], C[1], D[1]])

                            cum_distance = 0.0

        return np.array(triangle_pointsX), np.array(triangle_pointsY)

    def get_feature(
        self,
        feature,
        central_meridian=0.0,
        tessellate_degrees=None,
    ):
        """Create a geopandas.GeoDataFrame object containing geometries of reconstructed features.

        Notes
        -----
        The feature needed to produce the GeoDataFrame should already be constructed to a `time`.
        This function converts the feature into a set of Shapely geometries whose coordinates are
        passed to a geopandas GeoDataFrame.

        Parameters
        ----------
        feature : instance of <pygplates.Feature>
            A feature reconstructed to `time`.

        Returns
        -------
        gdf : instance of <geopandas.GeoDataFrame>
            A pandas.DataFrame that has a column with `feature` geometries.

        """
        shp = shapelify_features(
            feature,
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        gdf = gpd.GeoDataFrame({"geometry": shp}, geometry="geometry")
        return gdf

    def plot_feature(self, ax, feature, **kwargs):
        """Plot pygplates.FeatureCollection  or pygplates.Feature onto a map.

        Parameters
        ----------
        ax : instance of <cartopy.mpl.geoaxes.GeoAxes> or <cartopy.mpl.geoaxes.GeoAxesSubplot>
            A subclass of `matplotlib.axes.Axes` which represents a map Projection.
            The map should be set at a particular Cartopy projection.

        **kwargs :
            Keyword arguments for parameters such as `facecolor`, `alpha`,
            etc. for plotting coastline geometries.
            See `Matplotlib` keyword arguments
            [here](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.plot.html).

        Returns
        -------
        ax : instance of <geopandas.GeoDataFrame.plot>
            A standard cartopy.mpl.geoaxes.GeoAxes or cartopy.mpl.geoaxes.GeoAxesSubplot map
            with coastline features plotted onto the chosen map projection.
        """
        if "transform" in kwargs.keys():
            warnings.warn(
                "'transform' keyword argument is ignored by PlotTopologies",
                UserWarning,
            )
            kwargs.pop("transform")
        tessellate_degrees = kwargs.pop("tessellate_degrees", None)
        central_meridian = kwargs.pop("central_meridian", None)
        if central_meridian is None:
            central_meridian = _meridian_from_ax(ax)

        gdf = self.get_feature(
            feature,
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        if hasattr(ax, "projection"):
            gdf = _clean_polygons(data=gdf, projection=ax.projection)
        else:
            kwargs["transform"] = self.base_projection
        return gdf.plot(ax=ax, **kwargs)

    def get_coastlines(self, central_meridian=0.0, tessellate_degrees=None):
        """Create a geopandas.GeoDataFrame object containing geometries of reconstructed coastline polygons.

        Notes
        -----
        The `coastlines` needed to produce the GeoDataFrame are automatically constructed if the optional `time`
        parameter is passed to the `PlotTopologies` object before calling this function. `time` can be passed
        either when `PlotTopologies` is first called...

            gplot = gplately.PlotTopologies(..., time=100,...)

        or anytime afterwards, by setting:

            time = 100 #Ma
            gplot.time = time

        ...after which this function can be re-run. Once the `coastlines` are reconstructed, they are
        converted into Shapely polygons whose coordinates are passed to a geopandas GeoDataFrame.

        Returns
        -------
        gdf : instance of <geopandas.GeoDataFrame>
            A pandas.DataFrame that has a column with `coastlines` geometry.
        central_meridian : float
            Central meridian around which to perform wrapping; default: 0.0.
        tessellate_degrees : float or None
            If provided, geometries will be tessellated to this resolution prior
            to wrapping.

        Raises
        ------
        ValueError
            If the optional `time` parameter has not been passed to `PlotTopologies`. This is needed to construct
            `coastlines` to the requested `time` and thus populate the GeoDataFrame.

        """
        if self._time is None:
            raise ValueError(
                "No coastlines have been resolved. Set `PlotTopologies.time` to construct coastlines."
            )

        if self.coastlines is None:
            raise ValueError("Supply coastlines to PlotTopologies object")

        coastline_polygons = shapelify_feature_polygons(
            self.coastlines,
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        gdf = gpd.GeoDataFrame({"geometry": coastline_polygons}, geometry="geometry")
        return gdf

    def plot_coastlines(self, ax, **kwargs):
        """Plot reconstructed coastline polygons onto a standard map Projection.

        Notes
        -----
        The `coastlines` for plotting are accessed from the `PlotTopologies` object's
        `coastlines` attribute. These `coastlines` are reconstructed to the `time`
        passed to the `PlotTopologies` object and converted into Shapely polylines. The
        reconstructed `coastlines` are added onto the GeoAxes or GeoAxesSubplot map `ax` using
        GeoPandas.
        Map resentation details (e.g. facecolor, edgecolor, alpha…) are permitted as keyword
        arguments.

        Parameters
        ----------
        ax : instance of <cartopy.mpl.geoaxes.GeoAxes> or <cartopy.mpl.geoaxes.GeoAxesSubplot>
            A subclass of `matplotlib.axes.Axes` which represents a map Projection.
            The map should be set at a particular Cartopy projection.

        **kwargs :
            Keyword arguments for parameters such as `facecolor`, `alpha`,
            etc. for plotting coastline geometries.
            See `Matplotlib` keyword arguments
            [here](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.plot.html).

        Returns
        -------
        ax : instance of <geopandas.GeoDataFrame.plot>
            A standard cartopy.mpl.geoaxes.GeoAxes or cartopy.mpl.geoaxes.GeoAxesSubplot map
            with coastline features plotted onto the chosen map projection.
        """
        if "transform" in kwargs.keys():
            warnings.warn(
                "'transform' keyword argument is ignored by PlotTopologies",
                UserWarning,
            )
            kwargs.pop("transform")
        tessellate_degrees = kwargs.pop("tessellate_degrees", None)
        central_meridian = kwargs.pop("central_meridian", None)
        if central_meridian is None:
            central_meridian = _meridian_from_ax(ax)

        gdf = self.get_coastlines(
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        if hasattr(ax, "projection"):
            gdf = _clean_polygons(data=gdf, projection=ax.projection)
        else:
            kwargs["transform"] = self.base_projection
        return gdf.plot(ax=ax, **kwargs)

    def get_continents(self, central_meridian=0.0, tessellate_degrees=None):
        """Create a geopandas.GeoDataFrame object containing geometries of reconstructed continental polygons.

        Notes
        -----
        The `continents` needed to produce the GeoDataFrame are automatically constructed if the optional `time`
        parameter is passed to the `PlotTopologies` object before calling this function. `time` can be passed
        either when `PlotTopologies` is first called...

            gplot = gplately.PlotTopologies(..., time=100,...)

        or anytime afterwards, by setting:

            time = 100 #Ma
            gplot.time = time

        ...after which this function can be re-run. Once the `continents` are reconstructed, they are
        converted into Shapely polygons whose coordinates are passed to a geopandas GeoDataFrame.

        Returns
        -------
        gdf : instance of <geopandas.GeoDataFrame>
            A pandas.DataFrame that has a column with `continents` geometry.
        central_meridian : float
            Central meridian around which to perform wrapping; default: 0.0.
        tessellate_degrees : float or None
            If provided, geometries will be tessellated to this resolution prior
            to wrapping.

        Raises
        ------
        ValueError
            If the optional `time` parameter has not been passed to `PlotTopologies`. This is needed to construct
            `continents` to the requested `time` and thus populate the GeoDataFrame.

        """
        if self._time is None:
            raise ValueError(
                "No continents have been resolved. Set `PlotTopologies.time` to construct continents."
            )

        if self.continents is None:
            raise ValueError("Supply continents to PlotTopologies object")

        continent_polygons = shapelify_feature_polygons(
            self.continents,
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        gdf = gpd.GeoDataFrame({"geometry": continent_polygons}, geometry="geometry")
        return gdf

    def plot_continents(self, ax, **kwargs):
        """Plot reconstructed continental polygons onto a standard map Projection.

        Notes
        -----
        The `continents` for plotting are accessed from the `PlotTopologies` object's
        `continents` attribute. These `continents` are reconstructed to the `time`
        passed to the `PlotTopologies` object and converted into Shapely polygons.
        The reconstructed `coastlines` are plotted onto the GeoAxes or GeoAxesSubplot map `ax` using
        GeoPandas.
        Map presentation details (e.g. facecolor, edgecolor, alpha…) are permitted as
        keyword arguments.

        Parameters
        ----------
        ax : instance of <cartopy.mpl.geoaxes.GeoAxes> or <cartopy.mpl.geoaxes.GeoAxesSubplot>
            A subclass of `matplotlib.axes.Axes` which represents a map Projection.
            The map should be set at a particular Cartopy projection.

        **kwargs :
            Keyword arguments for parameters such as `facecolor`, `alpha`,
            etc. for plotting continental geometries.
            See `Matplotlib` keyword arguments
            [here](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.plot.html).

        Returns
        -------
        ax : instance of <geopandas.GeoDataFrame.plot>
            A standard cartopy.mpl.geoaxes.GeoAxes or cartopy.mpl.geoaxes.GeoAxesSubplot map
            with continent features plotted onto the chosen map projection.
        """
        if "transform" in kwargs.keys():
            warnings.warn(
                "'transform' keyword argument is ignored by PlotTopologies",
                UserWarning,
            )
            kwargs.pop("transform")
        tessellate_degrees = kwargs.pop("tessellate_degrees", None)
        central_meridian = kwargs.pop("central_meridian", None)
        if central_meridian is None:
            central_meridian = _meridian_from_ax(ax)

        gdf = self.get_continents(
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        if hasattr(ax, "projection"):
            gdf = _clean_polygons(data=gdf, projection=ax.projection)
        else:
            kwargs["transform"] = self.base_projection
        return gdf.plot(ax=ax, **kwargs)

    def get_continent_ocean_boundaries(
        self,
        central_meridian=0.0,
        tessellate_degrees=None,
    ):
        """Create a geopandas.GeoDataFrame object containing geometries of reconstructed continent-ocean
        boundary lines.

        Notes
        -----
        The `COBs` needed to produce the GeoDataFrame are automatically constructed if the optional `time`
        parameter is passed to the `PlotTopologies` object before calling this function. `time` can be passed
        either when `PlotTopologies` is first called...

            gplot = gplately.PlotTopologies(..., time=100,...)

        or anytime afterwards, by setting:

            time = 100 #Ma
            gplot.time = time

        ...after which this function can be re-run. Once the `COBs` are reconstructed, they are
        converted into Shapely lines whose coordinates are passed to a geopandas GeoDataFrame.

        Returns
        -------
        gdf : instance of <geopandas.GeoDataFrame>
            A pandas.DataFrame that has a column with `COBs` geometry.
        central_meridian : float
            Central meridian around which to perform wrapping; default: 0.0.
        tessellate_degrees : float or None
            If provided, geometries will be tessellated to this resolution prior
            to wrapping.

        Raises
        ------
        ValueError
            If the optional `time` parameter has not been passed to `PlotTopologies`. This is needed to construct
            `COBs` to the requested `time` and thus populate the GeoDataFrame.

        """
        if self._time is None:
            raise ValueError(
                "No geometries have been resolved. Set `PlotTopologies.time` to construct topologies."
            )

        if self.COBs is None:
            raise ValueError("Supply COBs to PlotTopologies object")

        COB_lines = shapelify_feature_lines(
            self.COBs,
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        gdf = gpd.GeoDataFrame({"geometry": COB_lines}, geometry="geometry")
        return gdf

    def plot_continent_ocean_boundaries(self, ax, **kwargs):
        """Plot reconstructed continent-ocean boundary (COB) polygons onto a standard
        map Projection.

        Notes
        -----
        The `COBs` for plotting are accessed from the `PlotTopologies` object's
        `COBs` attribute. These `COBs` are reconstructed to the `time`
        passed to the `PlotTopologies` object and converted into Shapely polylines.
        The reconstructed `COBs` are plotted onto the GeoAxes or GeoAxesSubplot map
        `ax` using GeoPandas. Map presentation details (e.g. `facecolor`, `edgecolor`, `alpha`…)
        are permitted as keyword arguments.

        These COBs are transformed into shapely
        geometries and added onto the chosen map for a specific geological time (supplied to the
        PlotTopologies object). Map presentation details (e.g. facecolor, edgecolor, alpha…)
        are permitted.

        Parameters
        ----------
        ax : instance of <cartopy.mpl.geoaxes.GeoAxes> or <cartopy.mpl.geoaxes.GeoAxesSubplot>
            A subclass of `matplotlib.axes.Axes` which represents a map Projection.
            The map should be set at a particular Cartopy projection.

        **kwargs :
            Keyword arguments for parameters such as `facecolor`, `alpha`,
            etc. for plotting COB geometries.
            See `Matplotlib` keyword arguments
            [here](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.plot.html).

        Returns
        -------
        ax : instance of <geopandas.GeoDataFrame.plot>
            A standard cartopy.mpl.geoaxes.GeoAxes or cartopy.mpl.geoaxes.GeoAxesSubplot map
            with COB features plotted onto the chosen map projection.
        """
        if "transform" in kwargs.keys():
            warnings.warn(
                "'transform' keyword argument is ignored by PlotTopologies",
                UserWarning,
            )
            kwargs.pop("transform")
        tessellate_degrees = kwargs.pop("tessellate_degrees", None)
        central_meridian = kwargs.pop("central_meridian", None)
        if central_meridian is None:
            central_meridian = _meridian_from_ax(ax)

        gdf = self.get_continent_ocean_boundaries(
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        if hasattr(ax, "projection"):
            gdf = _clean_polygons(data=gdf, projection=ax.projection)
        else:
            kwargs["transform"] = self.base_projection
        return gdf.plot(ax=ax, **kwargs)

    def get_ridges(self, central_meridian=0.0, tessellate_degrees=None):
        """Create a geopandas.GeoDataFrame object containing geometries of reconstructed ridge lines.

        Notes
        -----
        The `ridges` needed to produce the GeoDataFrame are automatically constructed if the optional `time`
        parameter is passed to the `PlotTopologies` object before calling this function. `time` can be passed
        either when `PlotTopologies` is first called...

            gplot = gplately.PlotTopologies(..., time=100,...)

        or anytime afterwards, by setting:

            time = 100 #Ma
            gplot.time = time

        ...after which this function can be re-run. Once the `ridges` are reconstructed, they are
        converted into Shapely lines whose coordinates are passed to a geopandas GeoDataFrame.

        Returns
        -------
        gdf : instance of <geopandas.GeoDataFrame>
            A pandas.DataFrame that has a column with `ridges` geometry.
        central_meridian : float
            Central meridian around which to perform wrapping; default: 0.0.
        tessellate_degrees : float or None
            If provided, geometries will be tessellated to this resolution prior
            to wrapping.

        Raises
        ------
        ValueError
            If the optional `time` parameter has not been passed to `PlotTopologies`. This is needed to construct
            `ridges` to the requested `time` and thus populate the GeoDataFrame.

        """
        if self._time is None:
            raise ValueError(
                "No ridges have been resolved. Set `PlotTopologies.time` to construct ridges."
            )

        if self.ridges is None:
            raise ValueError("No ridge topologies passed to PlotTopologies.")

        ridge_lines = shapelify_feature_lines(
            self.ridges,
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        gdf = gpd.GeoDataFrame({"geometry": ridge_lines}, geometry="geometry")
        return gdf

    def plot_ridges(self, ax, color="black", **kwargs):
        """Plot reconstructed ridge polylines onto a standard map Projection.

        Notes
        -----
        The `ridges` for plotting are accessed from the `PlotTopologies` object's
        `ridges` attribute. These `ridges` are reconstructed to the `time`
        passed to the `PlotTopologies` object and converted into Shapely polylines.
        The reconstructed `ridges` are plotted onto the GeoAxes or GeoAxesSubplot map
        `ax` using GeoPandas. Map presentation details (e.g. `facecolor`, `edgecolor`, `alpha`…)
        are permitted as keyword arguments.

        Ridge geometries are wrapped to the dateline using
        pyGPlates' [DateLineWrapper](https://www.gplates.org/docs/pygplates/generated/pygplates.datelinewrapper)
        by splitting a polyline into multiple polylines at the dateline. This is to avoid
        horizontal lines being formed between polylines at longitudes of -180 and 180 degrees.
        Point features near the poles (-89 & 89 degree latitude) are also clipped to ensure
        compatibility with Cartopy.

        Parameters
        ----------
        ax : instance of <cartopy.mpl.geoaxes.GeoAxes> or <cartopy.mpl.geoaxes.GeoAxesSubplot>
            A subclass of `matplotlib.axes.Axes` which represents a map Projection.
            The map should be set at a particular Cartopy projection.

        color : str, default=’black’
            The colour of the ridge lines. By default, it is set to black.

        **kwargs :
            Keyword arguments for parameters such as `alpha`, etc. for
            plotting ridge geometries.
            See `Matplotlib` keyword arguments
            [here](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.plot.html).

        Returns
        -------
        ax : instance of <geopandas.GeoDataFrame.plot>
            A standard cartopy.mpl.geoaxes.GeoAxes or cartopy.mpl.geoaxes.GeoAxesSubplot map
            with ridge features plotted onto the chosen map projection.
        """
        if "transform" in kwargs.keys():
            warnings.warn(
                "'transform' keyword argument is ignored by PlotTopologies",
                UserWarning,
            )
            kwargs.pop("transform")
        tessellate_degrees = kwargs.pop("tessellate_degrees", None)
        central_meridian = kwargs.pop("central_meridian", None)
        if central_meridian is None:
            central_meridian = _meridian_from_ax(ax)

        gdf = self.get_ridges(
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        if hasattr(ax, "projection"):
            gdf = _clean_polygons(data=gdf, projection=ax.projection)
        else:
            kwargs["transform"] = self.base_projection
        return gdf.plot(ax=ax, facecolor="none", edgecolor=color, **kwargs)

    def get_ridges_and_transforms(
        self,
        central_meridian=0.0,
        tessellate_degrees=None,
    ):
        """Create a geopandas.GeoDataFrame object containing geometries of reconstructed ridge and transform lines.

        Notes
        -----
        The `ridge_transforms` needed to produce the GeoDataFrame are automatically constructed if the optional `time`
        parameter is passed to the `PlotTopologies` object before calling this function. `time` can be passed
        either when `PlotTopologies` is first called...

            gplot = gplately.PlotTopologies(..., time=100,...)

        or anytime afterwards, by setting:

            time = 100 #Ma
            gplot.time = time

        ...after which this function can be re-run. Once the `ridge_transforms` are reconstructed, they are
        converted into Shapely lines whose coordinates are passed to a geopandas GeoDataFrame.

        Returns
        -------
        gdf : instance of <geopandas.GeoDataFrame>
            A pandas.DataFrame that has a column with `ridges` geometry.
        central_meridian : float
            Central meridian around which to perform wrapping; default: 0.0.
        tessellate_degrees : float or None
            If provided, geometries will be tessellated to this resolution prior
            to wrapping.

        Raises
        ------
        ValueError
            If the optional `time` parameter has not been passed to `PlotTopologies`. This is needed to construct
            `ridge_transforms` to the requested `time` and thus populate the GeoDataFrame.

        """
        if self._time is None:
            raise ValueError(
                "No ridges and transforms have been resolved. Set `PlotTopologies.time` to construct ridges and transforms."
            )

        if self.ridge_transforms is None:
            raise ValueError(
                "No ridge and transform topologies passed to PlotTopologies."
            )

        ridge_transform_lines = shapelify_feature_lines(
            self.ridge_transforms,
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        gdf = gpd.GeoDataFrame({"geometry": ridge_transform_lines}, geometry="geometry")
        return gdf

    def plot_ridges_and_transforms(self, ax, color="black", **kwargs):
        """Plot reconstructed ridge & transform boundary polylines onto a standard map
        Projection.

        Notes
        -----
        The ridge & transform sections for plotting are accessed from the
        `PlotTopologies` object's `ridge_transforms` attribute. These `ridge_transforms`
        are reconstructed to the `time` passed to the `PlotTopologies` object and converted
        into Shapely polylines. The reconstructed `ridge_transforms` are plotted onto the
        GeoAxes or GeoAxesSubplot map `ax` using GeoPandas. Map presentation details
        (e.g. `facecolor`, `edgecolor`, `alpha`…) are permitted as keyword arguments.

        Note: Ridge & transform geometries are wrapped to the dateline using
        pyGPlates' [DateLineWrapper](https://www.gplates.org/docs/pygplates/generated/pygplates.datelinewrapper)
        by splitting a polyline into multiple polylines at the dateline. This is to avoid
        horizontal lines being formed between polylines at longitudes of -180 and 180 degrees.
        Point features near the poles (-89 & 89 degree latitude) are also clipped to ensure
        compatibility with Cartopy.

        Parameters
        ----------
        ax : instance of <cartopy.mpl.geoaxes.GeoAxes> or <cartopy.mpl.geoaxes.GeoAxesSubplot>
            A subclass of `matplotlib.axes.Axes` which represents a map Projection.
            The map should be set at a particular Cartopy projection.

        color : str, default=’black’
            The colour of the ridge & transform lines. By default, it is set to black.

        **kwargs :
            Keyword arguments for parameters such as ‘alpha’, etc. for
            plotting ridge & transform geometries.
            See `Matplotlib` keyword arguments
            [here](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.plot.html).

        Returns
        -------
        ax : instance of <geopandas.GeoDataFrame.plot>
            A standard cartopy.mpl.geoaxes.GeoAxes or cartopy.mpl.geoaxes.GeoAxesSubplot map
            with ridge & transform features plotted onto the chosen map projection.
        """
        if "transform" in kwargs.keys():
            warnings.warn(
                "'transform' keyword argument is ignored by PlotTopologies",
                UserWarning,
            )
            kwargs.pop("transform")
        tessellate_degrees = kwargs.pop("tessellate_degrees", None)
        central_meridian = kwargs.pop("central_meridian", None)
        if central_meridian is None:
            central_meridian = _meridian_from_ax(ax)

        gdf = self.get_ridges_and_transforms(
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        if hasattr(ax, "projection"):
            gdf = _clean_polygons(data=gdf, projection=ax.projection)
        else:
            kwargs["transform"] = self.base_projection
        return gdf.plot(ax=ax, facecolor="none", edgecolor=color, **kwargs)

    def get_transforms(self, central_meridian=0.0, tessellate_degrees=None):
        """Create a geopandas.GeoDataFrame object containing geometries of reconstructed transform lines.

        Notes
        -----
        The `transforms` needed to produce the GeoDataFrame are automatically constructed if the optional `time`
        parameter is passed to the `PlotTopologies` object before calling this function. `time` can be passed
        either when `PlotTopologies` is first called...

            gplot = gplately.PlotTopologies(..., time=100,...)

        or anytime afterwards, by setting:

            time = 100 #Ma
            gplot.time = time

        ...after which this function can be re-run. Once the `transforms` are reconstructed, they are
        converted into Shapely lines whose coordinates are passed to a geopandas GeoDataFrame.

        Returns
        -------
        gdf : instance of <geopandas.GeoDataFrame>
            A pandas.DataFrame that has a column with `transforms` geometry.
        central_meridian : float
            Central meridian around which to perform wrapping; default: 0.0.
        tessellate_degrees : float or None
            If provided, geometries will be tessellated to this resolution prior
            to wrapping.

        Raises
        ------
        ValueError
            If the optional `time` parameter has not been passed to `PlotTopologies`. This is needed to construct
            `transforms` to the requested `time` and thus populate the GeoDataFrame.
        """
        if self._time is None:
            raise ValueError(
                "No transforms have been resolved. Set `PlotTopologies.time` to construct transforms."
            )

        if self.transforms is None:
            raise ValueError("No transform topologies passed to PlotTopologies.")

        transform_lines = shapelify_feature_lines(
            self.transforms,
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        gdf = gpd.GeoDataFrame({"geometry": transform_lines}, geometry="geometry")
        return gdf

    def plot_transforms(self, ax, color="black", **kwargs):
        """Plot reconstructed transform boundary polylines onto a standard map.

        Notes
        -----
        The transform sections for plotting are accessed from the
        `PlotTopologies` object's `transforms` attribute. These `transforms`
        are reconstructed to the `time` passed to the `PlotTopologies` object and converted
        into Shapely polylines. The reconstructed `transforms` are plotted onto the
        GeoAxes or GeoAxesSubplot map `ax` using GeoPandas. Map presentation details
        (e.g. `facecolor`, `edgecolor`, `alpha`…) are permitted as keyword arguments.

        Transform geometries are wrapped to the dateline using
        pyGPlates' [DateLineWrapper](https://www.gplates.org/docs/pygplates/generated/pygplates.datelinewrapper)
        by splitting a polyline into multiple polylines at the dateline. This is to avoid
        horizontal lines being formed between polylines at longitudes of -180 and 180 degrees.
        Point features near the poles (-89 & 89 degree latitude) are also clipped to ensure
        compatibility with Cartopy.

        Parameters
        ----------
        ax : instance of <cartopy.mpl.geoaxes.GeoAxes> or <cartopy.mpl.geoaxes.GeoAxesSubplot>
            A subclass of `matplotlib.axes.Axes` which represents a map Projection.
            The map should be set at a particular Cartopy projection.

        color : str, default=’black’
            The colour of the transform lines. By default, it is set to black.

        **kwargs :
            Keyword arguments for parameters such as `alpha`, etc.
            for plotting transform geometries.
            See `Matplotlib` keyword arguments
            [here](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.plot.html).

        Returns
        -------
        ax : instance of <geopandas.GeoDataFrame.plot>
            A standard cartopy.mpl.geoaxes.GeoAxes or cartopy.mpl.geoaxes.GeoAxesSubplot map
            with transform features plotted onto the chosen map projection.
        """
        if "transform" in kwargs.keys():
            warnings.warn(
                "'transform' keyword argument is ignored by PlotTopologies",
                UserWarning,
            )
            kwargs.pop("transform")
        tessellate_degrees = kwargs.pop("tessellate_degrees", None)
        central_meridian = kwargs.pop("central_meridian", None)
        if central_meridian is None:
            central_meridian = _meridian_from_ax(ax)

        gdf = self.get_transforms(
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        if hasattr(ax, "projection"):
            gdf = _clean_polygons(data=gdf, projection=ax.projection)
        else:
            kwargs["transform"] = self.base_projection
        return gdf.plot(ax=ax, facecolor="none", edgecolor=color, **kwargs)

    def get_trenches(self, central_meridian=0.0, tessellate_degrees=None):
        """Create a geopandas.GeoDataFrame object containing geometries of reconstructed trench lines.

        Notes
        -----
        The `trenches` needed to produce the GeoDataFrame are automatically constructed if the optional `time`
        parameter is passed to the `PlotTopologies` object before calling this function. `time` can be passed
        either when `PlotTopologies` is first called...

            gplot = gplately.PlotTopologies(..., time=100,...)

        or anytime afterwards, by setting:

            time = 100 #Ma
            gplot.time = time

        ...after which this function can be re-run. Once the `trenches` are reconstructed, they are
        converted into Shapely lines whose coordinates are passed to a geopandas GeoDataFrame.

        Returns
        -------
        gdf : instance of <geopandas.GeoDataFrame>
            A pandas.DataFrame that has a column with `trenches` geometry.
        central_meridian : float
            Central meridian around which to perform wrapping; default: 0.0.
        tessellate_degrees : float or None
            If provided, geometries will be tessellated to this resolution prior
            to wrapping.

        Raises
        ------
        ValueError
            If the optional `time` parameter has not been passed to `PlotTopologies`. This is needed to construct
            `trenches` to the requested `time` and thus populate the GeoDataFrame.
        """
        if self._time is None:
            raise ValueError(
                "No trenches have been resolved. Set `PlotTopologies.time` to construct trenches."
            )

        if self.trenches is None:
            raise ValueError("No trenches passed to PlotTopologies.")

        trench_lines = shapelify_feature_lines(
            self.trenches,
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        gdf = gpd.GeoDataFrame({"geometry": trench_lines}, geometry="geometry")
        return gdf

    def plot_trenches(self, ax, color="black", **kwargs):
        """Plot reconstructed subduction trench polylines onto a standard map
        Projection.

        Notes
        -----
        The trench sections for plotting are accessed from the
        `PlotTopologies` object's `trenches` attribute. These `trenches`
        are reconstructed to the `time` passed to the `PlotTopologies` object and converted
        into Shapely polylines. The reconstructed `trenches` are plotted onto the
        GeoAxes or GeoAxesSubplot map `ax` using GeoPandas. Map presentation details
        (e.g. `facecolor`, `edgecolor`, `alpha`…) are permitted as keyword arguments.

        Trench geometries are wrapped to the dateline using
        pyGPlates' [DateLineWrapper](https://www.gplates.org/docs/pygplates/generated/pygplates.datelinewrapper)
        by splitting a polyline into multiple polylines at the dateline. This is to avoid
        horizontal lines being formed between polylines at longitudes of -180 and 180 degrees.
        Point features near the poles (-89 & 89 degree latitude) are also clipped to ensure
        compatibility with Cartopy.

        Parameters
        ----------
        ax : instance of <cartopy.mpl.geoaxes.GeoAxes> or <cartopy.mpl.geoaxes.GeoAxesSubplot>
            A subclass of `matplotlib.axes.Axes` which represents a map Projection.
            The map should be set at a particular Cartopy projection.

        color : str, default=’black’
            The colour of the trench lines. By default, it is set to black.

        **kwargs :
            Keyword arguments for parameters such as `alpha`, etc.
            for plotting trench geometries.
            See `Matplotlib` keyword arguments
            [here](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.plot.html).

        Returns
        -------
        ax : instance of <geopandas.GeoDataFrame.plot>
            A standard cartopy.mpl.geoaxes.GeoAxes or cartopy.mpl.geoaxes.GeoAxesSubplot map
            with transform features plotted onto the chosen map projection.
        """
        if "transform" in kwargs.keys():
            warnings.warn(
                "'transform' keyword argument is ignored by PlotTopologies",
                UserWarning,
            )
            kwargs.pop("transform")
        tessellate_degrees = kwargs.pop("tessellate_degrees", None)
        central_meridian = kwargs.pop("central_meridian", None)
        if central_meridian is None:
            central_meridian = _meridian_from_ax(ax)

        gdf = self.get_trenches(
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        if hasattr(ax, "projection"):
            gdf = _clean_polygons(data=gdf, projection=ax.projection)
        else:
            kwargs["transform"] = self.base_projection
        return gdf.plot(ax=ax, facecolor="none", edgecolor=color, **kwargs)

    def get_misc_boundaries(self, central_meridian=0.0, tessellate_degrees=None):
        """Create a geopandas.GeoDataFrame object containing geometries of other reconstructed lines.

        Notes
        -----
        The `other` geometries needed to produce the GeoDataFrame are automatically constructed if the optional `time`
        parameter is passed to the `PlotTopologies` object before calling this function. `time` can be passed
        either when `PlotTopologies` is first called...

            gplot = gplately.PlotTopologies(..., time=100,...)

        or anytime afterwards, by setting:

            time = 100 #Ma
            gplot.time = time

        ...after which this function can be re-run. Once the `other` geometries are reconstructed, they are
        converted into Shapely features whose coordinates are passed to a geopandas GeoDataFrame.

        Returns
        -------
        gdf : instance of <geopandas.GeoDataFrame>
            A pandas.DataFrame that has a column with `other` geometry.
        central_meridian : float
            Central meridian around which to perform wrapping; default: 0.0.
        tessellate_degrees : float or None
            If provided, geometries will be tessellated to this resolution prior
            to wrapping.

        Raises
        ------
        ValueError
            If the optional `time` parameter has not been passed to `PlotTopologies`. This is needed to construct
            `other` geometries to the requested `time` and thus populate the GeoDataFrame.
        """
        if self._time is None:
            raise ValueError(
                "No miscellaneous topologies have been resolved. Set `PlotTopologies.time` to construct them."
            )

        if self.other is None:
            raise ValueError("No miscellaneous topologies passed to PlotTopologies.")

        lines = shapelify_features(
            self.other,
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        gdf = gpd.GeoDataFrame({"geometry": lines}, geometry="geometry")
        return gdf

    def plot_misc_boundaries(self, ax, color="black", **kwargs):
        """Plot reconstructed miscellaneous plate boundary polylines onto a standard
        map Projection.

        Notes
        -----
        The miscellaneous boundary sections for plotting are accessed from the
        `PlotTopologies` object's `other` attribute. These `other` boundaries
        are reconstructed to the `time` passed to the `PlotTopologies` object and converted
        into Shapely polylines. The reconstructed `other` boundaries are plotted onto the
        GeoAxes or GeoAxesSubplot map `ax` using GeoPandas. Map presentation details
        (e.g. `facecolor`, `edgecolor`, `alpha`…) are permitted as keyword arguments.

        Miscellaneous boundary geometries are wrapped to the dateline using
        pyGPlates' [DateLineWrapper](https://www.gplates.org/docs/pygplates/generated/pygplates.datelinewrapper)
        by splitting a polyline into multiple polylines at the dateline. This is to avoid
        horizontal lines being formed between polylines at longitudes of -180 and 180 degrees.
        Point features near the poles (-89 & 89 degree latitude) are also clipped to ensure
        compatibility with Cartopy.

        Parameters
        ----------
        ax : instance of <cartopy.mpl.geoaxes.GeoAxes> or <cartopy.mpl.geoaxes.GeoAxesSubplot>
            A subclass of `matplotlib.axes.Axes` which represents a map Projection.
            The map should be set at a particular Cartopy projection.

        color : str, default=’black’
            The colour of the boundary lines. By default, it is set to black.

        **kwargs :
            Keyword arguments for parameters such as ‘alpha’, etc. for
            plotting miscellaneous boundary geometries.
            See `Matplotlib` keyword arguments
            [here](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.plot.html).

        Returns
        -------
        ax : instance of <geopandas.GeoDataFrame.plot>
            A standard cartopy.mpl.geoaxes.GeoAxes or cartopy.mpl.geoaxes.GeoAxesSubplot map
            with miscellaneous boundary features plotted onto the chosen map projection.
        """
        if "transform" in kwargs.keys():
            warnings.warn(
                "'transform' keyword argument is ignored by PlotTopologies",
                UserWarning,
            )
            kwargs.pop("transform")
        tessellate_degrees = kwargs.pop("tessellate_degrees", None)
        central_meridian = kwargs.pop("central_meridian", None)
        if central_meridian is None:
            central_meridian = _meridian_from_ax(ax)

        gdf = self.get_misc_boundaries(
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        if hasattr(ax, "projection"):
            gdf = _clean_polygons(data=gdf, projection=ax.projection)
        else:
            kwargs["transform"] = self.base_projection
        return gdf.plot(ax=ax, facecolor="none", edgecolor=color, **kwargs)

    def plot_subduction_teeth_deprecated(
        self, ax, spacing=0.1, size=2.0, aspect=1, color="black", **kwargs
    ):
        """Plot subduction teeth onto a standard map Projection.

        Notes
        -----
        Subduction teeth are tessellated from `PlotTopologies` object attributes `trench_left` and
        `trench_right`, and transformed into Shapely polygons for plotting.

        Parameters
        ----------
        ax : instance of <cartopy.mpl.geoaxes.GeoAxes> or <cartopy.mpl.geoaxes.GeoAxesSubplot>
            A subclass of `matplotlib.axes.Axes` which represents a map Projection.
            The map should be set at a particular Cartopy projection.

        spacing : float, default=0.1
            The tessellation threshold (in radians). Parametrises subduction tooth density.
            Triangles are generated only along line segments with distances that exceed
            the given threshold ‘spacing’.

        size : float, default=2.0
            Length of teeth triangle base.

        aspect : float, default=1
            Aspect ratio of teeth triangles. Ratio is 1.0 by default.

        color : str, default=’black’
            The colour of the teeth. By default, it is set to black.

        **kwargs :
            Keyword arguments for parameters such as ‘alpha’, etc. for
            plotting subduction tooth polygons.
            See `Matplotlib` keyword arguments
            [here](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.plot.html).

        Returns
        -------
        ax : instance of <geopandas.GeoDataFrame.plot>
            A standard cartopy.mpl.geoaxes.GeoAxes or cartopy.mpl.geoaxes.GeoAxesSubplot map
            with subduction teeth plotted onto the chosen map projection.
        """
        import shapely

        # add Subduction Teeth
        subd_xL, subd_yL = self._tessellate_triangles(
            self.trench_left,
            tesselation_radians=spacing,
            triangle_base_length=size,
            triangle_aspect=-aspect,
        )
        subd_xR, subd_yR = self._tessellate_triangles(
            self.trench_right,
            tesselation_radians=spacing,
            triangle_base_length=size,
            triangle_aspect=aspect,
        )

        teeth = []
        for tX, tY in zip(subd_xL, subd_yL):
            triangle_xy_points = np.c_[tX, tY]
            shp = shapely.geometry.Polygon(triangle_xy_points)
            teeth.append(shp)

        for tX, tY in zip(subd_xR, subd_yR):
            triangle_xy_points = np.c_[tX, tY]
            shp = shapely.geometry.Polygon(triangle_xy_points)
            teeth.append(shp)

        return ax.add_geometries(teeth, crs=self.base_projection, color=color, **kwargs)

    def get_subduction_direction(self):
        """Create a geopandas.GeoDataFrame object containing geometries of trench directions.

        Notes
        -----
        The `trench_left` and `trench_right` geometries needed to produce the GeoDataFrame are automatically
        constructed if the optional `time` parameter is passed to the `PlotTopologies` object before calling
        this function. `time` can be passed either when `PlotTopologies` is first called...

            gplot = gplately.PlotTopologies(..., time=100,...)

        or anytime afterwards, by setting:

            time = 100 #Ma
            gplot.time = time

        ...after which this function can be re-run. Once the `other` geometries are reconstructed, they are
        converted into Shapely features whose coordinates are passed to a geopandas GeoDataFrame.

        Returns
        -------
        gdf_left : instance of <geopandas.GeoDataFrame>
            A pandas.DataFrame that has a column with `trench_left` geometry.
        gdf_right : instance of <geopandas.GeoDataFrame>
            A pandas.DataFrame that has a column with `trench_right` geometry.

        Raises
        ------
        ValueError
            If the optional `time` parameter has not been passed to `PlotTopologies`. This is needed to construct
            `trench_left` or `trench_right` geometries to the requested `time` and thus populate the GeoDataFrame.
        """
        if self._time is None:
            raise ValueError(
                "No miscellaneous topologies have been resolved. Set `PlotTopologies.time` to construct them."
            )

        if self.trench_left is None or self.trench_right is None:
            raise ValueError(
                "No trench_left or trench_right topologies passed to PlotTopologies."
            )

        trench_left_features = shapelify_feature_lines(self.trench_left)
        trench_right_features = shapelify_feature_lines(self.trench_right)

        gdf_left = gpd.GeoDataFrame(
            {"geometry": trench_left_features}, geometry="geometry"
        )
        gdf_right = gpd.GeoDataFrame(
            {"geometry": trench_right_features}, geometry="geometry"
        )

        return gdf_left, gdf_right

    def plot_subduction_teeth(
        self, ax, spacing=0.07, size=None, aspect=None, color="black", **kwargs
    ):
        """Plot subduction teeth onto a standard map Projection.

        Notes
        -----
        Subduction teeth are tessellated from `PlotTopologies` object attributes `trench_left` and
        `trench_right`, and transformed into Shapely polygons for plotting.

        Parameters
        ----------
        ax : instance of <cartopy.mpl.geoaxes.GeoAxes> or <cartopy.mpl.geoaxes.GeoAxesSubplot>
            A subclass of `matplotlib.axes.Axes` which represents a map Projection.
            The map should be set at a particular Cartopy projection.

        spacing : float, default=0.07
            The tessellation threshold (in radians). Parametrises subduction tooth density.
            Triangles are generated only along line segments with distances that exceed
            the given threshold `spacing`.

        size : float, default=None
            Length of teeth triangle base (in radians). If kept at `None`, then
            `size = 0.5*spacing`.

        aspect : float, default=None
            Aspect ratio of teeth triangles. If kept at `None`, then `aspect = 2/3*size`.

        color : str, default='black'
            The colour of the teeth. By default, it is set to black.

        **kwargs :
            Keyword arguments parameters such as `alpha`, etc.
            for plotting subduction tooth polygons.
            See `Matplotlib` keyword arguments
            [here](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.plot.html).
        """
        if self._time is None:
            raise ValueError(
                "No topologies have been resolved. Set `PlotTopologies.time` to construct them."
            )
        if "transform" in kwargs.keys():
            warnings.warn(
                "'transform' keyword argument is ignored by PlotTopologies",
                UserWarning,
            )
            kwargs.pop("transform")

        central_meridian = _meridian_from_ax(ax)
        tessellate_degrees = np.rad2deg(spacing)

        try:
            projection = ax.projection
        except AttributeError:
            print(
                "The ax.projection does not exist. You must set projection to plot Cartopy maps, such as ax = plt.subplot(211, projection=cartopy.crs.PlateCarree())"
            )
            projection = None

        if isinstance(projection, ccrs.PlateCarree):
            spacing = math.degrees(spacing)
        else:
            spacing = spacing * EARTH_RADIUS * 1e3

        if aspect is None:
            aspect = 2.0 / 3.0
        if size is None:
            size = spacing * 0.5

        height = size * aspect

        trench_left_features = shapelify_feature_lines(
            self.trench_left,
            tessellate_degrees=tessellate_degrees,
            central_meridian=central_meridian,
        )
        trench_right_features = shapelify_feature_lines(
            self.trench_right,
            tessellate_degrees=tessellate_degrees,
            central_meridian=central_meridian,
        )

        plot_subduction_teeth(
            trench_left_features,
            size,
            "l",
            height,
            spacing,
            projection=projection,
            ax=ax,
            color=color,
            **kwargs,
        )
        plot_subduction_teeth(
            trench_right_features,
            size,
            "r",
            height,
            spacing,
            projection=projection,
            ax=ax,
            color=color,
            **kwargs,
        )

    def plot_plate_id(self, ax, plate_id, **kwargs):
        """Plot a plate polygon with an associated `plate_id` onto a standard map Projection.

        Parameters
        ----------
        ax : instance of <cartopy.mpl.geoaxes.GeoAxes> or <cartopy.mpl.geoaxes.GeoAxesSubplot>
            A subclass of `matplotlib.axes.Axes` which represents a map Projection.
            The map should be set at a particular Cartopy projection.

        plate_id : int
            A plate ID that identifies the continental polygon to plot. See the
            [Global EarthByte plate IDs list](https://www.earthbyte.org/webdav/ftp/earthbyte/GPlates/SampleData/FeatureCollections/Rotations/Global_EarthByte_PlateIDs_20071218.pdf)
            for a full list of plate IDs to plot.

        **kwargs :
            Keyword arguments for map presentation parameters such as
            `alpha`, etc. for plotting the grid.
            See `Matplotlib`'s `imshow` keyword arguments
            [here](https://matplotlib.org/3.5.1/api/_as_gen/matplotlib.axes.Axes.imshow.html).

        """
        tessellate_degrees = kwargs.pop("tessellate_degrees", None)
        central_meridian = kwargs.pop("central_meridian", None)
        if central_meridian is None:
            central_meridian = _meridian_from_ax(ax)

        for feature in self.topologies:
            if feature.get_reconstruction_plate_id() == plate_id:
                ft_plate = shapelify_feature_polygons(
                    [feature],
                    central_meridian=central_meridian,
                    tessellate_degrees=tessellate_degrees,
                )
                return ax.add_geometries(ft_plate, crs=self.base_projection, **kwargs)

    def plot_grid(self, ax, grid, extent=[-180, 180, -90, 90], **kwargs):
        """Plot a `MaskedArray` raster or grid onto a standard map Projection.

        Notes
        -----
        Uses Matplotlib's `imshow`
        [function](https://matplotlib.org/3.5.1/api/_as_gen/matplotlib.axes.Axes.imshow.html).

        Parameters
        ----------
        ax : instance of <cartopy.mpl.geoaxes.GeoAxes> or <cartopy.mpl.geoaxes.GeoAxesSubplot>
            A subclass of `matplotlib.axes.Axes` which represents a map Projection.
            The map should be set at a particular Cartopy projection.

        grid : MaskedArray or `gplately.grids.Raster`
            A `MaskedArray` with elements that define a grid. The number of rows in the raster
            corresponds to the number of latitudinal coordinates, while the number of raster
            columns corresponds to the number of longitudinal coordinates.

        extent : 1d array, default=[-180,180,-90,90]
            A four-element array to specify the [min lon, max lon, min lat, max lat] with
            which to constrain the grid image. If no extents are supplied, full global
            extent is assumed.

        **kwargs :
            Keyword arguments for map presentation parameters such as
            `alpha`, etc. for plotting the grid.
            See `Matplotlib`'s `imshow` keyword arguments
            [here](https://matplotlib.org/3.5.1/api/_as_gen/matplotlib.axes.Axes.imshow.html).

        Returns
        -------
        ax : instance of <geopandas.GeoDataFrame.plot>
            A standard cartopy.mpl.geoaxes.GeoAxes or cartopy.mpl.geoaxes.GeoAxesSubplot map
            with the grid plotted onto the chosen map projection.
        """
        # Override matplotlib default origin ('upper')
        origin = kwargs.pop("origin", "lower")

        from .grids import Raster

        if isinstance(grid, Raster):
            # extract extent and origin
            extent = grid.extent
            origin = grid.origin
            data = grid.data
        else:
            data = grid

        return ax.imshow(
            data,
            extent=extent,
            transform=self.base_projection,
            origin=origin,
            **kwargs,
        )

    def plot_grid_from_netCDF(self, ax, filename, **kwargs):
        """Read a raster from a netCDF file, convert it to a `MaskedArray` and plot it
        onto a standard map Projection.

        Notes
        -----
        `plot_grid_from_netCDF` uses Matplotlib's `imshow`
        [function](https://matplotlib.org/3.5.1/api/_as_gen/matplotlib.axes.Axes.imshow.html).

        Parameters
        ----------
        ax : instance of <cartopy.mpl.geoaxes.GeoAxes> or <cartopy.mpl.geoaxes.GeoAxesSubplot>
            A subclass of `matplotlib.axes.Axes` which represents a map Projection.
            The map should be set at a particular Cartopy projection.

        filename : str
            Full path to a netCDF filename.

        **kwargs :
            Keyword arguments for map presentation parameters for
            plotting the grid. See `Matplotlib`'s `imshow` keyword arguments
            [here](https://matplotlib.org/3.5.1/api/_as_gen/matplotlib.axes.Axes.imshow.html).

        Returns
        -------
        ax : instance of <geopandas.GeoDataFrame.plot>
            A standard cartopy.mpl.geoaxes.GeoAxes or cartopy.mpl.geoaxes.GeoAxesSubplot map
            with the netCDF grid plotted onto the chosen map projection.
        """
        # Override matplotlib default origin ('upper')
        origin = kwargs.pop("origin", "lower")

        from .grids import read_netcdf_grid

        raster, lon_coords, lat_coords = read_netcdf_grid(filename, return_grids=True)
        extent = [lon_coords[0], lon_coords[-1], lat_coords[0], lat_coords[-1]]

        if lon_coords[0] < lat_coords[-1]:
            origin = "lower"
        else:
            origin = "upper"

        return self.plot_grid(ax, raster, extent=extent, origin=origin, **kwargs)

    def plot_plate_motion_vectors(
        self, ax, spacingX=10, spacingY=10, normalise=False, **kwargs
    ):
        """Calculate plate motion velocity vector fields at a particular geological time
        and plot them onto a standard map Projection.

        Notes
        -----
        `plot_plate_motion_vectors` generates a MeshNode domain of point features using
        given spacings in the X and Y directions (`spacingX` and `spacingY`). Each point in
        the domain is assigned a plate ID, and these IDs are used to obtain equivalent stage
        rotations of identified tectonic plates over a 5 Ma time interval. Each point and
        its stage rotation are used to calculate plate velocities at a particular geological
        time. Velocities for each domain point are represented in the north-east-down
        coordinate system and plotted on a GeoAxes.

        Vector fields can be optionally normalised by setting `normalise` to `True`. This
        makes vector arrow lengths uniform.

        Parameters
        ----------
        ax : instance of <cartopy.mpl.geoaxes.GeoAxes> or <cartopy.mpl.geoaxes.GeoAxesSubplot>
            A subclass of `matplotlib.axes.Axes` which represents a map Projection.
            The map should be set at a particular Cartopy projection.

        spacingX : int, default=10
            The spacing in the X direction used to make the velocity domain point feature mesh.

        spacingY : int, default=10
            The spacing in the Y direction used to make the velocity domain point feature mesh.

        normalise : bool, default=False
            Choose whether to normalise the velocity magnitudes so that vector lengths are
            all equal.

        **kwargs :
            Keyword arguments for quiver presentation parameters for plotting
            the velocity vector field. See `Matplotlib` quiver keyword arguments
            [here](https://matplotlib.org/3.5.1/api/_as_gen/matplotlib.axes.Axes.quiver.html).

        Returns
        -------
        ax : instance of <geopandas.GeoDataFrame.plot>
            A standard cartopy.mpl.geoaxes.GeoAxes or cartopy.mpl.geoaxes.GeoAxesSubplot map
            with the velocity vector field plotted onto the chosen map projection.
        """

        lons = np.arange(-180, 180 + spacingX, spacingX)
        lats = np.arange(-90, 90 + spacingY, spacingY)
        lonq, latq = np.meshgrid(lons, lats)

        # create a feature from all the points
        velocity_domain_features = ptt.velocity_tools.make_GPML_velocity_feature(
            lonq.ravel(), latq.ravel()
        )

        rotation_model = self.plate_reconstruction.rotation_model
        topology_features = self.plate_reconstruction.topology_features

        delta_time = 5.0
        all_velocities = ptt.velocity_tools.get_plate_velocities(
            velocity_domain_features,
            topology_features,
            rotation_model,
            self.time,
            delta_time,
            "vector_comp",
        )

        X, Y, U, V = ptt.velocity_tools.get_x_y_u_v(lons, lats, all_velocities)

        if normalise:
            mag = np.hypot(U, V)
            mag[mag == 0] = 1
            U /= mag
            V /= mag

        with warnings.catch_warnings():
            warnings.simplefilter("ignore", UserWarning)
            quiver = ax.quiver(X, Y, U, V, transform=self.base_projection, **kwargs)
        return quiver

    def get_continental_rifts(
        self,
        central_meridian=0.0,
        tessellate_degrees=None,
    ):
        """Create a geopandas.GeoDataFrame object containing geometries of reconstructed contiental rift lines.

        Notes
        -----
        The `continental_rifts` needed to produce the GeoDataFrame are automatically constructed if the optional `time`
        parameter is passed to the `PlotTopologies` object before calling this function. `time` can be passed
        either when `PlotTopologies` is first called...

            gplot = gplately.PlotTopologies(..., time=100,...)

        or anytime afterwards, by setting:

            time = 100 #Ma
            gplot.time = time

        ...after which this function can be re-run. Once the `continental_rifts` are reconstructed, they are
        converted into Shapely lines whose coordinates are passed to a geopandas GeoDataFrame.

        Returns
        -------
        gdf : instance of <geopandas.GeoDataFrame>
            A pandas.DataFrame that has a column with `continental_rifts` geometry.
        central_meridian : float
            Central meridian around which to perform wrapping; default: 0.0.
        tessellate_degrees : float or None
            If provided, geometries will be tessellated to this resolution prior
            to wrapping.

        Raises
        ------
        ValueError
            If the optional `time` parameter has not been passed to `PlotTopologies`. This is needed to construct
            `continental_rifts` to the requested `time` and thus populate the GeoDataFrame.
        """
        if self._time is None:
            raise ValueError(
                "No continental rifts have been resolved. Set `PlotTopologies.time` to construct them."
            )

        if self.continental_rifts is None:
            raise ValueError("No continental rifts passed to PlotTopologies.")

        continental_rift_lines = shapelify_feature_lines(
            self.continental_rifts,
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        gdf = gpd.GeoDataFrame(
            {"geometry": continental_rift_lines}, geometry="geometry"
        )
        return gdf

    def plot_continental_rifts(self, ax, color="black", **kwargs):
        """Plot continental rifts on a standard map projection.

        Parameters
        ----------
        ax : instance of <cartopy.mpl.geoaxes.GeoAxes> or <cartopy.mpl.geoaxes.GeoAxesSubplot>
            A subclass of `matplotlib.axes.Axes` which represents a map Projection.
            The map should be set at a particular Cartopy projection.

        color : str, default=’black’
            The colour of the trench lines. By default, it is set to black.

        **kwargs :
            Keyword arguments for parameters such as `alpha`, etc.
            for plotting trench geometries.
            See `Matplotlib` keyword arguments
            [here](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.plot.html).

        Returns
        -------
        ax : instance of <geopandas.GeoDataFrame.plot>
            A standard cartopy.mpl.geoaxes.GeoAxes or cartopy.mpl.geoaxes.GeoAxesSubplot map
            with continental rifts plotted onto the chosen map projection.
        """
        if "transform" in kwargs.keys():
            warnings.warn(
                "'transform' keyword argument is ignored by PlotTopologies",
                UserWarning,
            )
            kwargs.pop("transform")
        tessellate_degrees = kwargs.pop("tessellate_degrees", None)
        central_meridian = kwargs.pop("central_meridian", None)
        if central_meridian is None:
            central_meridian = _meridian_from_ax(ax)

        gdf = self.get_continental_rifts(
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        if hasattr(ax, "projection"):
            gdf = _clean_polygons(data=gdf, projection=ax.projection)
        else:
            kwargs["transform"] = self.base_projection
        return gdf.plot(ax=ax, facecolor="none", edgecolor=color, **kwargs)

    def get_faults(self, central_meridian=0.0, tessellate_degrees=None):
        """Create a geopandas.GeoDataFrame object containing geometries of reconstructed fault lines.

        Notes
        -----
        The `faults` needed to produce the GeoDataFrame are automatically constructed if the optional `time`
        parameter is passed to the `PlotTopologies` object before calling this function. `time` can be passed
        either when `PlotTopologies` is first called...

            gplot = gplately.PlotTopologies(..., time=100,...)

        or anytime afterwards, by setting:

            time = 100 #Ma
            gplot.time = time

        ...after which this function can be re-run. Once the `faults` are reconstructed, they are
        converted into Shapely lines whose coordinates are passed to a geopandas GeoDataFrame.

        Returns
        -------
        gdf : instance of <geopandas.GeoDataFrame>
            A pandas.DataFrame that has a column with `faults` geometry.
        central_meridian : float
            Central meridian around which to perform wrapping; default: 0.0.
        tessellate_degrees : float or None
            If provided, geometries will be tessellated to this resolution prior
            to wrapping.

        Raises
        ------
        ValueError
            If the optional `time` parameter has not been passed to `PlotTopologies`. This is needed to construct
            `faults` to the requested `time` and thus populate the GeoDataFrame.
        """
        if self._time is None:
            raise ValueError(
                "No faults have been resolved. Set `PlotTopologies.time` to construct them."
            )

        if self.faults is None:
            raise ValueError("No faults passed to PlotTopologies.")

        fault_lines = shapelify_feature_lines(
            self.faults,
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        gdf = gpd.GeoDataFrame({"geometry": fault_lines}, geometry="geometry")
        return gdf

    def plot_faults(self, ax, color="black", **kwargs):
        """Plot faults on a standard map projection.

        Parameters
        ----------
        ax : instance of <cartopy.mpl.geoaxes.GeoAxes> or <cartopy.mpl.geoaxes.GeoAxesSubplot>
            A subclass of `matplotlib.axes.Axes` which represents a map Projection.
            The map should be set at a particular Cartopy projection.

        color : str, default=’black’
            The colour of the trench lines. By default, it is set to black.

        **kwargs :
            Keyword arguments for parameters such as `alpha`, etc.
            for plotting trench geometries.
            See `Matplotlib` keyword arguments
            [here](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.plot.html).

        Returns
        -------
        ax : instance of <geopandas.GeoDataFrame.plot>
            A standard cartopy.mpl.geoaxes.GeoAxes or cartopy.mpl.geoaxes.GeoAxesSubplot map
            with faults plotted onto the chosen map projection.
        """
        if "transform" in kwargs.keys():
            warnings.warn(
                "'transform' keyword argument is ignored by PlotTopologies",
                UserWarning,
            )
            kwargs.pop("transform")
        tessellate_degrees = kwargs.pop("tessellate_degrees", None)
        central_meridian = kwargs.pop("central_meridian", None)
        if central_meridian is None:
            central_meridian = _meridian_from_ax(ax)

        gdf = self.get_faults(
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        if hasattr(ax, "projection"):
            gdf = _clean_polygons(data=gdf, projection=ax.projection)
        else:
            kwargs["transform"] = self.base_projection
        return gdf.plot(ax=ax, facecolor="none", edgecolor=color, **kwargs)

    def get_fracture_zones(self, central_meridian=0.0, tessellate_degrees=None):
        """Create a geopandas.GeoDataFrame object containing geometries of reconstructed fracture zone lines.

        Notes
        -----
        The `fracture_zones` needed to produce the GeoDataFrame are automatically constructed if the optional `time`
        parameter is passed to the `PlotTopologies` object before calling this function. `time` can be passed
        either when `PlotTopologies` is first called...

            gplot = gplately.PlotTopologies(..., time=100,...)

        or anytime afterwards, by setting:

            time = 100 #Ma
            gplot.time = time

        ...after which this function can be re-run. Once the `fracture_zones` are reconstructed, they are
        converted into Shapely lines whose coordinates are passed to a geopandas GeoDataFrame.

        Returns
        -------
        gdf : instance of <geopandas.GeoDataFrame>
            A pandas.DataFrame that has a column with `fracture_zones` geometry.
        central_meridian : float
            Central meridian around which to perform wrapping; default: 0.0.
        tessellate_degrees : float or None
            If provided, geometries will be tessellated to this resolution prior
            to wrapping.

        Raises
        ------
        ValueError
            If the optional `time` parameter has not been passed to `PlotTopologies`. This is needed to construct
            `fracture_zones` to the requested `time` and thus populate the GeoDataFrame.
        """
        if self._time is None:
            raise ValueError(
                "No fracture zones have been resolved. Set `PlotTopologies.time` to construct them."
            )

        if self.fracture_zones is None:
            raise ValueError("No fracture zones passed to PlotTopologies.")

        fracture_zone_lines = shapelify_feature_lines(
            self.fracture_zones,
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        gdf = gpd.GeoDataFrame({"geometry": fracture_zone_lines}, geometry="geometry")
        return gdf

    def plot_fracture_zones(self, ax, color="black", **kwargs):
        """Plot fracture zones on a standard map projection.

        Parameters
        ----------
        ax : instance of <cartopy.mpl.geoaxes.GeoAxes> or <cartopy.mpl.geoaxes.GeoAxesSubplot>
            A subclass of `matplotlib.axes.Axes` which represents a map Projection.
            The map should be set at a particular Cartopy projection.

        color : str, default=’black’
            The colour of the trench lines. By default, it is set to black.

        **kwargs :
            Keyword arguments for parameters such as `alpha`, etc.
            for plotting trench geometries.
            See `Matplotlib` keyword arguments
            [here](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.plot.html).

        Returns
        -------
        ax : instance of <geopandas.GeoDataFrame.plot>
            A standard cartopy.mpl.geoaxes.GeoAxes or cartopy.mpl.geoaxes.GeoAxesSubplot map
            with fracture zones plotted onto the chosen map projection.
        """
        if "transform" in kwargs.keys():
            warnings.warn(
                "'transform' keyword argument is ignored by PlotTopologies",
                UserWarning,
            )
            kwargs.pop("transform")
        tessellate_degrees = kwargs.pop("tessellate_degrees", None)
        central_meridian = kwargs.pop("central_meridian", None)
        if central_meridian is None:
            central_meridian = _meridian_from_ax(ax)

        gdf = self.get_fracture_zones(
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        if hasattr(ax, "projection"):
            gdf = _clean_polygons(data=gdf, projection=ax.projection)
        else:
            kwargs["transform"] = self.base_projection
        return gdf.plot(ax=ax, facecolor="none", edgecolor=color, **kwargs)

    def get_inferred_paleo_boundaries(
        self,
        central_meridian=0.0,
        tessellate_degrees=None,
    ):
        """Create a geopandas.GeoDataFrame object containing geometries of reconstructed inferred paleo boundary lines.

        Notes
        -----
        The `inferred_paleo_boundaries` needed to produce the GeoDataFrame are automatically constructed if the optional `time`
        parameter is passed to the `PlotTopologies` object before calling this function. `time` can be passed
        either when `PlotTopologies` is first called...

            gplot = gplately.PlotTopologies(..., time=100,...)

        or anytime afterwards, by setting:

            time = 100 #Ma
            gplot.time = time

        ...after which this function can be re-run. Once the `inferred_paleo_boundaries` are reconstructed, they are
        converted into Shapely lines whose coordinates are passed to a geopandas GeoDataFrame.

        Returns
        -------
        gdf : instance of <geopandas.GeoDataFrame>
            A pandas.DataFrame that has a column with `inferred_paleo_boundaries` geometry.
        central_meridian : float
            Central meridian around which to perform wrapping; default: 0.0.
        tessellate_degrees : float or None
            If provided, geometries will be tessellated to this resolution prior
            to wrapping.

        Raises
        ------
        ValueError
            If the optional `time` parameter has not been passed to `PlotTopologies`. This is needed to construct
            `inferred_paleo_boundaries` to the requested `time` and thus populate the GeoDataFrame.
        """
        if self._time is None:
            raise ValueError(
                "No inferred paleo boundaries have been resolved. Set `PlotTopologies.time` to construct them."
            )

        if self.inferred_paleo_boundaries is None:
            raise ValueError("No inferred paleo boundaries passed to PlotTopologies.")

        inferred_paleo_boundary_lines = shapelify_feature_lines(
            self.inferred_paleo_boundaries,
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        gdf = gpd.GeoDataFrame(
            {"geometry": inferred_paleo_boundary_lines}, geometry="geometry"
        )
        return gdf

    def plot_inferred_paleo_boundaries(self, ax, color="black", **kwargs):
        """Plot inferred paleo boundaries on a standard map projection.

        Parameters
        ----------
        ax : instance of <cartopy.mpl.geoaxes.GeoAxes> or <cartopy.mpl.geoaxes.GeoAxesSubplot>
            A subclass of `matplotlib.axes.Axes` which represents a map Projection.
            The map should be set at a particular Cartopy projection.

        color : str, default=’black’
            The colour of the trench lines. By default, it is set to black.

        **kwargs :
            Keyword arguments for parameters such as `alpha`, etc.
            for plotting trench geometries.
            See `Matplotlib` keyword arguments
            [here](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.plot.html).

        Returns
        -------
        ax : instance of <geopandas.GeoDataFrame.plot>
            A standard cartopy.mpl.geoaxes.GeoAxes or cartopy.mpl.geoaxes.GeoAxesSubplot map
            with inferred paleo boundaries plotted onto the chosen map projection.
        """
        if "transform" in kwargs.keys():
            warnings.warn(
                "'transform' keyword argument is ignored by PlotTopologies",
                UserWarning,
            )
            kwargs.pop("transform")
        tessellate_degrees = kwargs.pop("tessellate_degrees", None)
        central_meridian = kwargs.pop("central_meridian", None)
        if central_meridian is None:
            central_meridian = _meridian_from_ax(ax)

        gdf = self.get_inferred_paleo_boundaries(
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        if hasattr(ax, "projection"):
            gdf = _clean_polygons(data=gdf, projection=ax.projection)
        else:
            kwargs["transform"] = self.base_projection
        return gdf.plot(ax=ax, facecolor="none", edgecolor=color, **kwargs)

    def get_terrane_boundaries(
        self,
        central_meridian=0.0,
        tessellate_degrees=None,
    ):
        """Create a geopandas.GeoDataFrame object containing geometries of reconstructed terrane boundary lines.

        Notes
        -----
        The `terrane_boundaries` needed to produce the GeoDataFrame are automatically constructed if the optional `time`
        parameter is passed to the `PlotTopologies` object before calling this function. `time` can be passed
        either when `PlotTopologies` is first called...

            gplot = gplately.PlotTopologies(..., time=100,...)

        or anytime afterwards, by setting:

            time = 100 #Ma
            gplot.time = time

        ...after which this function can be re-run. Once the `terrane_boundaries` are reconstructed, they are
        converted into Shapely lines whose coordinates are passed to a geopandas GeoDataFrame.

        Returns
        -------
        gdf : instance of <geopandas.GeoDataFrame>
            A pandas.DataFrame that has a column with `terrane_boundaries` geometry.
        central_meridian : float
            Central meridian around which to perform wrapping; default: 0.0.
        tessellate_degrees : float or None
            If provided, geometries will be tessellated to this resolution prior
            to wrapping.

        Raises
        ------
        ValueError
            If the optional `time` parameter has not been passed to `PlotTopologies`. This is needed to construct
            `terrane_boundaries` to the requested `time` and thus populate the GeoDataFrame.
        """
        if self._time is None:
            raise ValueError(
                "No terrane boundaries have been resolved. Set `PlotTopologies.time` to construct them."
            )

        if self.terrane_boundaries is None:
            raise ValueError("No terrane boundaries passed to PlotTopologies.")

        terrane_boundary_lines = shapelify_feature_lines(
            self.terrane_boundaries,
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        gdf = gpd.GeoDataFrame(
            {"geometry": terrane_boundary_lines}, geometry="geometry"
        )
        return gdf

    def plot_terrane_boundaries(self, ax, color="black", **kwargs):
        """Plot terrane boundaries on a standard map projection.

        Parameters
        ----------
        ax : instance of <cartopy.mpl.geoaxes.GeoAxes> or <cartopy.mpl.geoaxes.GeoAxesSubplot>
            A subclass of `matplotlib.axes.Axes` which represents a map Projection.
            The map should be set at a particular Cartopy projection.

        color : str, default=’black’
            The colour of the trench lines. By default, it is set to black.

        **kwargs :
            Keyword arguments for parameters such as `alpha`, etc.
            for plotting trench geometries.
            See `Matplotlib` keyword arguments
            [here](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.plot.html).

        Returns
        -------
        ax : instance of <geopandas.GeoDataFrame.plot>
            A standard cartopy.mpl.geoaxes.GeoAxes or cartopy.mpl.geoaxes.GeoAxesSubplot map
            with terrane boundaries plotted onto the chosen map projection.
        """
        if "transform" in kwargs.keys():
            warnings.warn(
                "'transform' keyword argument is ignored by PlotTopologies",
                UserWarning,
            )
            kwargs.pop("transform")
        tessellate_degrees = kwargs.pop("tessellate_degrees", None)
        central_meridian = kwargs.pop("central_meridian", None)
        if central_meridian is None:
            central_meridian = _meridian_from_ax(ax)

        gdf = self.get_terrane_boundaries(
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        if hasattr(ax, "projection"):
            gdf = _clean_polygons(data=gdf, projection=ax.projection)
        else:
            kwargs["transform"] = self.base_projection
        return gdf.plot(ax=ax, facecolor="none", edgecolor=color, **kwargs)

    def get_transitional_crusts(
        self,
        central_meridian=0.0,
        tessellate_degrees=None,
    ):
        """Create a geopandas.GeoDataFrame object containing geometries of reconstructed transitional crust lines.

        Notes
        -----
        The `transitional_crusts` needed to produce the GeoDataFrame are automatically constructed if the optional `time`
        parameter is passed to the `PlotTopologies` object before calling this function. `time` can be passed
        either when `PlotTopologies` is first called...

            gplot = gplately.PlotTopologies(..., time=100,...)

        or anytime afterwards, by setting:

            time = 100 #Ma
            gplot.time = time

        ...after which this function can be re-run. Once the `transitional_crusts` are reconstructed, they are
        converted into Shapely lines whose coordinates are passed to a geopandas GeoDataFrame.

        Returns
        -------
        gdf : instance of <geopandas.GeoDataFrame>
            A pandas.DataFrame that has a column with `transitional_crusts` geometry.
        central_meridian : float
            Central meridian around which to perform wrapping; default: 0.0.
        tessellate_degrees : float or None
            If provided, geometries will be tessellated to this resolution prior
            to wrapping.

        Raises
        ------
        ValueError
            If the optional `time` parameter has not been passed to `PlotTopologies`. This is needed to construct
            `transitional_crusts` to the requested `time` and thus populate the GeoDataFrame.
        """
        if self._time is None:
            raise ValueError(
                "No transitional crusts have been resolved. Set `PlotTopologies.time` to construct them."
            )

        if self.transitional_crusts is None:
            raise ValueError("No transitional crusts passed to PlotTopologies.")

        transitional_crust_lines = shapelify_feature_lines(
            self.transitional_crusts,
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        gdf = gpd.GeoDataFrame(
            {"geometry": transitional_crust_lines}, geometry="geometry"
        )
        return gdf

    def plot_transitional_crusts(self, ax, color="black", **kwargs):
        """Plot transitional crust on a standard map projection.

        Parameters
        ----------
        ax : instance of <cartopy.mpl.geoaxes.GeoAxes> or <cartopy.mpl.geoaxes.GeoAxesSubplot>
            A subclass of `matplotlib.axes.Axes` which represents a map Projection.
            The map should be set at a particular Cartopy projection.

        color : str, default=’black’
            The colour of the trench lines. By default, it is set to black.

        **kwargs :
            Keyword arguments for parameters such as `alpha`, etc.
            for plotting trench geometries.
            See `Matplotlib` keyword arguments
            [here](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.plot.html).

        Returns
        -------
        ax : instance of <geopandas.GeoDataFrame.plot>
            A standard cartopy.mpl.geoaxes.GeoAxes or cartopy.mpl.geoaxes.GeoAxesSubplot map
            with transitional crust plotted onto the chosen map projection.
        """
        if "transform" in kwargs.keys():
            warnings.warn(
                "'transform' keyword argument is ignored by PlotTopologies",
                UserWarning,
            )
            kwargs.pop("transform")
        tessellate_degrees = kwargs.pop("tessellate_degrees", None)
        central_meridian = kwargs.pop("central_meridian", None)
        if central_meridian is None:
            central_meridian = _meridian_from_ax(ax)

        gdf = self.get_transitional_crusts(
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        if hasattr(ax, "projection"):
            gdf = _clean_polygons(data=gdf, projection=ax.projection)
        else:
            kwargs["transform"] = self.base_projection
        return gdf.plot(ax=ax, facecolor="none", edgecolor=color, **kwargs)

    def get_orogenic_belts(
        self,
        central_meridian=0.0,
        tessellate_degrees=None,
    ):
        """Create a geopandas.GeoDataFrame object containing geometries of reconstructed orogenic belt lines.

        Notes
        -----
        The `orogenic_belts` needed to produce the GeoDataFrame are automatically constructed if the optional `time`
        parameter is passed to the `PlotTopologies` object before calling this function. `time` can be passed
        either when `PlotTopologies` is first called...

            gplot = gplately.PlotTopologies(..., time=100,...)

        or anytime afterwards, by setting:

            time = 100 #Ma
            gplot.time = time

        ...after which this function can be re-run. Once the `orogenic_belts` are reconstructed, they are
        converted into Shapely lines whose coordinates are passed to a geopandas GeoDataFrame.

        Returns
        -------
        gdf : instance of <geopandas.GeoDataFrame>
            A pandas.DataFrame that has a column with `orogenic_belts` geometry.
        central_meridian : float
            Central meridian around which to perform wrapping; default: 0.0.
        tessellate_degrees : float or None
            If provided, geometries will be tessellated to this resolution prior
            to wrapping.

        Raises
        ------
        ValueError
            If the optional `time` parameter has not been passed to `PlotTopologies`. This is needed to construct
            `orogenic_belts` to the requested `time` and thus populate the GeoDataFrame.
        """
        if self._time is None:
            raise ValueError(
                "No orogenic belts have been resolved. Set `PlotTopologies.time` to construct them."
            )

        if self.orogenic_belts is None:
            raise ValueError("No orogenic belts passed to PlotTopologies.")

        orogenic_belt_lines = shapelify_feature_lines(
            self.orogenic_belts,
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        gdf = gpd.GeoDataFrame({"geometry": orogenic_belt_lines}, geometry="geometry")
        return gdf

    def plot_orogenic_belts(self, ax, color="black", **kwargs):
        """Plot orogenic belts on a standard map projection.

        Parameters
        ----------
        ax : instance of <cartopy.mpl.geoaxes.GeoAxes> or <cartopy.mpl.geoaxes.GeoAxesSubplot>
            A subclass of `matplotlib.axes.Axes` which represents a map Projection.
            The map should be set at a particular Cartopy projection.

        color : str, default=’black’
            The colour of the trench lines. By default, it is set to black.

        **kwargs :
            Keyword arguments for parameters such as `alpha`, etc.
            for plotting trench geometries.
            See `Matplotlib` keyword arguments
            [here](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.plot.html).

        Returns
        -------
        ax : instance of <geopandas.GeoDataFrame.plot>
            A standard cartopy.mpl.geoaxes.GeoAxes or cartopy.mpl.geoaxes.GeoAxesSubplot map
            with orogenic belts plotted onto the chosen map projection.
        """
        if "transform" in kwargs.keys():
            warnings.warn(
                "'transform' keyword argument is ignored by PlotTopologies",
                UserWarning,
            )
            kwargs.pop("transform")
        tessellate_degrees = kwargs.pop("tessellate_degrees", None)
        central_meridian = kwargs.pop("central_meridian", None)
        if central_meridian is None:
            central_meridian = _meridian_from_ax(ax)

        gdf = self.get_orogenic_belts(
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        if hasattr(ax, "projection"):
            gdf = _clean_polygons(data=gdf, projection=ax.projection)
        else:
            kwargs["transform"] = self.base_projection
        return gdf.plot(ax=ax, facecolor="none", edgecolor=color, **kwargs)

    def get_sutures(self, central_meridian=0.0, tessellate_degrees=None):
        """Create a geopandas.GeoDataFrame object containing geometries of reconstructed suture lines.

        Notes
        -----
        The `sutures` needed to produce the GeoDataFrame are automatically constructed if the optional `time`
        parameter is passed to the `PlotTopologies` object before calling this function. `time` can be passed
        either when `PlotTopologies` is first called...

            gplot = gplately.PlotTopologies(..., time=100,...)

        or anytime afterwards, by setting:

            time = 100 #Ma
            gplot.time = time

        ...after which this function can be re-run. Once the `sutures` are reconstructed, they are
        converted into Shapely lines whose coordinates are passed to a geopandas GeoDataFrame.

        Returns
        -------
        gdf : instance of <geopandas.GeoDataFrame>
            A pandas.DataFrame that has a column with `sutures` geometry.
        central_meridian : float
            Central meridian around which to perform wrapping; default: 0.0.
        tessellate_degrees : float or None
            If provided, geometries will be tessellated to this resolution prior
            to wrapping.

        Raises
        ------
        ValueError
            If the optional `time` parameter has not been passed to `PlotTopologies`. This is needed to construct
            `sutures` to the requested `time` and thus populate the GeoDataFrame.
        """
        if self._time is None:
            raise ValueError(
                "No sutures have been resolved. Set `PlotTopologies.time` to construct them."
            )

        if self.sutures is None:
            raise ValueError("No sutures passed to PlotTopologies.")

        suture_lines = shapelify_feature_lines(
            self.sutures,
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        gdf = gpd.GeoDataFrame({"geometry": suture_lines}, geometry="geometry")
        return gdf

    def plot_sutures(self, ax, color="black", **kwargs):
        """Plot sutures on a standard map projection.

        Parameters
        ----------
        ax : instance of <cartopy.mpl.geoaxes.GeoAxes> or <cartopy.mpl.geoaxes.GeoAxesSubplot>
            A subclass of `matplotlib.axes.Axes` which represents a map Projection.
            The map should be set at a particular Cartopy projection.

        color : str, default=’black’
            The colour of the trench lines. By default, it is set to black.

        **kwargs :
            Keyword arguments for parameters such as `alpha`, etc.
            for plotting trench geometries.
            See `Matplotlib` keyword arguments
            [here](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.plot.html).

        Returns
        -------
        ax : instance of <geopandas.GeoDataFrame.plot>
            A standard cartopy.mpl.geoaxes.GeoAxes or cartopy.mpl.geoaxes.GeoAxesSubplot map
            with sutures plotted onto the chosen map projection.
        """
        if "transform" in kwargs.keys():
            warnings.warn(
                "'transform' keyword argument is ignored by PlotTopologies",
                UserWarning,
            )
            kwargs.pop("transform")
        tessellate_degrees = kwargs.pop("tessellate_degrees", None)
        central_meridian = kwargs.pop("central_meridian", None)
        if central_meridian is None:
            central_meridian = _meridian_from_ax(ax)

        gdf = self.get_sutures(
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        if hasattr(ax, "projection"):
            gdf = _clean_polygons(data=gdf, projection=ax.projection)
        else:
            kwargs["transform"] = self.base_projection
        return gdf.plot(ax=ax, facecolor="none", edgecolor=color, **kwargs)

    def get_continental_crusts(
        self,
        central_meridian=0.0,
        tessellate_degrees=None,
    ):
        """Create a geopandas.GeoDataFrame object containing geometries of reconstructed continental crust lines.

        Notes
        -----
        The `continental_crusts` needed to produce the GeoDataFrame are automatically constructed if the optional `time`
        parameter is passed to the `PlotTopologies` object before calling this function. `time` can be passed
        either when `PlotTopologies` is first called...

            gplot = gplately.PlotTopologies(..., time=100,...)

        or anytime afterwards, by setting:

            time = 100 #Ma
            gplot.time = time

        ...after which this function can be re-run. Once the `continental_crusts` are reconstructed, they are
        converted into Shapely lines whose coordinates are passed to a geopandas GeoDataFrame.

        Returns
        -------
        gdf : instance of <geopandas.GeoDataFrame>
            A pandas.DataFrame that has a column with `continental_crusts` geometry.
        central_meridian : float
            Central meridian around which to perform wrapping; default: 0.0.
        tessellate_degrees : float or None
            If provided, geometries will be tessellated to this resolution prior
            to wrapping.

        Raises
        ------
        ValueError
            If the optional `time` parameter has not been passed to `PlotTopologies`. This is needed to construct
            `continental_crusts` to the requested `time` and thus populate the GeoDataFrame.
        """
        if self._time is None:
            raise ValueError(
                "No continental crust topologies have been resolved. Set `PlotTopologies.time` to construct them."
            )

        if self.continental_crusts is None:
            raise ValueError(
                "No continental crust topologies passed to PlotTopologies."
            )

        continental_crust_lines = shapelify_feature_lines(
            self.continental_crusts,
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        gdf = gpd.GeoDataFrame(
            {"geometry": continental_crust_lines}, geometry="geometry"
        )
        return gdf

    def plot_continental_crusts(self, ax, color="black", **kwargs):
        """Plot continental crust lines on a standard map projection.

        Parameters
        ----------
        ax : instance of <cartopy.mpl.geoaxes.GeoAxes> or <cartopy.mpl.geoaxes.GeoAxesSubplot>
            A subclass of `matplotlib.axes.Axes` which represents a map Projection.
            The map should be set at a particular Cartopy projection.

        color : str, default=’black’
            The colour of the trench lines. By default, it is set to black.

        **kwargs :
            Keyword arguments for parameters such as `alpha`, etc.
            for plotting trench geometries.
            See `Matplotlib` keyword arguments
            [here](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.plot.html).

        Returns
        -------
        ax : instance of <geopandas.GeoDataFrame.plot>
            A standard cartopy.mpl.geoaxes.GeoAxes or cartopy.mpl.geoaxes.GeoAxesSubplot map
            with continental crust lines plotted onto the chosen map projection.
        """
        if "transform" in kwargs.keys():
            warnings.warn(
                "'transform' keyword argument is ignored by PlotTopologies",
                UserWarning,
            )
            kwargs.pop("transform")
        tessellate_degrees = kwargs.pop("tessellate_degrees", None)
        central_meridian = kwargs.pop("central_meridian", None)
        if central_meridian is None:
            central_meridian = _meridian_from_ax(ax)

        gdf = self.get_continental_crusts(
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        if hasattr(ax, "projection"):
            gdf = _clean_polygons(data=gdf, projection=ax.projection)
        else:
            kwargs["transform"] = self.base_projection
        return gdf.plot(ax=ax, facecolor="none", edgecolor=color, **kwargs)

    def get_extended_continental_crusts(
        self,
        central_meridian=0.0,
        tessellate_degrees=None,
    ):
        """Create a geopandas.GeoDataFrame object containing geometries of reconstructed extended continental crust lines.

        Notes
        -----
        The `extended_continental_crusts` needed to produce the GeoDataFrame are automatically constructed if the optional `time`
        parameter is passed to the `PlotTopologies` object before calling this function. `time` can be passed
        either when `PlotTopologies` is first called...

            gplot = gplately.PlotTopologies(..., time=100,...)

        or anytime afterwards, by setting:

            time = 100 #Ma
            gplot.time = time

        ...after which this function can be re-run. Once the `extended_continental_crusts` are reconstructed, they are
        converted into Shapely lines whose coordinates are passed to a geopandas GeoDataFrame.

        Returns
        -------
        gdf : instance of <geopandas.GeoDataFrame>
            A pandas.DataFrame that has a column with `extended_continental_crusts` geometry.
        central_meridian : float
            Central meridian around which to perform wrapping; default: 0.0.
        tessellate_degrees : float or None
            If provided, geometries will be tessellated to this resolution prior
            to wrapping.

        Raises
        ------
        ValueError
            If the optional `time` parameter has not been passed to `PlotTopologies`. This is needed to construct
            `extended_continental_crusts` to the requested `time` and thus populate the GeoDataFrame.
        """
        if self._time is None:
            raise ValueError(
                "No extended continental crust topologies have been resolved. Set `PlotTopologies.time` to construct them."
            )

        if self.extended_continental_crusts is None:
            raise ValueError(
                "No extended continental crust topologies passed to PlotTopologies."
            )

        extended_continental_crust_lines = shapelify_feature_lines(
            self.extended_continental_crusts,
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        gdf = gpd.GeoDataFrame(
            {"geometry": extended_continental_crust_lines}, geometry="geometry"
        )
        return gdf

    def plot_extended_continental_crusts(self, ax, color="black", **kwargs):
        """Plot extended continental crust lines on a standard map projection.

        Parameters
        ----------
        ax : instance of <cartopy.mpl.geoaxes.GeoAxes> or <cartopy.mpl.geoaxes.GeoAxesSubplot>
            A subclass of `matplotlib.axes.Axes` which represents a map Projection.
            The map should be set at a particular Cartopy projection.

        color : str, default=’black’
            The colour of the trench lines. By default, it is set to black.

        **kwargs :
            Keyword arguments for parameters such as `alpha`, etc.
            for plotting trench geometries.
            See `Matplotlib` keyword arguments
            [here](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.plot.html).

        Returns
        -------
        ax : instance of <geopandas.GeoDataFrame.plot>
            A standard cartopy.mpl.geoaxes.GeoAxes or cartopy.mpl.geoaxes.GeoAxesSubplot map
            with extended continental crust lines plotted onto the chosen map projection.
        """
        if "transform" in kwargs.keys():
            warnings.warn(
                "'transform' keyword argument is ignored by PlotTopologies",
                UserWarning,
            )
            kwargs.pop("transform")
        tessellate_degrees = kwargs.pop("tessellate_degrees", None)
        central_meridian = kwargs.pop("central_meridian", None)
        if central_meridian is None:
            central_meridian = _meridian_from_ax(ax)

        gdf = self.get_extended_continental_crusts(
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        if hasattr(ax, "projection"):
            gdf = _clean_polygons(data=gdf, projection=ax.projection)
        else:
            kwargs["transform"] = self.base_projection
        return gdf.plot(ax=ax, facecolor="none", edgecolor=color, **kwargs)

    def get_passive_continental_boundaries(
        self,
        central_meridian=0.0,
        tessellate_degrees=None,
    ):
        """Create a geopandas.GeoDataFrame object containing geometries of reconstructed passive continental boundary lines.

        Notes
        -----
        The `passive_continental_boundaries` needed to produce the GeoDataFrame are automatically constructed if the optional `time`
        parameter is passed to the `PlotTopologies` object before calling this function. `time` can be passed
        either when `PlotTopologies` is first called...

            gplot = gplately.PlotTopologies(..., time=100,...)

        or anytime afterwards, by setting:

            time = 100 #Ma
            gplot.time = time

        ...after which this function can be re-run. Once the `passive_continental_boundaries` are reconstructed, they are
        converted into Shapely lines whose coordinates are passed to a geopandas GeoDataFrame.

        Returns
        -------
        gdf : instance of <geopandas.GeoDataFrame>
            A pandas.DataFrame that has a column with `passive_continental_boundaries` geometry.
        central_meridian : float
            Central meridian around which to perform wrapping; default: 0.0.
        tessellate_degrees : float or None
            If provided, geometries will be tessellated to this resolution prior
            to wrapping.

        Raises
        ------
        ValueError
            If the optional `time` parameter has not been passed to `PlotTopologies`. This is needed to construct
            `passive_continental_boundaries` to the requested `time` and thus populate the GeoDataFrame.
        """
        if self._time is None:
            raise ValueError(
                "No passive continental boundaries have been resolved. Set `PlotTopologies.time` to construct them."
            )

        if self.passive_continental_boundaries is None:
            raise ValueError(
                "No passive continental boundaries passed to PlotTopologies."
            )

        passive_continental_boundary_lines = shapelify_feature_lines(
            self.passive_continental_boundaries,
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        gdf = gpd.GeoDataFrame(
            {"geometry": passive_continental_boundary_lines}, geometry="geometry"
        )
        return gdf

    def plot_passive_continental_boundaries(self, ax, color="black", **kwargs):
        """Plot passive continental boundaries on a standard map projection.

        Parameters
        ----------
        ax : instance of <cartopy.mpl.geoaxes.GeoAxes> or <cartopy.mpl.geoaxes.GeoAxesSubplot>
            A subclass of `matplotlib.axes.Axes` which represents a map Projection.
            The map should be set at a particular Cartopy projection.

        color : str, default=’black’
            The colour of the trench lines. By default, it is set to black.

        **kwargs :
            Keyword arguments for parameters such as `alpha`, etc.
            for plotting trench geometries.
            See `Matplotlib` keyword arguments
            [here](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.plot.html).

        Returns
        -------
        ax : instance of <geopandas.GeoDataFrame.plot>
            A standard cartopy.mpl.geoaxes.GeoAxes or cartopy.mpl.geoaxes.GeoAxesSubplot map
            with passive continental boundaries plotted onto the chosen map projection.
        """
        if "transform" in kwargs.keys():
            warnings.warn(
                "'transform' keyword argument is ignored by PlotTopologies",
                UserWarning,
            )
            kwargs.pop("transform")
        tessellate_degrees = kwargs.pop("tessellate_degrees", None)
        central_meridian = kwargs.pop("central_meridian", None)
        if central_meridian is None:
            central_meridian = _meridian_from_ax(ax)

        gdf = self.get_passive_continental_boundaries(
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        if hasattr(ax, "projection"):
            gdf = _clean_polygons(data=gdf, projection=ax.projection)
        else:
            kwargs["transform"] = self.base_projection
        return gdf.plot(ax=ax, facecolor="none", edgecolor=color, **kwargs)

    def get_slab_edges(self, central_meridian=0.0, tessellate_degrees=None):
        """Create a geopandas.GeoDataFrame object containing geometries of reconstructed slab edge lines.

        Notes
        -----
        The `slab_edges` needed to produce the GeoDataFrame are automatically constructed if the optional `time`
        parameter is passed to the `PlotTopologies` object before calling this function. `time` can be passed
        either when `PlotTopologies` is first called...

            gplot = gplately.PlotTopologies(..., time=100,...)

        or anytime afterwards, by setting:

            time = 100 #Ma
            gplot.time = time

        ...after which this function can be re-run. Once the `slab_edges` are reconstructed, they are
        converted into Shapely lines whose coordinates are passed to a geopandas GeoDataFrame.

        Returns
        -------
        gdf : instance of <geopandas.GeoDataFrame>
            A pandas.DataFrame that has a column with `slab_edges` geometry.
        central_meridian : float
            Central meridian around which to perform wrapping; default: 0.0.
        tessellate_degrees : float or None
            If provided, geometries will be tessellated to this resolution prior
            to wrapping.

        Raises
        ------
        ValueError
            If the optional `time` parameter has not been passed to `PlotTopologies`. This is needed to construct
            `slab_edges` to the requested `time` and thus populate the GeoDataFrame.
        """
        if self._time is None:
            raise ValueError(
                "No slab edges have been resolved. Set `PlotTopologies.time` to construct them."
            )

        if self.slab_edges is None:
            raise ValueError("No slab edges passed to PlotTopologies.")

        slab_edge_lines = shapelify_feature_lines(
            self.slab_edges,
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        gdf = gpd.GeoDataFrame({"geometry": slab_edge_lines}, geometry="geometry")
        return gdf

    def plot_slab_edges(self, ax, color="black", **kwargs):
        """Plot slab edges on a standard map projection.

        Parameters
        ----------
        ax : instance of <cartopy.mpl.geoaxes.GeoAxes> or <cartopy.mpl.geoaxes.GeoAxesSubplot>
            A subclass of `matplotlib.axes.Axes` which represents a map Projection.
            The map should be set at a particular Cartopy projection.

        color : str, default=’black’
            The colour of the trench lines. By default, it is set to black.

        **kwargs :
            Keyword arguments for parameters such as `alpha`, etc.
            for plotting trench geometries.
            See `Matplotlib` keyword arguments
            [here](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.plot.html).

        Returns
        -------
        ax : instance of <geopandas.GeoDataFrame.plot>
            A standard cartopy.mpl.geoaxes.GeoAxes or cartopy.mpl.geoaxes.GeoAxesSubplot map
            with slab edges plotted onto the chosen map projection.
        """
        if "transform" in kwargs.keys():
            warnings.warn(
                "'transform' keyword argument is ignored by PlotTopologies",
                UserWarning,
            )
            kwargs.pop("transform")
        tessellate_degrees = kwargs.pop("tessellate_degrees", None)
        central_meridian = kwargs.pop("central_meridian", None)
        if central_meridian is None:
            central_meridian = _meridian_from_ax(ax)

        gdf = self.get_slab_edges(
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        if hasattr(ax, "projection"):
            gdf = _clean_polygons(data=gdf, projection=ax.projection)
        else:
            kwargs["transform"] = self.base_projection
        return gdf.plot(ax=ax, facecolor="none", edgecolor=color, **kwargs)

    def get_misc_transforms(
        self,
        central_meridian=0.0,
        tessellate_degrees=None,
    ):
        """Create a geopandas.GeoDataFrame object containing geometries of reconstructed misc transform lines.

        Notes
        -----
        The `misc_transforms` needed to produce the GeoDataFrame are automatically constructed if the optional `time`
        parameter is passed to the `PlotTopologies` object before calling this function. `time` can be passed
        either when `PlotTopologies` is first called...

            gplot = gplately.PlotTopologies(..., time=100,...)

        or anytime afterwards, by setting:

            time = 100 #Ma
            gplot.time = time

        ...after which this function can be re-run. Once the `misc_transforms` are reconstructed, they are
        converted into Shapely lines whose coordinates are passed to a geopandas GeoDataFrame.

        Returns
        -------
        gdf : instance of <geopandas.GeoDataFrame>
            A pandas.DataFrame that has a column with `misc_transforms` geometry.
        central_meridian : float
            Central meridian around which to perform wrapping; default: 0.0.
        tessellate_degrees : float or None
            If provided, geometries will be tessellated to this resolution prior
            to wrapping.

        Raises
        ------
        ValueError
            If the optional `time` parameter has not been passed to `PlotTopologies`. This is needed to construct
            `misc_transforms` to the requested `time` and thus populate the GeoDataFrame.
        """
        if self._time is None:
            raise ValueError(
                "No miscellaneous transforms have been resolved. Set `PlotTopologies.time` to construct them."
            )

        if self.misc_transforms is None:
            raise ValueError("No miscellaneous transforms passed to PlotTopologies.")

        misc_transform_lines = shapelify_feature_lines(
            self.misc_transforms,
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        gdf = gpd.GeoDataFrame({"geometry": misc_transform_lines}, geometry="geometry")
        return gdf

    def plot_misc_transforms(self, ax, color="black", **kwargs):
        """Plot miscellaneous transform boundaries on a standard map projection.

        Parameters
        ----------
        ax : instance of <cartopy.mpl.geoaxes.GeoAxes> or <cartopy.mpl.geoaxes.GeoAxesSubplot>
            A subclass of `matplotlib.axes.Axes` which represents a map Projection.
            The map should be set at a particular Cartopy projection.

        color : str, default=’black’
            The colour of the trench lines. By default, it is set to black.

        **kwargs :
            Keyword arguments for parameters such as `alpha`, etc.
            for plotting trench geometries.
            See `Matplotlib` keyword arguments
            [here](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.plot.html).

        Returns
        -------
        ax : instance of <geopandas.GeoDataFrame.plot>
            A standard cartopy.mpl.geoaxes.GeoAxes or cartopy.mpl.geoaxes.GeoAxesSubplot map
            with miscellaneous transform boundaries plotted onto the chosen map projection.
        """
        if "transform" in kwargs.keys():
            warnings.warn(
                "'transform' keyword argument is ignored by PlotTopologies",
                UserWarning,
            )
            kwargs.pop("transform")
        tessellate_degrees = kwargs.pop("tessellate_degrees", None)
        central_meridian = kwargs.pop("central_meridian", None)
        if central_meridian is None:
            central_meridian = _meridian_from_ax(ax)

        gdf = self.get_misc_transforms(
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        if hasattr(ax, "projection"):
            gdf = _clean_polygons(data=gdf, projection=ax.projection)
        else:
            kwargs["transform"] = self.base_projection
        return gdf.plot(ax=ax, facecolor="none", edgecolor=color, **kwargs)

    def get_unclassified_features(
        self,
        central_meridian=0.0,
        tessellate_degrees=None,
    ):
        """Create a geopandas.GeoDataFrame object containing geometries of reconstructed unclassified feature lines.

        Notes
        -----
        The `unclassified_features` needed to produce the GeoDataFrame are automatically constructed if the optional `time`
        parameter is passed to the `PlotTopologies` object before calling this function. `time` can be passed
        either when `PlotTopologies` is first called...

            gplot = gplately.PlotTopologies(..., time=100,...)

        or anytime afterwards, by setting:

            time = 100 #Ma
            gplot.time = time

        ...after which this function can be re-run. Once the `unclassified_features` are reconstructed, they are
        converted into Shapely lines whose coordinates are passed to a geopandas GeoDataFrame.

        Returns
        -------
        gdf : instance of <geopandas.GeoDataFrame>
            A pandas.DataFrame that has a column with `unclassified_features` geometry.
        central_meridian : float
            Central meridian around which to perform wrapping; default: 0.0.
        tessellate_degrees : float or None
            If provided, geometries will be tessellated to this resolution prior
            to wrapping.

        Raises
        ------
        ValueError
            If the optional `time` parameter has not been passed to `PlotTopologies`. This is needed to construct
            `unclassified_features` to the requested `time` and thus populate the GeoDataFrame.
        """
        if self._time is None:
            raise ValueError(
                "No unclassified features have been resolved. Set `PlotTopologies.time` to construct them."
            )

        if self.unclassified_features is None:
            raise ValueError("No unclassified features passed to PlotTopologies.")

        unclassified_feature_lines = shapelify_feature_lines(
            self.unclassified_features,
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        gdf = gpd.GeoDataFrame(
            {"geometry": unclassified_feature_lines}, geometry="geometry"
        )
        return gdf

    def plot_unclassified_features(self, ax, color="black", **kwargs):
        """Plot GPML unclassified features on a standard map projection.

        Parameters
        ----------
        ax : instance of <cartopy.mpl.geoaxes.GeoAxes> or <cartopy.mpl.geoaxes.GeoAxesSubplot>
            A subclass of `matplotlib.axes.Axes` which represents a map Projection.
            The map should be set at a particular Cartopy projection.

        color : str, default=’black’
            The colour of the trench lines. By default, it is set to black.

        **kwargs :
            Keyword arguments for parameters such as `alpha`, etc.
            for plotting trench geometries.
            See `Matplotlib` keyword arguments
            [here](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.plot.html).

        Returns
        -------
        ax : instance of <geopandas.GeoDataFrame.plot>
            A standard cartopy.mpl.geoaxes.GeoAxes or cartopy.mpl.geoaxes.GeoAxesSubplot map
            with unclassified features plotted onto the chosen map projection.
        """
        if "transform" in kwargs.keys():
            warnings.warn(
                "'transform' keyword argument is ignored by PlotTopologies",
                UserWarning,
            )
            kwargs.pop("transform")
        tessellate_degrees = kwargs.pop("tessellate_degrees", None)
        central_meridian = kwargs.pop("central_meridian", None)
        if central_meridian is None:
            central_meridian = _meridian_from_ax(ax)

        gdf = self.get_unclassified_features(
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        if hasattr(ax, "projection"):
            gdf = _clean_polygons(data=gdf, projection=ax.projection)
        else:
            kwargs["transform"] = self.base_projection
        return gdf.plot(ax=ax, facecolor="none", edgecolor=color, **kwargs)

    def get_all_topologies(
        self,
        central_meridian=0.0,
        tessellate_degrees=None,
    ):
        """Create a geopandas.GeoDataFrame object containing geometries of reconstructed unclassified feature lines.

        Notes
        -----
        The `topologies` needed to produce the GeoDataFrame are automatically constructed if the optional `time`
        parameter is passed to the `PlotTopologies` object before calling this function. `time` can be passed
        either when `PlotTopologies` is first called...

            gplot = gplately.PlotTopologies(..., time=100,...)

        or anytime afterwards, by setting:

            time = 100 #Ma
            gplot.time = time

        ...after which this function can be re-run. Once the `topologies` are reconstructed, they are
        converted into Shapely lines whose coordinates are passed to a geopandas GeoDataFrame.

        Returns
        -------
        gdf : instance of <geopandas.GeoDataFrame>
            A pandas.DataFrame that has a column with `topologies` geometry.
        central_meridian : float
            Central meridian around which to perform wrapping; default: 0.0.
        tessellate_degrees : float or None
            If provided, geometries will be tessellated to this resolution prior
            to wrapping.

        Raises
        ------
        ValueError
            If the optional `time` parameter has not been passed to `PlotTopologies`. This is needed to construct
            `topologies` to the requested `time` and thus populate the GeoDataFrame.
        """
        if self._time is None:
            raise ValueError(
                "No topologies have been resolved. Set `PlotTopologies.time` to construct them."
            )

        if self.topologies is None:
            raise ValueError("No topologies passed to PlotTopologies.")

        all_topologies = shapelify_features(
            self.topologies,
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )

        # get plate IDs and feature types to add to geodataframe
        plate_IDs = []
        feature_types = []
        feature_names = []
        for topo in self.topologies:
            ft_type = topo.get_feature_type()

            plate_IDs.append(topo.get_reconstruction_plate_id())
            feature_types.append(ft_type)
            feature_names.append(ft_type.get_name())

        gdf = gpd.GeoDataFrame(
            {
                "geometry": all_topologies,
                "reconstruction_plate_ID": plate_IDs,
                "feature_type": feature_types,
                "feature_name": feature_names,
            },
            geometry="geometry",
        )
        return gdf

    def plot_all_topologies(self, ax, color="black", **kwargs):
        """Plot all topologies on a standard map projection.

        Parameters
        ----------
        ax : instance of <cartopy.mpl.geoaxes.GeoAxes> or <cartopy.mpl.geoaxes.GeoAxesSubplot>
            A subclass of `matplotlib.axes.Axes` which represents a map Projection.
            The map should be set at a particular Cartopy projection.

        color : str, default=’black’
            The colour of the trench lines. By default, it is set to black.

        **kwargs :
            Keyword arguments for parameters such as `alpha`, etc.
            for plotting trench geometries.
            See `Matplotlib` keyword arguments
            [here](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.plot.html).

        Returns
        -------
        ax : instance of <geopandas.GeoDataFrame.plot>
            A standard cartopy.mpl.geoaxes.GeoAxes or cartopy.mpl.geoaxes.GeoAxesSubplot map
            with unclassified features plotted onto the chosen map projection.
        """
        if "transform" in kwargs.keys():
            warnings.warn(
                "'transform' keyword argument is ignored by PlotTopologies",
                UserWarning,
            )
            kwargs.pop("transform")
        tessellate_degrees = kwargs.pop("tessellate_degrees", None)
        central_meridian = kwargs.pop("central_meridian", None)
        if central_meridian is None:
            central_meridian = _meridian_from_ax(ax)

        gdf = self.get_all_topologies(
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        if hasattr(ax, "projection"):
            gdf = _clean_polygons(data=gdf, projection=ax.projection)
        else:
            kwargs["transform"] = self.base_projection
        return gdf.plot(ax=ax, facecolor="none", edgecolor=color, **kwargs)

    def get_all_topological_sections(
        self,
        central_meridian=0.0,
        tessellate_degrees=None,
    ):
        """Create a geopandas.GeoDataFrame object containing geometries of
        resolved topological sections.

        Parameters
        ----------
        central_meridian : float
            Central meridian around which to perform wrapping; default: 0.0.
        tessellate_degrees : float or None
            If provided, geometries will be tessellated to this resolution prior
            to wrapping.

        Returns
        -------
        geopandas.GeoDataFrame
            A pandas.DataFrame that has a column with `topologies` geometry.

        Raises
        ------
        ValueError
            If the optional `time` parameter has not been passed to
            `PlotTopologies`. This is needed to construct `topologies`
            to the requested `time` and thus populate the GeoDataFrame.

        Notes
        -----
        The `topologies` needed to produce the GeoDataFrame are automatically
        constructed if the optional `time` parameter is passed to the
        `PlotTopologies` object before calling this function. `time` can be
        passed either when `PlotTopologies` is first called...

            gplot = gplately.PlotTopologies(..., time=100,...)

        or anytime afterwards, by setting:

            time = 100 # Ma
            gplot.time = time

        ...after which this function can be re-run. Once the `topologies`
        are reconstructed, they are converted into Shapely lines whose
        coordinates are passed to a geopandas GeoDataFrame.
        """
        if self._time is None:
            raise ValueError(
                "No topologies have been resolved. Set `PlotTopologies.time` to construct them."
            )

        if self.topologies is None:
            raise ValueError("No topologies passed to PlotTopologies.")

        topologies_list = [
            *self.ridge_transforms,
            *self.ridges,
            *self.transforms,
            *self.trenches,
            *self.trench_left,
            *self.trench_right,
            *self.other,
        ]

        # get plate IDs and feature types to add to geodataframe
        geometries = []
        plate_IDs = []
        feature_types = []
        feature_names = []
        for topo in topologies_list:
            converted = shapelify_features(
                topo,
                central_meridian=central_meridian,
                tessellate_degrees=tessellate_degrees,
            )
            if not isinstance(converted, BaseGeometry):
                if len(converted) > 1:
                    tmp = []
                    for i in converted:
                        if isinstance(i, BaseMultipartGeometry):
                            tmp.extend(list(i.geoms))
                        else:
                            tmp.append(i)
                    converted = tmp
                    del tmp
                    converted = linemerge(converted)
                elif len(converted) == 1:
                    converted = converted[0]
                else:
                    continue
            geometries.append(converted)
            plate_IDs.append(topo.get_reconstruction_plate_id())
            feature_types.append(topo.get_feature_type())
            feature_names.append(topo.get_name())

        gdf = gpd.GeoDataFrame(
            {
                "geometry": geometries,
                "reconstruction_plate_ID": plate_IDs,
                "feature_type": feature_types,
                "feature_name": feature_names,
            },
            geometry="geometry",
        )
        return gdf

    def plot_all_topological_sections(self, ax, color="black", **kwargs):
        """Plot all topologies on a standard map projection.

        Parameters
        ----------
        ax : cartopy.mpl.geoaxes.GeoAxes or cartopy.mpl.geoaxes.GeoAxesSubplot
            A subclass of `matplotlib.axes.Axes` which represents a map Projection.
            The map should be set at a particular Cartopy projection.

        color : str, default='black'
            The colour of the topology lines. By default, it is set to black.

        **kwargs
            Keyword arguments for parameters such as `alpha`, etc.
            for plotting trench geometries.
            See `Matplotlib` keyword arguments
            [here](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.plot.html).

        Returns
        -------
        ax : instance of <geopandas.GeoDataFrame.plot>
            A standard cartopy.mpl.geoaxes.GeoAxes or cartopy.mpl.geoaxes.GeoAxesSubplot map
            with unclassified features plotted onto the chosen map projection.
        """
        if "transform" in kwargs.keys():
            warnings.warn(
                "'transform' keyword argument is ignored by PlotTopologies",
                UserWarning,
            )
            kwargs.pop("transform")
        tessellate_degrees = kwargs.pop("tessellate_degrees", None)
        central_meridian = kwargs.pop("central_meridian", None)
        if central_meridian is None:
            central_meridian = _meridian_from_ax(ax)

        gdf = self.get_all_topological_sections(
            central_meridian=central_meridian,
            tessellate_degrees=tessellate_degrees,
        )
        if hasattr(ax, "projection"):
            gdf = _clean_polygons(data=gdf, projection=ax.projection)
        else:
            kwargs["transform"] = self.base_projection
        return gdf.plot(ax=ax, color=color, **kwargs)
