Source code for core.track_collection

# -*- coding: utf-8 -*-

"""
© Copyright Institut National de l'Information Géographique et Forestière (2020)
Contributors: 
    Yann Méneroux
    Marie-Dominique Van Damme
Creation date: 1th november 2020

tracklib library provides a variety of tools, operators and 
functions to manipulate GPS trajectories. It is a open source contribution 
of the LASTIG laboratory at the Institut National de l'Information 
Géographique et Forestière (the French National Mapping Agency).
See: https://tracklib.readthedocs.io
 
This software is governed by the CeCILL-C license under French law and
abiding by the rules of distribution of free software. You can  use, 
modify and/ or redistribute the software under the terms of the CeCILL-C
license as circulated by CEA, CNRS and INRIA at the following URL
"http://www.cecill.info". 

As a counterpart to the access to the source code and rights to copy,
modify and redistribute granted by the license, users are provided only
with a limited warranty  and the software's author,  the holder of the
economic rights,  and the successive licensors  have only  limited
liability. 

In this respect, the user's attention is drawn to the risks associated
with loading,  using,  modifying and/or developing or reproducing the
software by the user in light of its specific status of free software,
that may mean  that it is complicated to manipulate,  and  that  also
therefore means  that it is reserved for developers  and  experienced
professionals having in-depth computer knowledge. Users are therefore
encouraged to load and test the software's suitability as regards their
requirements in conditions enabling the security of their systems and/or 
data to be ensured and,  more generally, to use and operate it in the 
same conditions as regards security. 

The fact that you are presently reading this means that you have had
knowledge of the CeCILL-C license and that you accept its terms.



This module contain a class to manage the collections of tracks

"""

from typing import Literal   
import matplotlib.pyplot as plt
from random import randint

import tracklib as tracklib
from tracklib.core import removeNan, listify, compLike
#from tracklib.util.exceptions import *


