Skip to content

Commit

Permalink
Merge pull request #21 from brotherjack/master
Browse files Browse the repository at this point in the history
Allows for a greater number of datetime strings to be parsed
  • Loading branch information
xue35 committed Sep 24, 2020
2 parents 360742f + 045c8e0 commit 19fd21e
Show file tree
Hide file tree
Showing 8 changed files with 279 additions and 83 deletions.
22 changes: 18 additions & 4 deletions Manifest.toml
@@ -1,3 +1,5 @@
# This file is machine-generated - editing it directly is not advised

[[Base64]]
uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"

Expand Down Expand Up @@ -58,9 +60,14 @@ deps = ["Mmap"]
uuid = "8bb1440f-4735-579b-a4ab-409b98df4dab"

[[Distributed]]
deps = ["LinearAlgebra", "Random", "Serialization", "Sockets"]
deps = ["Random", "Serialization", "Sockets"]
uuid = "8ba89e20-285c-5b6f-9357-94700520ee1b"

[[ExprTools]]
git-tree-sha1 = "6f0517056812fd6aa3af23d4b70d5325a2ae4e95"
uuid = "e2ba6199-217a-4e67-a87a-7c52f15ade04"
version = "0.1.1"

[[EzXML]]
deps = ["BinaryProvider", "Libdl", "Pkg", "Printf", "Test"]
git-tree-sha1 = "ad00b79cca4bb3eabb4209217859c553af4401f5"
Expand All @@ -78,7 +85,7 @@ uuid = "0ef565a4-170c-5f04-8de2-149903a85f3d"
version = "0.5.0"

[[InteractiveUtils]]
deps = ["LinearAlgebra", "Markdown"]
deps = ["Markdown"]
uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240"

[[IteratorInterfaceExtensions]]
Expand All @@ -88,6 +95,7 @@ uuid = "82899510-4779-5014-852e-03e436cf321d"
version = "0.1.1"

[[LibGit2]]
deps = ["Printf"]
uuid = "76f85450-5226-5b5a-8eaa-529ad045b433"

[[Libdl]]
Expand All @@ -113,14 +121,20 @@ version = "0.4.0"
[[Mmap]]
uuid = "a63ad114-7e13-5084-954f-fe012c677804"

[[Mocking]]
deps = ["ExprTools"]
git-tree-sha1 = "916b850daad0d46b8c71f65f719c49957e9513ed"
uuid = "78c3b35d-d492-501b-9361-3d52fe80e533"
version = "0.7.1"

[[OrderedCollections]]
deps = ["Random", "Serialization", "Test"]
git-tree-sha1 = "c4c13474d23c60d20a67b217f1d7f22a40edf8f1"
uuid = "bac558e1-5e72-5ebc-8fee-abe8a469f55d"
version = "1.1.0"

[[Pkg]]
deps = ["Dates", "LibGit2", "Markdown", "Printf", "REPL", "Random", "SHA", "UUIDs"]
deps = ["Dates", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "UUIDs"]
uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"

[[Printf]]
Expand Down Expand Up @@ -215,7 +229,7 @@ uuid = "3bb67fe8-82b1-5028-8e26-92a6c54297fa"
version = "0.9.4"

[[UUIDs]]
deps = ["Random"]
deps = ["Random", "SHA"]
uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"

[[Unicode]]
Expand Down
1 change: 1 addition & 0 deletions Project.toml
Expand Up @@ -8,4 +8,5 @@ DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
EzXML = "8f5d6c58-4d21-5cfd-889c-e3ad7ee6a615"
Geodesy = "0ef565a4-170c-5f04-8de2-149903a85f3d"
Mocking = "78c3b35d-d492-501b-9361-3d52fe80e533"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
179 changes: 119 additions & 60 deletions src/TCX.jl
@@ -1,15 +1,21 @@
module TCX
using EzXML, Dates, DataFrames, Geodesy
using EzXML, Dates, DataFrames, Geodesy, Mocking
import Base.show

export parse_tcx_dir, parse_tcx_file, getActivityType, getDataFrame, getDistance, getDistance2, getDuration, getAverageSpeed, getAveragePace

const OK = 200
const CLIENT_ERROR = 400
const CLIENT_TCX_ERROR = 401
const NOT_FOUND = 404
const SERVER_ERROR = 500

struct TrackPoint
Time::DateTime
Latitude::Float64
Longtitude::Float64
HeartRateBpm::Int32
AltitueMeter::Float64
AltitudeMeter::Float64
DistanceMeter::Float64
end

Expand All @@ -23,102 +29,127 @@ struct TCXRecord
TrackPoints::Array{TrackPoint}
end

DictDateFormats = Dict(
:24 => "yyyy-mm-ddTHH:MM:SS.sssZ",
:20 => "yyyy-mm-ddTHH:MM:SSZ",
)

