Source code for tracklib.io.TrackReader

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

# For type annotation
from __future__ import annotations   
from typing import Union, Literal

import csv
import io
import os
from xml.dom import minidom

from tracklib.core.ObsTime import ObsTime
from tracklib.core.ObsCoords import ENUCoords, GeoCoords, ECEFCoords
from tracklib.core.Obs import Obs
from tracklib.core.Track import Track
from tracklib.core.TrackCollection import TrackCollection
import tracklib.core.Utils as utils
from tracklib.io.TrackFormat import TrackFormat


class TrackReader:
    """
    This class offers static methods to load track or track collection
    from GPX, CSV files. Geometry can be structured in coordinates or in a wkt.
    """

[docs] @staticmethod def readFromCsv (path: str, id_E:int=-1, id_N:int=-1, id_U:int=-1, id_T:int=-1, separator:str=",", DateIni=-1, timeUnit=1, h=0, com="#", no_data_value=-999999, srid="ENUCoords", read_all=False, selector=None, verbose=False, ) -> Union(Track, TrackCollection): """ Read track(s) from CSV file(s) with geometry structured in coordinates. The method assumes a single track in file. If only path is provided as input parameters: file format is infered from extension according to file track_file_format If only path and a string s parameters are provied, the name of file format is set equal to s. Parameters ----------- :param str path: file or directory :param int id_E: index (starts from 0) of column containing coordinate X (for ECEF), longitude (GEO) or E (ENU) -1 if file format is used :param int id_N: index (starts from 0) of column containing coordinate Y (for ECEF), latitude (GEO) or N (ENU) -1 if file format is used :param int id_U: index (starts from 0) of column containing Z (for ECEF), height or altitude (GEO/ENU) -1 if file format is used :param int id_T: index (starts from 0) of column containing timestamp (in seconds x timeUnit or in time_fmt format) -1 if file format is used :param str separator: separating characters (can be multiple characters). Can be c (comma), b (blankspace), s (semi-column) :param GPSTime DateIni: initial date (in time_fmt format) if timestamps are provided in seconds (-1 if not used) :param float timeUnit: number of seconds per unit of time in id_T column :param int h: number of heading line :param str com: comment character (lines starting with cmt on the top left are skipped) :param int no_data_value: a special float or integer indicating that record is non-valid and should be skipped :param str srid: coordinate system of points ("ENU", "Geo" or "ECEF") :param bool read_all: if flag read_all is True, read AF in the tag extension :param Selector selector: to select track with a Selection which combine different constraint :return: a track or a collection of tracks contains in wkt files. """ if os.path.isdir(path): TRACES = TrackCollection() LISTFILE = os.listdir(path) for f in LISTFILE: p = path + "/" + f trace = TrackReader.readFromCsv( p, id_E, id_N, id_U, id_T, separator, DateIni, timeUnit, h, com, no_data_value, srid, read_all, selector, verbose, ) if trace is None: continue if not selector is None: if not selector.contains(trace): continue TRACES.addTrack(trace) return TRACES elif not os.path.isfile(path): return None if verbose: print("Loading file " + path) # ------------------------------------------------------- # Infering file format from extension or name # ------------------------------------------------------- if id_N == -1: if id_E == -1: fmt = TrackFormat(path, 1) # Read by extension else: fmt = TrackFormat(id_E, 0) # Read by name else: fmt = TrackFormat("", -1) # Read from input parameters fmt.id_E = id_E fmt.id_N = id_N fmt.id_U = id_U fmt.id_T = id_T fmt.DateIni = DateIni fmt.separator = separator fmt.h = h fmt.com = com fmt.no_data_value = no_data_value fmt.srid = srid fmt.read_all = read_all # ------------------------------------------------------- # Reading data according to file format # ------------------------------------------------------- if os.path.basename(path).split(".")[0] != None: track = Track(track_id=os.path.basename(path).split(".")[0]) else: track = Track() time_fmt_save = ObsTime.getReadFormat() ObsTime.setReadFormat(fmt.time_fmt) id_special = [fmt.id_E, fmt.id_N] if fmt.id_U >= 0: id_special.append(fmt.id_U) if fmt.id_T >= 0: id_special.append(fmt.id_T) with open(path) as fp: # Header for i in range(fmt.h): line = fp.readline() if line[0] == fmt.com: line = line[1:] name_non_special = line.split(fmt.separator) line = fp.readline().strip() # Obs per line while line: if line.strip()[0] == fmt.com: name_non_special = line[1:].split(fmt.separator) line = fp.readline().strip() continue fields = line.strip().split(fmt.separator) fields = [s for s in fields if s] if fmt.id_T != -1: if isinstance(fmt.DateIni, int): time = ObsTime.readTimestamp(fields[fmt.id_T]) else: time = fmt.DateIni.addSec((float)(fields[fmt.id_T])*timeUnit) else: time = ObsTime() E = (float)(fields[fmt.id_E]) N = (float)(fields[fmt.id_N]) if (int(E) != fmt.no_data_value) and (int(N) != fmt.no_data_value): if fmt.id_U >= 0: U = (float)(fields[fmt.id_U]) else: U = 0 if not fmt.srid.upper() in [ "ENUCOORDS", "ENU", "GEOCOORDS", "GEO", "ECEFCOORDS", "ECEF", ]: print("Error: unknown coordinate type [" + str(srid) + "]") exit() if fmt.srid.upper() in ["ENUCOORDS", "ENU"]: point = Obs(ENUCoords(E, N, U), time) if fmt.srid.upper() in ["GEOCOORDS", "GEO"]: point = Obs(GeoCoords(E, N, U), time) if fmt.srid.upper() in ["ECEFCOORDS", "ECEF"]: point = Obs(ECEFCoords(E, N, U), time) track.addObs(point) line = fp.readline().strip() fp.close() # Reading other features if fmt.read_all: name_non_special = [s.strip() for s in name_non_special if s] for i in range(len(fields)): if not (i in id_special): track.createAnalyticalFeature(name_non_special[i]) with open(path) as fp: # Header for i in range(fmt.h): fp.readline() line = fp.readline() counter = 0 while line: if line.strip()[0] == fmt.com: line = fp.readline().strip() continue fields = line.split(fmt.separator) fields = [s for s in fields if s] for i in range(len(fields)): if not (i in id_special): val = fields[i].strip() if not (name_non_special[i][-1] == "&"): try: val = float(val) except ValueError: val = str(val).replace('"', "") track.setObsAnalyticalFeature( name_non_special[i], counter, val ) line = fp.readline().strip() counter += 1 fp.close() ObsTime.setReadFormat(time_fmt_save) if track is None: return None if not selector is None: if not selector.contains(track): return None if verbose: print (" File " + path + " loaded: " + (str)(track.size()) + " point(s) registered") return track
NMEA_GGA = "GGA" NMEA_RMC = "RMC" NMEA_GPGGA = "GPGGA" NMEA_GNGGA = "GNGGA" NMEA_GPRMC = "GPRMC" NMEA_GNRMC = "GNRMC"
[docs] @staticmethod def readFromNMEA(path, frame=NMEA_GGA): """The method assumes a single track in file.""" track = Track() if frame.upper() == TrackReader.NMEA_GGA: pattern = ["$GNGGA"] TMP_NB_SATS = [] TMP_HDOP = [] with open(path, "rb") as fp: line = str(fp.readline()) while not (line in ["", "b''"]): if pattern[0] in line: frame = line.split(pattern[0])[1] fields = frame.split(",") h = int(fields[1][0:2]) m = int(fields[1][2:4]) s = int(fields[1][4:6]) ms = int(fields[1][7:9]) time = ObsTime(hour=h, min=m, sec=s, ms=ms) if fields[4] == "": line = str(fp.readline()) continue if fields[2] == "": line = str(fp.readline()) continue lon = float(fields[4]) / 100 lat = float(fields[2]) / 100 lon = int(lon) + (lon - int(lon)) * 100 / 60 lat = int(lat) + (lat - int(lat)) * 100 / 60 hgt = float(fields[9]) if fields[5] == "W": lon = -lon if fields[3] == "S": lat = -lat track.addObs(Obs(GeoCoords(lon, lat, hgt), time)) TMP_NB_SATS.append(int(fields[7])) TMP_HDOP.append(float(fields[8])) # print(fields) line = str(fp.readline()) track.createAnalyticalFeature("nb_sats") track.createAnalyticalFeature("hdop") for i in range(len(TMP_NB_SATS)): track.setObsAnalyticalFeature("nb_sats", i, TMP_NB_SATS[i]) track.setObsAnalyticalFeature("hdop", i, TMP_HDOP[i]) return track
[docs] @staticmethod def readFromWkt(path:str, id_geom, id_user=-1, id_track=-1, separator=";", h=0, srid="ENUCoords", bboxFilter=None, doublequote:bool=False, verbose=False) -> TrackCollection: """ Read track(s) (one per line) from a CSV file, with geometry provided in wkt. Parameters ----------- :param str path: csv file :param int id_geom: index of the column that contains the geometry :param int id_user: index of the column that contains the id of the user of the track :param int id_track: index of the column that contains the id of the track :param str separator: separating characters (can be multiple characters). Can be c (comma), b (blankspace), s (semi-column) :param int h: number of heading line :param str srid: coordinate system of points ("ENU", "Geo" or "ECEF") :param ?? bboxFilter: :param bool doublequote: when True, quotechar is doubled. When False, the escapechar is used as a prefix to the quotechar :return: collection of tracks contains in wkt files. """ if separator == " ": print("Error: separator must not be space for reading WKT file") exit() TRACES = TrackCollection() with open(path, newline="") as csvfile: spamreader = csv.reader(csvfile, delimiter=separator, doublequote=doublequote) # Header for i in range(h): next(spamreader) for fields in spamreader: if len(fields) <= 0: continue track = Track() if id_user >= 0: track.uid = fields[id_user] if id_track >= 0: track.tid = fields[id_track] wkt = fields[id_geom] if wkt[0:4] == "POLY": wkt = fields[id_geom].split("((")[1].split("))")[0] wkt = wkt.split(",") elif wkt[0:4] == "LINE": wkt = fields[id_geom].split("(")[1].split(")")[0] wkt = wkt.split(",") elif wkt[0:7] == "MULTIPO": wkt = fields[id_geom].split("((")[1].split("))")[0] wkt = wkt.split(",") if wkt[0] == "(": wkt = wkt[1:] wkt = wkt.split("),(")[0] # Multipolygon not handled yet else: print("this type of wkt is not yet implemented") for s in wkt: sl = s.split(" ") x = float(sl[0]) y = float(sl[1]) if len(sl) == 3: z = float(sl[2]) else: z = 0.0 point = Obs(utils.makeCoords(x, y, z, srid.upper()), ObsTime()) track.addObs(point) if not (bboxFilter is None): xmin = bboxFilter[0] ymin = bboxFilter[1] xmax = bboxFilter[2] ymax = bboxFilter[3] 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: break if not inside: continue TRACES.addTrack(track) if verbose: print(len(TRACES), " wkt tracks loaded") return TRACES
# ========================================================================= # GPX #
[docs] @staticmethod def readFromGpx(path:str, srid:Literal["GEO", "ENU"] ="GEO", type: Literal["trk", "rte"]="trk", read_all=False) -> TrackCollection: """ Reads (multiple) tracks or routes from gpx file(s). Parameters ----------- :param str path: file or directory :param str srid: coordinate system of points ("ENU", "Geo" or "ECEF") :param str type: may be “trk” to load track points or “rte” to load vertex from the route :param bool read_all: if flag read_all is True, read AF in the tag extension :return: collection of tracks contains in Gpx files. """ TRACES = TrackCollection() if os.path.isdir(path): LISTFILE = os.listdir(path) for f in LISTFILE: if path[len(path)-1:] == '/': collection = TrackReader.readFromGpx(path + f) else: collection = TrackReader.readFromGpx(path + '/' + f) TRACES.addTrack(collection.getTrack(0)) return TRACES elif os.path.isfile(path) or isinstance(io.StringIO(path), io.IOBase): format_old = ObsTime.getReadFormat() ObsTime.setReadFormat("4Y-2M-2D 2h:2m:2s") doc = minidom.parse(path) trks = doc.getElementsByTagName(type) for trk in trks: if os.path.basename(path).split(".")[0] != None: trace = Track(track_id=os.path.basename(path).split(".")[0]) else: trace = Track() extensions = dict() trkpts = trk.getElementsByTagName(type + "pt") for trkpt in trkpts: lon = float(trkpt.attributes["lon"].value) lat = float(trkpt.attributes["lat"].value) hgt = -1 # TODO: utils.NAN eles = trkpt.getElementsByTagName("ele") if eles.length > 0: hgt = float(eles[0].firstChild.data) time = "" times = trkpt.getElementsByTagName("time") if times.length > 0: time = ObsTime(times[0].firstChild.data) else: time = ObsTime() point = Obs(utils.makeCoords(lon, lat, hgt, srid), time) trace.addObs(point) if read_all: tagextentions = trkpt.getElementsByTagName("extensions") for tagextention in tagextentions: for ext in tagextention.childNodes: if ext.nodeType == minidom.Node.ELEMENT_NODE: if ext.tagName not in extensions: extensions[ext.tagName] = [] val = ext.firstChild.nodeValue if utils.isfloat(val): extensions[ext.tagName].append(float(val)) elif utils.islist(val): import json extensions[ext.tagName].append(json.loads(val)) else: extensions[ext.tagName].append(val) # .. # if read_all: for key in extensions.keys(): trace.createAnalyticalFeature(key) for i in range(trace.size()): trace.setObsAnalyticalFeature(key, i, extensions[key][i]) TRACES.addTrack(trace) # pourquoi ? # --> pour remettre le format comme il etait avant la lecture :) ObsTime.setReadFormat(format_old) collection = TrackCollection(TRACES) return collection else: print ('path is not a file, not a dir') return None