예제 #1
0
def test_append():
    list_1 = DoublyLinkedList()
    list_1.append(0)

    assert list_1[0] == 0
    assert list_1[-1] == 0
    assert list_1.head is list_1.tail
예제 #2
0
class LocationPacketTrack:
    """
    a collection of location packets in chronological order
    """
    def __init__(
        self,
        packets: List[LocationPacket] = None,
        name: str = None,
        crs: CRS = None,
        **attributes,
    ):
        """
        :param packets: iterable of packets
        :param name: name of packet track
        :param crs: coordinate reference system to use
        """

        if name is None:
            name = "packet track"
        elif not isinstance(name, str):
            name = str(name)

        self.name = name
        self.packets = DoublyLinkedList(None)
        self.crs = crs if crs is not None else DEFAULT_CRS
        self.attributes = attributes

        if packets is not None:
            self.extend(packets)

        self.__length = len(self)
        self.__data = None

    def append(self, packet: LocationPacket):
        if packet not in self.packets:
            if packet.crs != self.crs:
                packet.transform_to(self.crs)
            self.packets.append(packet)

    def extend(self, packets: List[LocationPacket]):
        for packet in packets:
            self.append(packet)

    def sort(self, inplace: bool = False):
        if inplace:
            instance = self
        else:
            instance = copy(self)
        instance.packets.sort(inplace=True)
        return instance

    @property
    def data(self) -> DataFrame:
        if self.__data is None or len(self) != self.__length:
            records = [{
                "time": packet.time,
                "x": packet.coordinates[0],
                "y": packet.coordinates[1],
                "altitude": packet.coordinates[2],
            } for packet in self.packets]
            for index, packet_delta in enumerate(
                    numpy.insert(numpy.diff(self.packets), 0,
                                 Distance(0, 0, 0, self.crs))):
                records[index].update({
                    "interval": packet_delta.interval,
                    "overground_distance": packet_delta.overground,
                    "ascent": packet_delta.ascent,
                    "ground_speed": packet_delta.ground_speed,
                    "ascent_rate": packet_delta.ascent_rate,
                })

            self.__data = DataFrame.from_records(records)

        return self.__data

    @property
    def times(self) -> numpy.ndarray:
        return self.data["time"].values

    @property
    def coordinates(self) -> numpy.ndarray:
        return self.data[["x", "y", "altitude"]].values

    @property
    def altitudes(self) -> numpy.ndarray:
        return self.coordinates[:, -1]

    @property
    def intervals(self) -> numpy.ndarray:
        return self.data["interval"].values

    @property
    def overground_distances(self) -> numpy.ndarray:
        """overground distances between packets"""
        return self.data["overground_distance"].values

    @property
    def ascents(self) -> numpy.ndarray:
        """differences in altitude between packets"""
        return self.data["ascent"].values

    @property
    def ascent_rates(self) -> numpy.ndarray:
        """instantaneous ascent rates between packets"""
        return self.data["ascent_rate"].values

    @property
    def ground_speeds(self) -> numpy.ndarray:
        """instantaneous overground speeds between packets"""
        return self.data["ground_speed"].values

    @property
    def cumulative_overground_distances(self) -> numpy.ndarray:
        """cumulative overground distances from start"""
        return numpy.cumsum(self.overground_distances)

    @property
    def time_to_ground(self) -> timedelta:
        """estimated time to reach the ground at the current rate of descent"""

        current_ascent_rate = self.ascent_rates[-1]
        if current_ascent_rate < 0:
            # TODO implement landing location as the intersection of the predicted descent track with a local DEM
            # TODO implement a time to impact calc based off of standard atmo
            return timedelta(seconds=self.altitudes[-1] /
                             abs(current_ascent_rate))
        else:
            return timedelta(seconds=-1)

    @property
    def distance_downrange(self) -> float:
        """direct overground distance between first and last packets only"""
        if len(self.packets) > 0:
            return self.packets[-1].overground_distance(
                self.coordinates[0, :2])
        else:
            return 0.0

    @property
    def length(self) -> float:
        """total length of the packet track over the ground"""
        return sum(
            [distance.overground for distance in self.packets.difference])

    def __getitem__(
        self, index: Union[int, Iterable[int], slice]
    ) -> Union[LocationPacket, "LocationPacketTrack"]:
        if isinstance(index, str):
            try:
                index = parse_date(index)
            except:
                index = int(index)
        if isinstance(index, int):
            return self.packets[index]
        elif isinstance(index, Iterable) or isinstance(index, slice):
            if isinstance(index, numpy.ndarray) and index.dtype == bool:
                index = numpy.where(index)[0]
            if isinstance(index, slice) or len(index) > 0:
                packets = self.packets[index]
            else:
                packets = None
            return self.__class__(packets=packets,
                                  crs=self.crs,
                                  **self.attributes)
        elif isinstance(index, (datetime, numpy.datetime64)):
            if not isinstance(index, numpy.datetime64):
                index = numpy.datetime64(index)
            matching_packets = []
            for packet in self.packets:
                if packet.time == index:
                    matching_packets.append(packet)
            if len(matching_packets) == 0:
                maximum_interval = max(self.intervals)
                if (index > min(self.times) - maximum_interval
                        and index < max(self.times) + maximum_interval):
                    smallest_difference = None
                    closest_time = None
                    for packet in self.packets:
                        packet_time = numpy.datetime64(packet.time)
                        difference = abs(packet_time - index)
                        if (smallest_difference is None
                                or difference < smallest_difference):
                            smallest_difference = difference
                            closest_time = packet_time
                    return self[closest_time]
                else:
                    raise IndexError(
                        f"time index out of range: {min(self.times) - maximum_interval} <= {index} <= {max(self.times) + maximum_interval}"
                    )
            elif len(matching_packets) == 1:
                return matching_packets[0]
            else:
                return self.__class__(packets=matching_packets,
                                      name=self.name,
                                      crs=self.crs)
        else:
            raise ValueError(f"unrecognized index: {index}")

    def __iter__(self):
        return iter(self.packets)

    def __reversed__(self):
        return reversed(self.packets)

    def __contains__(self, item) -> bool:
        return item in self.packets

    def __len__(self) -> int:
        return len(self.packets)

    def __eq__(self, other) -> bool:
        return self.packets == other.packets and self.attributes == other.attributes

    def __str__(self) -> str:
        return str(list(self))

    @property
    def dataframe(self) -> DataFrame:
        return DataFrame({
            "name": [self.name for _ in range(len(self))],
            "times":
            self.times,
            "x":
            self.coordinates[:, 0],
            "y":
            self.coordinates[:, 1],
            "z":
            self.coordinates[:, 2],
            "intervals":
            self.intervals,
            "overground_distances":
            self.overground_distances,
            "ascents":
            self.ascents,
            "ascent_rates":
            self.ascent_rates,
            "ground_speeds":
            self.ground_speeds,
            "cumulative_overground_distances":
            self.cumulative_overground_distances,
        })

    @classmethod
    def from_file(cls, filename: PathLike) -> List["LocationPacketTrack"]:
        """
        load packet tracks from the given file

        :param filename: file path to GeoJSON
        :return: packet tracks
        """

        with open(filename) as input_file:
            data = geojson.load(input_file)

        points = []
        linestrings = []
        for feature in data["features"]:
            if feature["geometry"]["type"] == "Point":
                points.append(feature)
            elif feature["geometry"]["type"] == "LineString":
                linestrings.append(feature)

        tracks = []
        for linestring in linestrings:
            packets = []
            for coordinate in linestring["geometry"]["coordinates"]:
                for index, point in enumerate(points):
                    if point["geometry"]["coordinates"] == coordinate:
                        packet = LocationPacket(
                            x=point["geometry"]["coordinates"][0],
                            y=point["geometry"]["coordinates"][1],
                            z=point["geometry"]["coordinates"][2],
                            **point["properties"],
                        )
                        packets.append(packet)
                        if index != 0:
                            points.pop(index)
                        break
            track = LocationPacketTrack(packets=packets,
                                        **linestring["properties"])

            tracks.append(track)

        return tracks