function parse_tcx_file(file::String)
file_path = abspath(file)
if isfile(file_path) == false
return 404, nothing
end

xmldoc = try readxml(file_path)
catch e
if isa(e, EzXML.XMLError)
# Not a valid XML document
@warn "Invalid XML document: $file_path"
return 400, nothing
else
return 500, nothing
end
end

root_element = root(xmldoc)
function parse_tcx(tcxdoc::EzXML.Document)
root_element = root(tcxdoc)
# Check if TCX
if nodename(root_element) != "TrainingCenterDatabase"
@warn "Invalid TCX document: $file_path"
return 400, nothing
return CLIENT_TCX_ERROR, nothing
end

# Type - "/*/*[1]/*[1]/@Sport"
aType = nodecontent(findfirst("/*/*[1]/*[1]/@Sport", xmldoc))
aType = nodecontent(findfirst("/*/*[1]/*[1]/@Sport", tcxdoc))
# Id - "/*/*[1]/*/*[1]"
xid =nodecontent(findfirst("/*/*[1]/*/*[1]", xmldoc))
aId = DateTime(xid,DictDateFormats[length(xid)])
xid = nodecontent(findfirst("/*/*[1]/*/*[1]", tcxdoc))

aId = convertToDateTime(xid)
# Name = "/*/*[1]/*[1]/*[3]"
aName = nodecontent(findfirst("/*/*[1]/*/*[2]", xmldoc))
aName = nodecontent(findfirst("/*/*[1]/*/*[2]", tcxdoc))
# Lap - "/*/*[1]/*/*[2]"
# TotalSeconds - "/*/*[1]/*/*[2]/*[1]"
aTime = parse(Float64, nodecontent(findfirst("/*/*[1]/*/*[2]/*[1]", xmldoc)))
aDistance = parse(Float64, nodecontent(findfirst("/*/*[1]/*/*[2]/*[2]", xmldoc)))
aTime = parse(Float64, nodecontent(findfirst("/*/*[1]/*/*[2]/*[1]", tcxdoc)))
aDistance = parse(Float64, nodecontent(findfirst("/*/*[1]/*/*[2]/*[2]", tcxdoc)))
# DistanceMeters - "/*/*[1]/*/*[2]/*[2]"
# AverageHeartRateBpm - "/*/*[1]/*/*[2]/*[5]/*[1]"
xbpm = findfirst("/*/*[1]/*/*[2]/*[5]/*[1]", xmldoc)
xbpm = findfirst("/*/*[1]/*/*[2]/*[5]/*[1]", tcxdoc)
if xbpm === nothing
aHeartRateBpm = 0
else
aHeartRateBpm = parse(Int32, nodecontent(xbpm))
end
# TrackPoints - "/*/*[1]/*/*[2]/*[9]/*"
tp_Points = findall("/*/*[1]/*/*[2]/*[9]/*", xmldoc)
tp_Points = findall("/*/*[1]/*/*[2]/*[9]/*", tcxdoc)
aTrackPoints = Array{TrackPoint, size(tp_Points, 1)}[]
for tp in tp_Points
xtime = nodecontent(findfirst("./*[1]", tp))
tp_time = DateTime(xtime, DictDateFormats[length(xtime)])
xlat = findfirst("./*[2]/*[1]", tp)
if xlat !== nothing
tp_lat = parse(Float64, nodecontent(xlat))
else
continue
end
tp_lont = parse(Float64, nodecontent(findfirst("./*[2]/*[2]", tp)))
xbpm = findfirst("./*[5]/*[1]", tp)
if xbpm !== nothing
tp_bpm = parse(Int32, nodecontent(findfirst("./*[5]/*[1]", tp)))
else
tp_bm = 0
end
tp_dist = parse(Float64, nodecontent(findfirst("./*[3]", tp)))
tp_alt = parse(Float64, nodecontent(findfirst("./*[4]", tp)))
xtime = nodecontent(findfirst("./*[local-name()='Time']", tp))
tp_time = convertToDateTime(xtime)
tp_lat = parseNode(Float64, "./*[local-name()='Position']/*[local-name()='LatitudeDegrees']", tp)
tp_lont = parseNode(Float64, "./*[local-name()='Position']/*[local-name()='LatitudeDegrees']", tp)
tp_bpm = parseNode(Int32, "./*[local-name()='HeartRateBpm']/*[1]", tp)
tp_dist = parseNode(Float64, "./*[local-name()='TPX']", tp)
tp_alt = parseNode(Float64, "./*[local-name()='AltitudeMeters']", tp)

aTrackPoints = vcat(aTrackPoints, TrackPoint(tp_time, tp_lat, tp_lont, tp_bpm, tp_dist, tp_alt))
end

