text: import logging
  import sys
  import warnings
  from datetime import datetime, timedelta, timezone
  from functools import lru_cache
  from operator import attrgetter
  from pathlib import Path
  from typing import (
      TYPE_CHECKING,
      Any,
      Callable,
      Dict,
      Iterable,
      Iterator,
      List,
      NoReturn,
      Optional,
      Set,
      Tuple,
      Type,
      TypeVar,
      Union,
      cast,
      overload,
  )
  
  if sys.version_info >= (3, 8):
      from typing import Literal
  else:
      from typing_extensions import Literal
  
  import numpy as np
  import numpy.typing as npt
  import pandas as pd
  import pyproj
  from pandas.core.internals import DatetimeTZBlock
  from shapely.geometry import LineString, MultiPoint, Point, Polygon, base
  from shapely.ops import transform
  
  from ..algorithms.douglas_peucker import douglas_peucker
  from ..algorithms.navigation import NavigationFeatures
  from ..algorithms.phases import FuzzyLogic
  from ..core.types import ProgressbarType
  from . import geodesy as geo
  from .iterator import FlightIterator, flight_iterator
  from .mixins import GeographyMixin, HBoxMixin, PointMixin, ShapelyMixin
  from .time import deltalike, time_or_delta, timelike, to_datetime, to_timedelta
  
  if TYPE_CHECKING:
      import altair as alt  # noqa: F401
      from cartopy.mpl.geoaxes import GeoAxesSubplot  # noqa: F401
      from matplotlib.artist import Artist  # noqa: F401
      from matplotlib.axes._subplots import Axes  # noqa: F401
  
      from ..data.adsb.raw_data import RawData  # noqa: F401
      from .airspace import Airspace  # noqa: F401
      from .lazy import LazyTraffic  # noqa: F401
      from .traffic import Traffic  # noqa: F401
  
  
  T = TypeVar("T", bound="Flight")
  
  if str(pd.__version__) < "1.3":
  
      def _tz_interpolate(
          data: DatetimeTZBlock, *args: Any, **kwargs: Any
      ) -> DatetimeTZBlock:
          return data.astype(int).interpolate(*args, **kwargs).astype(data.dtype)
  
      DatetimeTZBlock.interpolate = _tz_interpolate
  
  else:
      # - with version 1.3.0, interpolate returns a list
      # - Windows require "int64" as "int" may be interpreted as "int32" and raise
      #   an error (was not raised before 1.3.0)
  
      def _tz_interpolate(
          data: DatetimeTZBlock, *args: Any, **kwargs: Any
      ) -> DatetimeTZBlock:
          coerced = data.coerce_to_target_dtype("int64")
          interpolated, *_ = coerced.interpolate(*args, **kwargs)
          return interpolated
  
      DatetimeTZBlock.interpolate = _tz_interpolate
  
  
  def _split(
      data: pd.DataFrame, value: Union[str, int], unit: Optional[str]
  ) -> Iterator[pd.DataFrame]:
      # This method helps splitting a flight into several.
      if data.shape[0] < 2:
          return
      diff = data.timestamp.diff().values
      if unit is None:
          delta = pd.Timedelta(value).to_timedelta64()
      else:
          delta = np.timedelta64(value, unit)
      # There seems to be a change with numpy >= 1.18
      # max() now may return NaN, therefore the following fix
      max_ = np.nanmax(diff)  # type: ignore
      if max_ > delta:
          # np.nanargmax seems bugged with timestamps
          argmax = np.where(diff == max_)[0][0]
          yield from _split(data.iloc[:argmax], value, unit)
          yield from _split(data.iloc[argmax:], value, unit)  # noqa
      else:
          yield data
  
  
  # flake B008
  attrgetter_duration = attrgetter("duration")
  
  # flake B006
  default_angle_features = ["track", "heading"]
  
  
  class Position(PointMixin, pd.core.series.Series):  # type: ignore
      def plot(
          self,
          ax: "Axes",
          text_kw: Optional[Dict[str, Any]] = None,
          shift: Optional[Dict[str, Any]] = None,
          **kwargs: Any,
      ) -> List["Artist"]:  # coverage: ignore
  
          from ..drawing.markers import aircraft as aircraft_marker
          from ..drawing.markers import rotate_marker
  
          visualdict = dict(s=300)
          if hasattr(self, "track"):
              visualdict["marker"] = rotate_marker(aircraft_marker, self.track)
  
          if text_kw is None:
              text_kw = dict()
          else:
              # since we may modify it, let's make a copy
              text_kw = {**text_kw}
  
          if "s" not in text_kw and hasattr(self, "callsign"):
              text_kw["s"] = self.callsign
  
          return super().plot(ax, text_kw, shift, **{**visualdict, **kwargs})
  
  
  class MetaFlight(type):
      def __getattr__(cls, name: str) -> Callable[..., Any]:
          if name.startswith("aligned_on_"):
              return lambda flight: cls.aligned_on_ils(flight, name[11:])
          if name.startswith("takeoff_runway_"):
              return lambda flight: cls.takeoff_from_runway(flight, name[15:])
          if name.startswith("on_parking_"):
              return lambda flight: cls.on_parking_position(flight, name[11:])
          if name.startswith("pushback_"):
              return lambda flight: cls.pushback(flight, name[9:])
          if name.startswith("landing_at_"):
              return lambda flight: cls.landing_at(flight, name[11:])
          if name.startswith("takeoff_from_"):
              return lambda flight: cls.takeoff_from(flight, name[13:])
          raise AttributeError
  
  
  class Flight(
      HBoxMixin,
      GeographyMixin,
      ShapelyMixin,
      NavigationFeatures,
      FuzzyLogic,
      metaclass=MetaFlight,
  ):
      """Flight is the most basic class associated to a trajectory.
      Flights are the building block of all processing methods, built on top of
      pandas DataFrame. The minimum set of required features are:
  
      - ``icao24``: the ICAO transponder ID of an aircraft;
      - ``callsign``: an identifier which may be associated with the
        registration of an aircraft, with its mission (VOR calibration,
        firefighting) or with a route (for a commercial aircraft);
      - ``timestamp``: timezone aware timestamps are preferable.
        Some methods may work with timezone naive timestamps but the behaviour
        is not guaranteed;
      - ``latitude``, ``longitude``: in degrees, WGS84 (EPSG:4326);
      - ``altitude``: in feet.
  
      .. note::
          The ``flight_id`` (identifier for a trajectory) may be used in place of
          a pair of (``icao24``, ``callsign``). More features may also be provided
          for further processing, e.g. ``groundspeed``, ``vertical_rate``,
          ``track``, ``heading``, ``IAS`` (indicated airspeed) or ``squawk``.
  
      .. note::
  
          All navigation related methods are described more in depth on a
          `dedicated page <navigation.html>`_.
  
      **Abridged contents:**
  
          - properties:
            `callsign <#traffic.core.Flight.callsign>`_,
            `flight_id <#traffic.core.Flight.flight_id>`_,
            `icao24 <#traffic.core.Flight.icao24>`_,
            `number <#traffic.core.Flight.number>`_,
            `registration <#traffic.core.Flight.registration>`_,
            `start <#traffic.core.Flight.start>`_,
            `stop <#traffic.core.Flight.stop>`_,
            `typecode <#traffic.core.Flight.typecode>`_
          - time related methods:
            `after() <#traffic.core.Flight.after>`_,
            `at() <#traffic.core.Flight.at>`_,
            `at_ratio() <#traffic.core.Flight.at_ratio>`_,
            `before() <#traffic.core.Flight.before>`_,
            `between() <#traffic.core.Flight.between>`_,
            `first() <#traffic.core.Flight.first>`_,
            `last() <#traffic.core.Flight.last>`_,
            `skip() <#traffic.core.Flight.skip>`_,
            `shorten() <#traffic.core.Flight.shorten>`_
          - geometry related methods:
            `airborne() <#traffic.core.Flight.airborne>`_,
            `clip() <#traffic.core.Flight.clip>`_,
            `compute_wind() <#traffic.core.Flight.compute_wind>`_,
            `compute_xy() <#traffic.core.Flight.compute_xy>`_,
            `distance() <#traffic.core.Flight.distance>`_,
            `inside_bbox() <#traffic.core.Flight.inside_bbox>`_,
            `intersects() <#traffic.core.Flight.intersects>`_,
            `project_shape() <#traffic.core.Flight.project_shape>`_,
            `simplify() <#traffic.core.Flight.simplify>`_,
            `unwrap() <#traffic.core.Flight.unwrap>`_
          - filtering and resampling methods:
            `comet() <#traffic.core.Flight.comet>`_,
            `filter() <#traffic.core.Flight.filter>`_,
            `resample() <#traffic.core.Flight.resample>`_,
          - visualisation with altair:
            `chart() <#traffic.core.Flight.chart>`_,
            `geoencode() <#traffic.core.Flight.geoencode>`_
          - visualisation with leaflet: `map_leaflet() <leaflet.html>`_
          - visualisation with Matplotlib:
            `plot() <#traffic.core.Flight.plot>`_,
            `plot_time() <#traffic.core.Flight.plot_time>`_
  
      .. tip::
          Sample flights are provided for testing purposes in module
          ``traffic.data.samples``
  
      """
  
      __slots__ = ("data",)
  
      # --- Special methods ---
  
      def __add__(
          self, other: Union[Literal[0], "Flight", "Traffic"]
      ) -> "Traffic":
          """
          As Traffic is thought as a collection of Flights, the sum of two Flight
          objects returns a Traffic object
          """
          # keep import here to avoid recursion
          from .traffic import Traffic  # noqa: F811
  
          if other == 0:
              # useful for compatibility with sum() function
              return Traffic(self.data)
  
          # This just cannot return None in this case.
          return Traffic.from_flights([self, other])  # type: ignore
  
      def __radd__(
          self, other: Union[Literal[0], "Flight", "Traffic"]
      ) -> "Traffic":
          """
          As Traffic is thought as a collection of Flights, the sum of two Flight
          objects returns a Traffic object
          """
          return self + other
  
      def __len__(self) -> int:
          """Number of samples associated to a trajectory.
  
          The basic behaviour is to return the number of lines in the underlying
          DataFrame. However in some cases, as positions may be wrongly repeated
          in some database systems (e.g. OpenSky Impala shell), we take the
          `last_position` field into account for counting the number of unique
          detected positions.
  
          Note that when an aircraft is onground, `last_position` is a more
          relevant criterion than (`latitude`, `longitude`) since a grounded
          aircraft may be repeatedly emitting the same position.
          """
  
          if "last_position" in self.data.columns:
              data = self.data.drop_duplicates("last_position")
              return data.shape[0]  # type: ignore
          else:
              return self.data.shape[0]  # type: ignore
  
      def _info_html(self) -> str:
          title = f"<b>Flight {self.title}</b>"
          title += "<ul>"
          title += f"<li><b>aircraft:</b> {self.aircraft}</li>"
          if self.origin is not None:
              title += f"<li><b>from:</b> {self.origin} ({self.start})</li>"
          else:
              title += f"<li><b>from:</b> {self.start}</li>"
          if self.destination is not None:
              title += f"<li><b>to:</b> {self.destination} ({self.stop})</li>"
          else:
              title += f"<li><b>to:</b> {self.stop}</li>"
          if self.diverted is not None:
              title += f"<li><b>diverted to: {self.diverted}</b></li>"
          title += "</ul>"
          return title
  
      def _repr_html_(self) -> str:
          title = self._info_html()
          no_wrap_div = '<div style="white-space: nowrap">{}</div>'
          return title + no_wrap_div.format(self._repr_svg_())
  
      @lru_cache()
      def _repr_svg_(self) -> Optional[str]:
          # even 25m should be enough to limit the size of resulting notebooks!
          if self.shape is None:
              return None
  
          if len(self.shape.coords) < 1000:
              return super()._repr_svg_()
  
          return super(
              Flight,
              # cast should be useless but return type of simplify() is Union
              cast(Flight, self.resample("1s").simplify(25)),
          )._repr_svg_()
  
      def __repr__(self) -> str:
          output = f"Flight {self.title}"
          output += f"\naircraft: {self.aircraft}"
          if self.origin is not None:
              output += f"\norigin: {self.origin} ({self.start})"
          else:
              output += f"\norigin: {self.start}"
          if self.destination is not None:
              output += f"\ndestination: {self.destination} ({self.stop})"
          else:
              output += f"\ndestination: {self.stop}"
          return output
  
      @property
      def __geo_interface__(self) -> Dict[str, Any]:
          if self.shape is None:
              # Returns an empty geometry
              return {"type": "GeometryCollection", "geometries": []}
          return self.shape.__geo_interface__  # type: ignore
  
      def __getattr__(self, name: str) -> Any:
          """Helper to facilitate method chaining without lambda.
  
          Example usage:
  
          flight.altitude_max
              => flight.max('altitude')
          flight.vertical_rate_std
              => flight.std('vertical_rate')
  
          Flight.feature_gt("altitude_max", 10000)
              => lambda f: f.max('altitude') > 10000
          """
          msg = f"'{self.__class__.__name__}' has no attribute '{name}'"
          if "_" not in name:
              raise AttributeError(msg)
          *name_split, agg = name.split("_")
          feature = "_".join(name_split)
          if feature not in self.data.columns:
              raise AttributeError(msg)
          return getattr(self.data[feature], agg)()
  
      def filter_if(self, test: Callable[["Flight"], bool]) -> Optional["Flight"]:
          # TODO deprecate if pipe() does a good job?
          return self if test(self) else None
  
      def has(
          self, method: Union[str, Callable[["Flight"], Iterator["Flight"]]]
      ) -> bool:
          """Returns True if flight.method() returns a non-empty iterator.
  
          Example usage:
  
          >>> flight.has("go_around")
          >>> flight.has("runway_change")
          >>> flight.has(lambda f: f.aligned_on_ils("LFBO"))
          """
          return self.next(method) is not None  # noqa: B305
  
      def sum(
          self, method: Union[str, Callable[["Flight"], Iterator["Flight"]]]
      ) -> int:
          """Returns the number of segments returns by flight.method().
  
          Example usage:
  
          >>> flight.sum("go_around")
          >>> flight.sum("runway_change")
          >>> flight.sum(lambda f: f.aligned_on_ils("LFBO"))
          """
          fun = (
              getattr(self.__class__, method)
              if isinstance(method, str)
              else method
          )
          return sum(1 for _ in fun(self))
  
      def all(
          self, method: Union[str, Callable[["Flight"], Iterator["Flight"]]]
      ) -> Optional["Flight"]:
          """Returns the concatenation of segments returns by flight.method().
  
          Example usage:
  
          >>> flight.all("go_around")
          >>> flight.all("runway_change")
          >>> flight.all(lambda f: f.aligned_on_ils("LFBO"))
          """
          fun = (
              getattr(self.__class__, method)
              if isinstance(method, str)
              else method
          )
          t = sum(flight.assign(index_=i) for i, flight in enumerate(fun(self)))
          if t == 0:
              return None
          return Flight(t.data)  # type: ignore
  
      def next(
          self,
          method: Union[str, Callable[["Flight"], Iterator["Flight"]]],
      ) -> Optional["Flight"]:
          """
          Returns the first segment of trajectory yielded by flight.method()
  
          >>> flight.next("go_around")
          >>> flight.next("runway_change")
          >>> flight.next(lambda f: f.aligned_on_ils("LFBO"))
          """
          fun = (
              getattr(self.__class__, method)
              if isinstance(method, str)
              else method
          )
          return next(fun(self), None)
  
      def final(
          self,
          method: Union[str, Callable[["Flight"], Iterator["Flight"]]],
      ) -> Optional["Flight"]:
          """
          Returns the final (last) segment of trajectory yielded by
          flight.method()
  
          >>> flight.final("go_around")
          >>> flight.final("runway_change")
          >>> flight.final(lambda f: f.aligned_on_ils("LFBO"))
          """
          fun = (
              getattr(self.__class__, method)
              if isinstance(method, str)
              else method
          )
          segment = None
          for segment in fun(self):  # noqa: B007
              continue
          return segment  # type: ignore
  
      # --- Iterators ---
  
      @property
      def timestamp(self) -> Iterator[pd.Timestamp]:
          yield from self.data.timestamp
  
      @property
      def coords(self) -> Iterator[Tuple[float, float, float]]:
          data = self.data.query("longitude == longitude")
          if "altitude" not in data.columns:
              data = data.assign(altitude=0)
          yield from zip(data["longitude"], data["latitude"], data["altitude"])
  
      def coords4d(
          self, delta_t: bool = False
      ) -> Iterator[Tuple[float, float, float, float]]:
          data = self.data.query("longitude == longitude")
          if delta_t:
              time = (data.timestamp - data.timestamp.min()).dt.total_seconds()
          else:
              time = data["timestamp"]
  
          yield from zip(
              time, data["longitude"], data["latitude"], data["altitude"]
          )
  
      @property
      def xy_time(self) -> Iterator[Tuple[float, float, float]]:
          self_filtered = self.query("longitude == longitude")
          if self_filtered is None:
              return None
          iterator = iter(zip(self_filtered.coords, self_filtered.timestamp))
          while True:
              next_ = next(iterator, None)
              if next_ is None:
                  return
              coords, time = next_
              yield (coords[0], coords[1], time.to_pydatetime().timestamp())
  
      # --- Properties (and alike) ---
  
      def min(self, feature: str) -> Any:
          """Returns the minimum value of given feature.
  
          >>> flight.min('altitude')  # dummy example
          24000
          """
          return self.data[feature].min()
  
      def max(self, feature: str) -> Any:
          """Returns the maximum value of given feature.
  
          >>> flight.max('altitude')  # dummy example
          35000
          """
          return self.data[feature].max()
  
      def mean(self, feature: str) -> Any:
          """Returns the average value of given feature.
  
          >>> flight.mean('vertical_rate')  # dummy example
          -1000
          """
          return self.data[feature].mean()
  
      def feature_gt(
          self,
          feature: Union[str, Callable[["Flight"], Any]],
          value: Any,
          strict: bool = True,
      ) -> bool:
          """Returns True if feature(flight) is greater than value.
  
          This is fully equivalent to `f.longer_than("1 minute")`:
  
          >>> f.feature_gt("duration", pd.Timedelta('1 minute'))
          True
  
          This is equivalent to `f.max('altitude') > 35000`:
  
          >>> f.feature_gt(lambda f: f.max("altitude"), 35000)
          True
  
          The second one can be useful for stacking operations during
          lazy evaluation.
          """
          if isinstance(feature, str):
              feature = attrgetter(feature)
          attribute = feature(self)
          if strict:
              return attribute > value  # type: ignore
          return attribute >= value  # type: ignore
  
      def feature_lt(
          self,
          feature: Union[str, Callable[["Flight"], Any]],
          value: Any,
          strict: bool = True,
      ) -> bool:
          """Returns True if feature(flight) is less than value.
  
          This is fully equivalent to `f.shorter_than("1 minute")`:
  
          >>> f.feature_lt("duration", pd.Timedelta('1 minute'))
          True
  
          This is equivalent to `f.max('altitude') < 35000`:
  
          >>> f.feature_lt(lambda f: f.max("altitude"), 35000)
          True
  
          The second one can be useful for stacking operations during
          lazy evaluation.
          """
          if isinstance(feature, str):
              feature = attrgetter(feature)
          attribute = feature(self)
          if strict:
              return attribute < value  # type: ignore
          return attribute <= value  # type: ignore
  
      def shorter_than(
          self, value: Union[str, timedelta, pd.Timedelta], strict: bool = True
      ) -> bool:
          """Returns True if flight duration is shorter than value."""
          if isinstance(value, str):
              value = pd.Timedelta(value)
          return self.feature_lt(attrgetter("duration"), value, strict)
  
      def longer_than(
          self, value: Union[str, timedelta, pd.Timedelta], strict: bool = True
      ) -> bool:
          """Returns True if flight duration is longer than value."""
          if isinstance(value, str):
              value = pd.Timedelta(value)
          return self.feature_gt(attrgetter("duration"), value, strict)
  
      def abs(self, features: Union[str, List[str]], **kwargs: Any) -> "Flight":
          """Assign absolute versions of features to new columns.
  
          >>> flight.abs("track")
  
          The two following commands are equivalent:
  
          >>> flight.abs(["track", "heading"])
          >>> flight.abs(track="track_abs", heading="heading_abs")
  
          """
          assign_dict = dict()
          if isinstance(features, str):
              features = [features]
          if isinstance(features, Iterable):
              for feature in features:
                  assign_dict[feature + "_abs"] = self.data[feature].abs()
          for key, value in kwargs.items():
              assign_dict[value] = self.data[key].abs()
          return self.assign(**assign_dict)
  
      def diff(self, features: Union[str, List[str]], **kwargs: Any) -> "Flight":
          """Assign differential versions of features to new columns.
  
          >>> flight.diff("track")
  
          The two following commands are equivalent:
  
          >>> flight.diff(["track", "heading"])
          >>> flight.diff(track="track_diff", heading="heading_diff")
  
          """
          assign_dict = dict()
          if isinstance(features, str):
              features = [features]
          if isinstance(features, Iterable):
              for feature in features:
                  assign_dict[feature + "_diff"] = self.data[feature].diff()
          for key, value in kwargs.items():
              assign_dict[value] = self.data[key].diff()
          return self.assign(**assign_dict)
  
      @property
      def start(self) -> pd.Timestamp:
          """Returns the minimum value of timestamp."""
          return self.min("timestamp")
  
      @property
      def stop(self) -> pd.Timestamp:
          """Returns the maximum value of timestamp."""
          return self.max("timestamp")
  
      @property
      def duration(self) -> pd.Timedelta:
          """Returns the duration of the flight."""
          return self.stop - self.start
  
      def _get_unique(
          self, field: str, warn: bool = True
      ) -> Union[str, Set[str], None]:
          if field not in self.data.columns:
              return None
          tmp = self.data[field].unique()
          if len(tmp) == 1:
              return tmp[0]  # type: ignore
          if warn:
              logging.warning(
                  f"Several {field}s for one flight, consider splitting"
              )
          return set(tmp)
  
      @property
      def callsign(self) -> Union[str, Set[str], None]:
          """Returns the unique callsign value(s) associated to the Flight.
  
          A callsign is an identifier sent by an aircraft during its flight. It
          may be associated with the registration of an aircraft, its mission or
          with a route for a commercial aircraft.
          """
          callsign = self._get_unique("callsign")
          if callsign != callsign:
              raise ValueError("NaN appearing in callsign field")
          return callsign
  
      @property
      def number(self) -> Union[str, Set[str], None]:
          """Returns the unique number value(s) associated to the Flight.
  
          This field is reserved for the commercial number of the flight, prefixed
          by the two letter code of the airline.
          For instance, AFR292 is the callsign and AF292 is the flight number.
  
          Callsigns are often more complicated as they are designed to limit
          confusion on the radio: hence DLH02X can be the callsign associated
          to flight number LH1100.
          """
          return self._get_unique("number")
  
      @property
      def flight_id(self) -> Union[str, Set[str], None]:
          """Returns the unique flight_id value(s) of the DataFrame.
  
          Neither the icao24 (the aircraft) nor the callsign (the route) is a
          reliable way to identify trajectories. You can either use an external
          source of data to assign flight ids (for example DDR files by
          Eurocontrol, identifiers by FlightRadar24, etc.) or assign a flight_id
          by yourself (see ``Flight.assign_id(name: str)`` method).
  
          The ``Traffic.assign_id()`` method uses a heuristic based on the
          timestamps associated to callsign/icao24 pairs to automatically assign a
          ``flight_id`` and separate flights.
  
          """
          return self._get_unique("flight_id")
  
      @property
      def title(self) -> str:
          title = str(self.callsign)
          number = self.number
          flight_id = self.flight_id
  
          if number is not None:
              title += f" – {number}"
  
          if flight_id is not None:
              title += f" ({flight_id})"
  
          return title
  
      @property
      def origin(self) -> Union[str, Set[str], None]:
          """Returns the unique origin value(s),
          None if not available in the DataFrame.
  
          The origin airport is usually represented as a ICAO or a IATA code.
  
          The ICAO code of an airport is represented by 4 letters (e.g. EHAM for
          Amsterdam Schiphol International Airport) and the IATA code is
          represented by 3 letters and more familiar to the public (e.g. AMS for
          Amsterdam)
  
          """
          return self._get_unique("origin")
  
      @property
      def destination(self) -> Union[str, Set[str], None]:
          """Returns the unique destination value(s),
          None if not available in the DataFrame.
  
          The destination airport is usually represented as a ICAO or a IATA code.
  
          The ICAO code of an airport is represented by 4 letters (e.g. EHAM for
          Amsterdam Schiphol International Airport) and the IATA code is
          represented by 3 letters and more familiar to the public (e.g. AMS for
          Amsterdam)
  
          """
          return self._get_unique("destination")
  
      @property
      def diverted(self) -> Union[str, Set[str], None]:
          """Returns the unique diverted value(s),
          None if not available in the DataFrame.
  
          The diverted airport is usually represented as a ICAO or a IATA code.
  
          The ICAO code of an airport is represented by 4 letters (e.g. EHAM for
          Amsterdam Schiphol International Airport) and the IATA code is
          represented by 3 letters and more familiar to the public (e.g. AMS for
          Amsterdam)
  
          """
          return self._get_unique("diverted")
  
      @property
      def squawk(self) -> Set[str]:
          """Returns all the unique squawk values in the trajectory.
  
          A squawk code is a four-digit number assigned by ATC and set on the
          transponder. Some squawk codes are reserved for specific situations and
          emergencies, e.g. 7700 for general emergency, 7600 for radio failure or
          7500 for hijacking.
          """
          return set(self.data.squawk.unique())
  
      @property
      def icao24(self) -> Union[str, Set[str], None]:
          """Returns the unique icao24 value(s) of the DataFrame.
  
          icao24 (ICAO 24-bit address) is a unique identifier associated to a
          transponder. These identifiers correlate to the aircraft registration.
  
          For example icao24 code 'ac82ec' is associated to 'N905NA'.
          """
          icao24 = self._get_unique("icao24")
          if icao24 != icao24:
              raise ValueError("NaN appearing in icao24 field")
          return icao24
  
      @property
      def registration(self) -> Optional[str]:
          from ..data import aircraft
  
          if not isinstance(self.icao24, str):
              return None
          res = aircraft[self.icao24]
          res = res.query("registration == registration and registration != ''")
          if res.shape[0] == 1:
              return res.iloc[0].registration  # type: ignore
          return None
  
      @property
      def typecode(self) -> Optional[str]:
          from ..data import aircraft
  
          if not isinstance(self.icao24, str):
              return None
          res = aircraft[self.icao24]
          res = res.query("typecode == typecode and typecode != ''")
          if res.shape[0] == 1:
              return res.iloc[0].typecode  # type: ignore
          return None
  
      @property
      def aircraft(self) -> Optional[str]:
          from ..data import aircraft
  
          if not isinstance(self.icao24, str):
              return None
  
          res = str(self.icao24)
          ac = aircraft.get_unique(res)
  
          if ac is None:
              return res
  
          registration = ac["registration"]
          typecode = ac["typecode"]
          flag = ac["flag"]
  
          if registration is not None:
              res += f" · {flag} {registration}"
          else:
              res = f"{flag} {res}"
  
          if typecode is not None:
              res += f" ({typecode})"
  
          return res
  
      # -- Time handling, splitting, interpolation and resampling --
  
      def skip(
          self, value: deltalike = None, **kwargs: Any
      ) -> Optional["Flight"]:
          """Removes the first n days, hours, minutes or seconds of the Flight.
  
          The elements passed as kwargs as passed as is to the datetime.timedelta
          constructor.
  
          Example usage:
  
          >>> flight.skip(minutes=10)
          >>> flight.skip("1H")
          >>> flight.skip(10)  # seconds by default
          """
          delta = to_timedelta(value, **kwargs)
          bound = self.start + delta  # noqa: F841 => used in the query
          # full call is necessary to keep @bound as a local variable
          df = self.data.query("timestamp >= @bound")
          if df.shape[0] == 0:
              return None
          return self.__class__(df)
  
      def first(
          self, value: deltalike = None, **kwargs: Any
      ) -> Optional["Flight"]:
          """Returns the first n days, hours, minutes or seconds of the Flight.
  
          The elements passed as kwargs as passed as is to the datetime.timedelta
          constructor.
  
          Example usage:
  
          >>> flight.first(minutes=10)
          >>> flight.first("1H")
          >>> flight.first(10)  # seconds by default
          """
          delta = to_timedelta(value, **kwargs)
          bound = self.start + delta  # noqa: F841 => used in the query
          # full call is necessary to keep @bound as a local variable
          df = self.data.query("timestamp < @bound")
          if df.shape[0] == 0:
              return None
          return self.__class__(df)
  
      def shorten(
          self, value: deltalike = None, **kwargs: Any
      ) -> Optional["Flight"]:
          """Removes the last n days, hours, minutes or seconds of the Flight.
  
          The elements passed as kwargs as passed as is to the datetime.timedelta
          constructor.
  
          Example usage:
  
          >>> flight.shorten(minutes=10)
          >>> flight.shorten("1H")
          >>> flight.shorten(10)  # seconds by default
          """
          delta = to_timedelta(value, **kwargs)
          bound = self.stop - delta  # noqa: F841 => used in the query
          # full call is necessary to keep @bound as a local variable
          df = self.data.query("timestamp <= @bound")
          if df.shape[0] == 0:
              return None
          return self.__class__(df)
  
      def last(
          self, value: deltalike = None, **kwargs: Any
      ) -> Optional["Flight"]:
          """Returns the last n days, hours, minutes or seconds of the Flight.
  
          The elements passed as kwargs as passed as is to the datetime.timedelta
          constructor.
  
          Example usage:
  
          >>> flight.last(minutes=10)
          >>> flight.last("1H")
          >>> flight.last(10)  # seconds by default
          """
          delta = to_timedelta(value, **kwargs)
          bound = self.stop - delta  # noqa: F841 => used in the query
          # full call is necessary to keep @bound as a local variable
          df = self.data.query("timestamp > @bound")
          if df.shape[0] == 0:
              return None
          return self.__class__(df)
  
      def before(self, time: timelike, strict: bool = True) -> Optional["Flight"]:
          """Returns the part of the trajectory flown before a given timestamp.
  
          - ``time`` can be passed as a string, an epoch, a Python datetime, or
            a Pandas timestamp.
          """
          return self.between(self.start, time, strict)
  
      def after(self, time: timelike, strict: bool = True) -> Optional["Flight"]:
          """Returns the part of the trajectory flown after a given timestamp.
  
          - ``time`` can be passed as a string, an epoch, a Python datetime, or
            a Pandas timestamp.
          """
          return self.between(time, self.stop, strict)
  
      def between(
          self, start: timelike, stop: time_or_delta, strict: bool = True
      ) -> Optional["Flight"]:
          """Returns the part of the trajectory flown between start and stop.
  
          - ``start`` and ``stop`` can be passed as a string, an epoch, a Python
            datetime, or a Pandas timestamp.
          - ``stop`` can also be passed as a timedelta.
  
          """
  
          # Corner cases when start or stop are None or NaT
          if start is None or start != start:
              return self.before(stop, strict=strict)
  
          if stop is None or stop != stop:
              return self.after(start, strict=strict)
  
          start = to_datetime(start)
          if isinstance(stop, timedelta):
              stop = start + stop
          else:
              stop = to_datetime(stop)
  
          # full call is necessary to keep @start and @stop as local variables
          # return self.query('@start < timestamp < @stop')  => not valid
          if strict:
              df = self.data.query("@start < timestamp < @stop")
          else:
              df = self.data.query("@start <= timestamp <= @stop")
  
          if df.shape[0] == 0:
              return None
  
          return self.__class__(df)
  
      def at(self, time: Optional[timelike] = None) -> Optional[Position]:
          """Returns the position in the trajectory at a given timestamp.
  
          - ``time`` can be passed as a string, an epoch, a Python datetime, or
            a Pandas timestamp.
  
          - If no time is passed (default), the last know position is returned.
          - If no position is available at the given timestamp, None is returned.
            If you expect a position at any price, consider `Flight.resample
            <#traffic.core.Flight.resample>`_
  
          """
  
          if time is None:
              return Position(self.data.ffill().iloc[-1])
  
          index = to_datetime(time)
          df = self.data.set_index("timestamp")
          if index not in df.index:
              id_ = getattr(self, "flight_id", self.callsign)
              logging.warning(f"No index {index} for flight {id_}")
              return None
          return Position(df.loc[index])
  
      def at_ratio(self, ratio: float = 0.5) -> Optional[Position]:
          """Returns a position on the trajectory.
  
          This method is convenient to place a marker on the trajectory in
          visualisation output.
  
          - ``Flight.at_ratio(0)`` is the first point in the trajectory.
          - ``Flight.at_ratio(1)`` is the last point of the trajectory
            (equivalent to ``Flight.at()``)
          """
          if ratio < 0 or ratio > 1:
              raise RuntimeError("ratio must be comprised between 0 and 1")
  
          subset = self.between(
              self.start, self.start + ratio * self.duration, strict=False
          )
  
          assert subset is not None
          return subset.at()
  
      @flight_iterator
      def sliding_windows(
          self,
          duration: deltalike,
          step: deltalike,
      ) -> Iterator["Flight"]:
  
          duration_ = to_timedelta(duration)
          step_ = to_timedelta(step)
  
          first = self.first(duration_)
          if first is None:
              return
  
          yield first
  
          after = self.after(self.start + step_)
          if after is not None:
              yield from after.sliding_windows(duration_, step_)
  
      @overload
      def split(self, value: int, unit: str) -> FlightIterator:
          ...
  
      @overload
      def split(  # noqa: F811
          self, value: str, unit: None = None
      ) -> FlightIterator:
          ...
  
      @flight_iterator
      def split(  # noqa: F811
          self, value: Union[int, str] = 10, unit: Optional[str] = None
      ) -> Iterator["Flight"]:
          """Iterates on legs of a Flight based on the distrution of timestamps.
  
          By default, the method stops a flight and yields a new one after a gap
          of 10 minutes without data.
  
          The length of the gap (here 10 minutes) can be expressed:
  
          - in the NumPy style: ``Flight.split(10, 'm')`` (see
            ``np.timedelta64``);
          - in the pandas style: ``Flight.split('10T')`` (see ``pd.Timedelta``)
  
          """
          if isinstance(value, int) and unit is None:
              # default value is 10 m
              unit = "m"
  
          for data in _split(self.data, value, unit):
              yield self.__class__(data)
  
      def max_split(
          self,
          value: Union[int, str] = "10T",
          unit: Optional[str] = None,
          key: Callable[[Optional["Flight"]], Any] = attrgetter_duration,
      ) -> Optional["Flight"]:
          """Returns the biggest (by default, longest) part of trajectory.
  
          Example usage:
  
          >>> from traffic.data.samples import elal747
          >>> elal747.query("altitude < 15000").max_split()
          Flight ELY1747
          aircraft: 738043 · 🇮🇱 4X-ELC (B744)
          origin: LIRF (2019-11-03 12:14:40+00:00)
          destination: LLBG (2019-11-03 14:13:00+00:00)
  
          In this example, the fancy part of the trajectory occurs below
          15,000 ft. The command extracts the plane pattern.
  
          """
  
          # warnings.warn("Use split().max() instead.", DeprecationWarning)
          return max(
              self.split(value, unit),  # type: ignore
              key=key,
              default=None,
          )
  
      def apply_segments(
          self,
          fun: Callable[..., "LazyTraffic"],
          name: str,
          *args: Any,
          **kwargs: Any,
      ) -> Optional["Flight"]:
          return getattr(self, name)(*args, **kwargs)(fun)  # type: ignore
  
      def apply_time(
          self,
          freq: str = "1T",
          merge: bool = True,
          **kwargs: Any,
      ) -> "Flight":
          """Apply features on time windows.
  
          The following is performed:
  
          - a new column `rounded` rounds the timestamp at the given rate;
          - the groupby/apply is operated with parameters passed in apply;
          - if merge is True, the new column in merged into the Flight,
            otherwise a pd.DataFrame is returned.
  
          For example:
  
          >>> f.agg_time("10T", straight=lambda df: Flight(df).distance())
  
          returns a Flight with a new column straight with the great circle
          distance between points sampled every 10 minutes.
          """
  
          if len(kwargs) == 0:
              raise RuntimeError("No feature provided for aggregation.")
          temp_flight = self.assign(
              rounded=lambda df: df.timestamp.dt.round(freq)
          )
  
          agg_data = None
  
          for label, fun in kwargs.items():
              agg_data = (
                  agg_data.merge(  # type: ignore
                      temp_flight.groupby("rounded")
                      .apply(lambda df: fun(self.__class__(df)))
                      .rename(label),
                      left_index=True,
                      right_index=True,
                  )
                  if agg_data is not None
                  else temp_flight.groupby("rounded")
                  .apply(lambda df: fun(self.__class__(df)))
                  .rename(label)
                  .to_frame()
              )
  
          if not merge:  # mostly for debugging purposes
              return agg_data  # type: ignore
  
          return temp_flight.merge(agg_data, left_on="rounded", right_index=True)
  
      def agg_time(
          self,
          freq: str = "1T",
          merge: bool = True,
          **kwargs: Any,
      ) -> "Flight":
          """Aggregate features on time windows.
  
          The following is performed:
  
          - a new column `rounded` rounds the timestamp at the given rate;
          - the groupby/agg is operated with parameters passed in kwargs;
          - if merge is True, the new column in merged into the Flight,
            otherwise a pd.DataFrame is returned.
  
          For example:
  
          >>> f.agg_time('3T', groundspeed='mean')
  
          returns a Flight with a new column groundspeed_mean with groundspeed
          averaged per intervals of 3 minutes.
          """
  
          def flatten(
              data: pd.DataFrame, how: Callable[..., Any] = "_".join
          ) -> pd.DataFrame:
              data.columns = (
                  [
                      how(filter(None, map(str, levels)))
                      for levels in data.columns.values
                  ]
                  if isinstance(data.columns, pd.MultiIndex)
                  else data.columns
              )
              return data
  
          if len(kwargs) == 0:
              raise RuntimeError("No feature provided for aggregation.")
          temp_flight = self.assign(
              rounded=lambda df: df.timestamp.dt.round(freq)
          )
  
          # force the agg_data to be multi-indexed in columns
          kwargs_modified: Dict["str", List[Any]] = dict(
              (
                  key,
                  list(value)
                  if any(isinstance(value, x) for x in [list, tuple])
                  else [value],
              )
              for key, value in kwargs.items()
          )
          agg_data = flatten(temp_flight.groupby("rounded").agg(kwargs_modified))
  
          if not merge:
              # WARN: Return type is inconsistent but this is mostly for
              # debugging purposes
              return agg_data  # type: ignore
  
          return temp_flight.merge(agg_data, left_on="rounded", right_index=True)
  
      def handle_last_position(self) -> "Flight":
          # The following is True for all data coming from the Impala shell.
          # The following is an attempt to fix #7
          # Note the fun/fast way to produce 1 or trigger NaN (division by zero)
          data = self.data.sort_values("timestamp")
          if "last_position" in self.data.columns:
              data = (
                  data.assign(
                      _mark=lambda df: df.last_position
                      != df.shift(1).last_position
                  ).assign(
                      latitude=lambda df: df.latitude * df._mark / df._mark,
                      longitude=lambda df: df.longitude * df._mark / df._mark,
                      altitude=lambda df: df.altitude * df._mark / df._mark,
                  )
                  # keeping last_position causes more problems (= Nan) than
                  # anything. Safer to just remove it for now. Like it or not!
                  .drop(columns=["_mark", "last_position"])
              )
  
          return self.__class__(data)
  
      def resample(self, rule: Union[str, int] = "1s") -> "Flight":
          """Resample the trajectory at a given frequency or number of points.
  
          If the rule is a string representing a pandas `time series frequency
          <https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#timeseries-offset-aliases>`_
          is passed, then the data is resampled along the timestamp axis, then
          interpolated.
  
          If the rule is an integer, the trajectory is resampled to the given
          number of evenly distributed points per trajectory.
          """
  
          if isinstance(rule, str):
              data = (
                  self.handle_last_position()
                  .unwrap()  # avoid filled gaps in track and heading
                  .assign(start=self.start, stop=self.stop)
                  .data.set_index("timestamp")
                  .resample(rule)
                  .first()  # better performance than min() for duplicate index
                  .interpolate()
                  .reset_index()
                  .fillna(method="pad")
              )
          elif isinstance(rule, int):
              # ./site-packages/pandas/core/indexes/base.py:2820: FutureWarning:
              # Converting timezone-aware DatetimeArray to timezone-naive ndarray
              # with 'datetime64[ns]' dtype. In the future, this will return an
              # ndarray with 'object' dtype where each element is a
              # 'pandas.Timestamp' with the correct 'tz'.
              # To accept the future behavior, pass 'dtype=object'.
              # To keep the old behavior, pass 'dtype="datetime64[ns]"'.
              data = (
                  self.handle_last_position()
                  .unwrap()  # avoid filled gaps in track and heading
                  .assign(tz_naive=lambda d: d.timestamp.astype("datetime64[ns]"))
                  .data.set_index("tz_naive")
                  .asfreq((self.stop - self.start) / (rule - 1), method="nearest")
                  .reset_index()
                  .drop(columns="tz_naive")
              )
          else:
              raise TypeError("rule must be a str or an int")
  
          if "track_unwrapped" in data.columns:
              data = data.assign(track=lambda df: df.track_unwrapped % 360)
          if "heading_unwrapped" in data.columns:
              data = data.assign(heading=lambda df: df.heading_unwrapped % 360)
  
          return self.__class__(data)
  
      def filter(
          self,
          strategy: Optional[
              Callable[[pd.DataFrame], pd.DataFrame]
          ] = lambda x: x.bfill().ffill(),
          **kwargs: int,
      ) -> "Flight":
  
          """Filters the trajectory given features with a median filter.
  
          The method first applies a median filter on each feature of the
          DataFrame. A default kernel size is applied for a number of features
          (resp. latitude, longitude, altitude, track, groundspeed, IAS, TAS) but
          other kernel values may be passed as kwargs parameters.
  
          Rather than returning averaged values, the method computes thresholds
          on sliding windows (as an average of squared differences) and replace
          unacceptable values with NaNs.
  
          Then, a strategy may be applied to fill the NaN values, by default a
          forward/backward fill. Other strategies may be passed, for instance *do
          nothing*: ``None``; or *interpolate*: ``lambda x: x.interpolate()``.
  
          .. note::
              This method if often more efficient when applied several times with
              different kernel values.Kernel values may be passed as integers, or
              list/tuples of integers for cascade of filters:
  
              .. code:: python
  
                  # this cascade of filters appears to work well on altitude
                  flight.filter(altitude=17).filter(altitude=53)
  
                  # this is equivalent to the default value
                  flight.filter(altitude=(17, 53))
  
          """
  
          import scipy.signal
  
          ks_dict: Dict[str, Union[int, Iterable[int]]] = {
              "altitude": (17, 53),
              "selected_mcp": (17, 53),
              "selected_fms": (17, 53),
              "IAS": 23,
              "TAS": 23,
              "Mach": 23,
              "groundspeed": 5,
              "compute_gs": (17, 53),
              "compute_track": 17,
              "onground": 3,
              "latitude": 1,  # the method doesn't apply well to positions
              "longitude": 1,
              **kwargs,
          }
  
          if strategy is None:
  
              def identity(x: pd.DataFrame) -> pd.DataFrame:
                  return x  # noqa: E704
  
              strategy = identity
  
          def cascaded_filters(
              df: pd.DataFrame,
              feature: str,
              kernel_size: int,
              filt: Callable[[pd.Series, int], Any] = scipy.signal.medfilt,
          ) -> pd.DataFrame:
              """Produces a mask for data to be discarded.
  
              The filtering applies a low pass filter (e.g medfilt) to a signal
              and measures the difference between the raw and the filtered signal.
  
              The average of the squared differences is then produced (sq_eps) and
              used as a threashold for filtering.
  
              Errors may raised if the kernel_size is too large
              """
              y = df[feature].astype(float)
              y_m = filt(y, kernel_size)
              sq_eps = (y - y_m) ** 2
              return pd.DataFrame(
                  {
                      "timestamp": df["timestamp"],
                      "y": y,
                      "y_m": y_m,
                      "sq_eps": sq_eps,
                      "sigma": np.sqrt(filt(sq_eps, kernel_size)),
                  },
                  index=df.index,
              )
  
          new_data = self.data.sort_values(by="timestamp").copy()
  
          if len(kwargs) == 0:
              features = [
                  cast(str, feature)
                  for feature in self.data.columns
                  if self.data[feature].dtype
                  in [np.float32, np.float64, np.int32, np.int64]
              ]
          else:
              features = list(kwargs.keys())
  
          kernels_size: List[Union[int, Iterable[int]]] = [0 for _ in features]
          for idx, feature in enumerate(features):
              kernels_size[idx] = ks_dict.get(feature, 17)
  
          for feat, ks_list in zip(features, kernels_size):
  
              if isinstance(ks_list, int):
                  ks_list = [ks_list]
              else:
                  ks_list = list(ks_list)
  
              for ks in ks_list:
                  # Prepare each feature for the filtering
                  df = cascaded_filters(new_data[["timestamp", feat]], feat, ks)
  
                  # Decision to accept/reject data points in the time series
                  new_data.loc[df.sq_eps > df.sigma, feat] = None
  
          data = strategy(new_data)
  
          if "onground" in data.columns:
              data = data.assign(onground=data.onground.astype(bool))
  
          return self.__class__(data)
  
      def filter_position(self, cascades: int = 2) -> Optional["Flight"]:
          # TODO improve based on agg_time or EKF
          flight: Optional["Flight"] = self
          for _ in range(cascades):
              if flight is None:
                  return None
              flight = flight.cumulative_distance().query(
                  "compute_gs < compute_gs.mean() + 3 * compute_gs.std()"
              )
          return flight
  
      def comet(self, **kwargs: Any) -> "Flight":
          """Computes a comet for a trajectory.
  
          The method uses the last position of a trajectory (method `at()
          <#traffic.core.Flight.at>`_) and uses the ``track`` (in degrees),
          ``groundspeed`` (in knots) and ``vertical_rate`` (in ft/min) values to
          interpolate the trajectory in a straight line.
  
          The elements passed as kwargs as passed as is to the datetime.timedelta
          constructor.
  
          Example usage:
  
          .. code:: python
  
              flight.comet(minutes=10)
              flight.before("2018-12-24 23:55").comet(minutes=10)  # Merry XMas!
  
          """
  
          last_line = self.at()
          if last_line is None:
              raise ValueError("Unknown data for this flight")
          window = self.last(seconds=20)
          delta = timedelta(**kwargs)
  
          if window is None:
              raise RuntimeError("Flight expect at least 20 seconds of data")
  
          new_gs = window.data.groundspeed.mean()
          new_vr = window.data.vertical_rate.mean()
  
          new_lat, new_lon, _ = geo.destination(
              last_line.latitude,
              last_line.longitude,
              last_line.track,
              new_gs * delta.total_seconds() * 1852 / 3600,
          )
  
          new_alt = last_line.altitude + new_vr * delta.total_seconds() / 60
  
          return Flight(
              pd.DataFrame.from_records(
                  [
                      last_line,
                      pd.Series(
                          {
                              "timestamp": last_line.timestamp + delta,
                              "latitude": new_lat,
                              "longitude": new_lon,
                              "altitude": new_alt,
                              "groundspeed": new_gs,
                              "vertical_rate": new_vr,
                          }
                      ),
                  ]
              ).ffill()
          )
  
      # -- Air traffic management --
  
      def assign_id(
          self, name: str = "{self.callsign}_{idx:>03}", idx: int = 0
      ) -> "Flight":
          """Assigns a flight_id to a Flight.
  
          This method is more generally used by the corresponding Traffic and
          LazyTraffic methods but works fine on Flight as well.
          """
          return self.assign(flight_id=name.format(self=self, idx=idx))
  
      def onground(self) -> Optional["Flight"]:
          if "altitude" not in self.data.columns:
              return self
          if "onground" in self.data.columns and self.data.onground.dtype == bool:
              return self.query("onground or altitude != altitude")
          else:
              return self.query("altitude != altitude")
  
      def airborne(self) -> Optional["Flight"]:
          """Returns the airborne part of the Flight.
  
          The airborne part is determined by an ``onground`` flag or null values
          in the altitude column.
          """
          if "altitude" not in self.data.columns:
              return None
          if "onground" in self.data.columns and self.data.onground.dtype == bool:
              return self.query("not onground and altitude == altitude")
          else:
              return self.query("altitude == altitude")
  
      def unwrap(self, features: Union[None, str, List[str]] = None) -> "Flight":
          """Unwraps angles in the DataFrame.
  
          All features representing angles may be unwrapped (through Numpy) to
          avoid gaps between 359° and 1°.
  
          The method applies by default to features ``track`` and ``heading``.
          More or different features may be passed in parameter.
          """
          if features is None:
              features = default_angle_features
  
          if isinstance(features, str):
              features = [features]
  
          reset = self.reset_index(drop=True)
  
          result_dict = dict()
          for feature in features:
              if feature not in reset.data.columns:
                  continue
              series = reset.data[feature].astype(float)
              idx = ~series.isnull()
              result_dict[f"{feature}_unwrapped"] = pd.Series(
                  np.degrees(
                      np.unwrap(np.radians(series.loc[idx]))  # type: ignore
                  ),
                  index=series.loc[idx].index,
              )
  
          return reset.assign(**result_dict)
  
      def compute_wind(self) -> "Flight":
          """Computes the wind triangle for each timestamp.
  
          This method requires ``groundspeed``, ``track``, true airspeed
          (``TAS``), and ``heading`` features. The groundspeed and the track angle
          are usually available in ADS-B messages; the heading and the true
          airspeed may be decoded in EHS messages.
  
          .. note::
              Check the `query_ehs() <#traffic.core.Flight.query_ehs>`_ method to
              find a way to enrich your flight with such features. Note that this
              data is not necessarily available depending on the location.
          """
  
          if any(w not in self.data.columns for w in ["heading", "TAS"]):
              raise RuntimeError(
                  "No wind data in trajectory. Consider Flight.query_ehs()"
              )
  
          return self.assign(
              wind_u=self.data.groundspeed * np.sin(np.radians(self.data.track))
              - self.data.TAS * np.sin(np.radians(self.data.heading)),
              wind_v=self.data.groundspeed * np.cos(np.radians(self.data.track))
              - self.data.TAS * np.cos(np.radians(self.data.heading)),
          )
  
      def plot_wind(
          self,
          ax: "GeoAxesSubplot",
          resolution: Union[int, str, Dict[str, float], None] = "5T",
          filtered: bool = False,
          **kwargs: Any,
      ) -> List["Artist"]:  # coverage: ignore
          """Plots the wind field seen by the aircraft on a Matplotlib axis.
  
          The Flight supports Cartopy axis as well with automatic projection. If
          no projection is provided, a default `PlateCarree
          <https://scitools.org.uk/cartopy/docs/v0.15/crs/projections.html#platecarree>`_
          is applied.
  
          The `resolution` argument may be:
  
              - None for a raw plot;
              - an integer or a string to pass to a `Flight.resample()
                <#traffic.core.Flight.resample>`__ method as a preprocessing
                before plotting;
              - or a dictionary, e.g dict(latitude=4, longitude=4), if you
                want a grid with a resolution of 4 points per latitude and
                longitude degree.
  
          Example usage:
  
          .. code:: python
  
              from traffic.drawing import Mercator
              fig, ax = plt.subplots(1, subplot_kw=dict(projection=Mercator()))
              (
                  flight
                  .resample("1s")
                  .query('altitude > 10000')
                  .compute_wind()
                  .plot_wind(ax, alpha=.5)
              )
  
          """
  
          from cartopy.crs import PlateCarree
  
          if "projection" in ax.__dict__ and "transform" not in kwargs:
              kwargs["transform"] = PlateCarree()
  
          if any(w not in self.data.columns for w in ["wind_u", "wind_v"]):
              raise RuntimeError(
                  "No wind data in trajectory. Consider Flight.compute_wind()"
              )
  
          copy_self: Optional[Flight] = self
  
          if filtered:
              copy_self = self.filter(roll=17)
              if copy_self is None:
                  return []
              copy_self = copy_self.query("roll.abs() < .5")
              if copy_self is None:
                  return []
              copy_self = copy_self.filter(wind_u=17, wind_v=17)
  
          if copy_self is None:
              return []
  
          if resolution is not None:
  
              if isinstance(resolution, (int, str)):
                  data = copy_self.resample(resolution).data
  
              if isinstance(resolution, dict):
                  r_lat = resolution.get("latitude", None)
                  r_lon = resolution.get("longitude", None)
  
                  if r_lat is not None and r_lon is not None:
                      data = (
                          copy_self.assign(
                              latitude=lambda x: (
                                  (r_lat * x.latitude).round() / r_lat
                              ),
                              longitude=lambda x: (
                                  (r_lon * x.longitude).round() / r_lon
                              ),
                          )
                          .groupby(["latitude", "longitude"])
                          .agg(dict(wind_u="mean", wind_v="mean"))
                          .reset_index()
                      )
  
          return ax.barbs(  # type: ignore
              data.longitude.values,
              data.latitude.values,
              data.wind_u.values,
              data.wind_v.values,
              **kwargs,
          )
  
      # -- Distances --
  
      def bearing(
          self, other: PointMixin, column_name: str = "bearing"
      ) -> "Flight":
          # temporary, should implement full stuff
          size = self.data.shape[0]
          return self.assign(
              **{
                  column_name: geo.bearing(
                      self.data.latitude.values,
                      self.data.longitude.values,
                      other.latitude * np.ones(size),
                      other.longitude * np.ones(size),
                  )
                  % 360
              }
          )
  
      @overload
      def distance(  # type: ignore
          self, other: None = None, column_name: str = "distance"
      ) -> float:
          ...
  
      @overload
      def distance(  # noqa: F811
          self,
          other: Union["Airspace", Polygon, PointMixin],
          column_name: str = "distance",
      ) -> "Flight":
          ...
  
      @overload
      def distance(  # noqa: F811
          self, other: "Flight", column_name: str = "distance"
      ) -> Optional[pd.DataFrame]:
          ...
  
      def distance(  # noqa: F811
          self,
          other: Union[None, "Flight", "Airspace", Polygon, PointMixin] = None,
          column_name: str = "distance",
      ) -> Union[None, float, "Flight", pd.DataFrame]:
  
          """Computes the distance from a Flight to another entity.
  
          The behaviour is different according to the type of the second
          element:
  
          - if the other element is None (i.e. flight.distance()), the method
            returns a distance in nautical miles between the first and last
            recorded positions in the DataFrame.
  
          - if the other element is a Flight, the method returns a pandas
            DataFrame with corresponding data from both flights, aligned
            with their timestamps, and two new columns with `lateral` and
            `vertical` distances (resp. in nm and ft) separating them.
  
          - otherwise, the same Flight is returned enriched with a new
            column (by default, named "distance") with the distance of each
            point of the trajectory to the geometrical element.
  
          .. warning::
  
              - An Airspace is (currently) considered as its flattened
                representation
              - Computing a distance to a polygon is quite slow at the moment.
                Consider a strict resampling (e.g. one point per minute, "1T")
                before calling the method.
  
          """
  
          if other is None:
              first = self.at_ratio(0)
              last = self.at_ratio(1)
              if first is None or last is None:
                  return 0
              return (
                  geo.distance(
                      first.latitude,
                      first.longitude,
                      last.latitude,
                      last.longitude,
                  )
                  / 1852  # in nautical miles
              )
  
          if isinstance(other, PointMixin):
              size = self.data.shape[0]
              return self.assign(
                  **{
                      column_name: geo.distance(
                          self.data.latitude.values,
                          self.data.longitude.values,
                          other.latitude * np.ones(size),
                          other.longitude * np.ones(size),
                      )
                      / 1852  # in nautical miles
                  }
              )
  
          from .airspace import Airspace  # noqa: F811
  
          if isinstance(other, Airspace):
              other = other.flatten()
  
          if isinstance(other, Polygon):
              bounds = other.bounds
  
              projection = pyproj.Proj(
                  proj="aea",  # equivalent projection
                  lat_1=bounds[1],
                  lat_2=bounds[3],
                  lat_0=(bounds[1] + bounds[3]) / 2,
                  lon_0=(bounds[0] + bounds[2]) / 2,
              )
  
              transformer = pyproj.Transformer.from_proj(
                  pyproj.Proj("epsg:4326"), projection, always_xy=True
              )
              projected_shape = transform(transformer.transform, other)
  
              self_xy = self.compute_xy(projection)
  
              return self.assign(
                  **{
                      column_name: list(
                          projected_shape.exterior.distance(p)
                          * (-1 if projected_shape.contains(p) else 1)
                          for p in MultiPoint(
                              list(zip(self_xy.data.x, self_xy.data.y))
                          )
                      )
                  }
              )
  
          start = max(self.start, other.start)
          stop = min(self.stop, other.stop)
          f1, f2 = (self.between(start, stop), other.between(start, stop))
          if f1 is None or f2 is None:
              return None
  
          cols = ["timestamp", "latitude", "longitude", "altitude"]
          cols += ["icao24", "callsign"]
          if "flight_id" in f1.data.columns:
              cols.append("flight_id")
          table = f1.data[cols].merge(f2.data[cols], on="timestamp")
  
          return table.assign(
              lateral=geo.distance(
                  table.latitude_x.values,
                  table.longitude_x.values,
                  table.latitude_y.values,
                  table.longitude_y.values,
              )
              / 1852,  # in nautical miles
              vertical=(table.altitude_x - table.altitude_y).abs(),
          )
  
      def cumulative_distance(
          self,
          compute_gs: bool = True,
          compute_track: bool = True,
          *,
          reverse: bool = False,
          **kwargs: Any,
      ) -> "Flight":
  
          """Enrich the structure with new ``cumdist`` column computed from
          latitude and longitude columns.
  
          The first ``cumdist`` value is 0, then distances are computed (in
          **nautical miles**) and summed between consecutive positions. The last
          value is the total length of the trajectory.
  
          When the ``compute_gs`` flag is set to True (default), an additional
          ``compute_gs`` is also added. This value can be compared with the
          decoded ``groundspeed`` value in ADSB messages.
  
          When the ``compute_track`` flag is set to True (default), an additional
          ``compute_track`` is also added. This value can be compared with the
          decoded ``track`` value in ADSB messages.
  
          """
  
          if "compute_groundspeed" in kwargs:
              warnings.warn("Use compute_gs argument", DeprecationWarning)
              compute_gs = kwargs["compute_groundspeed"]
  
          cur_sorted = self.sort_values("timestamp", ascending=not reverse)
          coords = cur_sorted.data[["timestamp", "latitude", "longitude"]]
  
          delta = pd.concat([coords, coords.add_suffix("_1").diff()], axis=1)
          delta_1 = delta.iloc[1:]
          d = geo.distance(
              delta_1.latitude.values,
              delta_1.longitude.values,
              (delta_1.latitude + delta_1.latitude_1).values,
              (delta_1.longitude + delta_1.longitude_1).values,
          )
  
          res = cur_sorted.assign(
              cumdist=np.pad(  # type: ignore
                  d.cumsum() / 1852, (1, 0), "constant"
              )
          )
  
          if compute_gs:
              gs = d / delta_1.timestamp_1.dt.total_seconds() * (3600 / 1852)
              res = res.assign(
                  compute_gs=np.abs(np.pad(gs, (1, 0), "edge"))  # type: ignore
              )
  
          if compute_track:
              track = geo.bearing(
                  delta_1.latitude.values,
                  delta_1.longitude.values,
                  (delta_1.latitude + delta_1.latitude_1).values,
                  (delta_1.longitude + delta_1.longitude_1).values,
              )
              track = np.where(track > 0, track, 360 + track)
              res = res.assign(
                  compute_track=np.abs(
                      np.pad(track, (1, 0), "edge")  # type: ignore
                  )
              )
  
          return res.sort_values("timestamp", ascending=True)
  
      # -- Geometry operations --
  
      @property
      def linestring(self) -> Optional[LineString]:
          # longitude is implicit I guess
          if "latitude" not in self.data.columns:
              return None
          coords = list(self.coords)
          if len(coords) < 2:
              return None
          return LineString(coords)
  
      @property
      def shape(self) -> Optional[LineString]:
          return self.linestring
  
      @property
      def point(self) -> Optional[Position]:
          return self.at()
  
      def simplify(
          self,
          tolerance: float,
          altitude: Optional[str] = None,
          z_factor: float = 3.048,
          return_mask: bool = False,
      ) -> Union[npt.NDArray[np.float64], "Flight"]:
          """Simplifies a trajectory with Douglas-Peucker algorithm.
  
          The method uses latitude and longitude, projects the trajectory to a
          conformal projection and applies the algorithm. If x and y features are
          already present in the DataFrame (after a call to `compute_xy()
          <#traffic.core.Flight.compute_xy>`_ for instance) then this projection
          is taken into account.
  
          - By default, a 2D version is called, unless you pass a column name for
            ``altitude``.
          - You may scale the z-axis for more relevance (``z_factor``). The
            default value works well in most situations.
  
          The method returns a Flight unless you specify ``return_mask=True``.
          """
  
          if "x" in self.data.columns and "y" in self.data.columns:
              kwargs = dict(x="x", y="y")
          else:
              kwargs = dict(lat="latitude", lon="longitude")
  
          mask = douglas_peucker(
              df=self.data,
              tolerance=tolerance,
              z=altitude,
              z_factor=z_factor,
              **kwargs,
          )
  
          if return_mask:
              return mask
          else:
              return self.__class__(self.data.loc[mask])
  
      def intersects(self, shape: Union[ShapelyMixin, base.BaseGeometry]) -> bool:
          # implemented and monkey-patched in airspace.py
          # given here for consistency in types
          ...
  
      @flight_iterator
      def clip_iterate(
          self, shape: Union[ShapelyMixin, base.BaseGeometry], strict: bool = True
      ) -> Iterator["Flight"]:
          list_coords = list(self.xy_time)
          if len(list_coords) < 2:
              return None
  
          linestring = LineString(list_coords)
          if not isinstance(shape, base.BaseGeometry):
              shape = shape.shape
  
          intersection = linestring.intersection(shape)
  
          if intersection.is_empty:
              return None
  
          if isinstance(intersection, Point):
              return None
  
          if isinstance(intersection, LineString):
              time_list = list(
                  datetime.fromtimestamp(t, timezone.utc)
                  for t in np.stack(intersection.coords)[:, 2]
              )
              between = self.between(
                  min(time_list), max(time_list), strict=strict
              )
              if between is not None:
                  yield between
              return None
  
          def _clip_generator() -> Iterable[Tuple[datetime, datetime]]:
              for segment in intersection:
                  times: List[datetime] = list(
                      datetime.fromtimestamp(t, timezone.utc)
                      for t in np.stack(segment.coords)[:, 2]
                  )
                  yield min(times), max(times)
  
          # it is actually not so simple because of self intersecting trajectories
          prev_t1, prev_t2 = None, None
  
          for t1, t2 in _clip_generator():
              if prev_t2 is not None and t1 > prev_t2:
                  between = self.between(prev_t1, prev_t2, strict=strict)
                  if between is not None:
                      yield between
                  prev_t1, prev_t2 = t1, t2
              elif prev_t2 is None:
                  prev_t1, prev_t2 = t1, t2
              else:
                  prev_t1, prev_t2 = min(prev_t1, t1), max(prev_t2, t2)
  
          if prev_t2 is not None:
              between = self.between(prev_t1, prev_t2, strict=strict)
              if between is not None:
                  yield between
  
      def clip(
          self, shape: Union[ShapelyMixin, base.BaseGeometry], strict: bool = True
      ) -> Optional["Flight"]:
          """Clips the trajectory to a given shape.
  
          For a shapely Geometry, the first time of entry and the last time of
          exit are first computed before returning the part of the trajectory
          between the two timestamps.
  
          Most of the time, aircraft do not repeatedly come out and in an
          airspace, but computation errors may sometimes give this impression.
          As a consequence, the clipped trajectory may have points outside the
          shape.
  
          .. warning::
              Altitudes are not taken into account.
  
          """
  
          t1 = None
          for segment in self.clip_iterate(shape, strict=strict):
              if t1 is None:
                  t1 = segment.start
              t2 = segment.stop
  
          if t1 is None:
              return None
  
          clipped_flight = self.between(t1, t2, strict=strict)
  
          if clipped_flight is None or clipped_flight.shape is None:
              return None
  
          return clipped_flight
  
      # -- OpenSky specific methods --
  
      def query_opensky_sensors(self, where_condition: str = "") -> pd.DataFrame:
          from ..data import opensky
  
          return (
              opensky.request(
                  "select s.ITEM, count(*) from state_vectors_data4, "
                  "state_vectors_data4.serials s "
                  f"where icao24='{self.icao24}' and "
                  f"{where_condition} "
                  "{before_time}<=time and time < {after_time} and "
                  "{before_hour}<=hour and hour < {after_hour} "
                  "group by s.ITEM;",
                  self.start,
                  self.stop,
                  columns=["serial", "count"],
              )
              .groupby("serial")
              .sum()
          )
  
      def query_opensky(self, **kwargs: Any) -> Optional["Flight"]:
          """Returns data from the same Flight as stored in OpenSky database.
  
          This may be useful if you write your own parser for data from a
          different channel. The method will use the ``callsign`` and ``icao24``
          attributes to build a request for current Flight in the OpenSky Network
          database.
  
          The kwargs argument helps overriding arguments from the query, namely
          start, stop, callsign and icao24.
  
          Returns None if no data is found.
  
          .. note::
              Read more about access to the OpenSky Network database `here
              <opensky_impala.html>`_
          """
  
          from ..data import opensky
  
          query_params = {
              "start": self.start,
              "stop": self.stop,
              "callsign": self.callsign,
              "icao24": self.icao24,
              "return_flight": True,
              **kwargs,
          }
          return cast(Optional["Flight"], opensky.history(**query_params))
  
      def query_ehs(
          self,
          data: Union[None, pd.DataFrame, "RawData"] = None,
          failure_mode: str = "warning",
          progressbar: Union[bool, ProgressbarType[Any]] = True,
      ) -> "Flight":
          """Extends data with extra columns from EHS messages.
  
          By default, raw messages are requested from the OpenSky Network
          database.
  
          .. warning::
              Making a lot of small requests can be very inefficient and may look
              like a denial of service. If you get the raw messages using a
              different channel, you can provide the resulting dataframe as a
              parameter. See the page about `OpenSky Impala access
              <opensky_impala.html>`_
  
          The data parameter expect three columns: ``icao24``, ``rawmsg`` and
          ``mintime``, in conformance with the OpenSky API.
  
          .. note::
              Read more about access to the OpenSky Network database `here
              <opensky_impala.html>`_
          """
  
          from ..data import opensky
          from ..data.adsb.raw_data import RawData  # noqa: F811
  
          if not isinstance(self.icao24, str):
              raise RuntimeError("Several icao24 for this flight")
  
          if not isinstance(self.callsign, str):
              raise RuntimeError("Several callsigns for this flight")
  
          def fail_warning() -> "Flight":
              """Called when nothing can be added to data."""
              id_ = self.flight_id
              if id_ is None:
                  id_ = self.callsign
              logging.warning(f"No data on Impala for flight {id_}.")
              return self
  
          def fail_silent() -> "Flight":
              return self
  
          failure_dict = dict(warning=fail_warning, silent=fail_silent)
          failure = failure_dict[failure_mode]
  
          if data is None:
              ext = opensky.extended(self.start, self.stop, icao24=self.icao24)
              df = ext.data if ext is not None else None
          else:
              df = data if isinstance(data, pd.DataFrame) else data.data
              df = df.query(
                  "icao24 == @self.icao24 and "
                  "@self.start < mintime < @self.stop"
              )
  
          if df is None or df.shape[0] == 0:
              return failure()
  
          timestamped_df = df.sort_values("mintime").assign(
              timestamp=lambda df: df.mintime.dt.round("s")
          )
  
          referenced_df = (
              timestamped_df.merge(self.data, on="timestamp", how="outer")
              .sort_values("timestamp")
              .rename(
                  columns=dict(
                      altitude="alt",
                      altitude_y="alt",
                      groundspeed="spd",
                      track="trk",
                  )
              )[["timestamp", "latitude", "longitude", "alt", "spd", "trk"]]
              .ffill()
              .drop_duplicates()  # bugfix! NEVER ERASE THAT LINE!
              .merge(
                  timestamped_df[["timestamp", "icao24", "rawmsg"]],
                  on="timestamp",
                  how="right",
              )
          )
  
          identifier = (
              self.flight_id if self.flight_id is not None else self.callsign
          )
  
          t = RawData(referenced_df).decode(
              reference=self.origin if isinstance(self.origin, str) else None,
              progressbar=progressbar,
              progressbar_kw=dict(leave=False, desc=f"{identifier}:"),
          )
  
          extended = t[self.icao24] if t is not None else None
          if extended is None:
              return failure()
  
          # fix for https://stackoverflow.com/q/53657210/1595335
          if "last_position" in self.data.columns:
              extended = extended.assign(last_position=pd.NaT)
          if "start" in self.data.columns:
              extended = extended.assign(start=pd.NaT)
          if "stop" in self.data.columns:
              extended = extended.assign(stop=pd.NaT)
  
          aggregate = extended + self
          if "flight_id" in self.data.columns:
              aggregate.data.flight_id = self.flight_id
  
          # sometimes weird callsigns are decoded and should be discarded
          # so it seems better to filter on callsign rather than on icao24
          flight = aggregate[self.callsign]
          if flight is None:
              return failure()
  
          if self.number is not None:
              flight = flight.assign(number=self.number)
          if self.origin is not None:
              flight = flight.assign(origin=self.origin)
          if self.destination is not None:
              flight = flight.assign(destination=self.destination)
  
          return flight.sort_values("timestamp")
  
      # -- Visualisation --
  
      def plot(
          self, ax: "GeoAxesSubplot", **kwargs: Any
      ) -> List["Artist"]:  # coverage: ignore
          """Plots the trajectory on a Matplotlib axis.
  
          The Flight supports Cartopy axis as well with automatic projection. If
          no projection is provided, a default `PlateCarree
          <https://scitools.org.uk/cartopy/docs/v0.15/crs/projections.html#platecarree>`_
          is applied.
  
          Example usage:
  
          .. code:: python
  
              from traffic.drawing import Mercator
              fig, ax = plt.subplots(1, subplot_kw=dict(projection=Mercator())
              flight.plot(ax, alpha=.5)
  
          .. note::
              See also `geoencode() <#traffic.core.Flight.geoencode>`_ for the
              altair equivalent.
  
          """
  
          from cartopy.crs import PlateCarree
  
          if "projection" in ax.__dict__ and "transform" not in kwargs:
              kwargs["transform"] = PlateCarree()
          if self.shape is not None:
              return ax.plot(*self.shape.xy, **kwargs)  # type: ignore
          return []
  
      def chart(self, *features: str) -> "alt.Chart":  # coverage: ignore
          """
          Initializes an altair Chart based on Flight data.
  
          The features passed in parameters are dispatched to allow plotting
          multiple features on the same graph.
  
          Example usage:
  
          .. code:: python
  
              # Most simple usage
              flight.chart().encode(alt.Y("altitude"))
  
              # With some configuration
              flight.chart().encode(
                  alt.X(
                      "utcyearmonthdatehoursminutes(timestamp)",
                      axis=alt.Axis(title=None, format="%H:%M"),
                  ),
                  alt.Y("altitude", title="altitude (in ft)"),
                  alt.Color("callsign")
              )
  
          For a more complex graph plotting similar physical quantities on the
          same graph, and other quantities on a different graph, the following
          snippet may be of use.
  
          .. code:: python
  
              # More advanced with several plots on the same graph
              base = (
                  flight.chart("altitude", "groundspeed", "IAS")
                  .encode(
                      alt.X(
                          "utcyearmonthdatehoursminutesseconds(timestamp)",
                          axis=alt.Axis(title=None, format="%H:%M"),
                      )
                  )
                  .properties(height=200)
              )
  
              alt.vconcat(
                  base.transform_filter('datum.variable != "altitude"').encode(
                      alt.Y(
                          "value:Q",
                          axis=alt.Axis(title="speed (in kts)"),
                          scale=alt.Scale(zero=False),
                      )
                  ),
                  base.transform_filter('datum.variable == "altitude"').encode(
                      alt.Y("value:Q", title="altitude (in ft)")
                  ),
              )
  
          .. note::
              See also `plot_time() <#traffic.core.Flight.plot_time>`_ for the
              Matplotlib equivalent.
  
          """
          import altair as alt
  
          base = alt.Chart(self.data).encode(
              alt.X(
                  "utcyearmonthdatehoursminutesseconds(timestamp)",
                  title='alt.X("utcyearmonthdatehoursminutesseconds(timestamp)")',
              ),
          )
          if len(features) > 0:
              base = base.transform_fold(
                  list(features), as_=["variable", "value"]
              ).encode(alt.Y("value:Q"), alt.Color("variable:N"))
  
          return base.mark_line()
  
      def encode(self, **kwargs: Any) -> NoReturn:  # coverage: ignore
          """
          DEPRECATED: Use Flight.chart() method instead.
          """
          raise DeprecationWarning("Use Flight.chart() method instead")
  
      def plot_time(
          self,
          ax: "Axes",
          y: Union[str, List[str]],
          secondary_y: Union[None, str, List[str]] = None,
          **kwargs: Any,
      ) -> None:  # coverage: ignore
          """Plots the given features according to time.
  
          The method ensures:
  
          - only non-NaN data are displayed (no gap in the plot);
          - the timestamp is naively converted to UTC if not localized.
  
          Example usage:
  
          .. code:: python
  
              ax = plt.axes()
              # most simple version
              flight.plot(ax, 'altitude')
              # or with several comparable features and twin axes
              flight.plot(
                  ax, ['altitude', 'groundspeed, 'IAS', 'TAS'],
                  secondary_y=['altitude']
              )
  
          .. note::
              See also `chart() <#traffic.core.Flight.chart>`_ for the altair
              equivalent.
  
          """
          if isinstance(y, str):
              y = [y]
          if isinstance(secondary_y, str):
              secondary_y = [secondary_y]
          if secondary_y is None:
              secondary_y = []
  
          localized = self.data.timestamp.dt.tz is not None
          for column in y:
              kw = {
                  **kwargs,
                  **dict(
                      y=column,
                      secondary_y=column if column in secondary_y else "",
                  ),
              }
              subtab = self.data.query(f"{column} == {column}")
  
              if localized:
                  (
                      subtab.assign(
                          timestamp=lambda df: df.timestamp.dt.tz_convert("utc")
                      ).plot(ax=ax, x="timestamp", **kw)
                  )
              else:
                  (
                      subtab.assign(
                          timestamp=lambda df: df.timestamp.dt.tz_localize(
                              datetime.now().astimezone().tzinfo
                          ).dt.tz_convert("utc")
                      ).plot(ax=ax, x="timestamp", **kw)
                  )
  
      @classmethod
      def from_file(
          cls: Type[T], filename: Union[Path, str], **kwargs: Any
      ) -> Optional[T]:
  
          """Read data from various formats.
  
          This class method dispatches the loading of data in various format to
          the proper ``pandas.read_*`` method based on the extension of the
          filename.
  
          - .pkl and .pkl.gz dispatch to ``pandas.read_pickle``;
          - .parquet and .parquet.gz dispatch to ``pandas.read_parquet``;
          - .json and .json.gz dispatch to ``pandas.read_json``;
          - .csv and .csv.gz dispatch to ``pandas.read_csv``;
          - .h5 dispatch to ``pandas.read_hdf``.
  
          Other extensions return ``None``.
          Specific arguments may be passed to the underlying ``pandas.read_*``
          method with the kwargs argument.
  
          Example usage:
  
          >>> t = Flight.from_file("example_flight.csv")
          """
  
          tentative = super().from_file(filename, **kwargs)
          if tentative is None:
              return None
  
          # Special treatment for flights to download from flightradar24
          cols_fr24 = {
              "Altitude",
              "Callsign",
              "Direction",
              "Position",
              "Speed",
              "Timestamp",
              "UTC",
          }
          if set(tentative.data.columns) != cols_fr24:
              return tentative
  
          latlon = tentative.data.Position.str.split(pat=",", expand=True)
          return (
              tentative.assign(
                  latitude=latlon[0].astype(float),
                  longitude=latlon[1].astype(float),
                  timestamp=lambda df: pd.to_datetime(df.UTC),
              )
              .rename(
                  columns={
                      "UTC": "timestamp",
                      "Altitude": "altitude",
                      "Callsign": "callsign",
                      "Speed": "groundspeed",
                      "Direction": "track",
                  }
              )
              .drop(columns=["Timestamp", "Position"])
          )
  