rowingdata 3.6.26__tar.gz → 3.7.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {rowingdata-3.6.26/rowingdata.egg-info → rowingdata-3.7.1}/PKG-INFO +3 -2
- rowingdata-3.7.1/rowingdata/fitwrite.py +344 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/rowingdata.py +105 -8
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/tcxtools.py +21 -2
- {rowingdata-3.6.26 → rowingdata-3.7.1/rowingdata.egg-info}/PKG-INFO +3 -2
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata.egg-info/SOURCES.txt +2 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata.egg-info/requires.txt +1 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/setup.py +10 -2
- rowingdata-3.7.1/testdata/cottwich.csv +796 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/tests/test_rowingdata.py +70 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/LICENSE +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/MANIFEST.in +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/README.rst +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/bin/crewnerddata.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/bin/crewnerddata.tcx +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/bin/testdata.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/.buildinfo +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/_images/2x20min.png +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/_images/editrower.JPG +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/_images/greghoc.png +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/_images/image001.png +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/_images/otwlogbook.JPG +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/_images/otwscreenshot.JPG +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/_images/screenshot.JPG +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/_images/screenshotlogbook.JPG +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/_images/woensdag.png +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/_sources/index.txt +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/_sources/modules.txt +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/_sources/rowingdata.txt +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/_static/ajax-loader.gif +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/_static/alabaster.css +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/_static/basic.css +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/_static/comment-bright.png +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/_static/comment-close.png +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/_static/comment.png +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/_static/custom.css +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/_static/doctools.js +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/_static/down-pressed.png +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/_static/down.png +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/_static/file.png +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/_static/jquery-1.11.1.js +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/_static/jquery.js +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/_static/minus.png +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/_static/plus.png +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/_static/pygments.css +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/_static/searchtools.js +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/_static/underscore-1.3.1.js +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/_static/underscore.js +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/_static/up-pressed.png +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/_static/up.png +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/_static/websupport.js +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/genindex.html +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/index.html +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/modules.html +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/objects.inv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/py-modindex.html +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/rowingdata.html +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/search.html +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/docs/_build/html/searchindex.js +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/__init__.py +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/__main__.py +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/boatedit.py +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/checkdatafiles.py +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/copystats.py +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/crewnerdplot.py +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/crewnerdplottime.py +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/csvparsers.py +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/ergdataplot.py +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/ergdataplottime.py +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/ergdatatotcx.py +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/ergstickplot.py +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/ergstickplottime.py +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/ergsticktotcx.py +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/gpxtools.py +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/gpxwrite.py +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/konkatenaadje.py +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/laptesting.py +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/obsolete.py +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/otherparsers.py +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/painsled_desktop_plot.py +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/painsled_desktop_plottime.py +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/painsled_desktop_toc2.py +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/painsledplot.py +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/painsledplottime.py +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/painsledtoc2.py +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/roweredit.py +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/rowproplot.py +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/rowproplottime.py +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/speedcoachplot.py +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/speedcoachplottime.py +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/speedcoachtoc2.py +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/tcxplot.py +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/tcxplot_nogeo.py +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/tcxplottime.py +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/tcxplottime_nogeo.py +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/tcxtoc2.py +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/trainingparser.py +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/utils.py +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/windcorrected.py +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata/writetcx.py +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata.egg-info/dependency_links.txt +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata.egg-info/not-zip-safe +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/rowingdata.egg-info/top_level.txt +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/setup.cfg +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/2016-03-25-0758.CSV +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/2016-03-25-0758_data.CSV +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/2x20min_o.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/2x2km.tcx +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/3km_cd_o.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/3x250m.tcx +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/EUBoatCoach.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/EmpowerSpeedCoachForce.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/NKEmporfromgreg.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/NKLiNKv130.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/NoHR.tcx +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/PainsledForce.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/RP_interval.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/RP_interval.csv_o.CSV +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/RP_testdata.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/SpdCoach2_imp_inconsistent.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/SpdCoachAmbiguous.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/SpeedCoach GPS Workout.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/SpeedCoach2Link_interval.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/SpeedCoach2Linkv1.27.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/SpeedCoach2v2.12.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/Speedcoach2example.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/aritmo.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/bc1.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/bc2.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/bc3.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/boatcoach.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/boatcoach_fixed_distance.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/boatcoach_otw.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/correctedpainsled.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/coxmate.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/crewnerd_interval.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/crewnerddata.CSV +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/crewnerddata.tcx +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/crewnerddata_o.CSV +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/cumvalues.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/ergdata_example.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/ergdata_example.csv_o.CSV +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/ergstick.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/ergstick.csv_o.CSV +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/ergstick2.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/ergstick_o.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/example.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/example_data.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/faketcx.tcx +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/float.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/herodata.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/humon.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/impeller_empower.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/invalidchar.tcx +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/kinomap.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/mystery.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/nk_logbook.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/painsled.tcx +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/painsled_desktop_example.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/painsled_out_data.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/painsled_to_csv_20160221-105218_erg-400150318_760465m.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/quiske_per_stroke_left.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/quiske_per_stroke_new.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/quiske_per_stroke_right.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/quiske_per_stroke_seat.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/recover.tcx +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/ritmointervals.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/rowingdata_eth.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/rowinginmotionexample.TCX +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/rowinginmotionexample.TCX_o.CSV +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/rowperfect.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/rowpro5.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/rowpro_carrick.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/rp3_curve.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/rp3intervals.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/rp3intervals2.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/rp_out.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/smartrow.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/smartrow_intervals.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/spdcoach2noheader.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/speedcoach2test2.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/speedcoach3test3.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/speedcoachexample.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/speedcoachexample.csv_o.CSV +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/strava_export.tcx +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/summarytest.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/testdata.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/testdata_part1.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/testdata_part2.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/testdatasummary.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/testlapidx.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/testtcs_210614.tcx +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/testtcx.tcx +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/testtcx_210614.tcx +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata/tim.csv +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/testdata//345/210/222/350/210/271.csv" +0 -0
- {rowingdata-3.6.26 → rowingdata-3.7.1}/tests/testparser.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: rowingdata
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.7.1
|
|
4
4
|
Summary: The rowingdata library to create colorful plots from CrewNerd, Painsled and other rowing data tools
|
|
5
|
-
Home-page:
|
|
5
|
+
Home-page: https://github.com/sanderroosendaal/rowingdata
|
|
6
6
|
Author: Sander Roosendaal
|
|
7
7
|
Author-email: roosendaalsander@gmail.com
|
|
8
8
|
License: MIT
|
|
@@ -15,6 +15,7 @@ Requires-Dist: scipy
|
|
|
15
15
|
Requires-Dist: matplotlib
|
|
16
16
|
Requires-Dist: pandas
|
|
17
17
|
Requires-Dist: fitparse
|
|
18
|
+
Requires-Dist: fit-tool>=0.9.14
|
|
18
19
|
Requires-Dist: arrow>=1.0.2
|
|
19
20
|
Requires-Dist: python-dateutil
|
|
20
21
|
Requires-Dist: docopt
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FIT file export for rowingdata.
|
|
3
|
+
Exports rowingdata DataFrames to Garmin FIT format for compatibility with
|
|
4
|
+
Intervals.icu and other platforms.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import absolute_import
|
|
7
|
+
from __future__ import print_function
|
|
8
|
+
|
|
9
|
+
import datetime
|
|
10
|
+
import numpy as np
|
|
11
|
+
from dateutil import parser as ps
|
|
12
|
+
import arrow
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
from fit_tool.base_type import BaseType
|
|
16
|
+
from fit_tool.developer_field import DeveloperField
|
|
17
|
+
from fit_tool.fit_file_builder import FitFileBuilder
|
|
18
|
+
from fit_tool.profile.messages.developer_data_id_message import DeveloperDataIdMessage
|
|
19
|
+
from fit_tool.profile.messages.field_description_message import FieldDescriptionMessage
|
|
20
|
+
from fit_tool.profile.messages.file_id_message import FileIdMessage
|
|
21
|
+
from fit_tool.profile.messages.activity_message import ActivityMessage
|
|
22
|
+
from fit_tool.profile.messages.session_message import SessionMessage
|
|
23
|
+
from fit_tool.profile.messages.lap_message import LapMessage
|
|
24
|
+
from fit_tool.profile.messages.record_message import RecordMessage
|
|
25
|
+
from fit_tool.profile.messages.event_message import EventMessage
|
|
26
|
+
from fit_tool.profile.profile_type import (
|
|
27
|
+
FileType, Manufacturer, Sport, SubSport,
|
|
28
|
+
Event, EventType, Activity,
|
|
29
|
+
)
|
|
30
|
+
FIT_TOOL_AVAILABLE = True
|
|
31
|
+
except ImportError:
|
|
32
|
+
FIT_TOOL_AVAILABLE = False
|
|
33
|
+
|
|
34
|
+
# Developer field definitions for rowing-specific columns (no native FIT equivalent).
|
|
35
|
+
# StrokeDistance is here: native cycle_length is uint8/scale100 (max 2.55m), wrong for rowing (7–12m typical).
|
|
36
|
+
# (field_id, df_column, name, base_type, size, scale, units)
|
|
37
|
+
ROWING_DEV_FIELDS = [
|
|
38
|
+
(0, ' DriveLength (meters)', 'DriveLength', BaseType.UINT16, 2, 100, 'm'),
|
|
39
|
+
(1, ' DriveTime (ms)', 'DriveTime', BaseType.UINT16, 2, 1, 'ms'),
|
|
40
|
+
(2, ' DragFactor', 'DragFactor', BaseType.UINT16, 2, 1, ''),
|
|
41
|
+
(3, ' StrokeRecoveryTime (ms)', 'StrokeRecoveryTime', BaseType.UINT16, 2, 1, 'ms'),
|
|
42
|
+
(4, ' AverageDriveForce (lbs)', 'AverageDriveForceLbs', BaseType.UINT16, 2, 10, 'lbs'),
|
|
43
|
+
(5, ' PeakDriveForce (lbs)', 'PeakDriveForceLbs', BaseType.UINT16, 2, 10, 'lbs'),
|
|
44
|
+
(6, ' AverageDriveForce (N)', 'AverageDriveForceN', BaseType.UINT16, 2, 10, 'N'),
|
|
45
|
+
(7, ' PeakDriveForce (N)', 'PeakDriveForceN', BaseType.UINT16, 2, 10, 'N'),
|
|
46
|
+
(8, ' AverageBoatSpeed (m/s)', 'AverageBoatSpeed', BaseType.UINT16, 2, 100, 'm/s'),
|
|
47
|
+
(9, ' WorkoutState', 'WorkoutState', BaseType.UINT8, 1, 1, ''),
|
|
48
|
+
(10, ' StrokeDistance (meters)', 'StrokeDistance', BaseType.UINT16, 2, 100, 'm'),
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
# Sport string to FIT enum mapping
|
|
52
|
+
SPORT_MAP = {
|
|
53
|
+
'rowing': Sport.ROWING,
|
|
54
|
+
'water': Sport.ROWING,
|
|
55
|
+
'indoor_rowing': Sport.ROWING,
|
|
56
|
+
'indoor rowing': Sport.ROWING,
|
|
57
|
+
'run': Sport.RUNNING,
|
|
58
|
+
'running': Sport.RUNNING,
|
|
59
|
+
'bike': Sport.CYCLING,
|
|
60
|
+
'cycling': Sport.CYCLING,
|
|
61
|
+
'swim': Sport.SWIMMING,
|
|
62
|
+
'swimming': Sport.SWIMMING,
|
|
63
|
+
'other': Sport.GENERIC,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _valid_position(lat, lon):
|
|
68
|
+
"""True if lat/lon are valid degrees for FIT (rough bounds)."""
|
|
69
|
+
return (not (np.isnan(lat) or np.isnan(lon)) and
|
|
70
|
+
-90 <= lat <= 90 and -180 <= lon <= 180 and (lat != 0 or lon != 0))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _sport_to_fit(sport_str):
|
|
74
|
+
"""Map sport string to FIT Sport enum."""
|
|
75
|
+
if sport_str is None:
|
|
76
|
+
return Sport.ROWING
|
|
77
|
+
key = str(sport_str).lower().strip()
|
|
78
|
+
return SPORT_MAP.get(key, Sport.ROWING)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def write_fit(file_name, df, row_date="2016-01-01", notes="Exported by Rowingdata",
|
|
82
|
+
sport="rowing", use_developer_fields=True):
|
|
83
|
+
"""
|
|
84
|
+
Write rowingdata DataFrame to a FIT activity file.
|
|
85
|
+
|
|
86
|
+
Parameters
|
|
87
|
+
----------
|
|
88
|
+
file_name : str
|
|
89
|
+
Output file path (e.g. 'activity.fit')
|
|
90
|
+
df : pandas.DataFrame
|
|
91
|
+
DataFrame with rowingdata columns (TimeStamp, Horizontal, Cadence, etc.)
|
|
92
|
+
row_date : str or datetime
|
|
93
|
+
Workout date (used when timestamps are relative)
|
|
94
|
+
notes : str
|
|
95
|
+
Activity notes
|
|
96
|
+
sport : str
|
|
97
|
+
Sport type: 'rowing', 'indoor_rowing', 'other', etc.
|
|
98
|
+
use_developer_fields : bool
|
|
99
|
+
If True, include rowing-specific columns as developer fields when present.
|
|
100
|
+
If False, export only standard FIT fields (timestamp, distance, cadence,
|
|
101
|
+
heart_rate, power, speed, position).
|
|
102
|
+
"""
|
|
103
|
+
if not FIT_TOOL_AVAILABLE:
|
|
104
|
+
raise ImportError("fit-tool is required for FIT export. Install with: pip install fit-tool")
|
|
105
|
+
|
|
106
|
+
# Get or compute cum_dist
|
|
107
|
+
if 'cum_dist' not in df.columns and ' Horizontal (meters)' in df.columns:
|
|
108
|
+
from .csvparsers import make_cumvalues
|
|
109
|
+
res = make_cumvalues(df[' Horizontal (meters)'])
|
|
110
|
+
df = df.copy()
|
|
111
|
+
df['cum_dist'] = res[0]
|
|
112
|
+
|
|
113
|
+
nr_rows = len(df)
|
|
114
|
+
if nr_rows == 0:
|
|
115
|
+
raise ValueError("Cannot export empty DataFrame to FIT")
|
|
116
|
+
|
|
117
|
+
# Parse row_date and determine timestamps
|
|
118
|
+
dateobj = ps.parse(str(row_date))
|
|
119
|
+
timezero = arrow.get(datetime.datetime(2000, 1, 1)).timestamp()
|
|
120
|
+
seconds = df['TimeStamp (sec)'].values
|
|
121
|
+
|
|
122
|
+
if seconds[0] < timezero:
|
|
123
|
+
# Relative timestamps: add row_date
|
|
124
|
+
unixtimes = seconds + arrow.get(dateobj).timestamp()
|
|
125
|
+
else:
|
|
126
|
+
unixtimes = seconds
|
|
127
|
+
|
|
128
|
+
# FIT timestamps in milliseconds (fit-tool convention for timestamp fields)
|
|
129
|
+
start_time_ms = int(unixtimes[0] * 1000)
|
|
130
|
+
total_elapsed_ms = int((unixtimes[-1] - unixtimes[0]) * 1000) if nr_rows > 1 else 0
|
|
131
|
+
total_elapsed_s = (unixtimes[-1] - unixtimes[0]) if nr_rows > 1 else 0.0 # seconds, for Activity/Session/Lap
|
|
132
|
+
|
|
133
|
+
# Arrays for record messages
|
|
134
|
+
try:
|
|
135
|
+
distance_m = df['cum_dist'].values
|
|
136
|
+
except KeyError:
|
|
137
|
+
distance_m = df[' Horizontal (meters)'].values
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
cadence = np.round(df[' Cadence (stokes/min)'].values).astype(int)
|
|
141
|
+
except KeyError:
|
|
142
|
+
cadence = np.zeros(nr_rows, dtype=int)
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
heart_rate = df[' HRCur (bpm)'].values.astype(int)
|
|
146
|
+
heart_rate = np.clip(heart_rate, 0, 255) # uint8
|
|
147
|
+
except KeyError:
|
|
148
|
+
heart_rate = np.zeros(nr_rows, dtype=int)
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
power = df[' Power (watts)'].values.astype(int)
|
|
152
|
+
power = np.clip(power, 0, 65535) # uint16
|
|
153
|
+
except KeyError:
|
|
154
|
+
power = np.zeros(nr_rows, dtype=int)
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
pace = df[' Stroke500mPace (sec/500m)'].values
|
|
158
|
+
# enhanced_speed in m/s; avoid div by zero
|
|
159
|
+
pace_safe = np.where(pace > 0, pace, np.inf)
|
|
160
|
+
enhanced_speed = 500.0 / pace_safe
|
|
161
|
+
enhanced_speed = np.where(np.isfinite(enhanced_speed), enhanced_speed, 0)
|
|
162
|
+
except KeyError:
|
|
163
|
+
enhanced_speed = np.zeros(nr_rows)
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
lat = df[' latitude'].values
|
|
167
|
+
lon = df[' longitude'].values
|
|
168
|
+
except KeyError:
|
|
169
|
+
lat = np.zeros(nr_rows)
|
|
170
|
+
lon = np.zeros(nr_rows)
|
|
171
|
+
|
|
172
|
+
# Developer fields: which columns exist and their arrays
|
|
173
|
+
use_dev = use_developer_fields and FIT_TOOL_AVAILABLE
|
|
174
|
+
dev_arrays = {}
|
|
175
|
+
dev_specs = []
|
|
176
|
+
DEV_DATA_IDX = 0
|
|
177
|
+
if use_dev:
|
|
178
|
+
for fd in ROWING_DEV_FIELDS:
|
|
179
|
+
field_id, col, name, base_type, size, scale, units = fd
|
|
180
|
+
if col in df.columns:
|
|
181
|
+
arr = df[col].values
|
|
182
|
+
arr = np.nan_to_num(arr, nan=0.0, posinf=0.0, neginf=0.0)
|
|
183
|
+
# Clip to avoid overflow: encoded = value * scale must fit in base_type
|
|
184
|
+
if base_type == BaseType.UINT8:
|
|
185
|
+
arr = np.clip(arr, 0, 255)
|
|
186
|
+
elif base_type == BaseType.UINT16:
|
|
187
|
+
max_display = 65535.0 / scale if scale else 65535.0
|
|
188
|
+
arr = np.clip(arr, 0, max_display)
|
|
189
|
+
dev_arrays[field_id] = arr
|
|
190
|
+
dev_specs.append((field_id, col, name, base_type, size, scale, units))
|
|
191
|
+
|
|
192
|
+
# Build FIT file
|
|
193
|
+
min_str = 50 if dev_specs else 8
|
|
194
|
+
builder = FitFileBuilder(auto_define=True, min_string_size=min_str)
|
|
195
|
+
|
|
196
|
+
# File ID
|
|
197
|
+
file_id = FileIdMessage()
|
|
198
|
+
file_id.type = FileType.ACTIVITY
|
|
199
|
+
file_id.manufacturer = Manufacturer.GARMIN.value
|
|
200
|
+
file_id.product = 0
|
|
201
|
+
file_id.time_created = start_time_ms
|
|
202
|
+
file_id.serial_number = 0x12345678
|
|
203
|
+
builder.add(file_id)
|
|
204
|
+
|
|
205
|
+
# Activity message
|
|
206
|
+
activity = ActivityMessage()
|
|
207
|
+
activity.timestamp = start_time_ms
|
|
208
|
+
activity.total_timer_time = total_elapsed_s # seconds; fit-tool applies scale 1000
|
|
209
|
+
activity.num_sessions = 1
|
|
210
|
+
activity.type = Activity.MANUAL
|
|
211
|
+
activity.event = Event.TIMER
|
|
212
|
+
activity.event_type = EventType.START
|
|
213
|
+
builder.add(activity)
|
|
214
|
+
|
|
215
|
+
# Timer start event
|
|
216
|
+
event_start = EventMessage()
|
|
217
|
+
event_start.event = Event.TIMER
|
|
218
|
+
event_start.event_type = EventType.START
|
|
219
|
+
event_start.timestamp = start_time_ms
|
|
220
|
+
builder.add(event_start)
|
|
221
|
+
|
|
222
|
+
# Session message
|
|
223
|
+
total_dist = float(distance_m[-1]) if nr_rows > 0 else 0
|
|
224
|
+
try:
|
|
225
|
+
total_calories = int(df[' Calories (kCal)'].max())
|
|
226
|
+
except (KeyError, ValueError):
|
|
227
|
+
total_calories = 0
|
|
228
|
+
|
|
229
|
+
avg_hr = int(np.mean(heart_rate[heart_rate > 0])) if np.any(heart_rate > 0) else 0
|
|
230
|
+
max_hr = int(np.max(heart_rate)) if nr_rows > 0 else 0
|
|
231
|
+
avg_cadence = int(np.mean(cadence[cadence > 0])) if np.any(cadence > 0) else 0
|
|
232
|
+
avg_power = int(np.mean(power[power > 0])) if np.any(power > 0) else 0
|
|
233
|
+
|
|
234
|
+
session = SessionMessage()
|
|
235
|
+
session.message_index = 0
|
|
236
|
+
session.timestamp = start_time_ms
|
|
237
|
+
session.start_time = start_time_ms
|
|
238
|
+
session.total_elapsed_time = total_elapsed_s # seconds; fit-tool applies scale 1000
|
|
239
|
+
session.total_timer_time = total_elapsed_s
|
|
240
|
+
session.total_distance = int(round(total_dist)) # fit-tool applies scale 100
|
|
241
|
+
session.total_calories = total_calories
|
|
242
|
+
session.sport = _sport_to_fit(sport)
|
|
243
|
+
session.sub_sport = SubSport.GENERIC
|
|
244
|
+
if avg_hr > 0:
|
|
245
|
+
session.avg_heart_rate = avg_hr
|
|
246
|
+
if max_hr > 0:
|
|
247
|
+
session.max_heart_rate = max_hr
|
|
248
|
+
if avg_cadence > 0:
|
|
249
|
+
session.avg_cadence = avg_cadence
|
|
250
|
+
if avg_power > 0:
|
|
251
|
+
session.avg_power = avg_power
|
|
252
|
+
builder.add(session)
|
|
253
|
+
|
|
254
|
+
# Lap message (one lap for the whole session for now)
|
|
255
|
+
lap = LapMessage()
|
|
256
|
+
lap.message_index = 0
|
|
257
|
+
lap.timestamp = int(unixtimes[-1] * 1000) if nr_rows > 0 else start_time_ms
|
|
258
|
+
lap.start_time = start_time_ms
|
|
259
|
+
lap.total_elapsed_time = total_elapsed_s # seconds; fit-tool applies scale 1000
|
|
260
|
+
lap.total_timer_time = total_elapsed_s
|
|
261
|
+
lap.total_distance = int(round(total_dist)) # fit-tool applies scale 100
|
|
262
|
+
lap.total_calories = total_calories
|
|
263
|
+
lap.sport = _sport_to_fit(sport)
|
|
264
|
+
lap.sub_sport = SubSport.GENERIC
|
|
265
|
+
if avg_hr > 0:
|
|
266
|
+
lap.avg_heart_rate = avg_hr
|
|
267
|
+
if max_hr > 0:
|
|
268
|
+
lap.max_heart_rate = max_hr
|
|
269
|
+
if avg_cadence > 0:
|
|
270
|
+
lap.avg_cadence = avg_cadence
|
|
271
|
+
if avg_power > 0:
|
|
272
|
+
lap.avg_power = avg_power
|
|
273
|
+
# fit-tool expects position in degrees (it applies semicircle conversion internally)
|
|
274
|
+
if _valid_position(lat[0], lon[0]) and _valid_position(lat[-1], lon[-1]):
|
|
275
|
+
lap.start_position_lat = float(lat[0])
|
|
276
|
+
lap.start_position_long = float(lon[0])
|
|
277
|
+
lap.end_position_lat = float(lat[-1])
|
|
278
|
+
lap.end_position_long = float(lon[-1])
|
|
279
|
+
builder.add(lap)
|
|
280
|
+
|
|
281
|
+
# Developer data (ID + field descriptions) when we have developer fields
|
|
282
|
+
DEV_DATA_IDX = 0
|
|
283
|
+
if dev_specs:
|
|
284
|
+
dev_id_msg = DeveloperDataIdMessage()
|
|
285
|
+
dev_id_msg.application_id = b'rowingdata'
|
|
286
|
+
dev_id_msg.developer_data_index = DEV_DATA_IDX
|
|
287
|
+
builder.add(dev_id_msg)
|
|
288
|
+
for field_id, col, name, base_type, size, scale, units in dev_specs:
|
|
289
|
+
fd_msg = FieldDescriptionMessage()
|
|
290
|
+
fd_msg.developer_data_index = DEV_DATA_IDX
|
|
291
|
+
fd_msg.field_definition_number = field_id
|
|
292
|
+
fd_msg.fit_base_type_id = base_type.value
|
|
293
|
+
fd_msg.field_name = name
|
|
294
|
+
fd_msg.scale = int(scale) if scale == int(scale) else scale
|
|
295
|
+
fd_msg.offset = 0
|
|
296
|
+
fd_msg.units = units
|
|
297
|
+
builder.add(fd_msg)
|
|
298
|
+
|
|
299
|
+
# Record messages
|
|
300
|
+
for i in range(nr_rows):
|
|
301
|
+
dev_fields = []
|
|
302
|
+
if dev_specs:
|
|
303
|
+
for field_id, col, name, base_type, size, scale, units in dev_specs:
|
|
304
|
+
arr = dev_arrays[field_id]
|
|
305
|
+
val = float(arr[i])
|
|
306
|
+
if val != 0 or field_id == 9: # WorkoutState can be 0
|
|
307
|
+
# Pass display value; fit-tool encodes as raw = (value + offset) * scale.
|
|
308
|
+
# FieldDescription has same scale so readers decode: display = raw/scale.
|
|
309
|
+
dev = DeveloperField(
|
|
310
|
+
developer_data_index=DEV_DATA_IDX,
|
|
311
|
+
field_id=field_id,
|
|
312
|
+
size=size,
|
|
313
|
+
name=name,
|
|
314
|
+
base_type=base_type,
|
|
315
|
+
scale=scale,
|
|
316
|
+
offset=0,
|
|
317
|
+
units=units
|
|
318
|
+
)
|
|
319
|
+
dev.set_value(0, val)
|
|
320
|
+
dev_fields.append(dev)
|
|
321
|
+
rec = RecordMessage(developer_fields=dev_fields) if dev_fields else RecordMessage()
|
|
322
|
+
rec.timestamp = int(unixtimes[i] * 1000)
|
|
323
|
+
rec.distance = float(distance_m[i]) # fit-tool applies scale 100
|
|
324
|
+
rec.heart_rate = heart_rate[i] if heart_rate[i] > 0 else None
|
|
325
|
+
rec.cadence = cadence[i] if cadence[i] > 0 else None
|
|
326
|
+
rec.power = power[i] if power[i] > 0 else None
|
|
327
|
+
# fit-tool expects display value (m/s); it applies scale 1000 internally
|
|
328
|
+
rec.enhanced_speed = float(enhanced_speed[i]) if enhanced_speed[i] > 0 else None
|
|
329
|
+
|
|
330
|
+
if not (np.isnan(lat[i]) or lat[i] == 0) and not (np.isnan(lon[i]) or lon[i] == 0):
|
|
331
|
+
rec.position_lat = float(lat[i])
|
|
332
|
+
rec.position_long = float(lon[i])
|
|
333
|
+
|
|
334
|
+
builder.add(rec)
|
|
335
|
+
|
|
336
|
+
# Timer stop event
|
|
337
|
+
event_stop = EventMessage()
|
|
338
|
+
event_stop.event = Event.TIMER
|
|
339
|
+
event_stop.event_type = EventType.STOP_ALL
|
|
340
|
+
event_stop.timestamp = int(unixtimes[-1] * 1000) if nr_rows > 0 else start_time_ms
|
|
341
|
+
builder.add(event_stop)
|
|
342
|
+
|
|
343
|
+
fit_file = builder.build()
|
|
344
|
+
fit_file.to_file(file_name)
|
|
@@ -5,7 +5,7 @@ from __future__ import print_function
|
|
|
5
5
|
from six.moves import range
|
|
6
6
|
from six.moves import input
|
|
7
7
|
|
|
8
|
-
__version__ = "3.
|
|
8
|
+
__version__ = "3.7.1"
|
|
9
9
|
|
|
10
10
|
from collections import Counter
|
|
11
11
|
|
|
@@ -83,10 +83,12 @@ try:
|
|
|
83
83
|
from . import gpxwrite
|
|
84
84
|
from . import trainingparser
|
|
85
85
|
from . import writetcx
|
|
86
|
+
from . import fitwrite
|
|
86
87
|
except (ValueError,ImportError): # pragma: no cover
|
|
87
88
|
import rowingdata.gpxwrite
|
|
88
89
|
import rowingdata.trainingparser
|
|
89
90
|
import rowingdata.writetcx
|
|
91
|
+
import rowingdata.fitwrite
|
|
90
92
|
|
|
91
93
|
import requests
|
|
92
94
|
|
|
@@ -166,7 +168,7 @@ def main(): # pragma: no cover
|
|
|
166
168
|
def my_autopct(pct, cutoff=5): # pragma: no cover
|
|
167
169
|
return ('%4.1f%%' % pct) if pct > cutoff else ''
|
|
168
170
|
|
|
169
|
-
def nanstozero(nr):
|
|
171
|
+
def nanstozero(nr: float) -> float:
|
|
170
172
|
if isnan(nr) or isinf(nr):
|
|
171
173
|
return 0
|
|
172
174
|
else:
|
|
@@ -187,7 +189,7 @@ def post_progress(secret,progressurl,progress): # pragma: no cover
|
|
|
187
189
|
|
|
188
190
|
|
|
189
191
|
# toekomstmuziek - nog niet gebruikt
|
|
190
|
-
def make_subplot(ax,r,df,param,mode=['distance','ote'],bars=None,barnames=None): # pragma: no cover
|
|
192
|
+
def make_subplot(ax,r,df,param,mode=['distance','ote'],bars=None,barnames=None,barlimits=None,barverbosenames=None): # pragma: no cover
|
|
191
193
|
if 'distance' in mode:
|
|
192
194
|
xcolumn = 'cum_dist'
|
|
193
195
|
dist_max = 1000
|
|
@@ -207,10 +209,13 @@ def make_subplot(ax,r,df,param,mode=['distance','ote'],bars=None,barnames=None):
|
|
|
207
209
|
if barlimits is None:
|
|
208
210
|
barlimits = ['lim_ut2','lim_ut1','lim_at','lim_tr','lim_an','lim_max']
|
|
209
211
|
if barverbosenames is None:
|
|
210
|
-
barverbosenames = list(
|
|
212
|
+
barverbosenames = list(r.rwr.hrzones)
|
|
211
213
|
|
|
212
214
|
colors = ['gray','y','g','blue','violet','r']
|
|
213
215
|
|
|
216
|
+
dist_increments = df.loc[:, xcolumn].diff()
|
|
217
|
+
dist_increments[0] = dist_increments[1]
|
|
218
|
+
|
|
214
219
|
for i in range(len(bars)):
|
|
215
220
|
ax.bar(df.loc[:,xcolumn],df.loc[:,barnames[i]],
|
|
216
221
|
width=dist_increments,
|
|
@@ -259,11 +264,12 @@ def make_subplot(ax,r,df,param,mode=['distance','ote'],bars=None,barnames=None):
|
|
|
259
264
|
majorLocator = (1000)
|
|
260
265
|
|
|
261
266
|
|
|
267
|
+
majorTickFormatter = FuncFormatter(format_pace_tick)
|
|
262
268
|
ax.yaxis.set_major_formatter(majorTickFormatter)
|
|
263
269
|
if 'time' in mode:
|
|
264
270
|
ax.set_xlabel('Time (sec)')
|
|
265
271
|
else:
|
|
266
|
-
ax.
|
|
272
|
+
ax.set_xlabel('Distance (m)')
|
|
267
273
|
|
|
268
274
|
if end_dist < dist_max:
|
|
269
275
|
ax.set_xticks(list(range(dist_tick, end_dist, dist_tick)))
|
|
@@ -1228,7 +1234,7 @@ class summarydata: # pragma: no cover
|
|
|
1228
1234
|
totmin = totmin + 1
|
|
1229
1235
|
|
|
1230
1236
|
stri2 += "{nr:0>2}{sep}{td:0>5}{sep} {inttime:0>5} {sep}".format(
|
|
1231
|
-
nr=
|
|
1237
|
+
nr=index + 1,
|
|
1232
1238
|
sep=separator,
|
|
1233
1239
|
td=td,
|
|
1234
1240
|
inttime=inttime
|
|
@@ -1676,7 +1682,7 @@ def boatedit(fileName="my1x.txt"): # pragma: no cover
|
|
|
1676
1682
|
))
|
|
1677
1683
|
if (strin != ""):
|
|
1678
1684
|
try:
|
|
1679
|
-
rg.spread = float(
|
|
1685
|
+
rg.spread = float(strin)
|
|
1680
1686
|
except ValueError:
|
|
1681
1687
|
print("Not a valid number. Keeping original value")
|
|
1682
1688
|
else:
|
|
@@ -1688,7 +1694,7 @@ def boatedit(fileName="my1x.txt"): # pragma: no cover
|
|
|
1688
1694
|
))
|
|
1689
1695
|
if (strin != ""):
|
|
1690
1696
|
try:
|
|
1691
|
-
rg.span = float(
|
|
1697
|
+
rg.span = float(strin)
|
|
1692
1698
|
except ValueError:
|
|
1693
1699
|
print("Not a valid number. Keeping original value")
|
|
1694
1700
|
|
|
@@ -2178,6 +2184,67 @@ class rowingdata:
|
|
|
2178
2184
|
"""
|
|
2179
2185
|
|
|
2180
2186
|
def __init__(self, *args, **kwargs):
|
|
2187
|
+
"""Initialize a rowingdata object from a CSV file or pandas DataFrame.
|
|
2188
|
+
|
|
2189
|
+
This is the main class for reading and analyzing rowing workout data.
|
|
2190
|
+
It parses CSV files from various rowing ergometer and on-the-water
|
|
2191
|
+
rowing systems, performs data validation and enrichment, and provides
|
|
2192
|
+
methods for analysis, visualization, and export.
|
|
2193
|
+
|
|
2194
|
+
Parameters
|
|
2195
|
+
----------
|
|
2196
|
+
csvfile : str, optional
|
|
2197
|
+
Path to the CSV file containing rowing data. Can be gzipped (.gz).
|
|
2198
|
+
If not provided, a DataFrame must be passed via the 'df' parameter.
|
|
2199
|
+
df : pandas.DataFrame, optional
|
|
2200
|
+
A pre-loaded DataFrame containing rowing data. If provided, csvfile
|
|
2201
|
+
is ignored.
|
|
2202
|
+
rower : rowingdata.rower, optional
|
|
2203
|
+
A rower object containing personal data (HR thresholds, FTP, etc.).
|
|
2204
|
+
Defaults to a new rower() instance with default values.
|
|
2205
|
+
rowtype : str, optional
|
|
2206
|
+
Type of rowing data ('Indoor Rower', 'OTW', etc.).
|
|
2207
|
+
Defaults to 'Indoor Rower'.
|
|
2208
|
+
absolutetimestamps : bool, optional
|
|
2209
|
+
If True, timestamps are kept as Unix time (seconds since 1970).
|
|
2210
|
+
If False (default), timestamps are relative to workout start.
|
|
2211
|
+
debug : bool, optional
|
|
2212
|
+
If True, enables debug output. Defaults to False.
|
|
2213
|
+
|
|
2214
|
+
Attributes
|
|
2215
|
+
----------
|
|
2216
|
+
df : pandas.DataFrame
|
|
2217
|
+
The main data frame containing all rowing metrics.
|
|
2218
|
+
readfilename : str
|
|
2219
|
+
Name of the source file or 'rowing dataframe' if created from df.
|
|
2220
|
+
rwr : rowingdata.rower
|
|
2221
|
+
The rower object with personal training zones.
|
|
2222
|
+
rowtype : str
|
|
2223
|
+
Type of rowing data.
|
|
2224
|
+
empty : bool
|
|
2225
|
+
True if the DataFrame is empty.
|
|
2226
|
+
duration : float
|
|
2227
|
+
Total workout duration in seconds.
|
|
2228
|
+
dragfactor : float
|
|
2229
|
+
Drag factor from the rowing data (for ergometer data).
|
|
2230
|
+
stroke_count : int
|
|
2231
|
+
Total number of strokes in the workout.
|
|
2232
|
+
|
|
2233
|
+
Examples
|
|
2234
|
+
--------
|
|
2235
|
+
>>> # Load from CSV file
|
|
2236
|
+
>>> row = rowingdata(csvfile="workout.csv")
|
|
2237
|
+
>>> row.summary()
|
|
2238
|
+
|
|
2239
|
+
>>> # Load from DataFrame
|
|
2240
|
+
>>> import pandas as pd
|
|
2241
|
+
>>> df = pd.read_csv("workout.csv")
|
|
2242
|
+
>>> row = rowingdata(df=df)
|
|
2243
|
+
|
|
2244
|
+
>>> # With custom rower data
|
|
2245
|
+
>>> my_rower = rower(hrut2=140, hrmax=190, ftp=250)
|
|
2246
|
+
>>> row = rowingdata(csvfile="workout.csv", rower=my_rower)
|
|
2247
|
+
"""
|
|
2181
2248
|
|
|
2182
2249
|
if 'debug' in kwargs: # pragma: no cover
|
|
2183
2250
|
debug = kwargs['debug']
|
|
@@ -3061,6 +3128,36 @@ class rowingdata:
|
|
|
3061
3128
|
with open(fileName,'wb+') as f_out:
|
|
3062
3129
|
f_out.write(bytes)
|
|
3063
3130
|
|
|
3131
|
+
def exporttofit(self, fileName, notes="Exported by Rowingdata",
|
|
3132
|
+
sport="rowing", use_developer_fields=True):
|
|
3133
|
+
"""Export rowingdata to FIT format for Intervals.icu and other platforms.
|
|
3134
|
+
|
|
3135
|
+
Parameters
|
|
3136
|
+
----------
|
|
3137
|
+
fileName : str
|
|
3138
|
+
Output file path (e.g. 'activity.fit')
|
|
3139
|
+
notes : str
|
|
3140
|
+
Activity notes
|
|
3141
|
+
sport : str
|
|
3142
|
+
Sport type: 'rowing', 'indoor_rowing', 'water', 'other'
|
|
3143
|
+
use_developer_fields : bool
|
|
3144
|
+
If True, include rowing-specific columns (DriveLength, StrokeDistance,
|
|
3145
|
+
etc.) when present. Requires fit-tool and Intervals.icu importer support.
|
|
3146
|
+
"""
|
|
3147
|
+
if not self.empty:
|
|
3148
|
+
df = self.df.copy()
|
|
3149
|
+
res = make_cumvalues(df[' Horizontal (meters)'])
|
|
3150
|
+
df[' Horizontal (meters)'] = res[0]
|
|
3151
|
+
df['cum_dist'] = res[0]
|
|
3152
|
+
fitwrite.write_fit(
|
|
3153
|
+
fileName, df,
|
|
3154
|
+
row_date=self.rowdatetime.isoformat(),
|
|
3155
|
+
notes=notes, sport=sport,
|
|
3156
|
+
use_developer_fields=use_developer_fields
|
|
3157
|
+
)
|
|
3158
|
+
else: # pragma: no cover
|
|
3159
|
+
raise ValueError("Cannot export empty rowingdata session to FIT")
|
|
3160
|
+
|
|
3064
3161
|
def split_by_intervals(self):
|
|
3065
3162
|
""" Returns the work intervals as separate rowingdata objects"""
|
|
3066
3163
|
if self.empty:
|
|
@@ -380,16 +380,35 @@ def tcxtodf3(path):
|
|
|
380
380
|
extensions_element = trackpoint.find(".//{http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2}Extensions")
|
|
381
381
|
if extensions_element is not None:
|
|
382
382
|
# print child elements of extensions_element
|
|
383
|
-
|
|
383
|
+
# looks like so
|
|
384
|
+
#<ns0:Extensions xmlns:ns0="http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2" xmlns:ns1="http://www.garmin.com/xmlschemas/ActivityExtension/v2">
|
|
385
|
+
#<ns1:ActivityTrackpointExtension>
|
|
386
|
+
# <ns0:Extensions>
|
|
387
|
+
# <ns0:Check>29</ns0:Check>
|
|
388
|
+
# <ns0:Bounce>1</ns0:Bounce>
|
|
389
|
+
# <ns0:Speed>0.23</ns0:Speed>
|
|
390
|
+
# <ns0:AvgSpeed>2.29</ns0:AvgSpeed>
|
|
391
|
+
# <ns0:Mps>0.00</ns0:Mps>
|
|
392
|
+
# </ns0:Extensions>
|
|
393
|
+
#</ns1:ActivityTrackpointExtension>
|
|
394
|
+
#</ns0:Extensions>
|
|
395
|
+
# extract Check and Watts from the extensions
|
|
396
|
+
watts_node = extensions_element.find(".//{http://www.garmin.com/xmlschemas/ActivityExtension/v2}ActivityTrackpointExtension/{http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2}Extensions/{http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2}Watts")
|
|
397
|
+
check_factor = extensions_element.find(".//{http://www.garmin.com/xmlschemas/ActivityExtension/v2}ActivityTrackpointExtension/{http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2}Extensions/{http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2}Check")
|
|
398
|
+
|
|
399
|
+
if watts_node is None:
|
|
400
|
+
watts_node = extensions_element.find(".//{http://www.garmin.com/xmlschemas/ActivityExtension/v2}TPX/{http://www.garmin.com/xmlschemas/ActivityExtension/v2}Watts")
|
|
384
401
|
|
|
385
402
|
watts = float(clean_string(watts_node.text)) if watts_node is not None else 0
|
|
386
403
|
|
|
387
|
-
check_factor = extensions_element.find(".//{http://www.garmin.com/xmlschemas/ActivityExtension/v2}TPX/{http://www.garmin.com/xmlschemas/ActivityExtension/v2}Check")
|
|
404
|
+
#check_factor = extensions_element.find(".//{http://www.garmin.com/xmlschemas/ActivityExtension/v2}TPX/{http://www.garmin.com/xmlschemas/ActivityExtension/v2}Check")
|
|
388
405
|
try:
|
|
389
406
|
check_factor = float(clean_string(check_factor.text)) if check_factor is not None else 0
|
|
390
407
|
except ValueError:
|
|
391
408
|
check_factor = 0
|
|
392
409
|
|
|
410
|
+
|
|
411
|
+
|
|
393
412
|
speed_node = trackpoint.find(".//{http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2}Speed")
|
|
394
413
|
speed = float(clean_string(speed_node.text)) if speed_node is not None else 0
|
|
395
414
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: rowingdata
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.7.1
|
|
4
4
|
Summary: The rowingdata library to create colorful plots from CrewNerd, Painsled and other rowing data tools
|
|
5
|
-
Home-page:
|
|
5
|
+
Home-page: https://github.com/sanderroosendaal/rowingdata
|
|
6
6
|
Author: Sander Roosendaal
|
|
7
7
|
Author-email: roosendaalsander@gmail.com
|
|
8
8
|
License: MIT
|
|
@@ -15,6 +15,7 @@ Requires-Dist: scipy
|
|
|
15
15
|
Requires-Dist: matplotlib
|
|
16
16
|
Requires-Dist: pandas
|
|
17
17
|
Requires-Dist: fitparse
|
|
18
|
+
Requires-Dist: fit-tool>=0.9.14
|
|
18
19
|
Requires-Dist: arrow>=1.0.2
|
|
19
20
|
Requires-Dist: python-dateutil
|
|
20
21
|
Requires-Dist: docopt
|
|
@@ -63,6 +63,7 @@ rowingdata/ergdatatotcx.py
|
|
|
63
63
|
rowingdata/ergstickplot.py
|
|
64
64
|
rowingdata/ergstickplottime.py
|
|
65
65
|
rowingdata/ergsticktotcx.py
|
|
66
|
+
rowingdata/fitwrite.py
|
|
66
67
|
rowingdata/gpxtools.py
|
|
67
68
|
rowingdata/gpxwrite.py
|
|
68
69
|
rowingdata/konkatenaadje.py
|
|
@@ -128,6 +129,7 @@ testdata/boatcoach.csv
|
|
|
128
129
|
testdata/boatcoach_fixed_distance.csv
|
|
129
130
|
testdata/boatcoach_otw.csv
|
|
130
131
|
testdata/correctedpainsled.csv
|
|
132
|
+
testdata/cottwich.csv
|
|
131
133
|
testdata/coxmate.csv
|
|
132
134
|
testdata/crewnerd_interval.csv
|
|
133
135
|
testdata/crewnerddata.CSV
|