return 200, TCXRecord(aId, aName, aType, aDistance, aTime, aHeartRateBpm, aTrackPoints)
return OK, TCXRecord(aId, aName, aType, aDistance, aTime, aHeartRateBpm, aTrackPoints)
end

function parse_tcx_str(str::String)
try
status, parsed_tcx = parse_tcx(EzXML.parsexml(str))
warn_on_tcx_error(status, str, false)
return status, parsed_tcx
catch e
if isa(e, EzXML.XMLError)
@error "Invalid XML string: $str"
return CLIENT_ERROR, nothing
end
end
end

function parse_tcx_file(file::String)
file_path = abspath(file)
if isfile(file_path) == false
return NOT_FOUND, nothing
end
xmldoc = try @mock EzXML.readxml(file_path)
catch e
if isa(e, EzXML.XMLError)
# Not a valid XML document
@warn "Invalid XML document: $file_path"
return CLIENT_ERROR, nothing
else
throw(e)
end
end

status, parsed_tcx = parse_tcx(xmldoc)
warn_on_tcx_error(status, file_path, true)

return status, parsed_tcx
end

function warn_on_tcx_error(status::Int, thing::String, isFile::Bool)
if status == CLIENT_TCX_ERROR
@warn "Invalid TCX $(isFile ? document : string): $(thing)"
end
end

#=
= Parses an XML node based on an XPATH and data type.
=
= If the node is a `nothing` value, the function returns the data
= type's version of 0
=#
function parseNode(dType, path, node)
node_check = findfirst(path, node)
if node_check !== nothing
return parse(dType, nodecontent(node_check))
else
return dType(0)
end
end

function parse_tcx_dir(path::String)
if ispath(path) == false
@warn "Invalid path: $path"
return 500, nothing
return SERVER_ERROR, nothing
end

tcxArray = Array{TCXRecord}[]
searchdir(path, key) = filter(x->occursin(key, x), readdir(path))

for f in searchdir(path, ".tcx")
err, tcx = parse_tcx_file(joinpath(path, f))
if err == 200
if err == OK
tcxArray = vcat(tcxArray, tcx)
end
end

if length(tcxArray) > 0
return 200, tcxArray
return OK, tcxArray
else
return 404, nothing
return NOT_FOUND, nothing
end
end

Expand Down Expand Up @@ -149,8 +180,8 @@ function getDistance2(record::TCXRecord)
for i in 1:num_of_rows
if i < num_of_rows
total_distance += distance(
LLA(df[i, :Latitude], df[i, :Longtitude], df[i, :AltitueMeter]),
LLA(df[i+1, :Latitude], df[i+1, :Longtitude], df[i+1, :AltitueMeter])
LLA(df[i, :Latitude], df[i, :Longtitude], df[i, :AltitudeMeter]),
LLA(df[i+1, :Latitude], df[i+1, :Longtitude], df[i+1, :AltitudeMeter])
)
end
end
Expand All @@ -169,6 +200,34 @@ function getDuration(record::TCXRecord)
return record.DurationStatic
end

#=
= Converts a datetime string into the proper datetime based on string length.
=
= Will assume that an ArgumentError is due to
= https://github.com/JuliaLang/julia/issues/23049 and will attempt to work
= around this.
=#
function convertToDateTime(datestr::String)::DateTime
m = match(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z?|\.\d{1,3}Z?)", datestr)
format_prefix = "yyyy-mm-ddTHH:MM:SS"
if m === nothing
msg = "'$(datestr)' is improperly formatted. Must be in the form "
msg = msg * "'$(format_prefix)Z' or '$(format_prefix).sssZ'"
throw(ArgumentError(msg))
else
suffix = replace(m.captures[1], r"\d" => "s")
try
return @mock DateTime(m.match, format_prefix * suffix)
catch e
if isa(e, ArgumentError)
# OK! FINE! NO Z THEN!
return DateTime(m.match[1:end-1], format_prefix * (suffix[1:end-1]))
else
throw(e)
end
end
end
end

Base.show(io::IO, tcx::TCXRecord) = print(io, "$(tcx.ActivityType) $(tcx.DistanceStatic/1000) km at $(tcx.Id) for $(tcx.DurationStatic) seconds.")
end #module_end

10 changes: 7 additions & 3 deletions test/runtests.jl
@@ -1,9 +1,13 @@
using TCX
using Test, TCX

tcx_sample_file="centry_park_run.tcx"
tcx_centrypark_run_file="centry_park_run.tcx"
tcx_treadmill_run_file="treadmill_run.tcx"
gpx_sample_file="shanghai_marathon_2018.gpx"

include("test_tcx_read_file.jl")
include("test_tcx_read_dir.jl")
@testset "TCX tests" begin
include("test_tcx_read_file.jl")
include("test_tcx_read_dir.jl")
include("test_tcx_read_str.jl")
include("test_tcx.jl")
end

0 comments on commit 19fd21e

Please sign in to comment.