aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--Cargo.lock1044
-rw-r--r--Cargo.toml17
-rw-r--r--README.md141
-rw-r--r--src/main.rs406
5 files changed, 1610 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..890bfb5
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/target
+*.sw[lmnop]
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..1abdb9a
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,1044 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "ahash"
+version = "0.8.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42cd52102d3df161c77a887b608d7a4897d7cc112886a9537b738a887a03aaff"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5"
+
+[[package]]
+name = "android-tzdata"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anstream"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
+dependencies = [
+ "anstyle",
+ "windows-sys",
+]
+
+[[package]]
+name = "approx"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "atomic-polyfill"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4"
+dependencies = [
+ "critical-section",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "base64"
+version = "0.21.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
+
+[[package]]
+name = "bumpalo"
+version = "3.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d32a994c2b3ca201d9b263612a374263f05e7adde37c4707f693dcd375076d1f"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "cc"
+version = "1.0.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "chrono"
+version = "0.4.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b"
+dependencies = [
+ "android-tzdata",
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "serde",
+ "wasm-bindgen",
+ "windows-targets",
+]
+
+[[package]]
+name = "clap"
+version = "4.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c918d541ef2913577a0f9566e9ce27cb35b6df072075769e0b26cb5a554520da"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim 0.11.0",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.49",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
+
+[[package]]
+name = "critical-section"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216"
+
+[[package]]
+name = "darling"
+version = "0.14.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850"
+dependencies = [
+ "darling_core 0.14.4",
+ "darling_macro 0.14.4",
+]
+
+[[package]]
+name = "darling"
+version = "0.20.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c376d08ea6aa96aafe61237c7200d1241cb177b7d3a542d791f2d118e9cbb955"
+dependencies = [
+ "darling_core 0.20.6",
+ "darling_macro 0.20.6",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.14.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim 0.10.0",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.20.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33043dcd19068b8192064c704b3f83eb464f91f1ff527b44a4e2b08d9cdb8855"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim 0.10.0",
+ "syn 2.0.49",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.14.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e"
+dependencies = [
+ "darling_core 0.14.4",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.20.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5a91391accf613803c2a9bf9abccdbaa07c54b4244a5b64883f9c3c137c86be"
+dependencies = [
+ "darling_core 0.20.6",
+ "quote",
+ "syn 2.0.49",
+]
+
+[[package]]
+name = "deranged"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
+dependencies = [
+ "powerfmt",
+ "serde",
+]
+
+[[package]]
+name = "derive_builder"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8"
+dependencies = [
+ "derive_builder_macro",
+]
+
+[[package]]
+name = "derive_builder_core"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f"
+dependencies = [
+ "darling 0.14.4",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "derive_builder_macro"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e"
+dependencies = [
+ "derive_builder_core",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "earcutr"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79127ed59a85d7687c409e9978547cffb7dc79675355ed22da6b66fd5f6ead01"
+dependencies = [
+ "itertools",
+ "num-traits",
+]
+
+[[package]]
+name = "either"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
+
+[[package]]
+name = "equivalent"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+
+[[package]]
+name = "fit2mf2"
+version = "0.1.0"
+dependencies = [
+ "chrono",
+ "clap",
+ "fitparser",
+ "geo",
+ "geo-uri",
+ "serde",
+ "serde_json",
+ "serde_with",
+]
+
+[[package]]
+name = "fitparser"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c01b8defcd147fd7fa4536295e4c47ef7a592c128627fd44b3f79f0b9defda35"
+dependencies = [
+ "chrono",
+ "nom",
+ "serde",
+]
+
+[[package]]
+name = "float_next_after"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8"
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "geo"
+version = "0.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4841b40fdbccd4b7042bd6195e4de91da54af34c50632e371bcbfcdfb558b873"
+dependencies = [
+ "earcutr",
+ "float_next_after",
+ "geo-types",
+ "geographiclib-rs",
+ "log",
+ "num-traits",
+ "robust",
+ "rstar",
+ "spade",
+]
+
+[[package]]
+name = "geo-types"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "567495020b114f1ce9bed679b29975aa0bfae06ac22beacd5cfde5dabe7b05d6"
+dependencies = [
+ "approx",
+ "num-traits",
+ "rstar",
+ "serde",
+]
+
+[[package]]
+name = "geo-uri"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6b8812a079cb4a3b5b366c2d05db8e231e2f231c443acb3532daad996d89f71"
+dependencies = [
+ "derive_builder",
+ "serde",
+ "thiserror",
+]
+
+[[package]]
+name = "geographiclib-rs"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e5ed84f8089c70234b0a8e0aedb6dc733671612ddc0d37c6066052f9781960"
+dependencies = [
+ "libm",
+]
+
+[[package]]
+name = "hash32"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
+name = "hashbrown"
+version = "0.14.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
+dependencies = [
+ "ahash",
+ "allocator-api2",
+]
+
+[[package]]
+name = "heapless"
+version = "0.7.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f"
+dependencies = [
+ "atomic-polyfill",
+ "hash32",
+ "rustc_version",
+ "spin",
+ "stable_deref_trait",
+]
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "indexmap"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+dependencies = [
+ "autocfg",
+ "hashbrown 0.12.3",
+ "serde",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.14.3",
+ "serde",
+]
+
+[[package]]
+name = "itertools"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
+
+[[package]]
+name = "js-sys"
+version = "0.3.68"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.153"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
+
+[[package]]
+name = "libm"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
+
+[[package]]
+name = "lock_api"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
+
+[[package]]
+name = "memchr"
+version = "2.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+
+[[package]]
+name = "num-traits"
+version = "0.2.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
+dependencies = [
+ "autocfg",
+ "libm",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "robust"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbf4a6aa5f6d6888f39e980649f3ad6b666acdce1d78e95b8a2cb076e687ae30"
+
+[[package]]
+name = "rstar"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73111312eb7a2287d229f06c00ff35b51ddee180f017ab6dec1f69d62ac098d6"
+dependencies = [
+ "heapless",
+ "num-traits",
+ "smallvec",
+]
+
+[[package]]
+name = "rustc_version"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c"
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "semver"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0"
+
+[[package]]
+name = "serde"
+version = "1.0.196"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.196"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.49",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.113"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_with"
+version = "3.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15d167997bd841ec232f5b2b8e0e26606df2e7caa4c31b95ea9ca52b200bd270"
+dependencies = [
+ "base64",
+ "chrono",
+ "hex",
+ "indexmap 1.9.3",
+ "indexmap 2.2.3",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "serde_with_macros",
+ "time",
+]
+
+[[package]]
+name = "serde_with_macros"
+version = "3.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "865f9743393e638991566a8b7a479043c2c8da94a33e0a31f18214c9cae0a64d"
+dependencies = [
+ "darling 0.20.6",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.49",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
+
+[[package]]
+name = "spade"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61addf9117b11d1f5b4bf6fe94242ba25f59d2d4b2080544b771bd647024fd00"
+dependencies = [
+ "hashbrown 0.14.3",
+ "num-traits",
+ "robust",
+ "smallvec",
+]
+
+[[package]]
+name = "spin"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+dependencies = [
+ "lock_api",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
+
+[[package]]
+name = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
+[[package]]
+name = "strsim"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01"
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.49"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "915aea9e586f80826ee59f8453c1101f9d1c4b3964cd2460185ee8e299ada496"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.57"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.57"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.49",
+]
+
+[[package]]
+name = "time"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749"
+dependencies = [
+ "deranged",
+ "itoa",
+ "num-conv",
+ "powerfmt",
+ "serde",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
+
+[[package]]
+name = "time-macros"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.91"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.91"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.49",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.91"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.91"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.49",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.91"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838"
+
+[[package]]
+name = "windows-core"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
+
+[[package]]
+name = "zerocopy"
+version = "0.7.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.7.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.49",
+]
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..5c862bf
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "fit2mf2"
+version = "0.1.0"
+description = "Convert a Garmin FIT file into a Microformat entry"
+repository = "https://git.jesterpm.net/pub/jesterpm/fit2mf.git"
+license = "MIT"
+edition = "2021"
+
+[dependencies]
+clap = { version = "4.4.14", features = ["derive"] }
+chrono = "0.4"
+fitparser = "0.6"
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+serde_with = "3.6"
+geo-uri = { version = "0.2.1", features = ["serde"] }
+geo = "0.27.0"
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..365f9c8
--- /dev/null
+++ b/README.md
@@ -0,0 +1,141 @@
+fit2mf2
+=======
+
+A utility to convert a Garmin FIT data file into a microformat entry.
+
+
+Usage
+-----
+
+ Usage: fit2mf2 [OPTIONS] [FILE]
+
+ Arguments:
+ [FILE] FIT file to convert. If no file is given, reads from stdin.
+
+ Options:
+ -t, --category <CATEGORY> Category to add to the entry.
+ May be given more than once.
+
+ --hide <lat,long,radius> Location to mask in output. Radius in
+ meters. May be given more than once.
+
+ -h, --help
+
+
+Sample output
+-------------
+
+ {
+ "type": [
+ "h-entry"
+ ]
+ "properties": {
+ "activity": [
+ {
+ "sport": "running",
+ "start": "2021-06-19T14:51:50Z",
+ "start_position": "geo:47.6264,-122.3371",
+ "end_position": "geo:47.6264,-122.3371",
+ "max_heart_rate": {
+ "num": 195.0,
+ "unit": "bpm"
+ },
+ "max_speed": {
+ "num": 3.76,
+ "unit": "m/s"
+ },
+ "timer": {
+ "num": 3742.613,
+ "unit": "s"
+ },
+ "ascent": {
+ "num": 115.0,
+ "unit": "m"
+ },
+ "avg_heart_rate": {
+ "num": 169.0,
+ "unit": "bpm"
+ },
+ "avg_speed": {
+ "num": 2.7,
+ "unit": "m/s"
+ },
+ "calories": {
+ "num": 691.0,
+ "unit": "kcal"
+ },
+ "descent": {
+ "num": 171.0,
+ "unit": "m"
+ },
+ "distance": {
+ "num": 10104.88,
+ "unit": "m"
+ },
+ "duration": {
+ "num": 3742.613,
+ "unit": "s"
+ },
+ "laps": [
+ {
+ "sport": "running",
+ "start": "2021-06-19T14:51:50Z",
+ "start_position": "geo:47.6264,-122.3371",
+ "end_position": "geo:47.6336,-122.3401",
+ "ascent": {
+ "num": 7.0,
+ "unit": "m"
+ },
+ "avg_heart_rate": {
+ "num": 144.0,
+ "unit": "bpm"
+ },
+ "avg_speed": {
+ "num": 2.876,
+ "unit": "m/s"
+ },
+ "calories": {
+ "num": 53.0,
+ "unit": "kcal"
+ },
+ "descent": {
+ "num": 2.0,
+ "unit": "m"
+ },
+ "distance": {
+ "num": 1000.0,
+ "unit": "m"
+ },
+ "duration": {
+ "num": 347.712,
+ "unit": "s"
+ },
+ "max_heart_rate": {
+ "num": 177.0,
+ "unit": "bpm"
+ },
+ "max_speed": {
+ "num": 3.602,
+ "unit": "m/s"
+ },
+ "timer": {
+ "num": 347.712,
+ "unit": "s"
+ }
+ },
+ ...
+ ]
+ }
+ ],
+ "location": [
+ "geo:47.6264,-122.3371"
+ ],
+ "published": "2021-06-19T14:51:50Z"
+ },
+ }
+
+Contributing
+------------
+
+Send questions, bug reports, and patches to jesse@jesterpm.net.
+
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..d2c8f0d
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,406 @@
+use chrono::{DateTime, Utc};
+use clap::Parser;
+use fitparser::profile::MesgNum;
+use fitparser::{self, FitDataField, FitDataRecord};
+use geo::algorithm::geodesic_distance::GeodesicDistance;
+use geo::Point;
+use geo_uri::GeoUri;
+use serde::{Deserialize, Serialize};
+use serde_json::json;
+use std::fs::File;
+use std::io::{prelude::*, stdin, stdout};
+use std::process;
+use std::str::FromStr;
+
+#[derive(Parser, Debug)]
+struct Args {
+ #[arg(short = 't', long = "category")]
+ category: Vec<String>,
+
+ #[arg(long = "hide")]
+ hidden_location: Vec<ExclusionPoint>,
+
+ file: Option<String>,
+}
+
+const SEMICIRCLE: f64 = 11930465.0;
+
+#[derive(Debug, Copy, Clone)]
+struct ExclusionPoint {
+ center: Point,
+ radius: f64,
+}
+
+impl ExclusionPoint {
+ pub fn contains(&self, pos: &Point) -> bool {
+ let distance = self.center.geodesic_distance(pos);
+ distance <= self.radius
+ }
+}
+
+impl FromStr for ExclusionPoint {
+ type Err = Box<dyn std::error::Error>;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let mut it = s.split(',');
+ let latitude = it.next().unwrap().parse()?;
+ let longitude = it.next().unwrap().parse()?;
+ let radius = it.next().unwrap().parse()?;
+ Ok(ExclusionPoint {
+ center: Point::new(longitude, latitude),
+ radius,
+ })
+ }
+}
+
+impl From<String> for ExclusionPoint {
+ fn from(s: String) -> Self {
+ Self::from_str(&s).unwrap()
+ }
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+struct Measure {
+ num: f64,
+ unit: String,
+}
+
+fn as_measure(value: &FitDataField) -> Option<Measure> {
+ let num = match *value.value() {
+ fitparser::Value::Byte(v) => v as f64,
+ fitparser::Value::SInt8(v) => v as f64,
+ fitparser::Value::UInt8(v) => v as f64,
+ fitparser::Value::SInt16(v) => v as f64,
+ fitparser::Value::UInt16(v) => v as f64,
+ fitparser::Value::SInt32(v) => v as f64,
+ fitparser::Value::UInt32(v) => v as f64,
+ fitparser::Value::Float32(v) => v as f64,
+ fitparser::Value::Float64(v) => v,
+ fitparser::Value::UInt8z(v) => {
+ if v == 0 {
+ return None;
+ } else {
+ v as f64
+ }
+ }
+ fitparser::Value::UInt16z(v) => {
+ if v == 0 {
+ return None;
+ } else {
+ v as f64
+ }
+ }
+ fitparser::Value::UInt32z(v) => {
+ if v == 0 {
+ return None;
+ } else {
+ v as f64
+ }
+ }
+ fitparser::Value::SInt64(v) => v as f64,
+ fitparser::Value::UInt64(v) => v as f64,
+ fitparser::Value::UInt64z(v) => {
+ if v == 0 {
+ return None;
+ } else {
+ v as f64
+ }
+ }
+ fitparser::Value::Timestamp(_) => return None,
+ fitparser::Value::Enum(_) => return None,
+ fitparser::Value::String(_) => return None,
+ fitparser::Value::Array(_) => return None,
+ };
+ let unit = value.units().to_string();
+ Some(Measure { num, unit })
+}
+
+#[serde_with::skip_serializing_none]
+#[derive(Default, Serialize, Deserialize, Clone)]
+struct Session {
+ sport: Option<String>,
+ start: Option<DateTime<Utc>>,
+ start_position: Option<GeoUri>,
+ end_position: Option<GeoUri>,
+ duration: Option<Measure>,
+ timer: Option<Measure>,
+ avg_heart_rate: Option<Measure>,
+ max_heart_rate: Option<Measure>,
+ min_temperature: Option<Measure>,
+ avg_temperature: Option<Measure>,
+ max_temperature: Option<Measure>,
+ avg_speed: Option<Measure>,
+ max_speed: Option<Measure>,
+ ascent: Option<Measure>,
+ descent: Option<Measure>,
+ calories: Option<Measure>,
+ distance: Option<Measure>,
+ #[serde(skip_serializing_if = "Vec::is_empty")]
+ laps: Vec<Session>,
+}
+
+#[derive(Default, Serialize, Deserialize)]
+struct LapSlice {
+ index: u64,
+ count: u64,
+}
+
+#[derive(Default)]
+struct PositionBuilder {
+ latitude: Option<f64>,
+ longitude: Option<f64>,
+}
+
+impl PositionBuilder {
+ pub fn latitude(&mut self, field: &FitDataField) {
+ if let Some(value) = as_measure(field) {
+ self.latitude = Some(value.num / SEMICIRCLE);
+ } else {
+ self.latitude = None
+ }
+ }
+
+ pub fn longitude(&mut self, field: &FitDataField) {
+ if let Some(value) = as_measure(field) {
+ self.longitude = Some(value.num / SEMICIRCLE);
+ } else {
+ self.longitude = None
+ }
+ }
+
+ pub fn as_point(&self) -> Option<Point> {
+ if let (Some(lat), Some(lon)) = (self.latitude, self.longitude) {
+ Some(Point::new(lon, lat))
+ } else {
+ None
+ }
+ }
+}
+
+impl From<PositionBuilder> for Option<GeoUri> {
+ fn from(val: PositionBuilder) -> Self {
+ if let Some(lat) = val.latitude {
+ if let Some(long) = val.longitude {
+ return GeoUri::builder()
+ .latitude(round_geo(lat))
+ .longitude(round_geo(long))
+ .build()
+ .ok();
+ }
+ }
+ None
+ }
+}
+
+/// Truncate GPS noise
+fn round_geo(x: f64) -> f64 {
+ (x * 100000.0).round() / 100000.0
+}
+
+fn parse_session(args: &Args, record: &FitDataRecord) -> (Session, LapSlice) {
+ let mut session = Session::default();
+ let mut laps = LapSlice::default();
+ let mut start_position = PositionBuilder::default();
+ let mut end_position = PositionBuilder::default();
+ for field in record.fields() {
+ match field.name() {
+ "sport" => session.sport = Some(field.value().to_string()),
+ "start_time" => session.start = as_datetime(field),
+ "total_elapsed_time" => session.duration = as_measure(field),
+ "total_timer_time" => session.timer = as_measure(field),
+ "avg_heart_rate" => session.avg_heart_rate = as_measure(field),
+ "max_heart_rate" => session.max_heart_rate = as_measure(field),
+ "min_temperature" => session.min_temperature = as_measure(field),
+ "avg_temperature" => session.avg_temperature = as_measure(field),
+ "max_temperature" => session.max_temperature = as_measure(field),
+ "enhanced_avg_speed" => session.avg_speed = as_measure(field),
+ "enhanced_max_speed" => session.max_speed = as_measure(field),
+ "total_ascent" => session.ascent = as_measure(field),
+ "total_descent" => session.descent = as_measure(field),
+ "total_calories" => session.calories = as_measure(field),
+ "total_distance" => session.distance = as_measure(field),
+ "first_lap_index" => laps.index = as_u64(field).unwrap_or(0),
+ "num_laps" => laps.count = as_u64(field).unwrap_or(0),
+ "start_position_lat" => start_position.latitude(field),
+ "start_position_long" => start_position.longitude(field),
+ "end_position_lat" => end_position.latitude(field),
+ "end_position_long" => end_position.longitude(field),
+ _ => (),
+ }
+ }
+
+ // Just say no to hidden locations
+ if let Some(p) = start_position.as_point() {
+ if !args.hidden_location.iter().any(|nope| nope.contains(&p)) {
+ session.start_position = start_position.into();
+ }
+ }
+ if let Some(p) = end_position.as_point() {
+ if !args.hidden_location.iter().any(|nope| nope.contains(&p)) {
+ session.end_position = end_position.into();
+ }
+ }
+ (session, laps)
+}
+
+fn as_datetime(field: &FitDataField) -> Option<DateTime<Utc>> {
+ if let fitparser::Value::Timestamp(ts) = field.value() {
+ Some(ts.to_utc())
+ } else {
+ None
+ }
+}
+
+fn as_u64(field: &FitDataField) -> Option<u64> {
+ let value = match *field.value() {
+ fitparser::Value::Byte(v) => v as u64,
+ fitparser::Value::UInt8(v) => v as u64,
+ fitparser::Value::UInt16(v) => v as u64,
+ fitparser::Value::UInt32(v) => v as u64,
+ fitparser::Value::UInt64(v) => v,
+ fitparser::Value::SInt8(v) => {
+ if v >= 0 {
+ v as u64
+ } else {
+ 0
+ }
+ }
+ fitparser::Value::SInt16(v) => {
+ if v >= 0 {
+ v as u64
+ } else {
+ 0
+ }
+ }
+ fitparser::Value::SInt32(v) => {
+ if v >= 0 {
+ v as u64
+ } else {
+ 0
+ }
+ }
+ fitparser::Value::SInt64(v) => {
+ if v >= 0 {
+ v as u64
+ } else {
+ 0
+ }
+ }
+ fitparser::Value::Float32(_) => return None,
+ fitparser::Value::Float64(_) => return None,
+ fitparser::Value::UInt8z(v) => {
+ if v == 0 {
+ return None;
+ } else {
+ v as u64
+ }
+ }
+ fitparser::Value::UInt16z(v) => {
+ if v == 0 {
+ return None;
+ } else {
+ v as u64
+ }
+ }
+ fitparser::Value::UInt32z(v) => {
+ if v == 0 {
+ return None;
+ } else {
+ v as u64
+ }
+ }
+ fitparser::Value::UInt64z(v) => {
+ if v == 0 {
+ return None;
+ } else {
+ v
+ }
+ }
+ fitparser::Value::Timestamp(_) => return None,
+ fitparser::Value::Enum(_) => return None,
+ fitparser::Value::String(_) => return None,
+ fitparser::Value::Array(_) => return None,
+ };
+ Some(value)
+}
+
+fn run() -> Result<(), Box<dyn std::error::Error>> {
+ let args = Args::parse();
+ let mut reader: Box<dyn Read> = match args.file {
+ Some(ref filename) => Box::new(File::open(filename)?),
+ None => Box::new(stdin()),
+ };
+
+ let mut sessions = Vec::new();
+ let mut laps = Vec::new();
+
+ for msg in fitparser::from_reader(&mut reader)? {
+ match msg.kind() {
+ MesgNum::Session => {
+ sessions.push(parse_session(&args, &msg));
+ }
+ MesgNum::Lap => {
+ laps.push(parse_session(&args, &msg));
+ }
+ _ => (),
+ }
+ }
+
+ let activity: Vec<Session> = sessions
+ .into_iter()
+ .map(|(mut s, l)| {
+ let start = l.index as usize;
+ let end = (l.index + l.count) as usize;
+ if end > start {
+ s.laps = laps[start..end].iter().map(|(s, _)| s.clone()).collect();
+ s
+ } else {
+ s
+ }
+ })
+ .collect();
+
+ let location = activity
+ .iter()
+ .flat_map(|s| s.start_position)
+ .map(|uri| uri.to_string())
+ .take(1)
+ .collect();
+
+ let properties = EntryProperties {
+ published: activity.iter().flat_map(|s| s.start).min(),
+ category: args.category.clone(),
+ location,
+ activity,
+ };
+
+ let output = json!({
+ "type": ["h-entry"],
+ "properties": properties
+ });
+
+ serde_json::to_writer(stdout(), &output)?;
+
+ Ok(())
+}
+
+#[serde_with::skip_serializing_none]
+#[derive(Default, Serialize, Deserialize, Clone)]
+struct EntryProperties {
+ published: Option<DateTime<Utc>>,
+ #[serde(skip_serializing_if = "Vec::is_empty")]
+ category: Vec<String>,
+ #[serde(skip_serializing_if = "Vec::is_empty")]
+ location: Vec<String>,
+ activity: Vec<Session>,
+}
+
+fn main() {
+ process::exit(match run() {
+ Ok(_) => 0,
+ Err(e) => {
+ eprintln!("Error: {e}");
+ 1
+ }
+ })
+}