[docs] class TrackCollection: """TODO"""
[docs] def __init__(self, TRACES=[]): """ TRACES: list of Track """ self.__TRACES = TRACES.copy() self.spatial_index = None
[docs] def addTrack(self, track): """TODO""" self.__TRACES.append(track)
[docs] def size(self): """TODO""" return len(self.__TRACES)
[docs] def length(self): """TODO""" length = 0 for track in self: length += track.length() return length
[docs] def duration(self): """TODO""" duration = 0 for track in self: duration += track.duration() return duration
[docs] def getTracks(self): """TODO""" return self.__TRACES
[docs] def getTrack(self, i): """TODO""" return self.__TRACES[i]
[docs] def getTrackWithTid(self, tid): for i in range(self.size()): if self.getTrack(i).tid == tid: return self.getTrack(i) return None
[docs] def copy(self): """TODO""" TRACKS = TrackCollection() for i in range(self.size()): TRACKS.addTrack(self.getTrack(i).copy()) return TRACKS
[docs] def setTimeZone(self, zone): """TODO""" for i in range(len(self)): self[i].setTimeZone(zone)
[docs] def convertToTimeZone(self, zone): """TODO""" for i in range(len(self)): self[i].convertToZone(zone)
# Average frequency of tracks
[docs] def frequency(self, mode="temporal"): """TODO""" m = 0 for track in self: m += track.frequency(mode) return m / self.size()
def __iter__(self): """TODO""" yield from self.__TRACES # ========================================================================= # Spatial index creation, export and import functions # =========================================================================
[docs] def createSpatialIndex(self, resolution=None, verbose=True): """TODO""" self.spatial_index = tracklib.SpatialIndex(self, resolution, verbose)
[docs] def exportSpatialIndex(self, filename): """TODO""" self.spatial_index.save(filename)
[docs] def importSpatialIndex(self, filename): """TODO""" self.spatial_index = tracklib.SpatialIndex.load(filename)
# ========================================================================= # Track collection coordinate transformation # =========================================================================
[docs] def getSRID(self) -> int: """Return the SRID of collection :return: SRID of current collection """ return self.__TRACES[0].getSRID()
[docs] def toECEFCoords(self, base=None): """TODO""" if self.__TRACES[0].getSRID() == "Geo": for track in self.__TRACES: track.toECEFCoords() return if self.__TRACES[0].getSRID() == "ENU": if base == None: print( "Error: base coordinates should be specified for conversion ENU -> ECEF" ) exit() for track in self.__TRACES: track.toECEFCoords(base) return
[docs] def toENUCoords(self, base=None): """TODO""" if self.__TRACES[0].getSRID() in ["Geo", "ECEF"]: if base == None: base = self.__TRACES[0].getFirstObs().position message = "Warning: no reference point (base) provided for local projection to ENU coordinates. " message += "Arbitrarily used: " + str(base) print(message) for track in self.__TRACES: track.toENUCoords(base) return if self.__TRACES[0].getSRID() == "ENU": if base == None: print( "Error: new base coordinates should be specified for conversion ENU -> ENU" ) exit() for track in self.__TRACES: if track.base == None: print( "Error: former base coordinates should be specified for conversion ENU -> ENU" ) exit() track.toENUCoords(track.base, base) track.base = base.toGeoCoords() return base
[docs] def toGeoCoords(self, base=None): """TODO""" if self.__TRACES[0].getSRID() == "ECEF": for track in self.__TRACES: track.toGeoCoords() if self.__TRACES[0].getSRID() == "ENU": if base == None: print( "Error: base coordinates should be specified for conversion ENU -> Geo" ) exit() for track in self.__TRACES: track.toGeoCoords(base)
# Function to convert track to ENUCoords if it is in GeoCoords. Returns None # if no transformation operated, and returns used reference point otherwise
[docs] def toENUCoordsIfNeeded(self): """TODO""" base = None if self.getTrack(0).getSRID() in ["GEO", "Geo"]: base = self.getTrack(0).getObs(0).position.copy() self.toENUCoords(base) return base
# ========================================================================= # Thin plates smoothing # =========================================================================
[docs] def smooth(self, constraint=1e3): """TODO""" for track in self: track.smooth(constraint)
[docs] def summary(self): """ Print summary (complete wkt below) """ output = "-------------------------------------\n" output += "Number of GPS track: " + str(len(self.__TRACES)) + "\n" output += "-------------------------------------\n" SIZES = [] for trace in self.__TRACES: SIZES.append(trace.size()) output += " Nb of pt(s): " + str(SIZES) + "\n" # output += " Nb of pt(s): " + str(len(self.__POINTS)) + "\n" print(output)
[docs] def toWKT(self) -> str: """Transforms collection into WKT string, one linestring per track""" output = "MULTILINESTRING (" for track in self.__TRACES: output += "(" for i in range(track.size()): if self.getSRID() == "Geo": output += (str)(track[i].position.lon) + " " output += (str)(track[i].position.lat) elif self.getSRID() == "ENU": output += (str)(track[i].position.E) + " " output += (str)(track[i].position.N) if i != track.size() - 1: output += "," output += ")" output += ")" return output
[docs] def addAnalyticalFeature(self, algorithm, name=None): """TODO""" for trace in self.__TRACES: trace.addAnalyticalFeature(algorithm, name)
[docs] def getAnalyticalFeature(self, af_name, withNan=True): valuesAF = [] for track in self: values = track.getAnalyticalFeature(af_name) if not withNan: values = removeNan(values) valuesAF = valuesAF + values return valuesAF
[docs] def getTimestamps_str(self,withNan=True): """Return the timestamps in a list format.""" T = [] for track in self: for i in range(track.size()): T.append(track.getObs(i).timestamp.__str__()) return T
[docs] def operate(self, operator, arg1=None, arg2=None, arg3=None): """TODO""" for trace in self.__TRACES: trace.operate(operator, arg1, arg2, arg3)
[docs] def plot(self, symbols=None, markersize=[4], margin=0.05, append=False): """TODO""" if symbols is None: symbols = ["r-", "g-", "b-", "c-", "m-", "y-", "k-"] if len(self) == 0: return symbols = listify(symbols) markersize = listify(markersize) if not append: (xmin, xmax, ymin, ymax) = self.bbox().asTuple() dx = margin * (xmax - xmin) dy = margin * (ymax - ymin) plt.xlim([xmin - dx, xmax + dx]) plt.ylim([ymin - dy, ymax + dy]) Ns = len(symbols) Ms = len(markersize) for i in range(len(self.__TRACES)): trace = self.__TRACES[i] X = trace.getX() Y = trace.getY() plt.plot(X, Y, symbols[i % Ns], markersize=markersize[i % Ms])
[docs] def filterOnBBox(self, bbox): """TODO""" xmin, xmax, ymin, ymax = bbox.asTuple() for i in range(len(self) - 1, -1, -1): track = self[i] for j in range(len(track)): inside = True inside = inside & (track[j].position.getX() > xmin) inside = inside & (track[j].position.getY() > ymin) inside = inside & (track[j].position.getX() < xmax) inside = inside & (track[j].position.getY() < ymax) if not inside: self.removeTrack(track) break
[docs] def bbox(self): """TODO""" bbox = self.getTrack(0).bbox() for i in range(1, len(self)): bbox = bbox + self.getTrack(i).bbox() return bbox
[docs] def resample(self, delta, algo: Literal[1,2,3,4]=1, mode:Literal[1,2]=1): """Resampling tracks with linear interpolation :param delta: interpolation interval (time in sec if temporal mode is selected, space in meters if spatial). :param mode: Mode of interpolation. Available modes are: - MODE_SPATIAL (*mode=1*) - MODE_TEMPORAL (*mode=2*) :params algorithm: of interpolation. Available algorithm are : - ALGO_LINEAR (*algo=1*) - ALGO_THIN_SPLINES (*algo=2*) - ALGO_B_SPLINES (*algo=3*) - ALGO_GAUSSIAN_PROCESS (*algo=4*) **NB**: In temporal mode, argument may be: - an integer or float: interval in seconds - a list of timestamps where interpolation should be computed - a reference track """ for track in self: track.resample(delta, algo, mode)
[docs] def _collectionnify(tracks): """TODO""" if isinstance(tracks, list): return TrackCollection(tracks) else: return tracks
# ========================================================================= # Tracks simplification (returns a new track) # Tolerance is in the unit of track observation coordinates # MODE_SIMPLIFY_DOUGLAS_PEUCKER (1) # MODE_SIMPLIFY_VISVALINGAM (2) # =========================================================================
[docs] def simplify(self, tolerance, mode=1): """TODO""" output = self.copy() for i in range(len(output)): output[i] = output[i].simplify(tolerance, mode)
def __add__(self, collection): """[+] Concatenation of two track collections""" return TrackCollection(self.__TRACES + collection.__TRACES) # ------------------------------------------------------------ # [/] Even split track collection (returns n+1 collections) # ------------------------------------------------------------ def __truediv__(self, number): """TODO""" N = (int)(self.size() / number) # R = self.size()-N*number SPLITS = [] for i in range(number + 1): id_ini = i * N id_fin = min((i + 1) * N, self.size()) + 1 SPLITS.append(TrackCollection(self[id_ini:id_fin].copy())) return SPLITS # ------------------------------------------------------------ # [>] Removes first n points of track # ------------------------------------------------------------ def __gt__(self, nb_points): """TODO""" output = self.copy() for i in range(self.size()): output[i] = output[i] > nb_points return output # ------------------------------------------------------------ # [<] Removes last n points of track # ------------------------------------------------------------ def __lt__(self, nb_points): """TODO""" output = self.copy() for i in range(self.size()): output[i] = output[i] < nb_points return output # ------------------------------------------------------------ # [>=] Available operator # ------------------------------------------------------------ def __ge__(self, arg): """TODO""" return None # ------------------------------------------------------------ # [<=] Available operator # ------------------------------------------------------------ def __le__(self, arg): """TODO""" return None # ------------------------------------------------------------ # [!=] Available operator # ------------------------------------------------------------ def __neq__(self, arg): """TODO""" return None # ------------------------------------------------------------ # [Unary -] Available operator # ------------------------------------------------------------ def __neg__(self, arg): """TODO""" return None # ------------------------------------------------------------ # [**] Resample (spatial) according to a number of points # Linear interpolation and temporal resampling # ------------------------------------------------------------ def __pow__(self, nb_points): """TODO""" output = self.copy() for i in range(self.size()): output[i] **= nb_points return output # ------------------------------------------------------------ # [abs] Available operator # ------------------------------------------------------------ def __abs__(self): """TODO""" return None # ------------------------------------------------------------ # [len] Number of tracks in track collection # ------------------------------------------------------------ def __len__(self): """TODO""" return self.size() # ------------------------------------------------------------ # [-] Computes difference profile of 2 tracks # ------------------------------------------------------------ def __sub__(self, arg): """TODO""" print("Available operator not implemented yet") # ------------------------------------------------------------ # [*] Temporal resampling of tracks # ------------------------------------------------------------ def __mul__(self, number): """TODO""" output = self.copy() for i in range(self.size()): output[i] *= number return output # ------------------------------------------------------------ # [%] Remove one point out of n (or according to list pattern) # ------------------------------------------------------------ def __mod__(self, sample): """TODO""" output = self.copy() for i in range(self.size()): output[i] %= sample return output # ------------------------------------------------------------ # [//] Time resample of a tracks according to another track # ------------------------------------------------------------ def __floordiv__(self, track): """TODO""" output = self.copy() for t in output.__TRACES: t.resample(track) # Mode temporal / linear return output def __getitem__(self, n): """[[n]] Get and set track number n May be tuple with uid, tid """ if isinstance(n, tuple): tracks = TrackCollection() for track in self: if (compLike(track.uid, n[0])) and ( compLike(track.tid, n[1]) ): tracks.addTrack(track) return tracks return TrackCollection._collectionnify(self.__TRACES[n]) def __setitem__(self, n, track): self.__TRACES[n] = track # =========================================================================
[docs] def removeTrack(self, track): """TODO""" self.__TRACES.remove(track)
[docs] def removeEmptyTrack(self): """ Remove tracks without observation """ for track in self.__TRACES: if track.size() <= 0: self.removeTrack(track)
# ========================================================================= # SEGMENTATION, EXTRACT, REMOVE
[docs] def segmentation(self, afs_input, af_output, thresholds_max, mode_comparaison=1): """TODO""" for t in self.__TRACES: tracklib.algo.segmentation(t, afs_input, af_output, thresholds_max, mode_comparaison)
[docs] def split_segmentation(self, af_output): """ Découpe les traces suivant la segmentation définie par le paramètre af_output ET Remplace la trace par les traces splittées s'il y a une segmentation. """ NEW_TRACES = [] for track in self.__TRACES: TRACES_SPLIT = tracklib.algo.split(track, af_output) # on ajoute les traces splittées for split in TRACES_SPLIT: # print (split.size()) NEW_TRACES.append(split) return TrackCollection(NEW_TRACES)
# ========================================================================= # Adding noise to tracks # =========================================================================
[docs] def noise(self, sigma=5, kernel=None, force=False, cycle=False): """TODO""" for i in range(len(self)): self.__TRACES[i] = self.__TRACES[i].noise(sigma, kernel, force, cycle)
# ========================================================================= # Measures
[docs] def getLenth(self): L = [] for i in range(len(self)): L.append(self.__TRACES[i].length()) return L
# ========================================================================= #
[docs] def randNTracks(self, N): """ Return a sample of n tracks from the collection. Parameters ---------- N : int Returns ------- TrackCollection with n Tracks. """ if self.size() <= 0: return None if self.size() < N: return None if self.size() == N: return self TAB = set() while len(TAB) < N: n = randint(0, self.size()-1) TAB.add(n) sets = TrackCollection() for idx in TAB: sets.addTrack(self.getTrack(idx)) return sets