# -*- 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