├── .github └── workflows │ ├── coverage.yml │ └── goigc.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── binder ├── README.md ├── environment.yml ├── gliding-data-starter.ipynb ├── heatmap.png ├── postBuild └── start ├── cmd └── goigc │ ├── crawl.go │ ├── parse.go │ ├── phases.go │ ├── root.go │ └── root_test.go ├── deployments └── docker │ ├── Dockerfile │ └── Dockerfile-alpine ├── doc.go ├── docs └── images │ └── track.png ├── go.mod ├── pkg ├── igc │ ├── bruteforce.go │ ├── bruteforce_test.go │ ├── crawler.go │ ├── doc.go │ ├── doc_test.go │ ├── flight.go │ ├── optimize.go │ ├── optimize_test.go │ ├── parse.go │ ├── parse_test.go │ ├── phase.go │ ├── phase_test.go │ ├── point.go │ ├── point_test.go │ ├── track.go │ └── track_test.go ├── netcoupe │ ├── crawler.go │ ├── crawler_test.go │ └── doc.go └── version │ └── version.go ├── scripts ├── bench.sh ├── changelog.sh ├── coverage.sh ├── pprof.sh └── validate-license.sh └── testdata ├── optimize ├── optimize-long-flight-1.igc └── optimize-short-flight-1.igc ├── parse ├── parse-0-basic-flight.1.igc ├── parse-0-basic-flight.1.igc.golden ├── parse-0-basic-header.1.igc ├── parse-0-basic-header.1.igc.golden ├── parse-0-benchmark-0.igc ├── parse-0-benchmark-0.igc.golden ├── parse-0-invalid-record.0.igc ├── parse-a-no-uniqueid.1.igc ├── parse-a-no-uniqueid.1.igc.golden ├── parse-a-too-short.0.igc ├── parse-b-bad-fix-validity.0.igc ├── parse-b-bad-gnss-altitude.0.igc ├── parse-b-bad-pressure-altitude.0.igc ├── parse-b-wrong-size.0.igc ├── parse-c-bad-num-lines.0.igc ├── parse-c-invalid-declaration-date.0.igc ├── parse-c-invalid-declaration-date.0.igc.golden ├── parse-c-invalid-finish.0.igc ├── parse-c-invalid-flight-date.0.igc ├── parse-c-invalid-flight-date.0.igc.golden ├── parse-c-invalid-landing.0.igc ├── parse-c-invalid-num-of-tps.0.igc ├── parse-c-invalid-start.0.igc ├── parse-c-invalid-takeoff.0.igc ├── parse-c-invalid-task-number.0.igc ├── parse-c-invalid-tp.0.igc ├── parse-c-wrong-size-first-line.0.igc ├── parse-d-wrong-size.0.igc ├── parse-e-invalid-date.0.igc ├── parse-e-wrong-size.0.igc ├── parse-f-invalid-date.0.igc ├── parse-f-invalid-num-satellites.0.igc ├── parse-f-invalid-num-satellites.0.igc.golden ├── parse-f-wrong-size.0.igc ├── parse-h-bad-date.0.igc ├── parse-h-bad-fix-accuracy.0.igc ├── parse-h-bad-timezone.0.igc ├── parse-h-date-too-short.0.igc ├── parse-h-fix-accuracy-too-short.0.igc ├── parse-h-fix-accuracy-too-short.0.igc.golden ├── parse-h-gps-datum-too-short.0.igc ├── parse-h-invalid-pats.0.igc ├── parse-h-too-short.0.igc ├── parse-h-tzo-timezone.1.igc ├── parse-h-tzo-timezone.1.igc.golden ├── parse-h-unknown-field.0.igc ├── parse-i-invalid-value-for-field-number.0.igc ├── parse-i-wrong-size-with-fields.0.igc ├── parse-i-wrong-size.0.igc ├── parse-j-invalid-value-for-field-number.0.igc ├── parse-j-invalid-value-for-field-number.1.igc.golden ├── parse-j-wrong-size-with-fields.0.igc ├── parse-j-wrong-size.0.igc ├── parse-k-invalid-date.0.igc ├── parse-k-wrong-size-with-fields.0.igc └── parse-k-wrong-size.0.igc ├── phases ├── phases-long-flight-1.golden.igc ├── phases-long-flight-1.igc ├── phases-long-flight-1.igc.golden.kml ├── phases-short-flight-1.golden.igc ├── phases-short-flight-1.igc └── phases-short-flight-1.igc.golden.kml └── simplify ├── simplify-short-flight-1.igc └── simplify-short-flight-1.igc.golden /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: coverage 2 | on: 3 | push: 4 | branches: 5 | - '*' 6 | jobs: 7 | test: 8 | name: Test with Coverage 9 | runs-on: ubuntu-latest 10 | env: 11 | GOVER: 1.13.3 12 | GOPROXY: https://proxy.golang.org 13 | steps: 14 | - name: Set up Go ${{ env.GOVER }} 15 | uses: actions/setup-go@v1 16 | with: 17 | go-version: ${{ env.GOVER }} 18 | - name: Check out code 19 | uses: actions/checkout@master 20 | - name: Run make test-coverage 21 | run: | 22 | make test-coverage 23 | - name: Send coverage 24 | env: 25 | COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | run: | 27 | GO111MODULE=off go get github.com/mattn/goveralls 28 | $(go env GOPATH)/bin/goveralls -coverprofile=profile.cov -service=github 29 | -------------------------------------------------------------------------------- /.github/workflows/goigc.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright The ezgliding Authors. 3 | # 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | name: goigc 19 | 20 | on: 21 | push: 22 | branches: 23 | - '*' 24 | tags: 25 | - 'v*' 26 | paths-ignore: 27 | - 'CONTRIBUTING.md' 28 | - 'README.md' 29 | - 'docs/**' 30 | jobs: 31 | build: 32 | name: Build binaries 33 | runs-on: ubuntu-latest 34 | env: 35 | GOVER: 1.13.3 36 | GOPROXY: https://proxy.golang.org 37 | steps: 38 | - name: Set up Go ${{ env.GOVER }} 39 | uses: actions/setup-go@v1 40 | with: 41 | go-version: ${{ env.GOVER }} 42 | - name: Install golangci-lint 43 | if: matrix.target_arch != 'arm' 44 | run: | 45 | curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b "${{ env.GOROOT }}/bin" v1.21.0 46 | - name: Check out code 47 | uses: actions/checkout@v1 48 | - name: Make test 49 | run: | 50 | make test 51 | - name: Make build for cross platform binaries 52 | run: | 53 | make build-cross 54 | - name: Make changelog 55 | if: startsWith(github.ref, 'refs/tags/v') 56 | run: | 57 | sudo apt-get update 58 | sudo apt-get install ruby 59 | make changelog 60 | env: 61 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 62 | - name: Release cross platform binaries 63 | uses: softprops/action-gh-release@v1 64 | if: startsWith(github.ref, 'refs/tags/v') 65 | with: 66 | files: | 67 | _dist/** 68 | body_path: CHANGELOG.md 69 | env: 70 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 71 | - name: Build docker images 72 | run: | 73 | make docker 74 | - name: Push docker images 75 | if: startsWith(github.ref, 'refs/tags/v') 76 | run: | 77 | sudo docker login -u ${{ secrets.DOCKER_REGISTRY_ID }} -p ${{ secrets.DOCKER_REGISTRY_PASS }} 78 | make docker-push 79 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | .coverage/ 4 | _dist/ 5 | bin/ 6 | CHANGELOG.md 7 | go.sum 8 | profile.cov 9 | vendor/ 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Development 2 | 3 | go get github.com/mitchellh/gox 4 | go get golang.org/x/tools/cmd/goimports 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | 3 | Apache License 4 | Version 2.0, January 2004 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, 12 | and distribution as defined by Sections 1 through 9 of this document. 13 | 14 | "Licensor" shall mean the copyright owner or entity authorized by 15 | the copyright owner that is granting the License. 16 | 17 | "Legal Entity" shall mean the union of the acting entity and all 18 | other entities that control, are controlled by, or are under common 19 | control with that entity. For the purposes of this definition, 20 | "control" means (i) the power, direct or indirect, to cause the 21 | direction or management of such entity, whether by contract or 22 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 23 | outstanding shares, or (iii) beneficial ownership of such entity. 24 | 25 | "You" (or "Your") shall mean an individual or Legal Entity 26 | exercising permissions granted by this License. 27 | 28 | "Source" form shall mean the preferred form for making modifications, 29 | including but not limited to software source code, documentation 30 | source, and configuration files. 31 | 32 | "Object" form shall mean any form resulting from mechanical 33 | transformation or translation of a Source form, including but 34 | not limited to compiled object code, generated documentation, 35 | and conversions to other media types. 36 | 37 | "Work" shall mean the work of authorship, whether in Source or 38 | Object form, made available under the License, as indicated by a 39 | copyright notice that is included in or attached to the work 40 | (an example is provided in the Appendix below). 41 | 42 | "Derivative Works" shall mean any work, whether in Source or Object 43 | form, that is based on (or derived from) the Work and for which the 44 | editorial revisions, annotations, elaborations, or other modifications 45 | represent, as a whole, an original work of authorship. For the purposes 46 | of this License, Derivative Works shall not include works that remain 47 | separable from, or merely link (or bind by name) to the interfaces of, 48 | the Work and Derivative Works thereof. 49 | 50 | "Contribution" shall mean any work of authorship, including 51 | the original version of the Work and any modifications or additions 52 | to that Work or Derivative Works thereof, that is intentionally 53 | submitted to Licensor for inclusion in the Work by the copyright owner 54 | or by an individual or Legal Entity authorized to submit on behalf of 55 | the copyright owner. For the purposes of this definition, "submitted" 56 | means any form of electronic, verbal, or written communication sent 57 | to the Licensor or its representatives, including but not limited to 58 | communication on electronic mailing lists, source code control systems, 59 | and issue tracking systems that are managed by, or on behalf of, the 60 | Licensor for the purpose of discussing and improving the Work, but 61 | excluding communication that is conspicuously marked or otherwise 62 | designated in writing by the copyright owner as "Not a Contribution." 63 | 64 | "Contributor" shall mean Licensor and any individual or Legal Entity 65 | on behalf of whom a Contribution has been received by Licensor and 66 | subsequently incorporated within the Work. 67 | 68 | 2. Grant of Copyright License. Subject to the terms and conditions of 69 | this License, each Contributor hereby grants to You a perpetual, 70 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 71 | copyright license to reproduce, prepare Derivative Works of, 72 | publicly display, publicly perform, sublicense, and distribute the 73 | Work and such Derivative Works in Source or Object form. 74 | 75 | 3. Grant of Patent License. Subject to the terms and conditions of 76 | this License, each Contributor hereby grants to You a perpetual, 77 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 78 | (except as stated in this section) patent license to make, have made, 79 | use, offer to sell, sell, import, and otherwise transfer the Work, 80 | where such license applies only to those patent claims licensable 81 | by such Contributor that are necessarily infringed by their 82 | Contribution(s) alone or by combination of their Contribution(s) 83 | with the Work to which such Contribution(s) was submitted. If You 84 | institute patent litigation against any entity (including a 85 | cross-claim or counterclaim in a lawsuit) alleging that the Work 86 | or a Contribution incorporated within the Work constitutes direct 87 | or contributory patent infringement, then any patent licenses 88 | granted to You under this License for that Work shall terminate 89 | as of the date such litigation is filed. 90 | 91 | 4. Redistribution. You may reproduce and distribute copies of the 92 | Work or Derivative Works thereof in any medium, with or without 93 | modifications, and in Source or Object form, provided that You 94 | meet the following conditions: 95 | 96 | (a) You must give any other recipients of the Work or 97 | Derivative Works a copy of this License; and 98 | 99 | (b) You must cause any modified files to carry prominent notices 100 | stating that You changed the files; and 101 | 102 | (c) You must retain, in the Source form of any Derivative Works 103 | that You distribute, all copyright, patent, trademark, and 104 | attribution notices from the Source form of the Work, 105 | excluding those notices that do not pertain to any part of 106 | the Derivative Works; and 107 | 108 | (d) If the Work includes a "NOTICE" text file as part of its 109 | distribution, then any Derivative Works that You distribute must 110 | include a readable copy of the attribution notices contained 111 | within such NOTICE file, excluding those notices that do not 112 | pertain to any part of the Derivative Works, in at least one 113 | of the following places: within a NOTICE text file distributed 114 | as part of the Derivative Works; within the Source form or 115 | documentation, if provided along with the Derivative Works; or, 116 | within a display generated by the Derivative Works, if and 117 | wherever such third-party notices normally appear. The contents 118 | of the NOTICE file are for informational purposes only and 119 | do not modify the License. You may add Your own attribution 120 | notices within Derivative Works that You distribute, alongside 121 | or as an addendum to the NOTICE text from the Work, provided 122 | that such additional attribution notices cannot be construed 123 | as modifying the License. 124 | 125 | You may add Your own copyright statement to Your modifications and 126 | may provide additional or different license terms and conditions 127 | for use, reproduction, or distribution of Your modifications, or 128 | for any such Derivative Works as a whole, provided Your use, 129 | reproduction, and distribution of the Work otherwise complies with 130 | the conditions stated in this License. 131 | 132 | 5. Submission of Contributions. Unless You explicitly state otherwise, 133 | any Contribution intentionally submitted for inclusion in the Work 134 | by You to the Licensor shall be under the terms and conditions of 135 | this License, without any additional terms or conditions. 136 | Notwithstanding the above, nothing herein shall supersede or modify 137 | the terms of any separate license agreement you may have executed 138 | with Licensor regarding such Contributions. 139 | 140 | 6. Trademarks. This License does not grant permission to use the trade 141 | names, trademarks, service marks, or product names of the Licensor, 142 | except as required for reasonable and customary use in describing the 143 | origin of the Work and reproducing the content of the NOTICE file. 144 | 145 | 7. Disclaimer of Warranty. Unless required by applicable law or 146 | agreed to in writing, Licensor provides the Work (and each 147 | Contributor provides its Contributions) on an "AS IS" BASIS, 148 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 149 | implied, including, without limitation, any warranties or conditions 150 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 151 | PARTICULAR PURPOSE. You are solely responsible for determining the 152 | appropriateness of using or redistributing the Work and assume any 153 | risks associated with Your exercise of permissions under this License. 154 | 155 | 8. Limitation of Liability. In no event and under no legal theory, 156 | whether in tort (including negligence), contract, or otherwise, 157 | unless required by applicable law (such as deliberate and grossly 158 | negligent acts) or agreed to in writing, shall any Contributor be 159 | liable to You for damages, including any direct, indirect, special, 160 | incidental, or consequential damages of any character arising as a 161 | result of this License or out of the use or inability to use the 162 | Work (including but not limited to damages for loss of goodwill, 163 | work stoppage, computer failure or malfunction, or any and all 164 | other commercial damages or losses), even if such Contributor 165 | has been advised of the possibility of such damages. 166 | 167 | 9. Accepting Warranty or Additional Liability. While redistributing 168 | the Work or Derivative Works thereof, You may choose to offer, 169 | and charge a fee for, acceptance of support, warranty, indemnity, 170 | or other liability obligations and/or rights consistent with this 171 | License. However, in accepting such obligations, You may act only 172 | on Your own behalf and on Your sole responsibility, not on behalf 173 | of any other Contributor, and only if You agree to indemnify, 174 | defend, and hold each Contributor harmless for any liability 175 | incurred by, or claims asserted against, such Contributor by reason 176 | of your accepting any such warranty or additional liability. 177 | 178 | END OF TERMS AND CONDITIONS 179 | 180 | APPENDIX: How to apply the Apache License to your work. 181 | 182 | To apply the Apache License to your work, attach the following 183 | boilerplate notice, with the fields enclosed by brackets "[]" 184 | replaced with your own identifying information. (Don't include 185 | the brackets!) The text should be enclosed in the appropriate 186 | comment syntax for the file format. We also recommend that a 187 | file or class name and description of purpose be included on the 188 | same "printed page" as the copyright notice for easier 189 | identification within third-party archives. 190 | 191 | Copyright [yyyy] [name of copyright owner] 192 | 193 | Licensed under the Apache License, Version 2.0 (the "License"); 194 | you may not use this file except in compliance with the License. 195 | You may obtain a copy of the License at 196 | 197 | http://www.apache.org/licenses/LICENSE-2.0 198 | 199 | Unless required by applicable law or agreed to in writing, software 200 | distributed under the License is distributed on an "AS IS" BASIS, 201 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 202 | See the License for the specific language governing permissions and 203 | limitations under the License. 204 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright The ezgliding Authors. 2 | # 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | # Based on the helm Makefile: 17 | # https://github.com/helm/helm/blob/master/Makefile 18 | 19 | BINDIR := $(CURDIR)/bin 20 | DIST_DIRS := find * -type d -exec 21 | TARGETS := darwin/amd64 linux/amd64 linux/386 linux/arm linux/arm64 linux/ppc64le windows/amd64 22 | BINNAME ?= goigc 23 | 24 | GOPATH = $(shell go env GOPATH) 25 | GOROOT ?= /usr/share/go 26 | GOX = $(GOPATH)/bin/gox 27 | GOIMPORTS = $(GOPATH)/bin/goimports 28 | 29 | # go option 30 | PKG := ./... 31 | TAGS := 32 | TESTS := . 33 | TESTFLAGS := 34 | LDFLAGS := -w -s 35 | GOFLAGS := 36 | SRC := $(shell find . -type f -name '*.go' -print) 37 | 38 | # Required for globs to work correctly 39 | SHELL = /bin/bash 40 | 41 | GIT_BRANCH = $(shell git rev-parse --abbrev-ref HEAD) 42 | GIT_COMMIT = $(shell git rev-parse HEAD) 43 | GIT_SHA = $(shell git rev-parse --short HEAD) 44 | GIT_TAG = $(shell git describe --tags --abbrev=0 --exact-match 2>/dev/null) 45 | GIT_DIRTY = $(shell test -n "`git status --porcelain`" && echo "dirty" || echo "clean") 46 | 47 | ifdef VERSION 48 | BINARY_VERSION = $(VERSION) 49 | endif 50 | BINARY_VERSION ?= ${GIT_TAG} 51 | 52 | BASE_PKG = github.com/ezgliding/goigc 53 | # Only set Version if building a tag or VERSION is set 54 | ifneq ($(BINARY_VERSION),) 55 | LDFLAGS += -X ${BASE_PKG}/pkg/version.version=${BINARY_VERSION} 56 | endif 57 | 58 | # Clear the "unreleased" string in BuildMetadata 59 | ifneq ($(GIT_TAG),) 60 | LDFLAGS += -X ${BASE_PKG}/pkg/version.metadata= 61 | endif 62 | LDFLAGS += -X ${BASE_PKG}/pkg/version.commit=${GIT_COMMIT} 63 | LDFLAGS += -X ${BASE_PKG}/pkg/version.treestate=${GIT_DIRTY} 64 | 65 | .PHONY: all 66 | all: build 67 | 68 | # ------------------------------------------------------------------------------ 69 | # build 70 | 71 | .PHONY: build 72 | build: $(BINDIR)/$(BINNAME) 73 | 74 | $(BINDIR)/$(BINNAME): $(SRC) 75 | GO111MODULE=on go build $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)' -o $(BINDIR)/$(BINNAME) ./cmd/goigc 76 | 77 | # ------------------------------------------------------------------------------ 78 | # test 79 | 80 | .PHONY: test 81 | test: build 82 | test: TESTFLAGS += -race -v 83 | test: test-style 84 | test: test-unit 85 | 86 | .PHONY: test-unit 87 | test-unit: 88 | @echo 89 | @echo "==> Running unit tests <==" 90 | GO111MODULE=on go test $(GOFLAGS) -run $(TESTS) $(PKG) $(TESTFLAGS) 91 | 92 | .PHONY: test-golden 93 | test-golden: 94 | @echo 95 | @echo "==> Running unit tests with golden flag <==" 96 | # TODO(rochaporto): drop this mv once issue below is solved in corba 97 | # https://github.com/golang/go/issues/31859 98 | GO111MODULE=on mv cmd /tmp; go test $(GOFLAGS) -run $(TESTS) $(PKG) $(TESTFLAGS) -update || true; mv /tmp/cmd . 99 | 100 | .PHONY: test-coverage 101 | test-coverage: 102 | @echo 103 | @echo "==> Running unit tests with coverage <==" 104 | @ ./scripts/coverage.sh 105 | 106 | .PHONY: test-style 107 | test-style: 108 | GO111MODULE=on golangci-lint run 109 | @scripts/validate-license.sh 110 | 111 | .PHONY: coverage 112 | coverage: 113 | @scripts/coverage.sh 114 | 115 | .PHONY: format 116 | format: $(GOIMPORTS) 117 | GO111MODULE=on go list -f '{{.Dir}}' ./... | xargs $(GOIMPORTS) -w 118 | 119 | # ------------------------------------------------------------------------------ 120 | # dependencies 121 | 122 | # If go get is run from inside the project directory it will add the dependencies 123 | # to the go.mod file. To avoid that we change to a directory without a go.mod file 124 | # when downloading the following dependencies 125 | 126 | $(GOX): 127 | (cd /; GO111MODULE=on go get -u github.com/mitchellh/gox) 128 | 129 | $(GOIMPORTS): 130 | (cd /; GO111MODULE=on go get -u golang.org/x/tools/cmd/goimports) 131 | 132 | # ------------------------------------------------------------------------------ 133 | # release 134 | 135 | .PHONY: build-cross 136 | build-cross: LDFLAGS += -extldflags "-static" 137 | build-cross: $(GOX) 138 | GO111MODULE=on CGO_ENABLED=0 $(GOX) -parallel=3 -output="_dist/{{.OS}}-{{.Arch}}/$(BINNAME)_{{.OS}}_{{.Arch}}" -osarch='$(TARGETS)' $(GOFLAGS) -tags '$(TAGS)' -ldflags '$(LDFLAGS)' ./cmd/goigc 139 | 140 | .PHONY: dist 141 | dist: 142 | ( \ 143 | cd _dist && \ 144 | $(DIST_DIRS) cp ../LICENSE {} \; && \ 145 | $(DIST_DIRS) cp ../README.md {} \; && \ 146 | $(DIST_DIRS) tar -zcf goigc-${VERSION}-{}.tar.gz {} \; && \ 147 | $(DIST_DIRS) zip -r goigc-${VERSION}-{}.zip {} \; \ 148 | ) 149 | 150 | .PHONY: checksum 151 | checksum: 152 | for f in _dist/*.{gz,zip} ; do \ 153 | shasum -a 256 "$${f}" | awk '{print $$1}' > "$${f}.sha256" ; \ 154 | done 155 | 156 | .PHONY: changelog 157 | changelog: 158 | @./scripts/changelog.sh 159 | 160 | # ------------------------------------------------------------------------------ 161 | # docker 162 | DOCKER_TAG=${GIT_BRANCH} 163 | ifneq ($(GIT_TAG),) 164 | DOCKER_TAG = ${GIT_TAG} 165 | endif 166 | 167 | .PHONY: docker 168 | docker: build-cross 169 | sudo docker build -t ezgliding/goigc:${DOCKER_TAG} -f deployments/docker/Dockerfile . 170 | sudo docker build -t ezgliding/goigc:${DOCKER_TAG}-alpine -f deployments/docker/Dockerfile-alpine . 171 | 172 | .PHONY: docker-push 173 | docker-push: docker 174 | sudo docker push ezgliding/goigc:${DOCKER_TAG} 175 | sudo docker push ezgliding/goigc:${DOCKER_TAG}-alpine 176 | 177 | # ------------------------------------------------------------------------------ 178 | .PHONY: doc 179 | doc: 180 | godoc -http=:6060 -goroot $(GOROOT) 2>&1 & 181 | browse http://localhost:6060 182 | 183 | # ------------------------------------------------------------------------------ 184 | .PHONY: clean 185 | clean: 186 | @rm -rf $(BINDIR) ./_dist 187 | 188 | .PHONY: info 189 | info: 190 | @echo "Version: ${VERSION}" 191 | @echo "Git Tag: ${GIT_TAG}" 192 | @echo "Git Commit: ${GIT_COMMIT}" 193 | @echo "Git Tree State: ${GIT_DIRTY}" 194 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # goigc 2 | 3 | [![Build Status](https://github.com/ezgliding/goigc/workflows/goigc/badge.svg?event=push&branch=master)](https://github.com/ezgliding/goigc/actions?workflow=goigc) 4 | [![Coverage Status](https://coveralls.io/repos/github/ezgliding/goigc/badge.svg?branch=master)](https://coveralls.io/github/ezgliding/goigc?branch=master) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/ezgliding/goigc)](https://goreportcard.com/report/github.com/ezgliding/goigc) 6 | [![GoDoc](https://godoc.org/github.com/ezgliding/goigc?status.svg)](https://godoc.org/github.com/ezgliding/goigc) 7 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 8 | 9 | goigc is a portable utility to parse and analyse gliding flights, supporting 10 | [igc](http://www.fai.org/component/phocadownload/category/?download=5745:igc-flight-recorder-specification-edition-2-with-al1-2011-5-31) as well as other popular 11 | gps formats. 12 | 13 | You can use goigc to: 14 | * Parse and view metadata for flights in multiple formats (igc, gpx, ...) 15 | * Analyse flight phases, thermalling, straight flight, percentages, etc 16 | * Convert flights between all formats 17 | * Run flight optimization following popular competition rules (olc, netcoupe, 18 | ...) 19 | 20 | ## Install 21 | 22 | Binary downloads of the goigc client can be found in the 23 | [releases page](https://github.com/ezgliding/goigc/releases). 24 | 25 | ## Community and Contributing 26 | 27 | ## Roadmap 28 | 29 | The goigc roadmap uses [github 30 | milestones](https://github.com/ezgliding/goigc/milestones) to track the 31 | progress of the project. 32 | -------------------------------------------------------------------------------- /binder/README.md: -------------------------------------------------------------------------------- 1 | # Gliding Dataset 2 | 3 | [![Binder](http://mybinder.org/badge.svg)](http://mybinder.org/v2/gh/ezgliding/goigc/master?filepath=binder/gliding-data-starter.ipynb) 4 | 5 | This binder contains an open dataset with over 100k gliding flights and more 6 | than 6 million gliding phases, along with sample notebooks to better 7 | understand its data. 8 | 9 | Launch it via the badge above. 10 | 11 | ![heatmap](./heatmap.png) 12 | 13 | An alternative version is available on 14 | [kaggle](https://www.kaggle.com/rochaporto/gliding-data-starter/notebook). 15 | 16 | ## Contributing 17 | 18 | If you come up with nice visualizations or data analysis that you would like to 19 | share please do so opening pull requests. 20 | 21 | -------------------------------------------------------------------------------- /binder/environment.yml: -------------------------------------------------------------------------------- 1 | name: goigc 2 | channels: 3 | - conda-forge 4 | dependencies: 5 | - python 6 | - gdown 7 | - matplotlib 8 | - numpy 9 | - pandas 10 | - pip 11 | - pip: 12 | - git+https://github.com/python-visualization/branca 13 | - git+https://github.com/sknzl/folium@update-css-url-to-https 14 | -------------------------------------------------------------------------------- /binder/gliding-data-starter.ipynb: -------------------------------------------------------------------------------- 1 | {"cells":[{"metadata":{},"cell_type":"markdown","source":"# Introduction\n\nThis kernel gives a starter over the contents of the Gliding Data dataset.\n\nThe dataset includes metadata and calculated phases for over 100000 gliding flights from 2016 to 2019, mostly in the region of France but also Belgium, Switzerland and others. In total there are more than 6 million flights phases recorded.\n\n## Gliding\n\nGliding is a leisure aviation activity and sport where pilots fly unpowered aircraft known as gliders or sailplanes.\n\nThe principle of gliding is to climb using some sort of lift and convert the altitude gained into distance. Repeating this process allows for very long distance flights, often above 500km or even 1000km - the current [free distance world record](https://www.fai.org/record/7605) being just over 3000km in Patagonia. This is the same process birds follow to reduce the amount of effort required to travel great distances.\n\nThe most used sources of lift include:\n* thermals: where the air rises due to heating from the sun, often marked by cumulus clouds at the top\n* wave: created by strong winds and stable air layers facing a mountain or a high hill, often marked by lenticular clouds\n\nwith thermalling being by far the most common, and this dataset reflects that.\n\n![](http://www.flybc.org/thermals2010.jpg)\n\n## Data Recorders\n\nWhen flying pilots often carry some sort of GPS recording device which generates a track of points collected every few seconds. Each point contains fields for latitude, longitude and altitude (pressure and GPS) among several others, often stored in [IGC](http://www.ukiws.demon.co.uk/GFAC/documents/tech_spec_gnss.pdf) format. Often these tracks are uploaded to online competitions where they are shared with other pilots, and can be easily visualized.\n\n![](https://raw.githubusercontent.com/ezgliding/goigc/master/docs/images/track.png)\n\nThe data available in this dataset was scraped from the online competition [Netcoupe](https://netcoupe.net). The scraping, parsing and analysis was done using [goigc](https://github.com/ezgliding/goigc), an open source flight parser and analyser."},{"metadata":{},"cell_type":"markdown","source":"# Getting Started\n\nLet's start by loading the different files in the dataset and taking a peek at the records.\n\nThe available files include:\n* flight_websource: the metadata exposed in the online competition website, including additional information to what is present in the flight track\n* flight_track: the metadata collected directly from the GPS/IGC flight track\n* phases: the flight phases (cruising, circling) calculated from the GPS/IGC flight track (this is a large file, we load a subset below)\n* handicaps: a mostly static file with the different handicaps attributed to each glider type by the IGC, important when calculating flight performances"},{"metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"cell_type":"code","source":"import datetime\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\nimport random\n\nimport os\nfor dirname, _, filenames in os.walk('/tmp/gliding-data'):\n for filename in filenames:\n print(os.path.join(dirname, filename))","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","trusted":true},"cell_type":"code","source":"flight_websource = pd.read_csv(\"/tmp/gliding-data/flight_websource.csv\")\nflight_track = pd.read_csv(\"/tmp/gliding-data/flight_track.csv\")\nflight_phases = pd.read_csv(\"/tmp/gliding-data/phases.csv\", skiprows=lambda i: i>0 and random.random() > 0.5)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Flight Metadata\n\nThere's a lot of information contained in the two flight metadata files (websource and track)."},{"metadata":{"trusted":true},"cell_type":"code","source":"flight_websource.head(1)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"flight_track.head(1)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"The most useful information comes from the *websource* file as this information is passed directly by the pilot when submitting the flight to the online competition. Things like *Country* or *Region* provide useful statistics on how popular the sport is in different areas. As an example, what are the most popular regions considering the total number of flights? What about the most popular *Takeoff* location?"},{"metadata":{"trusted":true},"cell_type":"code","source":"flight_websource.groupby(['Country', 'Region'])['Region'].value_counts().sort_values(ascending=False).head(3)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"flight_websource.groupby(['Country', 'Year', 'Takeoff'])['Takeoff'].value_counts().sort_values(ascending=False).head(3)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"The three regions above match the Alps area, which is an expected result given this is a gliding Meca. The second result shows *Vinon* as the most popular takeoff in 2016, a big club also in the Southern Alps. But it's interesting to notice that more recently a club near Montpellier took over as the one with the most activity in terms of number of flights.\n\nGliding is a seasonal activity peaking in summer months. "},{"metadata":{"trusted":true},"cell_type":"code","source":"flight_websource['DayOfWeek'] = flight_websource.apply(lambda r: datetime.datetime.strptime(r['Date'], \"%Y-%m-%dT%H:%M:%SZ\").strftime(\"%A\"), axis=1)\nflight_websource.groupby(['DayOfWeek'])['DayOfWeek'].count().plot.bar()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"flight_websource['Month'] = flight_websource.apply(lambda r: datetime.datetime.strptime(r['Date'], \"%Y-%m-%dT%H:%M:%SZ\").strftime(\"%m\"), axis=1)\nflight_websource.groupby(['Month'])['Month'].count().plot.bar()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Merging Data\n\nWe can get additional flight metadata information from the *flight_track* file, but you can expect this data to be less reliable as it's often the case that the metadata in the flight recorder is not updated before a flight. It is very useful though to know more about what type of recorder was used, calibration settings, etc. It is also the source of the flight tracks used to generate the data in the *phases* file.\n\nIn some cases we want to handle columns from both flight metadata files, so it's useful to join the two sets. We can rely on the ID for this purpose."},{"metadata":{"trusted":true},"cell_type":"code","source":"flight_all = pd.merge(flight_websource, flight_track, how='left', on='ID')\nflight_all.head(1)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Flight Phases\n\nIn addition to the flight metadata provided in the files above, by analysing the GPS flight tracks we can generate a lot more interesting data.\n\nHere we take a look at flight phases, calculated using the [goigc](https://github.com/ezglding/goigc) tool. As described earlier to travel further glider pilots use thermals to gain altitude and then convert that altitude into distance. In the *phases* file we have a record of each individual phase detected for each of the 100k flights, and we'll focus on:\n* Circling (5): phase where a glider is gaining altitude by circling in an area of rising air\n* Cruising (3): phase where a glider is flying straight converting altitude into distance\n\nThese are indicated by the integer field *Type* below. Each phase has a set of additional fields with relevant statistics for each phase type: while circling the average climb rate (vario) and duration are interesting; while cruising the distance covered and LD (glide ratio) are more interesting."},{"metadata":{"trusted":true},"cell_type":"code","source":"flight_phases.head(1)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# Data Preparation\n\nAs a quick example of what is possible with this kind of data let's take try to map all *circling* phases as a HeatMap.\n\nFirst we need to do some treatment of the data: convert coordinates from radians to degrees, filter out unreasonable values (climb rates above 15m/s are due to errors in the recording device), convert the date to the expected format and desired grouping. In this case we're grouping all thermal phases by week."},{"metadata":{"trusted":true},"cell_type":"code","source":"phases = pd.merge(flight_phases, flight_websource[['TrackID', 'Distance', 'Speed']], on='TrackID')\nphases['Lat'] = np.rad2deg(phases['CentroidLatitude'])\nphases['Lng'] = np.rad2deg(phases['CentroidLongitude'])\n\nphases_copy = phases[phases.Type==5][phases.AvgVario<10][phases.AvgVario>2].copy()\nphases_copy.head(2)\n\n#phases_copy['AM'] = phases_copy.apply(lambda r: datetime.datetime.strptime(r['StartTime'], \"%Y-%m-%dT%H:%M:%SZ\").strftime(\"%p\"), axis=1)\n#phases_copy['Day'] = phases_copy.apply(lambda r: datetime.datetime.strptime(r['StartTime'], \"%Y-%m-%dT%H:%M:%SZ\").strftime(\"%j\"), axis=1)\n#phases_copy['Week'] = phases_copy.apply(lambda r: datetime.datetime.strptime(r['StartTime'], \"%Y-%m-%dT%H:%M:%SZ\").strftime(\"%W\"), axis=1)\n#phases_copy['Month'] = phases_copy.apply(lambda r: r['StartTime'][5:7], axis=1)\n#phases_copy['Year'] = phases_copy.apply(lambda r: r['StartTime'][0:4], axis=1)\n#phases_copy['YearMonth'] = phases_copy.apply(lambda r: r['StartTime'][0:7], axis=1)\n#phases_copy['YearMonthDay'] = phases_copy.apply(lambda r: r['StartTime'][0:10], axis=1)\n\n# use the corresponding function above to update the grouping to something other than week\nphases_copy['Group'] = phases_copy.apply(lambda r: datetime.datetime.strptime(r['StartTime'], \"%Y-%m-%dT%H:%M:%SZ\").strftime(\"%W\"), axis=1)\nphases_copy.head(1)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"## Visualization\n\nOnce we have the data ready we can visualize it over a map. We rely on folium for this."},{"metadata":{"trusted":true},"cell_type":"code","source":"# This is a workaround for this known issue:\n# https://github.com/python-visualization/folium/issues/812#issuecomment-582213307\n!pip install git+https://github.com/python-visualization/branca\n!pip install git+https://github.com/sknzl/folium@update-css-url-to-https","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"import folium\nfrom folium import plugins\nfrom folium import Choropleth, Circle, Marker\nfrom folium.plugins import HeatMap, HeatMapWithTime, MarkerCluster\n\n# folium.__version__ # should be '0.10.1+8.g4ea1307'\n# folium.branca.__version__ # should be '0.4.0+4.g6ac241a'","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Single HeatMap"},{"metadata":{"trusted":true},"cell_type":"code","source":"# we use a smaller sample to improve the visualization\n# a better alternative is to group entries by CellID, an example of this will be added later\nphases_single = phases_copy.sample(frac=0.01, random_state=1)\nm_5 = folium.Map(location=[47.06318, 5.41938], tiles='stamen terrain', zoom_start=7)\nHeatMap(\n phases_single[['Lat','Lng','AvgVario']], gradient={0.5: 'blue', 0.7: 'yellow', 1: 'red'},\n min_opacity=5, max_val=phases_single.AvgVario.max(), radius=4, max_zoom=7, blur=4, use_local_extrema=False).add_to(m_5)\n\nm_5","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### HeatMap over Time\n\nAnother cool possibility is to visualize the same data over time.\n\nIn this case we're grouping weekly and playing the data over one year.\n\nBoth the most popular areas and times of the year are pretty clear from this animation."},{"metadata":{"trusted":true},"cell_type":"code","source":"m_5 = folium.Map(location=[47.06318, 5.41938], tiles='stamen terrain', zoom_start=7)\n\ngroups = phases_copy.Group.sort_values().unique()\ndata = []\nfor g in groups:\n data.append(phases_copy.loc[phases_copy.Group==g,['Group','Lat','Lng','AvgVario']].groupby(['Lat','Lng']).sum().reset_index().values.tolist())\n \nHeatMapWithTime(\n data,\n index = list(phases_copy.Group.sort_values().unique()),\n gradient={0.1: 'blue', 0.3: 'yellow', 0.8: 'red'},\n auto_play=True, scale_radius=False, display_index=True, radius=4, min_speed=1, max_speed=6, speed_step=1,\n min_opacity=1, max_opacity=phases_copy.AvgVario.max(), use_local_extrema=True).add_to(m_5)\n\nm_5","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"","execution_count":null,"outputs":[]}],"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"}},"nbformat":4,"nbformat_minor":4} -------------------------------------------------------------------------------- /binder/heatmap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezgliding/goigc/2923ffc4206b52ff157ca1a679285af9515f7414/binder/heatmap.png -------------------------------------------------------------------------------- /binder/postBuild: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir -p /tmp/gliding-data 4 | 5 | cd /tmp/gliding-data 6 | gdown https://drive.google.com/uc?id=1_EcCg6CquBdewwJ9-mHvCH939ot31RE9 7 | 8 | cd $HOME 9 | -------------------------------------------------------------------------------- /binder/start: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd /tmp/gliding-data 4 | unzip gliding-data 5 | 6 | cd $HOME 7 | 8 | exec "$@" 9 | -------------------------------------------------------------------------------- /cmd/goigc/crawl.go: -------------------------------------------------------------------------------- 1 | // Copyright The ezgliding Authors. 2 | // 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | package main 17 | 18 | import ( 19 | "encoding/json" 20 | "fmt" 21 | "io/ioutil" 22 | "os" 23 | "time" 24 | 25 | "github.com/spf13/cobra" 26 | 27 | "github.com/ezgliding/goigc/pkg/igc" 28 | "github.com/ezgliding/goigc/pkg/netcoupe" 29 | ) 30 | 31 | func init() { 32 | crawlCmd.Flags().String("source", "netcoupe", "online web source to crawl") 33 | rootCmd.AddCommand(crawlCmd) 34 | } 35 | 36 | var crawlCmd = &cobra.Command{ 37 | Use: "crawl START END PATH", 38 | Short: "crawls flights from the given web source", 39 | Long: `Crawls the given web source for gliding flights between START and END date. 40 | 41 | Expected format for start and end dates is 2006-01-02. 42 | 43 | Results are stored under PATH with the following structure. 44 | 45 | PATH/YEAR 46 | /DD-MM-YYYY.json ( one json file per day with flight metadata ) 47 | /flights 48 | /TRACKID ( one file with the flight track in the original format ) 49 | `, 50 | Args: cobra.ExactArgs(3), 51 | RunE: func(cmd *cobra.Command, args []string) error { 52 | 53 | start, err := time.Parse("2006-01-02", args[0]) 54 | if err != nil { 55 | return err 56 | } 57 | end, err := time.Parse("2006-01-02", args[1]) 58 | if err != nil { 59 | return err 60 | } 61 | if start.Year() != end.Year() { 62 | return fmt.Errorf("Start and end year must be the same") 63 | } 64 | basePath := args[2] 65 | err = os.MkdirAll(fmt.Sprintf("%v/%v/flights", basePath, start.Year()), os.ModePerm) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | var n netcoupe.Netcoupe = netcoupe.NewNetcoupeYear(start.Year()) 71 | current := start 72 | for ; end.After(current.AddDate(0, 0, -1)); current = current.AddDate(0, 0, 1) { 73 | var flights []igc.Flight 74 | dbFile := fmt.Sprintf("%v/%v/%v.json", basePath, current.Year(), current.Format("02-01-2006")) 75 | if _, err := os.Stat(dbFile); os.IsNotExist(err) { 76 | flights, err = n.Crawl(current, current) 77 | if err != nil { 78 | return err 79 | } 80 | jsonFlights, err := json.MarshalIndent(flights, "", " ") 81 | if err != nil { 82 | return err 83 | } 84 | err = ioutil.WriteFile(dbFile, jsonFlights, 0644) 85 | if err != nil { 86 | return err 87 | } 88 | } else { 89 | b, err := ioutil.ReadFile(dbFile) 90 | if err != nil { 91 | return err 92 | } 93 | err = json.Unmarshal(b, &flights) 94 | if err != nil { 95 | return err 96 | } 97 | } 98 | 99 | for _, f := range flights { 100 | flightFile := fmt.Sprintf("%v/%v/flights/%v", basePath, current.Year(), f.TrackID) 101 | if _, err := os.Stat(flightFile); os.IsNotExist(err) { 102 | url := fmt.Sprintf("%v%v", n.TrackBaseUrl(), f.TrackID) 103 | data, err := n.Get(url) 104 | if err != nil { 105 | return err 106 | } 107 | err = ioutil.WriteFile(flightFile, data, 0644) 108 | if err != nil { 109 | return err 110 | } 111 | } 112 | } 113 | } 114 | return nil 115 | }, 116 | } 117 | -------------------------------------------------------------------------------- /cmd/goigc/parse.go: -------------------------------------------------------------------------------- 1 | // Copyright The ezgliding Authors. 2 | // 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | package main 17 | 18 | import ( 19 | "fmt" 20 | "io/ioutil" 21 | 22 | "github.com/spf13/cobra" 23 | 24 | "github.com/ezgliding/goigc/pkg/igc" 25 | ) 26 | 27 | func init() { 28 | // TODO(rochaporto): not yet supported, only igc 29 | parseCmd.Flags().String("format", "", "input file format - auto detection by default") 30 | parseCmd.Flags().Bool("no-points", false, "do not include individual points") 31 | parseCmd.Flags().String("output-format", "yaml", "output format for display") 32 | parseCmd.Flags().String("output-file", "/dev/stdout", "output file to write to") 33 | rootCmd.AddCommand(parseCmd) 34 | } 35 | 36 | var parseCmd = &cobra.Command{ 37 | Use: "parse FILE", 38 | Short: "parses information about the given flight", 39 | Long: "", 40 | Args: cobra.ExactArgs(1), 41 | RunE: func(cmd *cobra.Command, args []string) error { 42 | outputFile, err := cmd.Flags().GetString("output-file") 43 | if err != nil { 44 | return err 45 | } 46 | outputFormat, err := cmd.Flags().GetString("output-format") 47 | if err != nil { 48 | return err 49 | } 50 | 51 | trk, err := igc.ParseLocation(args[0]) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | noPoints, _ := cmd.Flags().GetBool("no-points") 57 | if noPoints { 58 | trk = igc.Track{Header: trk.Header} 59 | } 60 | result, err := trk.Encode(outputFormat) 61 | if err != nil { 62 | return err 63 | } 64 | if outputFile == "/dev/stdout" { 65 | fmt.Printf("%v", string(result)) 66 | } else { 67 | err = ioutil.WriteFile(outputFile, result, 0644) 68 | if err != nil { 69 | return err 70 | } 71 | } 72 | 73 | return nil 74 | }, 75 | } 76 | -------------------------------------------------------------------------------- /cmd/goigc/phases.go: -------------------------------------------------------------------------------- 1 | // Copyright The ezgliding Authors. 2 | // 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | package main 17 | 18 | import ( 19 | "fmt" 20 | "io/ioutil" 21 | 22 | "github.com/ezgliding/goigc/pkg/igc" 23 | "github.com/spf13/cobra" 24 | ) 25 | 26 | func init() { 27 | phasesCmd.Flags().String("output-format", "yaml", "output format for display") 28 | phasesCmd.Flags().String("output-file", "/dev/stdout", "output file to write to") 29 | rootCmd.AddCommand(phasesCmd) 30 | } 31 | 32 | var phasesCmd = &cobra.Command{ 33 | Use: "phases FILE", 34 | Short: "compute phases for the given flight", 35 | Long: "", 36 | Args: cobra.ExactArgs(1), 37 | RunE: func(cmd *cobra.Command, args []string) error { 38 | outputFile, err := cmd.Flags().GetString("output-file") 39 | if err != nil { 40 | return err 41 | } 42 | outputFormat, err := cmd.Flags().GetString("output-format") 43 | if err != nil { 44 | return err 45 | } 46 | 47 | trk, err := igc.ParseLocation(args[0]) 48 | if err != nil { 49 | return err 50 | } 51 | result, err := trk.EncodePhases(outputFormat) 52 | if err != nil { 53 | return err 54 | } 55 | if outputFile == "/dev/stdout" { 56 | fmt.Printf("%v", string(result)) 57 | } else { 58 | err = ioutil.WriteFile(outputFile, result, 0644) 59 | if err != nil { 60 | return err 61 | } 62 | } 63 | 64 | return nil 65 | }, 66 | } 67 | -------------------------------------------------------------------------------- /cmd/goigc/root.go: -------------------------------------------------------------------------------- 1 | // Copyright The ezgliding Authors. 2 | // 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | package main 17 | 18 | import ( 19 | "fmt" 20 | "os" 21 | 22 | "github.com/ezgliding/goigc/pkg/version" 23 | "github.com/spf13/cobra" 24 | ) 25 | 26 | var ( 27 | rootCmd = &cobra.Command{ 28 | Use: "goigc", 29 | Short: "goigc is a parser and analyser for gliding flights", 30 | Long: "", 31 | Version: fmt.Sprintf("%v %.7v %v %v", version.Version(), version.Commit(), 32 | version.BuildTime().Format("02/01/06 15:04:05"), version.Metadata()), 33 | Hidden: true, 34 | SilenceUsage: true, 35 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 36 | cmd.SilenceErrors, _ = cmd.Flags().GetBool("silent") 37 | }, 38 | } 39 | ) 40 | 41 | func init() { 42 | } 43 | 44 | func Execute() { 45 | if err := rootCmd.Execute(); err != nil { 46 | os.Exit(1) 47 | } 48 | } 49 | 50 | func main() { 51 | rootCmd.PersistentFlags().Bool("silent", false, "do not print any errors") 52 | rootCmd.SetVersionTemplate( 53 | `{{with .Name}}{{printf "%s " .}}{{end}}{{printf "%s" .Version}} 54 | `) 55 | Execute() 56 | } 57 | -------------------------------------------------------------------------------- /cmd/goigc/root_test.go: -------------------------------------------------------------------------------- 1 | // Copyright The ezgliding Authors. 2 | // 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package main 18 | 19 | import ( 20 | "bytes" 21 | "os" 22 | "testing" 23 | ) 24 | 25 | func TestRootCmd(t *testing.T) { 26 | 27 | tests := []struct { 28 | name string 29 | args []string 30 | envars map[string]string 31 | }{ 32 | { 33 | name: "defaults", 34 | args: []string{""}, 35 | }, 36 | } 37 | 38 | cmd := rootCmd 39 | 40 | for _, tt := range tests { 41 | t.Run(tt.name, func(t *testing.T) { 42 | buf := new(bytes.Buffer) 43 | for k, v := range tt.envars { 44 | os.Setenv(k, v) 45 | } 46 | cmd.SetOutput(buf) 47 | cmd.SetArgs(tt.args) 48 | err := cmd.Execute() 49 | if err != nil { 50 | t.Fatal() 51 | } 52 | 53 | }) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /deployments/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright The ezgliding Authors. 2 | # 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | FROM alpine:3 as alpine 17 | RUN apk add -U --no-cache ca-certificates 18 | 19 | FROM scratch 20 | WORKDIR / 21 | COPY --from=alpine /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 22 | ADD _dist/linux-amd64/goigc_linux_amd64 /usr/bin/goigc 23 | USER 1000 24 | ENTRYPOINT ["goigc"] 25 | CMD ["--help"] 26 | -------------------------------------------------------------------------------- /deployments/docker/Dockerfile-alpine: -------------------------------------------------------------------------------- 1 | # Copyright The ezgliding Authors. 2 | # 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | FROM alpine:3 17 | RUN apk add -U --no-cache ca-certificates bash 18 | ADD _dist/linux-amd64/goigc_linux_amd64 /usr/bin/goigc 19 | WORKDIR / 20 | USER 1000 21 | ENTRYPOINT ["goigc"] 22 | CMD ["--help"] 23 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Copyright The ezgliding Authors. 2 | // 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | // Package goigc contains functionality to parse and analyse gliding flights. 18 | // 19 | // goigc contains the following packages: 20 | // 21 | // The cmd packages contains command line utilities, namely the goigc binary. 22 | // 23 | // The pkg/igc provides the parsing and analysis code for the igc format. 24 | // 25 | package goigc 26 | 27 | // blank imports help docs. 28 | import ( 29 | // pkg/igc package 30 | _ "github.com/ezgliding/goigc/pkg/igc" 31 | // pkg/version package 32 | _ "github.com/ezgliding/goigc/pkg/version" 33 | ) 34 | -------------------------------------------------------------------------------- /docs/images/track.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezgliding/goigc/2923ffc4206b52ff157ca1a679285af9515f7414/docs/images/track.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ezgliding/goigc 2 | 3 | require ( 4 | github.com/PuerkitoBio/goquery v1.5.1 // indirect 5 | github.com/antchfx/htmlquery v1.2.2 // indirect 6 | github.com/antchfx/xmlquery v1.2.3 // indirect 7 | github.com/antchfx/xpath v1.1.5 // indirect 8 | github.com/gobwas/glob v0.2.3 // indirect 9 | github.com/gocolly/colly v1.2.0 10 | github.com/golang/geo v0.0.0-20181008215305-476085157cff 11 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect 12 | github.com/kennygrant/sanitize v1.2.4 // indirect 13 | github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca // indirect 14 | github.com/sirupsen/logrus v1.5.0 15 | github.com/spf13/cobra v0.0.5 16 | github.com/temoto/robotstxt v1.1.1 // indirect 17 | github.com/twpayne/go-kml v1.2.0 18 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e // indirect 19 | google.golang.org/appengine v1.6.5 // indirect 20 | gopkg.in/yaml.v3 v3.0.0-20191120175047-4206685974f2 21 | ) 22 | 23 | go 1.13 24 | -------------------------------------------------------------------------------- /pkg/igc/bruteforce.go: -------------------------------------------------------------------------------- 1 | // Copyright The ezgliding Authors. 2 | // 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package igc 18 | 19 | import ( 20 | "fmt" 21 | "time" 22 | ) 23 | 24 | // NewBruteForceOptimizer returns a BruteForceOptimizer with the given characteristics. 25 | // 26 | func NewBruteForceOptimizer(cache bool) Optimizer { 27 | return &bruteForceOptimizer{cache: cache} 28 | } 29 | 30 | type bruteForceOptimizer struct { 31 | cache bool 32 | } 33 | 34 | func (b *bruteForceOptimizer) Optimize(track Track, nPoints int, score Score) (Task, error) { 35 | time.Sleep(5 * time.Second) 36 | switch nPoints { 37 | 38 | case 1: 39 | return b.optimize1(track, score) 40 | case 2: 41 | return b.optimize2(track, score) 42 | default: 43 | return Task{}, fmt.Errorf("%v turn points not supported by this optimizer", nPoints) 44 | } 45 | } 46 | 47 | func (b *bruteForceOptimizer) optimize1(track Track, score Score) (Task, error) { 48 | 49 | var optimalDistance float64 50 | var distance float64 51 | var task Task 52 | var optimalTask Task 53 | 54 | for i := 0; i < len(track.Points)-2; i++ { 55 | for j := i + 1; j < len(track.Points)-1; j++ { 56 | for z := j + 1; z < len(track.Points); z++ { 57 | task = Task{ 58 | Start: track.Points[i], 59 | Turnpoints: []Point{track.Points[j]}, 60 | Finish: track.Points[z], 61 | } 62 | distance = task.Distance() 63 | if distance > optimalDistance { 64 | optimalDistance = distance 65 | optimalTask = Task(task) 66 | } 67 | } 68 | } 69 | } 70 | return optimalTask, nil 71 | } 72 | 73 | func (b *bruteForceOptimizer) optimize2(track Track, score Score) (Task, error) { 74 | 75 | var optimalDistance float64 76 | var distance float64 77 | var optimalTask Task 78 | 79 | for i := 0; i < len(track.Points)-3; i++ { 80 | for j := i + 1; j < len(track.Points)-2; j++ { 81 | for w := j + 1; w < len(track.Points)-1; w++ { 82 | for z := w + 1; z < len(track.Points); z++ { 83 | task := Task{ 84 | Start: track.Points[i], 85 | Turnpoints: []Point{track.Points[j], track.Points[w]}, 86 | Finish: track.Points[z], 87 | } 88 | distance = task.Distance() 89 | if distance > optimalDistance { 90 | optimalDistance = distance 91 | optimalTask = task 92 | } 93 | } 94 | } 95 | } 96 | } 97 | return optimalTask, nil 98 | } 99 | -------------------------------------------------------------------------------- /pkg/igc/bruteforce_test.go: -------------------------------------------------------------------------------- 1 | // Copyright The ezgliding Authors. 2 | // 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package igc 18 | 19 | import ( 20 | "fmt" 21 | "path/filepath" 22 | "testing" 23 | ) 24 | 25 | func TestBruteForceOptimize(t *testing.T) { 26 | opt := NewBruteForceOptimizer(false) 27 | 28 | for _, test := range optimizeTests { 29 | for tp, expected := range test.result { 30 | if tp > 1 { 31 | continue 32 | } 33 | t.Run(fmt.Sprintf("%v/%v", test.name, tp), func(t *testing.T) { 34 | track, err := ParseLocation(filepath.Join("testdata/optimize", fmt.Sprintf("%v.igc", test.name))) 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | task, err := opt.Optimize(track, tp, Distance) 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | result := task.Distance() 43 | if !test.valid(result, tp) { 44 | t.Errorf("expected %v got %v", expected, result) 45 | } 46 | }) 47 | } 48 | } 49 | } 50 | 51 | func BenchmarkBruteForceOptimize(b *testing.B) { 52 | opt := NewBruteForceOptimizer(false) 53 | 54 | for _, test := range benchmarkTests { 55 | for tp, expected := range test.result { 56 | if tp > 1 { 57 | continue 58 | } 59 | track, err := ParseLocation(filepath.Join("testdata/optimize", fmt.Sprintf("%v.igc", test.name))) 60 | if err != nil { 61 | b.Fatal(err) 62 | } 63 | b.Run(fmt.Sprintf("%v/%v", test.name, tp), func(b *testing.B) { 64 | task, err := opt.Optimize(track, tp, Distance) 65 | if err != nil { 66 | b.Fatal(err) 67 | } 68 | result := task.Distance() 69 | if !test.valid(result, tp) { 70 | b.Errorf("expected %v got %v", expected, result) 71 | } 72 | }) 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /pkg/igc/crawler.go: -------------------------------------------------------------------------------- 1 | // Copyright The ezgliding Authors. 2 | // 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package igc 18 | 19 | import ( 20 | "time" 21 | ) 22 | 23 | type FlightID string 24 | 25 | type Crawler interface { 26 | Crawl(time.Time, time.Time) ([]Flight, error) 27 | } 28 | -------------------------------------------------------------------------------- /pkg/igc/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright The ezgliding Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | 16 | /* 17 | Package igc provides means to parse and analyse files in the IGC format. 18 | 19 | This format is defined by the International Gliding Commission (IGC) and 20 | was created to set a standard for recording gliding flights. 21 | 22 | The full specification is available in Appendix A of the IGC FR Specification: 23 | http://www.fai.org/component/phocadownload/category/?download=11005 24 | 25 | Calculation of the optimal flight distance considering multiple turnpoints and 26 | FAI triangles are available via Optimizers. Available Optimizers include brute 27 | force, montecarlo method, genetic algorithms, etc. 28 | 29 | */ 30 | package igc 31 | -------------------------------------------------------------------------------- /pkg/igc/doc_test.go: -------------------------------------------------------------------------------- 1 | // Copyright The ezgliding Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | 16 | package igc 17 | 18 | import ( 19 | "fmt" 20 | ) 21 | 22 | // Parse and ParseLocation return a Track object. 23 | func Example_parse() { 24 | // We can parse passing a file location 25 | track, _ := ParseLocation("sample-flight.igc") 26 | 27 | // Or similarly giving directly the contents 28 | contents := ` 29 | AFLA001Some Additional Data 30 | HFDTE010203 31 | HFFXA500 32 | HFPLTPilotincharge:EZ PILOT 33 | ` 34 | track, _ = Parse(contents) 35 | 36 | // Accessing track metadata 37 | fmt.Printf("Track Pilot: %v", track.Pilot) 38 | fmt.Printf("Track Points %v", len(track.Pilot)) 39 | } 40 | 41 | // Calculate the total track distance using the Points. 42 | // 43 | // This is not a very useful metric (you should look at one of the Optimizers) 44 | // instead, but it is a good example of how to use the Point data in the Track. 45 | func Example_totaldistance() { 46 | track, _ := ParseLocation("sample-flight.igc") 47 | totalDistance := 0.0 48 | for i := 0; i < len(track.Points)-1; i++ { 49 | totalDistance += track.Points[i].Distance(track.Points[i+1]) 50 | } 51 | fmt.Printf("Distance was %v", totalDistance) 52 | } 53 | 54 | // Calculate the optimal track distance for the multiple possible tasks using the Brute Force Optimizer: 55 | func Example_optimize() { 56 | track, _ := ParseLocation("sample-flight.igc") 57 | 58 | // In this case we use a brute force optimizer 59 | o := NewBruteForceOptimizer(false) 60 | r, _ := o.Optimize(track, 1, Distance) 61 | fmt.Printf("Optimization result was: %v", r) 62 | } 63 | -------------------------------------------------------------------------------- /pkg/igc/flight.go: -------------------------------------------------------------------------------- 1 | // Copyright The ezgliding Authors. 2 | // 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package igc 18 | 19 | import ( 20 | "time" 21 | ) 22 | 23 | // Flight represents a flight submission in an online competition. 24 | // 25 | // It includes all the flight metadata available in the online competition, 26 | // including computed data like speed or distance. It also includes a url to 27 | // the flight track file (usually in IGC format). 28 | type Flight struct { 29 | URL string 30 | ID string 31 | Pilot string 32 | Club string 33 | Date time.Time 34 | Takeoff string 35 | Region string 36 | Country string 37 | Distance float64 38 | Points float64 39 | Glider string 40 | Type string 41 | TrackURL string 42 | TrackID string 43 | CompetitionID string 44 | CompetitionURL string 45 | Speed float64 46 | Comments string 47 | } 48 | -------------------------------------------------------------------------------- /pkg/igc/optimize.go: -------------------------------------------------------------------------------- 1 | // Copyright The ezgliding Authors. 2 | // 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package igc 18 | 19 | // Score functions calculate a score for the given Task. 20 | // 21 | // The main use of these functions is in passing them to the Optimizers, so 22 | // they can evaluate each Task towards different goals. 23 | // 24 | // Example functions include the total distance between all turn points or an 25 | // online competition (netcoupe, online contest) score which takes additional 26 | // metrics of each leg into account. 27 | type Score func(task Task) float64 28 | 29 | // Distance returns the sum of distances between each of the points in the Task. 30 | // 31 | // The sum is made calculating the distances between each two consecutive Points. 32 | func Distance(task Task) float64 { 33 | return task.Distance() 34 | } 35 | 36 | // Optimizer returns an optimal Task for the given turnpoints and Score function. 37 | // 38 | // Available score functions include MaxDistance and MaxPoints, but it is 39 | // possible to pass the Optimizer a custom function. 40 | // 41 | // Optimizers might not support a high number of turnpoints. As an example, the 42 | // BruteForceOptimizer does not perform well with nPoints > 2, and might decide 43 | // to return an error instead of attempting to finalize the optimization 44 | // indefinitely. 45 | type Optimizer interface { 46 | Optimize(track Track, nPoints int, score Score) (Task, error) 47 | } 48 | -------------------------------------------------------------------------------- /pkg/igc/optimize_test.go: -------------------------------------------------------------------------------- 1 | // Copyright The ezgliding Authors. 2 | // 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package igc 18 | 19 | import ( 20 | "math" 21 | "testing" 22 | ) 23 | 24 | const ( 25 | errorMargin float64 = 0.02 26 | ) 27 | 28 | type optimizeTest struct { 29 | name string 30 | result map[int]float64 31 | //margin float64 32 | } 33 | 34 | func (t *optimizeTest) validWithMargin(result float64, nTp int, errorMargin float64) bool { 35 | difference := math.Abs(t.result[nTp] - result) 36 | maxDifference := t.result[nTp] * errorMargin 37 | return difference < maxDifference 38 | } 39 | 40 | func (t *optimizeTest) valid(result float64, nTp int) bool { 41 | return t.validWithMargin(result, nTp, errorMargin) 42 | } 43 | 44 | var benchmarkTests = []optimizeTest{ 45 | { 46 | name: "optimize-short-flight-1", 47 | result: map[int]float64{1: 35.44619896425489}, 48 | }, 49 | } 50 | 51 | var optimizeTests = []optimizeTest{ 52 | // { 53 | // name: "optimize-short-flight-1", 54 | // result: map[int]float64{1: 35.44619896425489, 2: 0.0, 3: 507.80108709626626}, 55 | // }, 56 | } 57 | 58 | type distanceTest struct { 59 | t string 60 | task Task 61 | distance float64 62 | } 63 | 64 | var distanceTests = []distanceTest{ 65 | { 66 | t: "all-points-the-same-distance-zero", 67 | task: Task{ 68 | Start: NewPointFromDMD("4453183N", "00512633E"), 69 | Turnpoints: []Point{NewPointFromDMD("4453183N", "00512633E")}, 70 | Finish: NewPointFromDMD("4453183N", "00512633E"), 71 | }, 72 | distance: 0.0, 73 | }, 74 | { 75 | t: "valid-task-sequence", 76 | task: Task{ 77 | Start: NewPointFromDMD("4453183N", "00512633E"), 78 | Turnpoints: []Point{ 79 | NewPointFromDMD("4353800N", "00615200E"), 80 | NewPointFromDMD("4506750N", "00633950E"), 81 | NewPointFromDMD("4424783N", "00644500E"), 82 | }, 83 | Finish: NewPointFromDMD("4505550N", "00502883E"), 84 | }, 85 | distance: 507.80108709626626, 86 | }, 87 | } 88 | 89 | func TestDistance(t *testing.T) { 90 | var result float64 91 | for _, test := range distanceTests { 92 | t.Run(test.t, func(t *testing.T) { 93 | result = Distance(test.task) 94 | if result != test.distance { 95 | t.Errorf("expected %v got %v", test.distance, result) 96 | } 97 | }) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /pkg/igc/parse.go: -------------------------------------------------------------------------------- 1 | // Copyright The ezgliding Authors. 2 | // 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package igc 18 | 19 | import ( 20 | "fmt" 21 | "io/ioutil" 22 | "net/http" 23 | "strconv" 24 | "strings" 25 | "time" 26 | ) 27 | 28 | const ( 29 | // TimeFormat is the golang time.Parse format for IGC time. 30 | TimeFormat = "150405" 31 | // DateFormat is the golang time.Parse format for IGC time. 32 | DateFormat = "020106" 33 | ) 34 | 35 | // ParseLocation returns a Track object corresponding to the given file. 36 | // 37 | // It calls Parse internatlly, so the file content should be in IGC format. 38 | func ParseLocation(location string) (Track, error) { 39 | var content []byte 40 | resp, err := http.Get(location) 41 | // case http 42 | if err == nil { 43 | defer resp.Body.Close() 44 | content, err = ioutil.ReadAll(resp.Body) 45 | if err != nil { 46 | return Track{}, err 47 | } 48 | } else { // case file 49 | resp, err := ioutil.ReadFile(location) 50 | if err != nil { 51 | return Track{}, err 52 | } 53 | content = resp 54 | } 55 | 56 | return Parse(string(content)) 57 | } 58 | 59 | // ParseCleanLocation returns a cleaned up Track object. 60 | // 61 | // See ParseLocation(). 62 | func ParseCleanLocation(location string) (Track, error) { 63 | t, err := ParseLocation(location) 64 | if err != nil { 65 | return Track{}, err 66 | } 67 | return t.Cleanup() 68 | } 69 | 70 | // ParseClean returns a cleaned up Track object. 71 | // 72 | // See Parse(). 73 | func ParseClean(content string) (Track, error) { 74 | t, err := Parse(content) 75 | if err != nil { 76 | return Track{}, err 77 | } 78 | return t.Cleanup() 79 | } 80 | 81 | // Parse returns a Track object corresponding to the given content. 82 | // 83 | // The value of content should be a text string with all the flight data 84 | // in the IGC format. 85 | func Parse(content string) (Track, error) { 86 | f := NewTrack() 87 | var err error 88 | p := parser{} 89 | lines := strings.Split(content, "\n") 90 | for i := range lines { 91 | line := strings.TrimSpace(lines[i]) 92 | // ignore empty lines 93 | if len(strings.Trim(line, " ")) < 1 { 94 | continue 95 | } 96 | switch line[0] { 97 | case 'A': 98 | err = p.parseA(line, &f) 99 | case 'B': 100 | err = p.parseB(line, &f) 101 | case 'C': 102 | if !p.taskDone { 103 | err = p.parseC(lines[i:], &f) 104 | } 105 | case 'D': 106 | err = p.parseD(line, &f) 107 | case 'E': 108 | err = p.parseE(line, &f) 109 | case 'F': 110 | err = p.parseF(line, &f) 111 | case 'G': 112 | err = p.parseG(line, &f) 113 | case 'H': 114 | err = p.parseH(line, &f) 115 | case 'I': 116 | err = p.parseI(line) 117 | case 'J': 118 | err = p.parseJ(line) 119 | case 'K': 120 | err = p.parseK(line, &f) 121 | case 'L': 122 | err = p.parseL(line, &f) 123 | default: 124 | err = fmt.Errorf("invalid record :: %v", line) 125 | } 126 | if err != nil { 127 | return f, err 128 | } 129 | } 130 | 131 | return f, nil 132 | } 133 | 134 | type field struct { 135 | start int64 136 | end int64 137 | tlc string 138 | } 139 | 140 | type parser struct { 141 | IFields []field 142 | JFields []field 143 | taskDone bool 144 | numSat int 145 | } 146 | 147 | func (p *parser) parseA(line string, f *Track) error { 148 | if len(line) < 4 { 149 | return fmt.Errorf("line too short :: %v", line) 150 | } 151 | f.Manufacturer = line[1:4] 152 | if len(line) >= 7 { 153 | f.UniqueID = line[4:7] 154 | f.AdditionalData = line[7:] 155 | } 156 | return nil 157 | } 158 | 159 | func (p *parser) parseB(line string, f *Track) error { 160 | if len(line) < 35 { 161 | return fmt.Errorf("line too short :: %v", line) 162 | } 163 | pt := NewPointFromDMD( 164 | line[7:15], line[15:24]) 165 | 166 | var err error 167 | pt.Time, err = time.Parse(TimeFormat, line[1:7]) 168 | if err != nil { 169 | if !strings.Contains(err.Error(), "out of range") { 170 | return err 171 | } 172 | } 173 | pt.Time = pt.Time.AddDate(f.Date.Year(), int(f.Date.Month()), f.Date.Day()) 174 | if line[24] == 'A' || line[24] == 'V' { 175 | pt.FixValidity = line[24] 176 | } else { 177 | return fmt.Errorf("invalid fix validity :: %v", line) 178 | } 179 | pt.PressureAltitude, err = strconv.ParseInt(line[25:30], 10, 64) 180 | if err != nil { 181 | return err 182 | } 183 | pt.GNSSAltitude, err = strconv.ParseInt(line[30:35], 10, 64) 184 | if err != nil { 185 | return err 186 | } 187 | for _, f := range p.IFields { 188 | if int64(len(line)) < f.end { 189 | return fmt.Errorf("wrong line size :: %v", line) 190 | } 191 | pt.IData[f.tlc] = line[f.start-1 : f.end] 192 | } 193 | pt.NumSatellites = p.numSat 194 | f.Points = append(f.Points, pt) 195 | return nil 196 | } 197 | 198 | func (p *parser) parseC(lines []string, f *Track) error { 199 | line := lines[0] 200 | if len(line) < 25 { 201 | return fmt.Errorf("wrong line size :: %v", line) 202 | } 203 | var err error 204 | var nTP int 205 | if nTP, err = strconv.Atoi(line[23:25]); err != nil { 206 | return fmt.Errorf("invalid number of turnpoints :: %v", line) 207 | } 208 | if len(lines) < 5+nTP { 209 | return fmt.Errorf("invalid number of C record lines :: %v", lines) 210 | } 211 | if f.Task.DeclarationDate, err = time.Parse(DateFormat+TimeFormat, lines[0][1:13]); err != nil { 212 | f.Task.DeclarationDate = time.Time{} 213 | } 214 | if f.Task.Date, err = time.Parse(DateFormat, lines[0][13:19]); err != nil { 215 | f.Task.Date = time.Time{} 216 | } 217 | if f.Task.Number, err = strconv.Atoi(line[19:23]); err != nil { 218 | return err 219 | } 220 | f.Task.Description = line[25:] 221 | if f.Task.Takeoff, err = p.taskPoint(lines[1]); err != nil { 222 | return err 223 | } 224 | if f.Task.Start, err = p.taskPoint(lines[2]); err != nil { 225 | return err 226 | } 227 | for i := 0; i < nTP; i++ { 228 | var tp Point 229 | if tp, err = p.taskPoint(lines[3+i]); err != nil { 230 | return err 231 | } 232 | f.Task.Turnpoints = append(f.Task.Turnpoints, tp) 233 | } 234 | if f.Task.Finish, err = p.taskPoint(lines[3+nTP]); err != nil { 235 | return err 236 | } 237 | if f.Task.Landing, err = p.taskPoint(lines[4+nTP]); err != nil { 238 | return err 239 | } 240 | p.taskDone = true 241 | return nil 242 | } 243 | 244 | func (p *parser) taskPoint(line string) (Point, error) { 245 | if len(line) < 18 { 246 | return Point{}, fmt.Errorf("line too short :: %v", line) 247 | } 248 | pt := NewPointFromDMD( 249 | line[1:9], line[9:18]) 250 | pt.Description = line[18:] 251 | return pt, nil 252 | } 253 | 254 | func (p *parser) parseD(line string, f *Track) error { 255 | if len(line) < 6 { 256 | return fmt.Errorf("line too short :: %v", line) 257 | } 258 | if line[1] == '2' { 259 | f.DGPSStationID = line[2:6] 260 | } 261 | return nil 262 | } 263 | 264 | func (p *parser) parseE(line string, f *Track) error { 265 | if len(line) < 10 { 266 | return fmt.Errorf("line too short :: %v", line) 267 | } 268 | t, err := time.Parse(TimeFormat, line[1:7]) 269 | if err != nil { 270 | return err 271 | } 272 | f.Events = append(f.Events, Event{Time: t, Type: line[7:10], Data: line[10:]}) 273 | return nil 274 | } 275 | 276 | func (p *parser) parseF(line string, f *Track) error { 277 | if len(line) < 7 { 278 | return fmt.Errorf("line too short :: %v", line) 279 | } 280 | t, err := time.Parse(TimeFormat, line[1:7]) 281 | if err != nil { 282 | return err 283 | } 284 | ids := []string{} 285 | for i := 7; i < len(line)-1; i = i + 2 { 286 | ids = append(ids, line[i:i+2]) 287 | } 288 | f.Satellites = append(f.Satellites, Satellite{Time: t, Ids: ids}) 289 | p.numSat = len(ids) 290 | return nil 291 | } 292 | 293 | func (p *parser) parseG(line string, f *Track) error { 294 | f.Signature = f.Signature + line[1:] 295 | return nil 296 | } 297 | 298 | func (p *parser) parseH(line string, f *Track) error { 299 | var err error 300 | if len(line) < 5 { 301 | return fmt.Errorf("line too short :: %v", line) 302 | } 303 | 304 | switch line[2:5] { 305 | case "DTE": 306 | if len(line) < 11 { 307 | return fmt.Errorf("line too short :: %v", line) 308 | } 309 | if len(line) > 12 { 310 | f.Date, err = time.Parse(DateFormat, line[10:16]) 311 | } else { 312 | f.Date, err = time.Parse(DateFormat, line[5:11]) 313 | } 314 | case "FXA": 315 | if len(line) < 6 { 316 | return fmt.Errorf("line too short :: %v", line) 317 | } 318 | f.FixAccuracy, err = strconv.ParseInt(line[5:], 10, 64) 319 | case "PLT": 320 | f.Pilot = stripUpTo(line[5:], ":") 321 | case "CM2": 322 | f.Crew = stripUpTo(line[5:], ":") 323 | case "GTY", "GYT": 324 | f.GliderType = stripUpTo(line[5:], ":") 325 | case "GID": 326 | f.GliderID = stripUpTo(line[5:], ":") 327 | case "DTM": 328 | if len(line) < 8 { 329 | return fmt.Errorf("line too short :: %v", line) 330 | } 331 | f.GPSDatum = stripUpTo(line[5:], ":") 332 | case "RFW": 333 | f.FirmwareVersion = stripUpTo(line[5:], ":") 334 | case "RHW": 335 | f.HardwareVersion = stripUpTo(line[5:], ":") 336 | case "FTY": 337 | f.FlightRecorder = stripUpTo(line[5:], ":") 338 | case "GPS": 339 | f.GPS = line[5:] 340 | case "PRS": 341 | f.PressureSensor = stripUpTo(line[5:], ":") 342 | case "CID": 343 | f.CompetitionID = stripUpTo(line[5:], ":") 344 | case "CCL": 345 | f.CompetitionClass = stripUpTo(line[5:], ":") 346 | case "TZN", "TZO": 347 | z, err := strconv.ParseFloat( 348 | strings.TrimLeft(stripUpTo(line[5:], ":"), " "), 64) 349 | if err != nil { 350 | return err 351 | } 352 | f.Timezone = int(z) 353 | case "ATS": 354 | ats, err := strconv.ParseFloat(stripUpTo(line[5:], ":"), 64) 355 | if err != nil { 356 | return err 357 | } 358 | f.AltimeterPressure = ats / 100 359 | case "DB1": 360 | f.PilotBirth, err = time.Parse(DateFormat, stripUpTo(line[5:], ":")) 361 | if f.PilotBirth.After(time.Now()) { 362 | f.PilotBirth = f.PilotBirth.AddDate(-100, 0, 0) 363 | } 364 | case "MOP": 365 | f.MOPSensor = stripUpTo(line[5:], ":") 366 | case "SIT": 367 | f.Site = stripUpTo(line[5:], ":") 368 | case "OOI": 369 | f.Observation = stripUpTo(line[5:], ":") 370 | case "SOF": 371 | f.SoftwareVersion = stripUpTo(line[5:], ":") 372 | case "FSP": 373 | f.Specification = stripUpTo(line[5:], ":") 374 | case "ALG": 375 | f.GNSSModel = stripUpTo(line[5:], ":") 376 | case "ALP": 377 | f.PressureModel = stripUpTo(line[5:], ":") 378 | case "UNT": 379 | // seen once, not sure what it's supposed to mean 380 | default: 381 | err = fmt.Errorf("unknown record :: %v", line) 382 | } 383 | 384 | return err 385 | } 386 | 387 | func (p *parser) parseI(line string) error { 388 | if len(line) < 3 { 389 | return fmt.Errorf("line too short :: %v", line) 390 | } 391 | n, err := strconv.ParseInt(line[1:3], 10, 0) 392 | if err != nil { 393 | return fmt.Errorf("invalid number of I fields :: %v", line) 394 | } 395 | if len(line) != int(n*7+3) { 396 | return fmt.Errorf("wrong line size :: %v", line) 397 | } 398 | for i := 0; i < int(n); i++ { 399 | s := i*7 + 3 400 | start, _ := strconv.ParseInt(line[s:s+2], 10, 0) 401 | end, _ := strconv.ParseInt(line[s+2:s+4], 10, 0) 402 | tlc := line[s+4 : s+7] 403 | p.IFields = append(p.IFields, field{start: start, end: end, tlc: tlc}) 404 | } 405 | return nil 406 | } 407 | 408 | func (p *parser) parseJ(line string) error { 409 | if len(line) < 3 { 410 | return fmt.Errorf("line too short :: %v", line) 411 | } 412 | n, err := strconv.ParseInt(line[1:3], 10, 0) 413 | if err != nil { 414 | return fmt.Errorf("invalid number of J fields :: %v", line) 415 | } 416 | if len(line) != int(n*7+3) { 417 | return fmt.Errorf("wrong line size :: %v", line) 418 | } 419 | for i := 0; i < int(n); i++ { 420 | s := i*7 + 3 421 | start, _ := strconv.ParseInt(line[s:s+2], 10, 0) 422 | end, _ := strconv.ParseInt(line[s+2:s+4], 10, 0) 423 | tlc := line[s+4 : s+7] 424 | p.JFields = append(p.JFields, field{start: start, end: end, tlc: tlc}) 425 | } 426 | return nil 427 | } 428 | 429 | func (p *parser) parseK(line string, f *Track) error { 430 | if len(line) < 7 { 431 | return fmt.Errorf("line too short :: %v", line) 432 | } 433 | t, err := time.Parse(TimeFormat, line[1:7]) 434 | if err != nil { 435 | return err 436 | } 437 | fields := make(map[string]string) 438 | for _, f := range p.JFields { 439 | fields[f.tlc] = line[f.start-1 : f.end] 440 | } 441 | f.K = append(f.K, K{Time: t, Fields: fields}) 442 | return nil 443 | } 444 | 445 | func (p *parser) parseL(line string, f *Track) error { 446 | f.Logbook = append(f.Logbook, line[1:]) 447 | return nil 448 | } 449 | 450 | func stripUpTo(s string, sep string) string { 451 | i := strings.Index(s, sep) 452 | if i == -1 { 453 | return s 454 | } 455 | return s[i+1:] 456 | } 457 | -------------------------------------------------------------------------------- /pkg/igc/parse_test.go: -------------------------------------------------------------------------------- 1 | // Copyright The ezgliding Authors. 2 | // 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package igc 18 | 19 | import ( 20 | "encoding/json" 21 | "flag" 22 | "fmt" 23 | "io/ioutil" 24 | "path/filepath" 25 | "strconv" 26 | "strings" 27 | "testing" 28 | ) 29 | 30 | var update = flag.Bool("update", false, "update golden test data") 31 | 32 | func runTest(t *testing.T, ok bool, in, out string) { 33 | 34 | data, err := ioutil.ReadFile(in) 35 | if err != nil { 36 | t.Error(err) 37 | return 38 | } 39 | result, err := Parse(string(data)) 40 | if err != nil && !ok { 41 | return 42 | } else if err != nil { 43 | t.Error(err) 44 | } 45 | resultJSON, err := json.MarshalIndent(result, "", " ") 46 | if err != nil { 47 | t.Fatalf("%v :: %+v", err, result) 48 | } 49 | 50 | // update golden if flag is passed 51 | if *update { 52 | if err = ioutil.WriteFile(out, resultJSON, 0644); err != nil { 53 | t.Fatal(err) 54 | } 55 | } 56 | 57 | expectedJSON, err := ioutil.ReadFile(out) 58 | if err != nil { 59 | t.Fatal(err) 60 | } 61 | 62 | if string(resultJSON) != string(expectedJSON) { 63 | t.Errorf("expected\n%+v\ngot\n%+v", string(expectedJSON), string(resultJSON)) 64 | } 65 | } 66 | 67 | func TestParse(t *testing.T) { 68 | // testdata/parse file name format is testname.[1|0].igc 69 | match, err := filepath.Glob("../../testdata/parse/parse-*.igc") 70 | if err != nil { 71 | t.Fatal(err) 72 | } 73 | for _, in := range match { 74 | t.Run(in, func(t *testing.T) { 75 | parts := strings.Split(in, ".") 76 | ok, _ := strconv.ParseBool(parts[len(parts)-2]) 77 | out := fmt.Sprintf("%v.golden", in) 78 | 79 | runTest(t, ok, in, out) 80 | }) 81 | } 82 | } 83 | 84 | func TestParseLocationMissing(t *testing.T) { 85 | _, err := ParseLocation("does-not-exist") 86 | if err == nil { 87 | t.Errorf("no error returned for missing file") 88 | } 89 | } 90 | 91 | func TestParseLocationEmpty(t *testing.T) { 92 | _, err := ParseLocation("") 93 | if err == nil { 94 | t.Errorf("no error returned empty string location") 95 | } 96 | } 97 | 98 | func TestStripUpToMissing(t *testing.T) { 99 | s := "nocolonhere" 100 | r := stripUpTo(s, ":") 101 | if r != s { 102 | t.Errorf("expected %v got %v", s, r) 103 | } 104 | } 105 | 106 | // Parse a given file and get a Track object. 107 | func Example_parselocation() { 108 | track, _ := ParseLocation("sample-flight.igc") 109 | 110 | fmt.Printf("Track Pilot: %v", track.Pilot) 111 | fmt.Printf("Track Points %v", len(track.Pilot)) 112 | } 113 | 114 | // Parse directly flight contents and get a Track object. 115 | func Example_parsecontent() { 116 | // We could pass here a string with the full contents in IGC format 117 | track, _ := Parse(` 118 | AFLA001Some Additional Data 119 | HFDTE010203 120 | HFFXA500 121 | HFPLTPilotincharge:EZ PILOT 122 | `) 123 | 124 | fmt.Printf("Track Pilot: %v", track.Pilot) 125 | fmt.Printf("Track Points %v", len(track.Pilot)) 126 | } 127 | 128 | func BenchmarkParse(b *testing.B) { 129 | c, err := ioutil.ReadFile("../../testdata/parse/parse-0-benchmark-0.igc") 130 | if err != nil { 131 | b.Errorf("failed to load sample flight :: %v", err) 132 | } 133 | content := string(c) 134 | b.ResetTimer() 135 | for i := 0; i < b.N; i++ { 136 | _, _ = Parse(content) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /pkg/igc/phase.go: -------------------------------------------------------------------------------- 1 | // Copyright The ezgliding Authors. 2 | // 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | package igc 17 | 18 | import ( 19 | "bytes" 20 | "encoding/csv" 21 | "encoding/json" 22 | "fmt" 23 | "image/color" 24 | "math" 25 | "time" 26 | 27 | "github.com/golang/geo/s2" 28 | kml "github.com/twpayne/go-kml" 29 | "gopkg.in/yaml.v3" 30 | ) 31 | 32 | // PhaseType represents a flight phase. 33 | // 34 | // Possible values include Towing, PossibleCruising/Cruising, 35 | // PossibleCircling/Circling, Unknown. 36 | type PhaseType int 37 | 38 | const ( 39 | Unknown PhaseType = 0 40 | Towing PhaseType = 1 41 | PossibleCruising PhaseType = 2 42 | Cruising PhaseType = 3 43 | PossibleCircling PhaseType = 4 44 | Circling PhaseType = 5 45 | ) 46 | 47 | // CirclingType indicates Left, Right or Mixed circling. 48 | type CirclingType int 49 | 50 | const ( 51 | Mixed CirclingType = 0 52 | Left CirclingType = 1 53 | Right CirclingType = 2 54 | ) 55 | 56 | const ( 57 | // MinTurnRate is the min rate to consider circling 58 | MinTurnRate = 6.5 59 | // MaxTurnRate is the max rate considered for valid turns 60 | MaxTurnRate = 22.5 61 | // MinCirclingTime is used to decide when a switch to circling occurs. 62 | // This value is used when calculating flight phases to switch from 63 | // PossibleCircling to Circling. 64 | MinCirclingTime = 15 65 | // MinCruisingTime is used to decide when a switch to cruising occurs. 66 | // This value is used when calculating flight phases to switch from 67 | // PossibleCruising to Cruising. 68 | MinCruisingTime = 10 69 | ) 70 | 71 | // Phase is a flight phase (towing, cruising, circling). 72 | type Phase struct { 73 | Type PhaseType 74 | CirclingType CirclingType 75 | Start Point 76 | StartIndex int 77 | End Point 78 | EndIndex int 79 | AvgVario float64 80 | TopVario float64 81 | AvgGndSpeed float64 82 | TopGndSpeed float64 83 | Distance float64 84 | LD float64 85 | Centroid s2.LatLng 86 | CellID s2.CellID 87 | } 88 | 89 | // Phases returns the list of flight phases for the Track. 90 | // Each phases is one of Cruising, Circling, Towing or Unknown. 91 | func (track *Track) Phases() ([]Phase, error) { 92 | 93 | if len(track.phases) > 0 { 94 | return track.phases, nil 95 | } 96 | 97 | if len(track.Points) < 2 { 98 | return []Phase{}, fmt.Errorf("track has %v points, min 2 required", 99 | len(track.Points)) 100 | } 101 | 102 | var currPhase PhaseType 103 | var startIndex int 104 | var currPoint Point 105 | var turning bool 106 | //var turnRate float64 107 | 108 | currPhase = Cruising 109 | track.phases = []Phase{ 110 | Phase{Type: Cruising, StartIndex: 0, Start: track.Points[0]}, 111 | } 112 | 113 | // we need the bearings for each point to calculate turn rates 114 | var d float64 115 | for i := 1; i < len(track.Points); i++ { 116 | track.Points[i-1].bearing = track.Points[i-1].Bearing(track.Points[i]) 117 | d = track.Points[i-1].Distance(track.Points[i]) 118 | track.Points[i].distance = track.Points[i-1].distance + d 119 | track.Points[i].speed = d / track.Points[i].Time.Sub(track.Points[i-1].Time).Seconds() 120 | } 121 | 122 | for i := 0; i < len(track.Points)-1; i++ { 123 | currPoint = track.Points[i] 124 | turning, _ = track.isTurning(i) 125 | 126 | if currPhase == Cruising { 127 | // if cruising check for turning 128 | if turning { 129 | // set possible circling if turning 130 | currPhase = PossibleCircling 131 | startIndex = i 132 | } // else continue 133 | } else if currPhase == PossibleCircling { 134 | // if possible circling check for turning longer than min circling time 135 | if turning { 136 | if currPoint.Time.Sub(track.Points[startIndex].Time).Seconds() > MinCirclingTime { 137 | // if true then set circling 138 | currPhase = Circling 139 | track.wrapPhase(startIndex, Circling) 140 | } 141 | } else { 142 | // if not go back to cruising 143 | currPhase = Cruising 144 | } 145 | } else if currPhase == Circling { 146 | // if circling check for stopping to turn 147 | if !turning { 148 | // if stopping set possible cruising 149 | currPhase = PossibleCruising 150 | startIndex = i 151 | } 152 | } else if currPhase == PossibleCruising { 153 | // if possible cruising check for longer than min cruising 154 | if !turning { 155 | if currPoint.Time.Sub(track.Points[startIndex].Time).Seconds() > MinCruisingTime { 156 | // if true then set cruising 157 | currPhase = Cruising 158 | track.wrapPhase(startIndex, Cruising) 159 | } 160 | } else { 161 | // if not go back to circling 162 | currPhase = Circling 163 | } 164 | } 165 | } 166 | 167 | return track.phases, nil 168 | } 169 | 170 | func (track *Track) wrapPhase(index int, phaseType PhaseType) { 171 | p := &track.phases[len(track.phases)-1] 172 | 173 | p.EndIndex = index 174 | p.End = track.Points[index] 175 | 176 | // compute phase stats 177 | altGain := float64(p.End.GNSSAltitude - p.Start.GNSSAltitude) 178 | p.Distance = p.End.distance - p.Start.distance 179 | duration := p.Duration().Seconds() 180 | if duration != 0 { 181 | p.AvgVario = altGain / duration 182 | p.AvgGndSpeed = p.Distance / (duration / 3600) 183 | } 184 | 185 | if p.Type == Cruising && altGain != 0 { 186 | p.LD = p.Distance * 1000.0 / math.Abs(altGain) 187 | } 188 | pts := make([]s2.LatLng, p.EndIndex-p.StartIndex) 189 | for i := p.StartIndex; i < p.EndIndex; i++ { 190 | pts[i-p.StartIndex] = track.Points[i].LatLng 191 | } 192 | centroid := s2.LatLngFromPoint(s2.PolylineFromLatLngs(pts).Centroid()) 193 | p.CellID = s2.CellIDFromLatLng(centroid) 194 | p.CellID = p.CellID.Parent(14) 195 | p.Centroid = centroid 196 | 197 | track.phases = append(track.phases, Phase{Type: phaseType, StartIndex: index, Start: track.Points[index]}) 198 | } 199 | 200 | func (track *Track) isTurning(i int) (bool, float64) { 201 | turnRate := (track.Points[i+1].bearing - track.Points[i].bearing).Abs().Degrees() / track.Points[i+1].Time.Sub(track.Points[i].Time).Seconds() 202 | return math.Abs(turnRate) > MinTurnRate, turnRate 203 | } 204 | 205 | // Duration returns the duration of this flight phase. 206 | func (p *Phase) Duration() time.Duration { 207 | return p.End.Time.Sub(p.Start.Time) 208 | } 209 | 210 | func (track *Track) EncodePhases(format string) ([]byte, error) { 211 | 212 | phases, err := track.Phases() 213 | if err != nil { 214 | return []byte{}, err 215 | } 216 | 217 | switch format { 218 | case "json": 219 | return json.MarshalIndent(phases, "", " ") 220 | case "kml", "kmz": 221 | buf := new(bytes.Buffer) 222 | k, err := track.encodePhasesKML() 223 | if err != nil { 224 | return buf.Bytes(), err 225 | } 226 | if err := k.WriteIndent(buf, "", " "); err != nil { 227 | return buf.Bytes(), err 228 | } 229 | return buf.Bytes(), nil 230 | case "yaml": 231 | return yaml.Marshal(phases) 232 | case "csv": 233 | return track.encodePhasesCSV() 234 | default: 235 | return []byte{}, fmt.Errorf("unsupported format '%v'", format) 236 | } 237 | } 238 | 239 | func (track *Track) encodePhasesCSV() ([]byte, error) { 240 | 241 | phases, err := track.Phases() 242 | if err != nil { 243 | return []byte{}, err 244 | } 245 | records := make([][]string, len(phases)) 246 | /**records[0] = []string{ 247 | "TrackID", "Type", "CirclingType", "StartTime", "StartAlt", 248 | "StartIndex", "EndTime", "EndAlt", "EndIndex", "Duration", 249 | "AvgVario", "TopVario", "AvgGndSpeed", "TopGndSpeed", "Distance", 250 | "LD", "CentroidLat", "CentroidLng", "CellID"}*/ 251 | var p Phase 252 | for i := 0; i < len(phases); i++ { 253 | p = phases[i] 254 | records[i] = []string{ 255 | fmt.Sprintf("%d", track.Date.Year()), 256 | track.ID, 257 | fmt.Sprintf("%d", p.Type), 258 | fmt.Sprintf("%d", p.CirclingType), 259 | fmt.Sprintf("%v", p.Start.Time.Format("15:04:05")), 260 | fmt.Sprintf("%d", p.Start.GNSSAltitude), 261 | fmt.Sprintf("%d", p.StartIndex), 262 | fmt.Sprintf("%v", p.End.Time.Format("15:04:05")), 263 | fmt.Sprintf("%d", p.End.GNSSAltitude), 264 | fmt.Sprintf("%d", p.EndIndex), 265 | fmt.Sprintf("%f", p.Duration().Seconds()), 266 | fmt.Sprintf("%f", p.AvgVario), fmt.Sprintf("%f", p.TopVario), 267 | fmt.Sprintf("%f", p.AvgGndSpeed), 268 | fmt.Sprintf("%f", p.TopGndSpeed), 269 | fmt.Sprintf("%f", p.Distance), fmt.Sprintf("%f", p.LD), 270 | fmt.Sprintf("%f", p.Centroid.Lat.Degrees()), 271 | fmt.Sprintf("%f", p.Centroid.Lng.Degrees()), 272 | fmt.Sprintf("%d", p.CellID)} 273 | } 274 | 275 | buf := new(bytes.Buffer) 276 | w := csv.NewWriter(buf) 277 | err = w.WriteAll(records) 278 | if err != nil { 279 | return buf.Bytes(), err 280 | } 281 | return buf.Bytes(), nil 282 | } 283 | 284 | func (track *Track) encodePhasesKML() (*kml.CompoundElement, error) { 285 | 286 | result := kml.Document() 287 | result.Add( 288 | kml.SharedStyle( 289 | "cruising", 290 | kml.LineStyle( 291 | kml.Color(color.RGBA{R: 0, G: 0, B: 255, A: 127}), 292 | kml.Width(4), 293 | ), 294 | ), 295 | kml.SharedStyle( 296 | "circling", 297 | kml.LineStyle( 298 | kml.Color(color.RGBA{R: 0, G: 255, B: 0, A: 127}), 299 | kml.Width(4), 300 | ), 301 | ), 302 | kml.SharedStyle( 303 | "attempt", 304 | kml.LineStyle( 305 | kml.Color(color.RGBA{R: 255, G: 0, B: 0, A: 127}), 306 | kml.Width(4), 307 | ), 308 | ), 309 | ) 310 | 311 | phases, err := track.Phases() 312 | if err != nil { 313 | return result, err 314 | } 315 | 316 | for i := 0; i < len(phases)-2; i++ { 317 | phase := phases[i] 318 | coords := make([]kml.Coordinate, phase.EndIndex-phase.StartIndex+1) 319 | for i := phase.StartIndex; i <= phase.EndIndex; i++ { 320 | p := track.Points[i] 321 | coords[i-phase.StartIndex].Lat = p.Lat.Degrees() 322 | coords[i-phase.StartIndex].Lon = p.Lng.Degrees() 323 | coords[i-phase.StartIndex].Alt = float64(p.GNSSAltitude) 324 | } 325 | style := "#cruising" 326 | if phase.Type == Circling && phase.End.Time.Sub(phase.Start.Time).Seconds() < 45 { 327 | style = "#attempt" 328 | } else if phase.Type == Circling { 329 | style = "#circling" 330 | } 331 | result.Add( 332 | kml.Placemark( 333 | kml.StyleURL(style), 334 | kml.LineString( 335 | kml.Extrude(false), 336 | kml.Tessellate(false), 337 | kml.AltitudeMode("absolute"), 338 | kml.Coordinates(coords...), 339 | ), 340 | )) 341 | 342 | name := fmt.Sprintf("Lat: %v Lng: %v", 343 | phase.Centroid.Lat.Degrees(), phase.Centroid.Lng.Degrees()) 344 | desc := fmt.Sprintf("Alt Gain: %dm (%dm %dm)
Distance: %.2fkm
Speed: %.2fkm/h
LD: %v
Vario: %.1fm/s
Cell: %v
", 345 | phase.End.GNSSAltitude-phase.Start.GNSSAltitude, 346 | phase.Start.GNSSAltitude, phase.End.GNSSAltitude, phase.Distance, 347 | phase.AvgGndSpeed, phase.LD, phase.AvgVario, phase.CellID) 348 | result.Add( 349 | kml.Placemark( 350 | kml.Name(name), 351 | kml.Description(desc), 352 | kml.Point( 353 | kml.Coordinates(kml.Coordinate{ 354 | Lon: phase.Centroid.Lng.Degrees(), Lat: phase.Centroid.Lat.Degrees(), 355 | }), 356 | ), 357 | )) 358 | } 359 | return result, nil 360 | } 361 | -------------------------------------------------------------------------------- /pkg/igc/phase_test.go: -------------------------------------------------------------------------------- 1 | // Copyright The ezgliding Authors. 2 | // 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package igc 18 | 19 | import ( 20 | "bytes" 21 | "encoding/json" 22 | "fmt" 23 | "io/ioutil" 24 | "path/filepath" 25 | "testing" 26 | ) 27 | 28 | type phaseTest struct { 29 | name string 30 | result string //nolint 31 | } 32 | 33 | var phaseTests = []phaseTest{ 34 | { 35 | name: "phases-short-flight-1", 36 | result: "phases-short-flight-1.simple", 37 | }, 38 | { 39 | name: "phases-long-flight-1", 40 | result: "phases-long-flight-1.simple", 41 | }, 42 | } 43 | 44 | func TestPhases(t *testing.T) { 45 | for _, test := range phaseTests { 46 | t.Run(fmt.Sprintf("%v\n", test.name), func(t *testing.T) { 47 | f := filepath.Join("../../testdata/phases", fmt.Sprintf("%v.igc", test.name)) 48 | golden := fmt.Sprintf("../../testdata/phases/%v.golden.igc", test.name) 49 | track, err := ParseLocation(f) 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | phases, err := track.Phases() 54 | if err != nil { 55 | t.Fatal(err) 56 | } 57 | 58 | // update golden if flag is passed 59 | if *update { 60 | jsn, err := json.Marshal(phases) 61 | if err != nil { 62 | t.Fatalf("%+v :: %v\n", phases, err) 63 | } 64 | if err = ioutil.WriteFile(golden, jsn, 0644); err != nil { 65 | t.Fatal(err) 66 | } 67 | } 68 | 69 | b, _ := ioutil.ReadFile(golden) 70 | var goldenPhases []Phase 71 | _ = json.Unmarshal(b, &goldenPhases) 72 | if len(phases) != len(goldenPhases) { 73 | t.Errorf("expected %v got %v phases", len(goldenPhases), len(phases)) 74 | } 75 | }) 76 | } 77 | } 78 | 79 | func TestPhasesZeroOnePoints(t *testing.T) { 80 | trk := NewTrack() 81 | _, err := trk.Phases() 82 | if err == nil { 83 | t.Fatal("should get an error for track with 0 points") 84 | } 85 | 86 | trk.Points = []Point{Point{}} 87 | _, err = trk.Phases() 88 | if err == nil { 89 | t.Fatal("should get an error for track with 1 point") 90 | } 91 | } 92 | 93 | func TestEncodePhasesKML(t *testing.T) { 94 | for _, test := range phaseTests { 95 | t.Run(fmt.Sprintf("%v\n", test.name), func(t *testing.T) { 96 | f := filepath.Join("../../testdata/phases", fmt.Sprintf("%v.igc", test.name)) 97 | golden := fmt.Sprintf("%v.golden.kml", f) 98 | track, err := ParseLocation(f) 99 | if err != nil { 100 | t.Fatal(err) 101 | } 102 | 103 | kml, err := track.encodePhasesKML() 104 | if err != nil { 105 | t.Fatal(err) 106 | } 107 | 108 | buf := new(bytes.Buffer) 109 | err = kml.WriteIndent(buf, "", " ") 110 | if err != nil { 111 | t.Fatal(err) 112 | } 113 | // update golden if flag is passed 114 | if *update { 115 | if err = ioutil.WriteFile(golden, buf.Bytes(), 0644); err != nil { 116 | t.Fatal(err) 117 | } 118 | } 119 | 120 | b, _ := ioutil.ReadFile(golden) 121 | if string(b) != buf.String() { 122 | t.Errorf("expected\n%v\ngot\n%v\n", string(b), buf.String()) 123 | } 124 | }) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /pkg/igc/point.go: -------------------------------------------------------------------------------- 1 | // Copyright The ezgliding Authors. 2 | // 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package igc 18 | 19 | import ( 20 | "math" 21 | "strconv" 22 | "time" 23 | 24 | "github.com/golang/geo/s1" 25 | "github.com/golang/geo/s2" 26 | ) 27 | 28 | const ( 29 | // EarthRadius is the average earth radius. 30 | EarthRadius = 6371.0 31 | ) 32 | 33 | // Point represents a GPS recording (single point in the track). 34 | // 35 | // It is based on a golang-geo s2 LatLng, adding extra metadata such as 36 | // the Time the point was recorded, pressure and GNSS altitude, number of 37 | // satellites available and extra metadata added by the recorder. 38 | // 39 | // You can use all methods available for a s2.LatLng on this struct. 40 | type Point struct { 41 | s2.LatLng 42 | Time time.Time 43 | FixValidity byte 44 | PressureAltitude int64 45 | GNSSAltitude int64 46 | IData map[string]string 47 | NumSatellites int 48 | Description string 49 | bearing s1.Angle 50 | distance float64 51 | speed float64 52 | } 53 | 54 | // NewPoint returns a new Point set to latitude and longitude 0. 55 | func NewPoint() Point { 56 | return NewPointFromLatLng(0, 0) 57 | } 58 | 59 | // NewPointFromLatLng returns a new Point with the given latitude and longitude. 60 | func NewPointFromLatLng(lat float64, lng float64) Point { 61 | return Point{ 62 | LatLng: s2.LatLngFromDegrees(lat, lng), 63 | IData: make(map[string]string), 64 | } 65 | } 66 | 67 | // NewPointFromDMS returns a Point corresponding to the given string in DMS format. 68 | // 69 | // DecimalFromDMS includes more information regarding this format. 70 | func NewPointFromDMS(lat string, lng string) Point { 71 | return NewPointFromLatLng( 72 | DecimalFromDMS(lat), DecimalFromDMS(lng), 73 | ) 74 | } 75 | 76 | // DecimalFromDMS returns the decimal representation (in radians) of the given DMS. 77 | // 78 | // DMS is a representation of a coordinate in Decimal,Minutes,Seconds, with an 79 | // extra character indicating north, south, east, west. 80 | // 81 | // Examples: N512646, W0064312, S342244, E0021233 82 | func DecimalFromDMS(dms string) float64 { 83 | var degrees, minutes, seconds float64 84 | if len(dms) == 7 { 85 | degrees, _ = strconv.ParseFloat(dms[1:3], 64) 86 | minutes, _ = strconv.ParseFloat(dms[3:5], 64) 87 | seconds, _ = strconv.ParseFloat(dms[5:], 64) 88 | } else if len(dms) == 8 { 89 | degrees, _ = strconv.ParseFloat(dms[1:4], 64) 90 | minutes, _ = strconv.ParseFloat(dms[4:6], 64) 91 | seconds, _ = strconv.ParseFloat(dms[6:], 64) 92 | } else { 93 | return 0 94 | } 95 | var r float64 96 | r = degrees + (minutes / 60.0) + (seconds / 3600.0) 97 | if dms[0] == 'S' || dms[0] == 'W' { 98 | r = r * -1 99 | } 100 | return r 101 | } 102 | 103 | // NewPointFromDMD returns a Point corresponding to the given string in DMD format. 104 | // 105 | // DecimalFromDMD includes more information regarding this format. 106 | func NewPointFromDMD(lat string, lng string) Point { 107 | return NewPointFromLatLng( 108 | DecimalFromDMD(lat), DecimalFromDMD(lng), 109 | ) 110 | } 111 | 112 | // DecimalFromDMD returns the decimal representation (in radians) of the given DMD. 113 | // 114 | // DMD is a representation of a coordinate in Decimal,Minutes,100thMinute with an 115 | // extra character indicating north, south, east, west. 116 | // 117 | // Examples: N512688, W0064364, S342212, E0021275 118 | func DecimalFromDMD(dmd string) float64 { 119 | if len(dmd) != 8 && len(dmd) != 9 { 120 | return 0 121 | } 122 | 123 | var degrees, minutes, dminutes float64 124 | if dmd[0] == 'S' || dmd[0] == 'N' { 125 | degrees, _ = strconv.ParseFloat(dmd[1:3], 64) 126 | minutes, _ = strconv.ParseFloat(dmd[3:5], 64) 127 | dminutes, _ = strconv.ParseFloat(dmd[5:], 64) 128 | } else if dmd[len(dmd)-1] == 'S' || dmd[len(dmd)-1] == 'N' { 129 | degrees, _ = strconv.ParseFloat(dmd[0:2], 64) 130 | minutes, _ = strconv.ParseFloat(dmd[2:4], 64) 131 | dminutes, _ = strconv.ParseFloat(dmd[4:7], 64) 132 | } else if dmd[0] == 'W' || dmd[0] == 'E' { 133 | degrees, _ = strconv.ParseFloat(dmd[1:4], 64) 134 | minutes, _ = strconv.ParseFloat(dmd[4:6], 64) 135 | dminutes, _ = strconv.ParseFloat(dmd[6:], 64) 136 | } else if dmd[len(dmd)-1] == 'W' || dmd[len(dmd)-1] == 'E' { 137 | degrees, _ = strconv.ParseFloat(dmd[0:3], 64) 138 | minutes, _ = strconv.ParseFloat(dmd[3:5], 64) 139 | dminutes, _ = strconv.ParseFloat(dmd[5:8], 64) 140 | } 141 | var r float64 142 | r = degrees + ((minutes + (dminutes / 1000.0)) / 60.0) 143 | if dmd[0] == 'S' || dmd[0] == 'W' || dmd[len(dmd)-1] == 'S' || dmd[len(dmd)-1] == 'W' { 144 | r = r * -1 145 | } 146 | return r 147 | } 148 | 149 | // Distance returns the great circle distance in kms to the given point. 150 | // 151 | // Internally it uses the golang-geo s2 LatLng.Distance() method, but converts 152 | // its result (an angle) to kms considering the constant EarthRadius. 153 | func (p *Point) Distance(b Point) float64 { 154 | return float64(p.LatLng.Distance(b.LatLng) * EarthRadius) 155 | } 156 | 157 | // Speed returns the distance/time to the given point in km/h. 158 | func (p *Point) Speed(b Point) float64 { 159 | h := b.Time.Sub(p.Time).Hours() 160 | if h == 0 { 161 | return 0 162 | } 163 | return p.Distance(b) / h 164 | } 165 | 166 | // Bearing returns the bearing to the given point in degrees. 167 | func (p *Point) Bearing(b Point) s1.Angle { 168 | 169 | lat1 := p.Lat.Radians() 170 | lng1 := p.Lng.Radians() 171 | lat2 := b.Lat.Radians() 172 | lng2 := b.Lng.Radians() 173 | 174 | // https://www.movable-type.co.uk/scripts/latlong.html 175 | // ATAN2(COS(lat1)*SIN(lat2)-SIN(lat1)*COS(lat2)*COS(lon2-lon1), 176 | // SIN(lon2-lon1)*COS(lat2)) 177 | bearing := math.Atan2( 178 | math.Sin(lng2-lng1)*math.Cos(lat2), 179 | math.Cos(lat1)*math.Sin(lat2)-math.Sin(lat1)*math.Cos(lat2)*math.Cos(lng2-lng1)) 180 | 181 | return s1.Angle(bearing) 182 | } 183 | -------------------------------------------------------------------------------- /pkg/igc/point_test.go: -------------------------------------------------------------------------------- 1 | // Copyright The ezgliding Authors. 2 | // 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package igc 18 | 19 | import ( 20 | "testing" 21 | "time" 22 | ) 23 | 24 | type point struct { 25 | lat string 26 | lng string 27 | t time.Time 28 | } 29 | 30 | type PointTest struct { 31 | p1 point 32 | p2 point 33 | d float64 34 | b float64 35 | s float64 36 | } 37 | 38 | var dmdTests = []PointTest{ 39 | { 40 | p1: point{lat: "", lng: ""}, 41 | p2: point{lat: "", lng: ""}, 42 | d: 0, 43 | b: 0, 44 | }, 45 | { 46 | p1: point{lat: "5110179N", lng: "00102644W"}, 47 | p2: point{lat: "5110179N", lng: "00102644W"}, 48 | d: 0, 49 | b: 0, 50 | }, 51 | } 52 | 53 | var tests = []PointTest{ 54 | { 55 | p1: point{lat: "", lng: ""}, 56 | p2: point{lat: "", lng: ""}, 57 | d: 0, 58 | b: 0, 59 | }, 60 | { 61 | p1: point{lat: "N500359", lng: "W0054253", t: getTime("10:00:00")}, 62 | p2: point{lat: "N500359", lng: "W0054253", t: getTime("10:00:00")}, 63 | d: 0, 64 | b: 0, 65 | s: 0, 66 | }, 67 | { 68 | p1: point{lat: "N500359", lng: "W0054253", t: getTime("10:00:00")}, 69 | p2: point{lat: "N583838", lng: "W0030412", t: getTime("10:30:00")}, 70 | d: 968.8535467131387, 71 | b: 9.119818104504075, 72 | s: 968.8535467131387 / 0.5, 73 | }, 74 | { 75 | p1: point{lat: "S270201", lng: "E0303722", t: getTime("12:00:00")}, 76 | p2: point{lat: "N523838", lng: "W0030412", t: getTime("13:30:00")}, 77 | d: 9443.596093743798, 78 | b: -19.75024484768977, 79 | s: 9443.596093743798 / 1.5, 80 | }, 81 | } 82 | 83 | func TestPointDistanceDMD(t *testing.T) { 84 | for _, test := range dmdTests { 85 | p1 := NewPointFromDMD(test.p1.lat, test.p1.lng) 86 | p2 := NewPointFromDMD(test.p2.lat, test.p2.lng) 87 | result := p1.Distance(p2) 88 | if result != test.d { 89 | t.Errorf("p1: %v p2: %v :: expected distance %v got %+v", test.p1, test.p2, test.d, result) 90 | continue 91 | } 92 | } 93 | } 94 | 95 | func TestPointSpeed(t *testing.T) { 96 | for _, test := range tests { 97 | p1 := NewPointFromDMS(test.p1.lat, test.p1.lng) 98 | p1.Time = test.p1.t 99 | p2 := NewPointFromDMS(test.p2.lat, test.p2.lng) 100 | p2.Time = test.p2.t 101 | result := p1.Speed(p2) 102 | if result != test.s { 103 | t.Errorf("p1: %v p2: %v :: expected speed %v got %+v", test.p1, test.p2, test.s, result) 104 | continue 105 | } 106 | } 107 | } 108 | 109 | func TestPointDistance(t *testing.T) { 110 | for _, test := range tests { 111 | p1 := NewPointFromDMS(test.p1.lat, test.p1.lng) 112 | p2 := NewPointFromDMS(test.p2.lat, test.p2.lng) 113 | result := p1.Distance(p2) 114 | if result != test.d { 115 | t.Errorf("p1: %v p2: %v :: expected distance %v got %+v", test.p1, test.p2, test.d, result) 116 | continue 117 | } 118 | } 119 | } 120 | 121 | func TestPointBearing(t *testing.T) { 122 | for _, test := range tests { 123 | p1 := NewPointFromDMS(test.p1.lat, test.p1.lng) 124 | p2 := NewPointFromDMS(test.p2.lat, test.p2.lng) 125 | result := p1.Bearing(p2).Degrees() 126 | if result != test.b { 127 | t.Errorf("p1: %v p2: %v :: expected bearing %v got %+v", test.p1, test.p2, test.b, result) 128 | continue 129 | } 130 | } 131 | } 132 | 133 | func getTime(v string) time.Time { 134 | t, _ := time.Parse("15:04:05", v) 135 | return t 136 | } 137 | -------------------------------------------------------------------------------- /pkg/igc/track.go: -------------------------------------------------------------------------------- 1 | // Copyright The ezgliding Authors. 2 | // 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package igc 18 | 19 | import ( 20 | "archive/zip" 21 | "bytes" 22 | "encoding/csv" 23 | "encoding/json" 24 | "fmt" 25 | "time" 26 | 27 | "github.com/golang/geo/s1" 28 | "github.com/golang/geo/s2" 29 | kml "github.com/twpayne/go-kml" 30 | "gopkg.in/yaml.v3" 31 | ) 32 | 33 | // MaxSpeed is the maximum theoretical speed for a glider. 34 | // 35 | // It is used to detect bad GPS coordinates, which should be removed from the track. 36 | const MaxSpeed float64 = 500.0 37 | 38 | // Track holds all IGC flight data (header and GPS points). 39 | type Track struct { 40 | Header 41 | ID string 42 | Points []Point 43 | K []K 44 | Events []Event 45 | Satellites []Satellite 46 | Logbook []string 47 | Task Task 48 | DGPSStationID string 49 | Signature string 50 | phases []Phase 51 | } 52 | 53 | // NewTrack returns a new instance of Track, with fields initialized to zero. 54 | func NewTrack() Track { 55 | track := Track{} 56 | return track 57 | } 58 | 59 | // Header holds the meta information of a track. 60 | // 61 | // This is the H record in the IGC specification, section A3.2. 62 | type Header struct { 63 | Manufacturer string 64 | UniqueID string 65 | AdditionalData string 66 | Date time.Time 67 | Site string 68 | FixAccuracy int64 69 | Pilot string 70 | PilotBirth time.Time 71 | Crew string 72 | GliderType string 73 | GliderID string 74 | Observation string 75 | GPSDatum string 76 | FirmwareVersion string 77 | HardwareVersion string 78 | SoftwareVersion string // for non-igc flight recorders 79 | Specification string 80 | FlightRecorder string 81 | GPS string 82 | GNSSModel string 83 | PressureModel string 84 | PressureSensor string 85 | AltimeterPressure float64 86 | CompetitionID string 87 | CompetitionClass string 88 | Timezone int 89 | MOPSensor string 90 | } 91 | 92 | // K holds flight data needed less often than Points. 93 | // 94 | // This is the K record in the IGC specification, section A4.4. Fields 95 | // is a map between a given content type and its value, with the possible 96 | // content types being defined in the J record. 97 | // 98 | // Examples of content types include heading true (HDT) or magnetic (HDM), 99 | // airspeed (IAS), etc. 100 | type K struct { 101 | Time time.Time 102 | Fields map[string]string 103 | } 104 | 105 | // Satellite holds the IDs of the available satellites at a given Time. 106 | // 107 | // This is the F record in the IGC specification, section A4.3. 108 | type Satellite struct { 109 | Time time.Time 110 | Ids []string 111 | } 112 | 113 | // Event holds data records triggered at a given time. 114 | // 115 | // This is the E record in the IGC specification, section A4.2. The events 116 | // can be pilot initiated (with a PEV code), proximity alerts, etc. 117 | type Event struct { 118 | Time time.Time 119 | Type string 120 | Data string 121 | } 122 | 123 | // Task holds all the metadata put in a pre-declared task to be performed. 124 | // 125 | // This is the C record in the IGC specification, section A3.5. 126 | type Task struct { 127 | DeclarationDate time.Time 128 | Date time.Time 129 | Number int 130 | Takeoff Point 131 | Start Point 132 | Turnpoints []Point 133 | Finish Point 134 | Landing Point 135 | Description string 136 | } 137 | 138 | // Distance returns the total distance in kms between the turn points. 139 | // 140 | // It includes the Start and Finish fields as the first and last point, 141 | // respectively, with the Turnpoints in the middle. The return value is 142 | // sum of all distances between each consecutive point. 143 | func (task *Task) Distance() float64 { 144 | d := 0.0 145 | p := []Point{task.Start} 146 | p = append(p, task.Turnpoints...) 147 | p = append(p, task.Finish) 148 | for i := 0; i < len(p)-1; i++ { 149 | d += p[i].Distance(p[i+1]) 150 | } 151 | return d 152 | } 153 | 154 | // Manufacturer holds manufacturer name, short ID and char identifier. 155 | // 156 | // The list of manufacturers is defined in the IGC specification, 157 | // section A2.5.6. A map Manufacturers is available in this library. 158 | type Manufacturer struct { 159 | char byte //nolint 160 | short string //nolint 161 | name string //nolint 162 | } 163 | 164 | // Manufacturers holds the list of available manufacturers. 165 | // 166 | // This list is defined in the IGC specification, section A2.5.6. 167 | var Manufacturers = map[string]Manufacturer{ 168 | "GCS": {'A', "GCS", "Garrecht"}, 169 | "LGS": {'B', "LGS", "Logstream"}, 170 | "CAM": {'C', "CAM", "Cambridge Aero Instruments"}, 171 | "DSX": {'D', "DSX", "Data Swan/DSX"}, 172 | "EWA": {'E', "EWA", "EW Avionics"}, 173 | "FIL": {'F', "FIL", "Filser"}, 174 | "FLA": {'G', "FLA", "Flarm (Track Alarm)"}, 175 | "SCH": {'H', "SCH", "Scheffel"}, 176 | "ACT": {'I', "ACT", "Aircotec"}, 177 | "CNI": {'K', "CNI", "ClearNav Instruments"}, 178 | "NKL": {'K', "NKL", "NKL"}, 179 | "LXN": {'L', "LXN", "LX Navigation"}, 180 | "IMI": {'M', "IMI", "IMI Gliding Equipment"}, 181 | "NTE": {'N', "NTE", "New Technologies s.r.l."}, 182 | "NAV": {'O', "NAV", "Naviter"}, 183 | "PES": {'P', "PES", "Peschges"}, 184 | "PRT": {'R', "PRT", "Print Technik"}, 185 | "SDI": {'S', "SDI", "Streamline Data Instruments"}, 186 | "TRI": {'T', "TRI", "Triadis Engineering GmbH"}, 187 | "LXV": {'V', "LXV", "LXNAV d.o.o."}, 188 | "WES": {'W', "WES", "Westerboer"}, 189 | "XCS": {' ', "XCS", "XCSoar"}, 190 | "XYY": {'X', "XYY", "Other manufacturer"}, 191 | "ZAN": {'Z', "ZAN", "Zander"}, 192 | } 193 | 194 | func (track *Track) Cleanup() (Track, error) { 195 | clean := *track 196 | 197 | i := 1 198 | for i < len(clean.Points) { 199 | if clean.Points[i-1].Speed(clean.Points[i]) > MaxSpeed { 200 | clean.Points = append(clean.Points[:i], clean.Points[i+1:]...) 201 | } 202 | i = i + 1 203 | } 204 | return clean, nil 205 | } 206 | 207 | func (track *Track) Simplify(tolerance float64) (Track, error) { 208 | r := polylineFromPoints(track.Points).SubsampleVertices(s1.Angle(tolerance)) 209 | points := make([]Point, len(r)) 210 | for i, v := range r { 211 | points[i] = track.Points[v] 212 | } 213 | 214 | simplified := *track 215 | simplified.Points = points 216 | return simplified, nil 217 | } 218 | 219 | func (track *Track) Encode(format string) ([]byte, error) { 220 | switch format { 221 | case "json": 222 | return json.MarshalIndent(track, "", " ") 223 | case "kml", "kmz": 224 | return encodeKML(track, format) 225 | case "yaml": 226 | return yaml.Marshal(track) 227 | case "csv": 228 | return track.encodeCSV() 229 | default: 230 | return []byte{}, fmt.Errorf("unsupported format '%v'", format) 231 | } 232 | } 233 | 234 | func (track *Track) encodeCSV() ([]byte, error) { 235 | 236 | values := []string{ 237 | fmt.Sprintf("%d", track.Date.Year()), track.ID, 238 | track.Manufacturer, track.UniqueID, track.AdditionalData, 239 | track.Date.Format("020106"), track.Site, fmt.Sprintf("%d", track.FixAccuracy), track.Pilot, 240 | track.PilotBirth.Format("020106"), track.Crew, track.GliderType, track.GliderID, 241 | track.Observation, track.GPSDatum, track.FirmwareVersion, 242 | track.HardwareVersion, track.SoftwareVersion, track.Specification, 243 | track.FlightRecorder, track.GPS, track.GNSSModel, track.PressureModel, 244 | track.PressureSensor, fmt.Sprintf("%f", track.AltimeterPressure), 245 | track.CompetitionID, track.CompetitionClass, fmt.Sprintf("%d", track.Timezone), 246 | track.MOPSensor} 247 | 248 | buff := new(bytes.Buffer) 249 | w := csv.NewWriter(buff) 250 | err := w.Write(values) 251 | w.Flush() 252 | if err != nil { 253 | return buff.Bytes(), err 254 | } 255 | return buff.Bytes(), nil 256 | } 257 | 258 | func encodeKML(track *Track, format string) ([]byte, error) { 259 | 260 | metadata := fmt.Sprintf("%v : %v : %v", track.Date, track.Pilot, track.GliderType) 261 | 262 | phasesKML, err := track.encodePhasesKML() 263 | if err != nil { 264 | return []byte{}, err 265 | } 266 | 267 | k := kml.Document( 268 | kml.Name(metadata), 269 | kml.Description(""), 270 | phasesKML, 271 | ) 272 | 273 | buf := new(bytes.Buffer) 274 | if err := k.WriteIndent(buf, "", " "); err != nil { 275 | return buf.Bytes(), err 276 | } 277 | 278 | if format == "kmz" { 279 | zipbuf := new(bytes.Buffer) 280 | 281 | w := zip.NewWriter(zipbuf) 282 | f, err := w.Create("flight.kml") 283 | if err != nil { 284 | return []byte{}, err 285 | } 286 | _, err = f.Write(buf.Bytes()) 287 | if err != nil { 288 | return []byte{}, err 289 | } 290 | err = w.Close() 291 | if err != nil { 292 | return []byte{}, err 293 | } 294 | 295 | return zipbuf.Bytes(), nil 296 | } 297 | return buf.Bytes(), nil 298 | } 299 | 300 | func polylineFromPoints(points []Point) *s2.Polyline { 301 | p := make(s2.Polyline, len(points)) 302 | for k, v := range points { 303 | p[k] = s2.PointFromLatLng(v.LatLng) 304 | } 305 | return &p 306 | } 307 | -------------------------------------------------------------------------------- /pkg/igc/track_test.go: -------------------------------------------------------------------------------- 1 | // Copyright The ezgliding Authors. 2 | // 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package igc 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | "io/ioutil" 23 | "os" 24 | "path/filepath" 25 | "testing" 26 | 27 | "github.com/golang/geo/s2" 28 | ) 29 | 30 | const ( 31 | testDir = "/tmp/ezgliding/tests" 32 | ) 33 | 34 | type simplifyTest struct { 35 | name string 36 | result string //nolint 37 | } 38 | 39 | var simplifyTests = []simplifyTest{ 40 | { 41 | name: "simplify-short-flight-1", 42 | result: "simplify-short-flight-1.simple", 43 | }, 44 | } 45 | 46 | func TestSimplify(t *testing.T) { 47 | for _, test := range simplifyTests { 48 | t.Run(fmt.Sprintf("%v\n", test.name), func(t *testing.T) { 49 | f := filepath.Join("../../testdata/simplify", fmt.Sprintf("%v.igc", test.name)) 50 | golden := fmt.Sprintf("%v.golden", f) 51 | track, err := ParseLocation(f) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | simple, err := track.Simplify(0.0001) 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | 60 | // update golden if flag is passed 61 | if *update { 62 | jsn, _ := json.Marshal(simple) 63 | if err = ioutil.WriteFile(golden, jsn, 0644); err != nil { 64 | t.Fatal(err) 65 | } 66 | } 67 | 68 | b, _ := ioutil.ReadFile(golden) 69 | var goldenTrack Track 70 | _ = json.Unmarshal(b, &goldenTrack) 71 | if len(simple.Points) != len(goldenTrack.Points) { 72 | t.Errorf("expected %v got %v simple points", len(goldenTrack.Points), len(simple.Points)) 73 | } 74 | }) 75 | } 76 | } 77 | 78 | func TestSimplifyStats(t *testing.T) { 79 | 80 | simplifyDB := "testdata/simplify/db" 81 | csvfile, _ := os.Create(fmt.Sprintf("%v/simplify-stats.csv", testDir)) 82 | 83 | files, _ := ioutil.ReadDir(simplifyDB) 84 | fmt.Fprintf(csvfile, "id,total,clean,clean%%,simple001,simple001%%,simple0001,simple0001%%\n") 85 | for _, f := range files { 86 | track, _ := ParseLocation(fmt.Sprintf("%v/%v", simplifyDB, f.Name())) 87 | clean, err := track.Cleanup() 88 | if err != nil { 89 | t.Fatal(err) 90 | } 91 | if len(track.Points) == 0 { 92 | //t.Fatalf("track has no points :: %v", track) 93 | } else { 94 | // simplify using two different precisions 95 | simple001, err := clean.Simplify(0.001) 96 | if err != nil { 97 | t.Fatal(err) 98 | } 99 | simple0001, err := clean.Simplify(0.0001) 100 | if err != nil { 101 | t.Fatal(err) 102 | } 103 | // convert the whole lot to json for js usage 104 | jsn, _ := toLatLng(track) 105 | jsnClean, _ := toLatLng(clean) 106 | jsnSimple001, _ := toLatLng(simple001) 107 | jsnSimple0001, _ := toLatLng(simple0001) 108 | // generate the html/js content for visualization 109 | df := fmt.Sprintf("%v/js/%v.js", testDir, f.Name()) 110 | d, _ := os.Create(df) 111 | _, _ = d.WriteString(fmt.Sprintf("%s\n%s\n%s\n%s\n", append([]byte("path ="), jsn...), append([]byte("clean ="), jsnClean...), append([]byte("simple001 ="), jsnSimple001...), append([]byte("simple0001 ="), jsnSimple0001...))) 112 | d.Close() 113 | html := fmt.Sprintf(template, f.Name(), f.Name(), f.Name()) 114 | _ = ioutil.WriteFile(fmt.Sprintf("%v/%v.html", testDir, f.Name()), []byte(html), 0644) 115 | ptsClean := float64(len(clean.Points)) 116 | // optimize for both simplified tracks 117 | opt := NewBruteForceOptimizer(false) 118 | //task001, err := opt.Optimize(simple001, 3, Distance) 119 | task001 := Task{} 120 | task0001, err := opt.Optimize(simple0001, 2, Distance) 121 | if err != nil { 122 | t.Fatal(err) 123 | } 124 | // fill in the csv line for this flight with simplify stats 125 | fmt.Fprintf(csvfile, "%v,%v,%v,%.1f,%v,%.1f,%v,%v,%.1f,%.1f\n", 126 | f.Name(), 127 | len(track.Points), 128 | len(clean.Points), 129 | float64(len(clean.Points))/ptsClean*100.0, 130 | len(simple001.Points), 131 | float64(len(simple001.Points))/ptsClean*100.0, 132 | task001.Distance(), 133 | len(simple0001.Points), 134 | float64(len(simple0001.Points))/ptsClean*100.0, 135 | task0001.Distance()) 136 | } 137 | } 138 | csvfile.Close() 139 | } 140 | 141 | func toLatLng(track Track) ([]byte, error) { 142 | 143 | points := make([]s2.LatLng, len(track.Points)) 144 | for i, v := range track.Points { 145 | points[i] = v.LatLng 146 | } 147 | return json.Marshal(points) 148 | } 149 | 150 | var template = ` 151 | 152 | 153 | 154 | 155 | 156 | EzGliding Simplify 157 | 170 | 171 | 172 |
173 | 175 | 176 | 243 | 246 | 247 | 248 | ` 249 | -------------------------------------------------------------------------------- /pkg/netcoupe/crawler.go: -------------------------------------------------------------------------------- 1 | // Copyright The ezgliding Authors. 2 | // 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package netcoupe 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | "io" 23 | "net/url" 24 | "regexp" 25 | "strconv" 26 | "strings" 27 | "time" 28 | 29 | "github.com/ezgliding/goigc/pkg/igc" 30 | "github.com/gocolly/colly" 31 | log "github.com/sirupsen/logrus" 32 | ) 33 | 34 | // DailyUrlPattern is the main page to list netcoupe flights. 35 | const DailyUrlPattern = "https://%v.netcoupe.net/Results/DailyResults.aspx" 36 | 37 | // FlightBaseUrl is the base path to fetch flight details from a flight ID. 38 | const FlightBaseUrlPattern = "https://%v.netcoupe.net/Results/FlightDetail.aspx?FlightID=" 39 | 40 | // TrackBaseUrlPattern is the base path to download the flight track from a track ID. 41 | const TrackBaseUrlPattern = "https://%v.netcoupe.net/Download/DownloadIGC.aspx?FileID=" 42 | 43 | // This is a constant map. 44 | var httpHeaders = map[string][]string{ 45 | "Accept-Encoding": []string{"gzip, deflate"}, 46 | "Cache-Control": []string{"max-age=0"}, 47 | "Upgrade-Insecure-Requests": []string{"1"}, 48 | "DNT": []string{"1"}, 49 | "Origin": []string{"https://archive2019.netcoupe.net"}, 50 | "User-Agent": []string{"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/66.0.3359.181 Chrome/66.0.3359.181 Safari/537.36"}, 51 | "Accept": []string{"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8"}, 52 | "Accept-Language": []string{"en-US,en;q=0.9,de;q=0.8,fr;q=0.7,pt;q=0.6,es;q=0.5,it;q=0.4,ny;q=0.3"}, 53 | "Connection": []string{"keep-alive"}, 54 | "Referer": []string{"https://archive2019.netcoupe.net/Results/DailyResults.aspx"}} 55 | 56 | // Netcoupe implements a crawler for http://netcoupe.net. 57 | type Netcoupe struct { 58 | collector *colly.Collector 59 | year int 60 | baseUrl string 61 | } 62 | 63 | func NewNetcoupeYear(year int) Netcoupe { 64 | n := Netcoupe{} 65 | n.year = year 66 | n.baseUrl = "www" 67 | n.collector = colly.NewCollector() 68 | n.collector.AllowURLRevisit = true 69 | n.collector.UserAgent = httpHeaders["User-Agent"][0] 70 | if year != 0 { 71 | n.baseUrl = fmt.Sprintf("archive%v", year) 72 | } 73 | return n 74 | } 75 | 76 | func NewNetcoupe() Netcoupe { 77 | return NewNetcoupeYear(0) 78 | } 79 | 80 | // Crawl checks for flights on netcoupe.net. 81 | // 82 | // It works by querying flights for specific days, making it easier to iterate 83 | // through past data. The rules for flight submission are defined here: 84 | // http://www.planeur.net/_download/netcoupe/2018_np4.2.pdf 85 | // """ 86 | // Chaque performance doit être enregistrée dans un délai de 15 jours sur le 87 | // site de la NetCoupe (www.netcoupe.net) par le commandant de bord ou par le 88 | // Responsable de la NetCoupe de l’association avec l'accord du pilote. 89 | // """ 90 | // Which means that it's only worth to crawl for new flights back to 2 weeks max. 91 | func (n Netcoupe) Crawl(start time.Time, end time.Time) ([]igc.Flight, error) { 92 | var flights []igc.Flight 93 | 94 | // Do not allow start > end 95 | if end.Before(start) { 96 | return nil, errors.New("Invalid start end date pair") 97 | } 98 | 99 | r, _ := regexp.Compile(`.*DisplayFlightDetail\('(?P[0-9]+)'\).*`) 100 | 101 | c := n.newCollector() 102 | c.OnRequest(func(r *colly.Request) { 103 | log.WithFields(log.Fields{ 104 | "url": r.URL.String(), 105 | "headers": r.Headers}).Trace("Visiting flight list") 106 | }) 107 | c.OnError(func(r *colly.Response, err error) { 108 | log.WithFields(log.Fields{ 109 | "response": r, 110 | "error": err}).Error("Failed to visit url") 111 | // Poor handling of http 420 112 | if strings.Contains(err.Error(), "ENHANCE_YOUR_CALM") { 113 | time.Sleep(10 * time.Second) 114 | } 115 | }) 116 | 117 | d := n.newCollector() 118 | d.OnRequest(func(r *colly.Request) { 119 | log.WithFields(log.Fields{ 120 | "url": r.URL.String(), 121 | "headers": r.Headers}).Trace("Visiting flight details") 122 | }) 123 | 124 | c.OnHTML("table tr td:nth-child(4) a[href]", func(e *colly.HTMLElement) { 125 | id := r.FindStringSubmatch(e.Attr("href")) 126 | log.WithFields(log.Fields{ 127 | "flight_id": id[1]}).Trace("Scheduling visit to flight details") 128 | if len(id) == 2 { 129 | // TODO(rochaporto): handle error 130 | _ = d.Visit(fmt.Sprintf("%v%v", n.flightBaseUrl(), id[1])) 131 | } 132 | 133 | }) 134 | 135 | d.OnHTML("div center table[width]", func(e *colly.HTMLElement) { 136 | f := igc.Flight{} 137 | f.URL = e.Request.URL.String() 138 | f.ID = e.Request.URL.Query()["FlightID"][0] 139 | f.Pilot = e.ChildText("tbody tr:nth-child(3) td:nth-child(2) a") 140 | f.Club = e.ChildText("tbody tr:nth-child(5) td:nth-child(2) a") 141 | f.Date, _ = time.Parse("02/01/2006", e.ChildText("tbody tr:nth-child(8) td:nth-child(2) div")) 142 | f.Takeoff = e.ChildText("tbody tr:nth-child(9) td:nth-child(2) div") 143 | f.Region = e.ChildText("tbody tr:nth-child(10) td:nth-child(2) div") 144 | f.Country = e.ChildText("tbody tr:nth-child(11) td:nth-child(2) div") 145 | f.Distance = parseFloat(e.ChildText("tbody tr:nth-child(12) td:nth-child(2) div")) 146 | f.Points = parseFloat(e.ChildText("tbody tr:nth-child(13) td:nth-child(2) div")) 147 | f.Glider = e.ChildText("tbody tr:nth-child(14) td:nth-child(2) div table tbody tr td") 148 | 149 | i := 0 150 | if strings.Contains(e.ChildText("tbody tr:nth-child(15) td:nth-child(1) div"), "Comp") { 151 | f.CompetitionURL = e.ChildText("tbody tr:nth-child(15) td:nth-child(2) div") 152 | i = 1 153 | } 154 | f.Type = e.ChildText(fmt.Sprintf("tbody tr:nth-child(%v) td:nth-child(2) div", 15+i)) 155 | trackUrl, err := url.Parse( 156 | e.ChildAttr(fmt.Sprintf("tbody tr:nth-child(%v) td:nth-child(2) div a", 16+i), "href")) 157 | if err == nil && trackUrl.RawQuery != "" { 158 | f.TrackID = trackUrl.Query()["FileID"][0] 159 | f.TrackURL = fmt.Sprintf("%v%v", n.TrackBaseUrl(), f.TrackID) 160 | } 161 | f.Speed = parseFloat(e.ChildText(fmt.Sprintf("tbody tr:nth-child(%v) td:nth-child(2) div", 17+i))) 162 | f.Comments = e.ChildText(fmt.Sprintf("tbody tr:nth-child(%v) td:nth-child(2) div", 23+i)) 163 | 164 | flights = append(flights, f) 165 | }) 166 | 167 | current := time.Date(start.Year(), start.Month(), start.Day(), 12, 0, 0, 0, time.UTC) 168 | end = time.Date(end.Year(), end.Month(), end.Day(), 12, 0, 0, 0, time.UTC) 169 | for ; end.After(current.AddDate(0, 0, -1)); current = current.AddDate(0, 0, 1) { 170 | data := n.sessionHeaders(c) 171 | data["ddlDisplayRange"] = "0" 172 | data["ddlDisplayDate"] = current.Format("02/01/2006") 173 | data["rbgDisplayMode"] = "rbDisplayByDate" 174 | tmp := n.newCollector() 175 | tmp.OnHTML("input", func(e *colly.HTMLElement) { 176 | switch e.Attr("name") { 177 | case "__EVENTVALIDATION": 178 | data["__EVENTVALIDATION"] = e.Attr("value") 179 | case "__VIEWSTATE": 180 | data["__VIEWSTATE"] = e.Attr("value") 181 | case "__VIEWSTATEGENERATOR": 182 | data["__VIEWSTATEGENERATOR"] = e.Attr("value") 183 | } 184 | }) 185 | n.post(tmp, n.dailyUrl(), data) 186 | // Set header for single page results (by default it pages) 187 | data["__EVENTTARGET"] = "dgDailyResults$ctl01$ctl01" 188 | n.post(c, n.dailyUrl(), data) 189 | } 190 | 191 | log.WithFields(log.Fields{ 192 | "start": start, 193 | "end": end, 194 | "flights": flights, 195 | "num_flights": len(flights), 196 | }).Trace("Finishing crawling flights") 197 | return flights, nil 198 | } 199 | 200 | func (n Netcoupe) Get(url string) ([]byte, error) { 201 | var result []byte 202 | 203 | t := n.newCollector() 204 | t.OnRequest(func(r *colly.Request) { 205 | log.WithFields(log.Fields{ 206 | "url": r.URL.String(), 207 | "headers": r.Headers}).Trace("Visiting flight track") 208 | }) 209 | t.OnResponse(func(r *colly.Response) { 210 | result = r.Body 211 | }) 212 | err := t.Visit(url) 213 | 214 | return result, err 215 | } 216 | 217 | func (n Netcoupe) newCollector() *colly.Collector { 218 | return n.collector.Clone() 219 | } 220 | 221 | func (n Netcoupe) sessionHeaders(c *colly.Collector) map[string]string { 222 | headers := map[string]string{ 223 | "__EVENTARGUMENT": "", 224 | "__LASTFOCUS": "", 225 | "__EVENTTARGET": "ddlDisplayDate", 226 | } 227 | 228 | t := c.Clone() 229 | t.OnRequest(func(r *colly.Request) { 230 | log.WithFields(log.Fields{ 231 | "url": r.URL.String(), 232 | "headers": r.Headers}).Trace("Visiting for session data collection") 233 | 234 | }) 235 | t.OnHTML("input", func(e *colly.HTMLElement) { 236 | switch e.Attr("name") { 237 | case "__EVENTVALIDATION": 238 | headers["__EVENTVALIDATION"] = e.Attr("value") 239 | case "__VIEWSTATE": 240 | headers["__VIEWSTATE"] = e.Attr("value") 241 | case "__VIEWSTATEGENERATOR": 242 | headers["__VIEWSTATEGENERATOR"] = e.Attr("value") 243 | } 244 | }) 245 | // TODO(rochaporto): handle error 246 | _ = t.Request("GET", n.dailyUrl(), nil, nil, httpHeaders) 247 | 248 | return headers 249 | } 250 | 251 | func (n Netcoupe) post(c *colly.Collector, url string, data map[string]string) { 252 | cookies := c.Cookies(url) 253 | // TODO(rochaporto): handle error 254 | _ = c.SetCookies(url, cookies) 255 | dur, _ := time.ParseDuration("1m") 256 | c.SetRequestTimeout(dur) 257 | log.WithFields(log.Fields{ 258 | "url": url}).Trace("POST request") 259 | // TODO(rochaporto): handle error 260 | _ = c.Request("POST", url, createFormReader(data), nil, httpHeaders) 261 | } 262 | 263 | func (n Netcoupe) dailyUrl() string { 264 | return fmt.Sprintf(DailyUrlPattern, n.baseUrl) 265 | } 266 | 267 | func (n Netcoupe) flightBaseUrl() string { 268 | return fmt.Sprintf(FlightBaseUrlPattern, n.baseUrl) 269 | } 270 | 271 | func (n Netcoupe) TrackBaseUrl() string { 272 | return fmt.Sprintf(TrackBaseUrlPattern, n.baseUrl) 273 | } 274 | 275 | func parseFloat(s string) float64 { 276 | rs := strings.Replace(strings.TrimSpace(strings.Split(s, " ")[0]), ",", ".", -1) 277 | r, _ := strconv.ParseFloat(rs, 32) 278 | return r 279 | } 280 | 281 | func createFormReader(data map[string]string) io.Reader { 282 | form := url.Values{} 283 | for k, v := range data { 284 | form.Add(k, v) 285 | } 286 | return strings.NewReader(form.Encode()) 287 | } 288 | -------------------------------------------------------------------------------- /pkg/netcoupe/crawler_test.go: -------------------------------------------------------------------------------- 1 | // Copyright The ezgliding Authors. 2 | // 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | package netcoupe 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | "io/ioutil" 23 | "os" 24 | "testing" 25 | "time" 26 | 27 | "github.com/ezgliding/goigc/pkg/igc" 28 | log "github.com/sirupsen/logrus" 29 | ) 30 | 31 | func init() { 32 | log.SetLevel(log.TraceLevel) 33 | } 34 | 35 | func TestNetcoupeCrawler(t *testing.T) { 36 | start := time.Date(2018, time.December, 24, 12, 0, 0, 0, time.UTC) 37 | end := time.Date(2018, time.December, 25, 12, 0, 0, 0, time.UTC) 38 | 39 | var n Netcoupe = NewNetcoupeYear(2018) 40 | flights, err := n.Crawl(start, end) 41 | if err != nil { 42 | t.Errorf("%v", err) 43 | } 44 | 45 | if len(flights) <= 0 { 46 | t.Errorf("no flights returned") 47 | } 48 | 49 | jsonFlights, _ := json.MarshalIndent(flights, "", " ") 50 | fmt.Printf("%v\n", string(jsonFlights)) 51 | } 52 | 53 | func TestNetcoupeCrawlerDownload(t *testing.T) { 54 | 55 | year := 2019 56 | start := time.Date(year, 12, 31, 12, 0, 0, 0, time.UTC) 57 | end := time.Date(year, 12, 31, 12, 0, 0, 0, time.UTC) 58 | 59 | var n Netcoupe = NewNetcoupeYear(year) 60 | current := start 61 | for ; end.After(current.AddDate(0, 0, -1)); current = current.AddDate(0, 0, 1) { 62 | var flights []igc.Flight 63 | dbFile := fmt.Sprintf("db/%v/%v.json", year, current.Format("02-01-2006")) 64 | if _, err := os.Stat(dbFile); os.IsNotExist(err) { 65 | flights, err = n.Crawl(current, current) 66 | if err != nil { 67 | t.Errorf("%v", err) 68 | } 69 | jsonFlights, _ := json.MarshalIndent(flights, "", " ") 70 | // TODO(rochaporto): handle error 71 | _ = ioutil.WriteFile(dbFile, jsonFlights, 0644) 72 | } else { 73 | b, _ := ioutil.ReadFile(dbFile) 74 | err = json.Unmarshal(b, &flights) 75 | if err != nil { 76 | fmt.Printf("error parsing %v %v\n", dbFile, err) 77 | continue 78 | } 79 | } 80 | 81 | for _, f := range flights { 82 | flightFile := fmt.Sprintf("db/%v/flights/%v", year, f.TrackID) 83 | if _, err := os.Stat(flightFile); os.IsNotExist(err) { 84 | url := fmt.Sprintf("%v%v", n.TrackBaseUrl(), f.TrackID) 85 | data, _ := n.Get(url) 86 | // TODO(rochaporto): handle error 87 | _ = ioutil.WriteFile(flightFile, data, 0644) 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /pkg/netcoupe/doc.go: -------------------------------------------------------------------------------- 1 | // Copyright The ezgliding Authors. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | 16 | /* 17 | Package netcoupe provides crawler and optimizer implementations for netcoupe. 18 | 19 | https://www.netcoupe.net 20 | 21 | */ 22 | package netcoupe 23 | -------------------------------------------------------------------------------- /pkg/version/version.go: -------------------------------------------------------------------------------- 1 | // Copyright The ezgliding Authors. 2 | // 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | // 16 | 17 | // Package version holds version metadata for goigc. 18 | package version 19 | 20 | import "time" 21 | 22 | // Values to be injected during build (ldflags). 23 | var ( 24 | buildTime = time.Now() 25 | version = "unreleased" 26 | commit string 27 | metadata string 28 | ) 29 | 30 | // Version returns the goigc version. It is expected this is defined 31 | // as a semantic version number, or 'unreleased' for unreleased code. 32 | func Version() string { 33 | return version 34 | } 35 | 36 | // Commit returns the git commit SHA for the code that goigc was built from. 37 | func Commit() string { 38 | return commit 39 | } 40 | 41 | // Metadata returns metadata passed during build. 42 | func Metadata() string { 43 | return metadata 44 | } 45 | 46 | // BuildTime returns the date the package was built. 47 | func BuildTime() time.Time { 48 | return buildTime 49 | } 50 | -------------------------------------------------------------------------------- /scripts/bench.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright The ezgliding Authors. 4 | # 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | set -e 19 | set -x 20 | export TARGET=${1:-master} 21 | export PATH=/tmp/perf/cmd/benchstat:$PATH 22 | export GO111MODULE=on 23 | if [ ! -f /tmp/perf/cmd/benchstat/benchstat ]; then 24 | rm -rf /tmp/perf 25 | git clone https://github.com/golang/perf.git /tmp/perf 26 | cd /tmp/perf/cmd/benchstat 27 | go build . 28 | cd - 29 | fi 30 | 31 | REVBRANCH=$(git rev-parse --abbrev-ref HEAD) 32 | BRANCH=${TRAVIS_BRANCH:-$REVBRANCH} 33 | TARGET_RESULT="bench-${TARGET}.result" 34 | BRANCH_RESULT="bench-${BRANCH}.result" 35 | go test -bench=Benchmark* -run None > bench-${BRANCH}.result 36 | git config --replace-all remote.origin.fetch +refs/heads/*:refs/remotes/origin/* 37 | git fetch 38 | git fetch --tags 39 | git checkout -f $TARGET &> /dev/null 40 | go test -v -bench=Benchmark* -run None > bench-${TARGET}.result 41 | 42 | benchstat -delta-test none $TARGET_RESULT $BRANCH_RESULT 43 | rm -f $TARGET_RESULT $BRANCH_RESULT 44 | 45 | git checkout -f $BRANCH &> /dev/null 46 | -------------------------------------------------------------------------------- /scripts/changelog.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Copyright The ezgliding Authors. 4 | # 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # Based on the Helm script file: 19 | # github.com/helm/helm/scripts/coverage.sh 20 | 21 | set -euo pipefail 22 | 23 | if ! [ -x "$(command -v github_changelog_generator)" ]; then 24 | gem install --user-install github_changelog_generator 25 | export PATH=$PATH:$(ruby -r rubygems -e 'puts Gem.user_dir')/bin 26 | fi 27 | 28 | github_changelog_generator -u ezgliding -p goigc --token $GITHUB_TOKEN --since-tag $(git describe --abbrev=0 --tags `git rev-list --tags --skip=1 --max-count=1`) 29 | -------------------------------------------------------------------------------- /scripts/coverage.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Copyright The ezgliding Authors. 4 | # 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # Based on the Helm script file: 19 | # github.com/helm/helm/scripts/coverage.sh 20 | 21 | set -euo pipefail 22 | 23 | pushd / 24 | hash goveralls 2>/dev/null || go get github.com/mattn/goveralls 25 | popd 26 | 27 | go test -race -covermode atomic -coverprofile=profile.cov ./... 28 | go tool cover -func profile.cov 29 | case "${1-}" in 30 | --html) 31 | go tool cover -html profile.cov 32 | ;; 33 | esac 34 | 35 | -------------------------------------------------------------------------------- /scripts/pprof.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright The ezgliding Authors. 4 | # 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | set -e 19 | set -x 20 | go test -bench=Benchmark* -cpuprofile=cpu.pprof -memprofile=mem.pprof -run=x 21 | go tool pprof -top -cum cpu.pprof 22 | go tool pprof -top -cum mem.pprof 23 | -------------------------------------------------------------------------------- /scripts/validate-license.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright The ezgliding Authors. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | set -euo pipefail 17 | IFS=$'\n\t' 18 | 19 | find_files() { 20 | find . -not \( \ 21 | \( \ 22 | -wholename './vendor' \ 23 | -o -wholename '*testdata*' \ 24 | -o -wholename '*third_party*' \ 25 | \) -prune \ 26 | \) \ 27 | \( -name '*.go' -o -name '*.sh' \) 28 | } 29 | 30 | mapfile -t failed_license_header < <(find_files | xargs grep -L 'Licensed under the Apache License, Version 2.0 (the "License")') 31 | if (( ${#failed_license_header[@]} > 0 )); then 32 | echo "Some source files are missing license headers." 33 | printf '%s\n' "${failed_license_header[@]}" 34 | exit 1 35 | fi 36 | 37 | mapfile -t failed_copyright_header < <(find_files | xargs grep -L 'Copyright The ezgliding Authors.') 38 | if (( ${#failed_copyright_header[@]} > 0 )); then 39 | echo "Some source files are missing the copyright header." 40 | printf '%s\n' "${failed_copyright_header[@]}" 41 | exit 1 42 | fi 43 | -------------------------------------------------------------------------------- /testdata/parse/parse-0-basic-flight.1.igc: -------------------------------------------------------------------------------- 1 | I033638FXA3940SIU4143ENL 2 | J010812HDT 3 | C150701213841160701000102500KTri 4 | C5111359N00101899WEZ TAKEOFF 5 | C5110179N00102644WEZ START 6 | C5209092N00255227WEZ TP1 7 | C5230147N00017612WEZ TP2 8 | C5110179N00102644WEZ FINISH 9 | C5111359N00101899WEZ LANDING 10 | F160240040609123624 11 | D20331 12 | E160245ATS102312 13 | B1602455107126N00149300WA002880042919509020 14 | K16024800090 15 | B1603105107212N00149174WV002930043519608024 16 | LPLTLOG TEXT 17 | GREJNGJERJKNJKRE31895478537H43982FJN9248F942389T433T 18 | GJNJK2489IERGNV3089IVJE9GO398535J3894N358954983O0934 19 | -------------------------------------------------------------------------------- /testdata/parse/parse-0-basic-flight.1.igc.golden: -------------------------------------------------------------------------------- 1 | { 2 | "Manufacturer": "", 3 | "UniqueID": "", 4 | "AdditionalData": "", 5 | "Date": "0001-01-01T00:00:00Z", 6 | "Site": "", 7 | "FixAccuracy": 0, 8 | "Pilot": "", 9 | "PilotBirth": "0001-01-01T00:00:00Z", 10 | "Crew": "", 11 | "GliderType": "", 12 | "GliderID": "", 13 | "Observation": "", 14 | "GPSDatum": "", 15 | "FirmwareVersion": "", 16 | "HardwareVersion": "", 17 | "SoftwareVersion": "", 18 | "Specification": "", 19 | "FlightRecorder": "", 20 | "GPS": "", 21 | "GNSSModel": "", 22 | "PressureModel": "", 23 | "PressureSensor": "", 24 | "AltimeterPressure": 0, 25 | "CompetitionID": "", 26 | "CompetitionClass": "", 27 | "Timezone": 0, 28 | "MOPSensor": "", 29 | "ID": "", 30 | "Points": [ 31 | { 32 | "Lat": 0.89219078789206, 33 | "Lng": -0.03179408120716337, 34 | "Time": "0001-02-02T16:02:45Z", 35 | "FixValidity": 65, 36 | "PressureAltitude": 288, 37 | "GNSSAltitude": 429, 38 | "IData": { 39 | "ENL": "020", 40 | "FXA": "195", 41 | "SIU": "09" 42 | }, 43 | "NumSatellites": 6, 44 | "Description": "" 45 | }, 46 | { 47 | "Lat": 0.8922158042780052, 48 | "Lng": -0.03175742929287149, 49 | "Time": "0001-02-02T16:03:10Z", 50 | "FixValidity": 86, 51 | "PressureAltitude": 293, 52 | "GNSSAltitude": 435, 53 | "IData": { 54 | "ENL": "024", 55 | "FXA": "196", 56 | "SIU": "08" 57 | }, 58 | "NumSatellites": 6, 59 | "Description": "" 60 | } 61 | ], 62 | "K": [ 63 | { 64 | "Time": "0000-01-01T16:02:48Z", 65 | "Fields": { 66 | "HDT": "00090" 67 | } 68 | } 69 | ], 70 | "Events": [ 71 | { 72 | "Time": "0000-01-01T16:02:45Z", 73 | "Type": "ATS", 74 | "Data": "102312" 75 | } 76 | ], 77 | "Satellites": [ 78 | { 79 | "Time": "0000-01-01T16:02:40Z", 80 | "Ids": [ 81 | "04", 82 | "06", 83 | "09", 84 | "12", 85 | "36", 86 | "24" 87 | ] 88 | } 89 | ], 90 | "Logbook": [ 91 | "PLTLOG TEXT" 92 | ], 93 | "Task": { 94 | "DeclarationDate": "2001-07-15T21:38:41Z", 95 | "Date": "2001-07-16T00:00:00Z", 96 | "Number": 1, 97 | "Takeoff": { 98 | "Lat": 0.8934221176793421, 99 | "Lng": -0.0180056892281995, 100 | "Time": "0001-01-01T00:00:00Z", 101 | "FixValidity": 0, 102 | "PressureAltitude": 0, 103 | "GNSSAltitude": 0, 104 | "IData": {}, 105 | "NumSatellites": 0, 106 | "Description": "EZ TAKEOFF" 107 | }, 108 | "Start": { 109 | "Lat": 0.8930788695931164, 110 | "Lng": -0.018222400943655463, 111 | "Time": "0001-01-01T00:00:00Z", 112 | "FixValidity": 0, 113 | "PressureAltitude": 0, 114 | "GNSSAltitude": 0, 115 | "IData": {}, 116 | "NumSatellites": 0, 117 | "Description": "EZ START" 118 | }, 119 | "Turnpoints": [ 120 | { 121 | "Lat": 0.9102159666302401, 122 | "Lng": -0.050971468139868394, 123 | "Time": "0001-01-01T00:00:00Z", 124 | "FixValidity": 0, 125 | "PressureAltitude": 0, 126 | "GNSSAltitude": 0, 127 | "IData": {}, 128 | "NumSatellites": 0, 129 | "Description": "EZ TP1" 130 | }, 131 | { 132 | "Lat": 0.9163406178636969, 133 | "Lng": -0.0051231231310206885, 134 | "Time": "0001-01-01T00:00:00Z", 135 | "FixValidity": 0, 136 | "PressureAltitude": 0, 137 | "GNSSAltitude": 0, 138 | "IData": {}, 139 | "NumSatellites": 0, 140 | "Description": "EZ TP2" 141 | } 142 | ], 143 | "Finish": { 144 | "Lat": 0.8930788695931164, 145 | "Lng": -0.018222400943655463, 146 | "Time": "0001-01-01T00:00:00Z", 147 | "FixValidity": 0, 148 | "PressureAltitude": 0, 149 | "GNSSAltitude": 0, 150 | "IData": {}, 151 | "NumSatellites": 0, 152 | "Description": "EZ FINISH" 153 | }, 154 | "Landing": { 155 | "Lat": 0.8934221176793421, 156 | "Lng": -0.0180056892281995, 157 | "Time": "0001-01-01T00:00:00Z", 158 | "FixValidity": 0, 159 | "PressureAltitude": 0, 160 | "GNSSAltitude": 0, 161 | "IData": {}, 162 | "NumSatellites": 0, 163 | "Description": "EZ LANDING" 164 | }, 165 | "Description": "500KTri" 166 | }, 167 | "DGPSStationID": "0331", 168 | "Signature": "REJNGJERJKNJKRE31895478537H43982FJN9248F942389T433TJNJK2489IERGNV3089IVJE9GO398535J3894N358954983O0934" 169 | } -------------------------------------------------------------------------------- /testdata/parse/parse-0-basic-header.1.igc: -------------------------------------------------------------------------------- 1 | AFLA001Some Additional Data 2 | HFDTE010203 3 | HFFXA500 4 | HFPLTPilotincharge:EZ PILOT 5 | HFCM2Crew2:EZ CREW 6 | HFGTYGliderType:EZ TYPE 7 | HFGIDGliderID:EZ ID 8 | HFDTM100GPSDatum:WGS84 9 | HFRFWFirmwareVersion:v 0.1 10 | HFRHWHardwareVersion:v 0.2 11 | HFFTYFRType:EZ RECORDER,001 12 | HFGPSEZ GPS,002,12,5000 13 | HFPRSPressAltSensor:EZ PRESSURE 14 | HFCIDCompetitionID:EZ COMPID 15 | HFCCLCompetitionClass:EZ COMPCLASS 16 | HFMOPSENSOR:EZ MOPSENSOR 17 | HFTZNTimezone:2.00 18 | HFOOIOOID:EZ OOID 19 | HPATS:101369 20 | HPSITAIRPORT:EZ SITE 21 | HPDB1PILOT DAY OF BIRTH:111258 22 | HFSOF:EZ SOF 23 | HFFSP:EZ FSP 24 | HFALG:EZ ALG 25 | HFALP:EZ ALP 26 | -------------------------------------------------------------------------------- /testdata/parse/parse-0-basic-header.1.igc.golden: -------------------------------------------------------------------------------- 1 | { 2 | "Manufacturer": "FLA", 3 | "UniqueID": "001", 4 | "AdditionalData": "Some Additional Data", 5 | "Date": "2003-02-01T00:00:00Z", 6 | "Site": "EZ SITE", 7 | "FixAccuracy": 500, 8 | "Pilot": "EZ PILOT", 9 | "PilotBirth": "1958-12-11T00:00:00Z", 10 | "Crew": "EZ CREW", 11 | "GliderType": "EZ TYPE", 12 | "GliderID": "EZ ID", 13 | "Observation": "EZ OOID", 14 | "GPSDatum": "WGS84", 15 | "FirmwareVersion": "v 0.1", 16 | "HardwareVersion": "v 0.2", 17 | "SoftwareVersion": "EZ SOF", 18 | "Specification": "EZ FSP", 19 | "FlightRecorder": "EZ RECORDER,001", 20 | "GPS": "EZ GPS,002,12,5000", 21 | "GNSSModel": "EZ ALG", 22 | "PressureModel": "EZ ALP", 23 | "PressureSensor": "EZ PRESSURE", 24 | "AltimeterPressure": 1013.69, 25 | "CompetitionID": "EZ COMPID", 26 | "CompetitionClass": "EZ COMPCLASS", 27 | "Timezone": 2, 28 | "MOPSensor": "EZ MOPSENSOR", 29 | "ID": "", 30 | "Points": null, 31 | "K": null, 32 | "Events": null, 33 | "Satellites": null, 34 | "Logbook": null, 35 | "Task": { 36 | "DeclarationDate": "0001-01-01T00:00:00Z", 37 | "Date": "0001-01-01T00:00:00Z", 38 | "Number": 0, 39 | "Takeoff": { 40 | "Lat": 0, 41 | "Lng": 0, 42 | "Time": "0001-01-01T00:00:00Z", 43 | "FixValidity": 0, 44 | "PressureAltitude": 0, 45 | "GNSSAltitude": 0, 46 | "IData": null, 47 | "NumSatellites": 0, 48 | "Description": "" 49 | }, 50 | "Start": { 51 | "Lat": 0, 52 | "Lng": 0, 53 | "Time": "0001-01-01T00:00:00Z", 54 | "FixValidity": 0, 55 | "PressureAltitude": 0, 56 | "GNSSAltitude": 0, 57 | "IData": null, 58 | "NumSatellites": 0, 59 | "Description": "" 60 | }, 61 | "Turnpoints": null, 62 | "Finish": { 63 | "Lat": 0, 64 | "Lng": 0, 65 | "Time": "0001-01-01T00:00:00Z", 66 | "FixValidity": 0, 67 | "PressureAltitude": 0, 68 | "GNSSAltitude": 0, 69 | "IData": null, 70 | "NumSatellites": 0, 71 | "Description": "" 72 | }, 73 | "Landing": { 74 | "Lat": 0, 75 | "Lng": 0, 76 | "Time": "0001-01-01T00:00:00Z", 77 | "FixValidity": 0, 78 | "PressureAltitude": 0, 79 | "GNSSAltitude": 0, 80 | "IData": null, 81 | "NumSatellites": 0, 82 | "Description": "" 83 | }, 84 | "Description": "" 85 | }, 86 | "DGPSStationID": "", 87 | "Signature": "" 88 | } -------------------------------------------------------------------------------- /testdata/parse/parse-0-invalid-record.0.igc: -------------------------------------------------------------------------------- 1 | RANDOM GARBAGE 2 | -------------------------------------------------------------------------------- /testdata/parse/parse-a-no-uniqueid.1.igc: -------------------------------------------------------------------------------- 1 | AMLR 2 | -------------------------------------------------------------------------------- /testdata/parse/parse-a-no-uniqueid.1.igc.golden: -------------------------------------------------------------------------------- 1 | { 2 | "Manufacturer": "MLR", 3 | "UniqueID": "", 4 | "AdditionalData": "", 5 | "Date": "0001-01-01T00:00:00Z", 6 | "Site": "", 7 | "FixAccuracy": 0, 8 | "Pilot": "", 9 | "PilotBirth": "0001-01-01T00:00:00Z", 10 | "Crew": "", 11 | "GliderType": "", 12 | "GliderID": "", 13 | "Observation": "", 14 | "GPSDatum": "", 15 | "FirmwareVersion": "", 16 | "HardwareVersion": "", 17 | "SoftwareVersion": "", 18 | "Specification": "", 19 | "FlightRecorder": "", 20 | "GPS": "", 21 | "GNSSModel": "", 22 | "PressureModel": "", 23 | "PressureSensor": "", 24 | "AltimeterPressure": 0, 25 | "CompetitionID": "", 26 | "CompetitionClass": "", 27 | "Timezone": 0, 28 | "MOPSensor": "", 29 | "ID": "", 30 | "Points": null, 31 | "K": null, 32 | "Events": null, 33 | "Satellites": null, 34 | "Logbook": null, 35 | "Task": { 36 | "DeclarationDate": "0001-01-01T00:00:00Z", 37 | "Date": "0001-01-01T00:00:00Z", 38 | "Number": 0, 39 | "Takeoff": { 40 | "Lat": 0, 41 | "Lng": 0, 42 | "Time": "0001-01-01T00:00:00Z", 43 | "FixValidity": 0, 44 | "PressureAltitude": 0, 45 | "GNSSAltitude": 0, 46 | "IData": null, 47 | "NumSatellites": 0, 48 | "Description": "" 49 | }, 50 | "Start": { 51 | "Lat": 0, 52 | "Lng": 0, 53 | "Time": "0001-01-01T00:00:00Z", 54 | "FixValidity": 0, 55 | "PressureAltitude": 0, 56 | "GNSSAltitude": 0, 57 | "IData": null, 58 | "NumSatellites": 0, 59 | "Description": "" 60 | }, 61 | "Turnpoints": null, 62 | "Finish": { 63 | "Lat": 0, 64 | "Lng": 0, 65 | "Time": "0001-01-01T00:00:00Z", 66 | "FixValidity": 0, 67 | "PressureAltitude": 0, 68 | "GNSSAltitude": 0, 69 | "IData": null, 70 | "NumSatellites": 0, 71 | "Description": "" 72 | }, 73 | "Landing": { 74 | "Lat": 0, 75 | "Lng": 0, 76 | "Time": "0001-01-01T00:00:00Z", 77 | "FixValidity": 0, 78 | "PressureAltitude": 0, 79 | "GNSSAltitude": 0, 80 | "IData": null, 81 | "NumSatellites": 0, 82 | "Description": "" 83 | }, 84 | "Description": "" 85 | }, 86 | "DGPSStationID": "", 87 | "Signature": "" 88 | } -------------------------------------------------------------------------------- /testdata/parse/parse-a-too-short.0.igc: -------------------------------------------------------------------------------- 1 | AFL 2 | -------------------------------------------------------------------------------- /testdata/parse/parse-b-bad-fix-validity.0.igc: -------------------------------------------------------------------------------- 1 | B1603105107212N00149174WX002930043519608024 2 | -------------------------------------------------------------------------------- /testdata/parse/parse-b-bad-gnss-altitude.0.igc: -------------------------------------------------------------------------------- 1 | B1603105107212N00149174WV002930043a19608024 2 | -------------------------------------------------------------------------------- /testdata/parse/parse-b-bad-pressure-altitude.0.igc: -------------------------------------------------------------------------------- 1 | B1603105107212N00149174WV0029a0043519608024 2 | -------------------------------------------------------------------------------- /testdata/parse/parse-b-wrong-size.0.igc: -------------------------------------------------------------------------------- 1 | B110001 2 | -------------------------------------------------------------------------------- /testdata/parse/parse-c-bad-num-lines.0.igc: -------------------------------------------------------------------------------- 1 | C150701213841160701000102500KTri 2 | -------------------------------------------------------------------------------- /testdata/parse/parse-c-invalid-declaration-date.0.igc: -------------------------------------------------------------------------------- 1 | C350701213841160701000102500KTri 2 | C5111359N00101899WEZ TAKEOFF 3 | C5110179N00102644WEZ START 4 | C5209092N00255227WEZ TP1 5 | C5230147N00017612WEZ TP2 6 | C5110179N00102644WEZ FINISH 7 | C5111359N00101899WEZ LANDING 8 | -------------------------------------------------------------------------------- /testdata/parse/parse-c-invalid-declaration-date.0.igc.golden: -------------------------------------------------------------------------------- 1 | { 2 | "Manufacturer": "", 3 | "UniqueID": "", 4 | "AdditionalData": "", 5 | "Date": "0001-01-01T00:00:00Z", 6 | "Site": "", 7 | "FixAccuracy": 0, 8 | "Pilot": "", 9 | "PilotBirth": "0001-01-01T00:00:00Z", 10 | "Crew": "", 11 | "GliderType": "", 12 | "GliderID": "", 13 | "Observation": "", 14 | "GPSDatum": "", 15 | "FirmwareVersion": "", 16 | "HardwareVersion": "", 17 | "SoftwareVersion": "", 18 | "Specification": "", 19 | "FlightRecorder": "", 20 | "GPS": "", 21 | "GNSSModel": "", 22 | "PressureModel": "", 23 | "PressureSensor": "", 24 | "AltimeterPressure": 0, 25 | "CompetitionID": "", 26 | "CompetitionClass": "", 27 | "Timezone": 0, 28 | "MOPSensor": "", 29 | "ID": "", 30 | "Points": null, 31 | "K": null, 32 | "Events": null, 33 | "Satellites": null, 34 | "Logbook": null, 35 | "Task": { 36 | "DeclarationDate": "0001-01-01T00:00:00Z", 37 | "Date": "2001-07-16T00:00:00Z", 38 | "Number": 1, 39 | "Takeoff": { 40 | "Lat": 0.8934221176793421, 41 | "Lng": -0.0180056892281995, 42 | "Time": "0001-01-01T00:00:00Z", 43 | "FixValidity": 0, 44 | "PressureAltitude": 0, 45 | "GNSSAltitude": 0, 46 | "IData": {}, 47 | "NumSatellites": 0, 48 | "Description": "EZ TAKEOFF" 49 | }, 50 | "Start": { 51 | "Lat": 0.8930788695931164, 52 | "Lng": -0.018222400943655463, 53 | "Time": "0001-01-01T00:00:00Z", 54 | "FixValidity": 0, 55 | "PressureAltitude": 0, 56 | "GNSSAltitude": 0, 57 | "IData": {}, 58 | "NumSatellites": 0, 59 | "Description": "EZ START" 60 | }, 61 | "Turnpoints": [ 62 | { 63 | "Lat": 0.9102159666302401, 64 | "Lng": -0.050971468139868394, 65 | "Time": "0001-01-01T00:00:00Z", 66 | "FixValidity": 0, 67 | "PressureAltitude": 0, 68 | "GNSSAltitude": 0, 69 | "IData": {}, 70 | "NumSatellites": 0, 71 | "Description": "EZ TP1" 72 | }, 73 | { 74 | "Lat": 0.9163406178636969, 75 | "Lng": -0.0051231231310206885, 76 | "Time": "0001-01-01T00:00:00Z", 77 | "FixValidity": 0, 78 | "PressureAltitude": 0, 79 | "GNSSAltitude": 0, 80 | "IData": {}, 81 | "NumSatellites": 0, 82 | "Description": "EZ TP2" 83 | } 84 | ], 85 | "Finish": { 86 | "Lat": 0.8930788695931164, 87 | "Lng": -0.018222400943655463, 88 | "Time": "0001-01-01T00:00:00Z", 89 | "FixValidity": 0, 90 | "PressureAltitude": 0, 91 | "GNSSAltitude": 0, 92 | "IData": {}, 93 | "NumSatellites": 0, 94 | "Description": "EZ FINISH" 95 | }, 96 | "Landing": { 97 | "Lat": 0.8934221176793421, 98 | "Lng": -0.0180056892281995, 99 | "Time": "0001-01-01T00:00:00Z", 100 | "FixValidity": 0, 101 | "PressureAltitude": 0, 102 | "GNSSAltitude": 0, 103 | "IData": {}, 104 | "NumSatellites": 0, 105 | "Description": "EZ LANDING" 106 | }, 107 | "Description": "500KTri" 108 | }, 109 | "DGPSStationID": "", 110 | "Signature": "" 111 | } -------------------------------------------------------------------------------- /testdata/parse/parse-c-invalid-finish.0.igc: -------------------------------------------------------------------------------- 1 | C150701213841160701000101500KTri 2 | C5111359N00101899WEZ TAKEOFF 3 | C5110179N00102644WEZ START 4 | C5209092N00255227WEZ TP1 5 | C5110179N00102644 6 | C5111359N00101899WEZ LANDING 7 | -------------------------------------------------------------------------------- /testdata/parse/parse-c-invalid-flight-date.0.igc: -------------------------------------------------------------------------------- 1 | C150701213841360701000102500KTri 2 | C5111359N00101899WEZ TAKEOFF 3 | C5110179N00102644WEZ START 4 | C5209092N00255227WEZ TP1 5 | C5230147N00017612WEZ TP2 6 | C5110179N00102644WEZ FINISH 7 | C5111359N00101899WEZ LANDING 8 | -------------------------------------------------------------------------------- /testdata/parse/parse-c-invalid-flight-date.0.igc.golden: -------------------------------------------------------------------------------- 1 | { 2 | "Manufacturer": "", 3 | "UniqueID": "", 4 | "AdditionalData": "", 5 | "Date": "0001-01-01T00:00:00Z", 6 | "Site": "", 7 | "FixAccuracy": 0, 8 | "Pilot": "", 9 | "PilotBirth": "0001-01-01T00:00:00Z", 10 | "Crew": "", 11 | "GliderType": "", 12 | "GliderID": "", 13 | "Observation": "", 14 | "GPSDatum": "", 15 | "FirmwareVersion": "", 16 | "HardwareVersion": "", 17 | "SoftwareVersion": "", 18 | "Specification": "", 19 | "FlightRecorder": "", 20 | "GPS": "", 21 | "GNSSModel": "", 22 | "PressureModel": "", 23 | "PressureSensor": "", 24 | "AltimeterPressure": 0, 25 | "CompetitionID": "", 26 | "CompetitionClass": "", 27 | "Timezone": 0, 28 | "MOPSensor": "", 29 | "ID": "", 30 | "Points": null, 31 | "K": null, 32 | "Events": null, 33 | "Satellites": null, 34 | "Logbook": null, 35 | "Task": { 36 | "DeclarationDate": "2001-07-15T21:38:41Z", 37 | "Date": "0001-01-01T00:00:00Z", 38 | "Number": 1, 39 | "Takeoff": { 40 | "Lat": 0.8934221176793421, 41 | "Lng": -0.0180056892281995, 42 | "Time": "0001-01-01T00:00:00Z", 43 | "FixValidity": 0, 44 | "PressureAltitude": 0, 45 | "GNSSAltitude": 0, 46 | "IData": {}, 47 | "NumSatellites": 0, 48 | "Description": "EZ TAKEOFF" 49 | }, 50 | "Start": { 51 | "Lat": 0.8930788695931164, 52 | "Lng": -0.018222400943655463, 53 | "Time": "0001-01-01T00:00:00Z", 54 | "FixValidity": 0, 55 | "PressureAltitude": 0, 56 | "GNSSAltitude": 0, 57 | "IData": {}, 58 | "NumSatellites": 0, 59 | "Description": "EZ START" 60 | }, 61 | "Turnpoints": [ 62 | { 63 | "Lat": 0.9102159666302401, 64 | "Lng": -0.050971468139868394, 65 | "Time": "0001-01-01T00:00:00Z", 66 | "FixValidity": 0, 67 | "PressureAltitude": 0, 68 | "GNSSAltitude": 0, 69 | "IData": {}, 70 | "NumSatellites": 0, 71 | "Description": "EZ TP1" 72 | }, 73 | { 74 | "Lat": 0.9163406178636969, 75 | "Lng": -0.0051231231310206885, 76 | "Time": "0001-01-01T00:00:00Z", 77 | "FixValidity": 0, 78 | "PressureAltitude": 0, 79 | "GNSSAltitude": 0, 80 | "IData": {}, 81 | "NumSatellites": 0, 82 | "Description": "EZ TP2" 83 | } 84 | ], 85 | "Finish": { 86 | "Lat": 0.8930788695931164, 87 | "Lng": -0.018222400943655463, 88 | "Time": "0001-01-01T00:00:00Z", 89 | "FixValidity": 0, 90 | "PressureAltitude": 0, 91 | "GNSSAltitude": 0, 92 | "IData": {}, 93 | "NumSatellites": 0, 94 | "Description": "EZ FINISH" 95 | }, 96 | "Landing": { 97 | "Lat": 0.8934221176793421, 98 | "Lng": -0.0180056892281995, 99 | "Time": "0001-01-01T00:00:00Z", 100 | "FixValidity": 0, 101 | "PressureAltitude": 0, 102 | "GNSSAltitude": 0, 103 | "IData": {}, 104 | "NumSatellites": 0, 105 | "Description": "EZ LANDING" 106 | }, 107 | "Description": "500KTri" 108 | }, 109 | "DGPSStationID": "", 110 | "Signature": "" 111 | } -------------------------------------------------------------------------------- /testdata/parse/parse-c-invalid-landing.0.igc: -------------------------------------------------------------------------------- 1 | C150701213841160701000101500KTri 2 | C5111359N00101899WEZ TAKEOFF 3 | C5110179N00102644WEZ START 4 | C5209092N00255227WEZ TP1 5 | C5110179N00102644WEZ FINISH 6 | C5111359N00101899 7 | -------------------------------------------------------------------------------- /testdata/parse/parse-c-invalid-num-of-tps.0.igc: -------------------------------------------------------------------------------- 1 | C15070121384116070100010a 2 | -------------------------------------------------------------------------------- /testdata/parse/parse-c-invalid-start.0.igc: -------------------------------------------------------------------------------- 1 | C150701213841160701000101500KTri 2 | C5111359N00101899WEZ TAKEOFF 3 | C5110179N00102644 4 | C5209092N00255227WEZ TP1 5 | C5110179N00102644WEZ FINISH 6 | C5111359N00101899WEZ LANDING 7 | -------------------------------------------------------------------------------- /testdata/parse/parse-c-invalid-takeoff.0.igc: -------------------------------------------------------------------------------- 1 | C150701213841160701000101500KTri 2 | C5111359N00101899 3 | C5110179N00102644WEZ START 4 | C5209092N00255227WEZ TP1 5 | C5110179N00102644WEZ FINISH 6 | C5111359N00101899WEZ LANDING 7 | -------------------------------------------------------------------------------- /testdata/parse/parse-c-invalid-task-number.0.igc: -------------------------------------------------------------------------------- 1 | C150701213841160701000a01500KTri 2 | C5111359N00101899WEZ TAKEOFF 3 | C5110179N00102644WEZ START 4 | C5209092N00255227WEZ TP1 5 | C5110179N00102644WEZ FINISH 6 | C5111359N00101899WEZ LANDING 7 | -------------------------------------------------------------------------------- /testdata/parse/parse-c-invalid-tp.0.igc: -------------------------------------------------------------------------------- 1 | C150701213841160701000101500KTri 2 | C5111359N00101899WEZ TAKEOFF 3 | C5110179N00102644WEZ START 4 | C5209092N00255227 5 | C5110179N00102644WEZ FINISH 6 | C5111359N00101899WEZ LANDING 7 | -------------------------------------------------------------------------------- /testdata/parse/parse-c-wrong-size-first-line.0.igc: -------------------------------------------------------------------------------- 1 | C15070121384116070100010 2 | -------------------------------------------------------------------------------- /testdata/parse/parse-d-wrong-size.0.igc: -------------------------------------------------------------------------------- 1 | D2033 2 | -------------------------------------------------------------------------------- /testdata/parse/parse-e-invalid-date.0.igc: -------------------------------------------------------------------------------- 1 | E160271ATS 2 | -------------------------------------------------------------------------------- /testdata/parse/parse-e-wrong-size.0.igc: -------------------------------------------------------------------------------- 1 | E16024 2 | -------------------------------------------------------------------------------- /testdata/parse/parse-f-invalid-date.0.igc: -------------------------------------------------------------------------------- 1 | F1602710102 2 | -------------------------------------------------------------------------------- /testdata/parse/parse-f-invalid-num-satellites.0.igc: -------------------------------------------------------------------------------- 1 | F1602310a02 2 | -------------------------------------------------------------------------------- /testdata/parse/parse-f-invalid-num-satellites.0.igc.golden: -------------------------------------------------------------------------------- 1 | { 2 | "Manufacturer": "", 3 | "UniqueID": "", 4 | "AdditionalData": "", 5 | "Date": "0001-01-01T00:00:00Z", 6 | "Site": "", 7 | "FixAccuracy": 0, 8 | "Pilot": "", 9 | "PilotBirth": "0001-01-01T00:00:00Z", 10 | "Crew": "", 11 | "GliderType": "", 12 | "GliderID": "", 13 | "Observation": "", 14 | "GPSDatum": "", 15 | "FirmwareVersion": "", 16 | "HardwareVersion": "", 17 | "SoftwareVersion": "", 18 | "Specification": "", 19 | "FlightRecorder": "", 20 | "GPS": "", 21 | "GNSSModel": "", 22 | "PressureModel": "", 23 | "PressureSensor": "", 24 | "AltimeterPressure": 0, 25 | "CompetitionID": "", 26 | "CompetitionClass": "", 27 | "Timezone": 0, 28 | "MOPSensor": "", 29 | "ID": "", 30 | "Points": null, 31 | "K": null, 32 | "Events": null, 33 | "Satellites": [ 34 | { 35 | "Time": "0000-01-01T16:02:31Z", 36 | "Ids": [ 37 | "0a", 38 | "02" 39 | ] 40 | } 41 | ], 42 | "Logbook": null, 43 | "Task": { 44 | "DeclarationDate": "0001-01-01T00:00:00Z", 45 | "Date": "0001-01-01T00:00:00Z", 46 | "Number": 0, 47 | "Takeoff": { 48 | "Lat": 0, 49 | "Lng": 0, 50 | "Time": "0001-01-01T00:00:00Z", 51 | "FixValidity": 0, 52 | "PressureAltitude": 0, 53 | "GNSSAltitude": 0, 54 | "IData": null, 55 | "NumSatellites": 0, 56 | "Description": "" 57 | }, 58 | "Start": { 59 | "Lat": 0, 60 | "Lng": 0, 61 | "Time": "0001-01-01T00:00:00Z", 62 | "FixValidity": 0, 63 | "PressureAltitude": 0, 64 | "GNSSAltitude": 0, 65 | "IData": null, 66 | "NumSatellites": 0, 67 | "Description": "" 68 | }, 69 | "Turnpoints": null, 70 | "Finish": { 71 | "Lat": 0, 72 | "Lng": 0, 73 | "Time": "0001-01-01T00:00:00Z", 74 | "FixValidity": 0, 75 | "PressureAltitude": 0, 76 | "GNSSAltitude": 0, 77 | "IData": null, 78 | "NumSatellites": 0, 79 | "Description": "" 80 | }, 81 | "Landing": { 82 | "Lat": 0, 83 | "Lng": 0, 84 | "Time": "0001-01-01T00:00:00Z", 85 | "FixValidity": 0, 86 | "PressureAltitude": 0, 87 | "GNSSAltitude": 0, 88 | "IData": null, 89 | "NumSatellites": 0, 90 | "Description": "" 91 | }, 92 | "Description": "" 93 | }, 94 | "DGPSStationID": "", 95 | "Signature": "" 96 | } -------------------------------------------------------------------------------- /testdata/parse/parse-f-wrong-size.0.igc: -------------------------------------------------------------------------------- 1 | F16024 2 | -------------------------------------------------------------------------------- /testdata/parse/parse-h-bad-date.0.igc: -------------------------------------------------------------------------------- 1 | HFDTE330203 2 | -------------------------------------------------------------------------------- /testdata/parse/parse-h-bad-fix-accuracy.0.igc: -------------------------------------------------------------------------------- 1 | HFFXAAAA 2 | -------------------------------------------------------------------------------- /testdata/parse/parse-h-bad-timezone.0.igc: -------------------------------------------------------------------------------- 1 | HFTZNaa 2 | -------------------------------------------------------------------------------- /testdata/parse/parse-h-date-too-short.0.igc: -------------------------------------------------------------------------------- 1 | HFDTE33 2 | -------------------------------------------------------------------------------- /testdata/parse/parse-h-fix-accuracy-too-short.0.igc: -------------------------------------------------------------------------------- 1 | HFFXA2 2 | -------------------------------------------------------------------------------- /testdata/parse/parse-h-fix-accuracy-too-short.0.igc.golden: -------------------------------------------------------------------------------- 1 | { 2 | "Manufacturer": "", 3 | "UniqueID": "", 4 | "AdditionalData": "", 5 | "Date": "0001-01-01T00:00:00Z", 6 | "Site": "", 7 | "FixAccuracy": 2, 8 | "Pilot": "", 9 | "PilotBirth": "0001-01-01T00:00:00Z", 10 | "Crew": "", 11 | "GliderType": "", 12 | "GliderID": "", 13 | "Observation": "", 14 | "GPSDatum": "", 15 | "FirmwareVersion": "", 16 | "HardwareVersion": "", 17 | "SoftwareVersion": "", 18 | "Specification": "", 19 | "FlightRecorder": "", 20 | "GPS": "", 21 | "GNSSModel": "", 22 | "PressureModel": "", 23 | "PressureSensor": "", 24 | "AltimeterPressure": 0, 25 | "CompetitionID": "", 26 | "CompetitionClass": "", 27 | "Timezone": 0, 28 | "MOPSensor": "", 29 | "ID": "", 30 | "Points": null, 31 | "K": null, 32 | "Events": null, 33 | "Satellites": null, 34 | "Logbook": null, 35 | "Task": { 36 | "DeclarationDate": "0001-01-01T00:00:00Z", 37 | "Date": "0001-01-01T00:00:00Z", 38 | "Number": 0, 39 | "Takeoff": { 40 | "Lat": 0, 41 | "Lng": 0, 42 | "Time": "0001-01-01T00:00:00Z", 43 | "FixValidity": 0, 44 | "PressureAltitude": 0, 45 | "GNSSAltitude": 0, 46 | "IData": null, 47 | "NumSatellites": 0, 48 | "Description": "" 49 | }, 50 | "Start": { 51 | "Lat": 0, 52 | "Lng": 0, 53 | "Time": "0001-01-01T00:00:00Z", 54 | "FixValidity": 0, 55 | "PressureAltitude": 0, 56 | "GNSSAltitude": 0, 57 | "IData": null, 58 | "NumSatellites": 0, 59 | "Description": "" 60 | }, 61 | "Turnpoints": null, 62 | "Finish": { 63 | "Lat": 0, 64 | "Lng": 0, 65 | "Time": "0001-01-01T00:00:00Z", 66 | "FixValidity": 0, 67 | "PressureAltitude": 0, 68 | "GNSSAltitude": 0, 69 | "IData": null, 70 | "NumSatellites": 0, 71 | "Description": "" 72 | }, 73 | "Landing": { 74 | "Lat": 0, 75 | "Lng": 0, 76 | "Time": "0001-01-01T00:00:00Z", 77 | "FixValidity": 0, 78 | "PressureAltitude": 0, 79 | "GNSSAltitude": 0, 80 | "IData": null, 81 | "NumSatellites": 0, 82 | "Description": "" 83 | }, 84 | "Description": "" 85 | }, 86 | "DGPSStationID": "", 87 | "Signature": "" 88 | } -------------------------------------------------------------------------------- /testdata/parse/parse-h-gps-datum-too-short.0.igc: -------------------------------------------------------------------------------- 1 | HFDTM20 2 | -------------------------------------------------------------------------------- /testdata/parse/parse-h-invalid-pats.0.igc: -------------------------------------------------------------------------------- 1 | HPATS:1013aa 2 | -------------------------------------------------------------------------------- /testdata/parse/parse-h-too-short.0.igc: -------------------------------------------------------------------------------- 1 | HFFX 2 | -------------------------------------------------------------------------------- /testdata/parse/parse-h-tzo-timezone.1.igc: -------------------------------------------------------------------------------- 1 | HFTZOTimezone:2.00 2 | -------------------------------------------------------------------------------- /testdata/parse/parse-h-tzo-timezone.1.igc.golden: -------------------------------------------------------------------------------- 1 | { 2 | "Manufacturer": "", 3 | "UniqueID": "", 4 | "AdditionalData": "", 5 | "Date": "0001-01-01T00:00:00Z", 6 | "Site": "", 7 | "FixAccuracy": 0, 8 | "Pilot": "", 9 | "PilotBirth": "0001-01-01T00:00:00Z", 10 | "Crew": "", 11 | "GliderType": "", 12 | "GliderID": "", 13 | "Observation": "", 14 | "GPSDatum": "", 15 | "FirmwareVersion": "", 16 | "HardwareVersion": "", 17 | "SoftwareVersion": "", 18 | "Specification": "", 19 | "FlightRecorder": "", 20 | "GPS": "", 21 | "GNSSModel": "", 22 | "PressureModel": "", 23 | "PressureSensor": "", 24 | "AltimeterPressure": 0, 25 | "CompetitionID": "", 26 | "CompetitionClass": "", 27 | "Timezone": 2, 28 | "MOPSensor": "", 29 | "ID": "", 30 | "Points": null, 31 | "K": null, 32 | "Events": null, 33 | "Satellites": null, 34 | "Logbook": null, 35 | "Task": { 36 | "DeclarationDate": "0001-01-01T00:00:00Z", 37 | "Date": "0001-01-01T00:00:00Z", 38 | "Number": 0, 39 | "Takeoff": { 40 | "Lat": 0, 41 | "Lng": 0, 42 | "Time": "0001-01-01T00:00:00Z", 43 | "FixValidity": 0, 44 | "PressureAltitude": 0, 45 | "GNSSAltitude": 0, 46 | "IData": null, 47 | "NumSatellites": 0, 48 | "Description": "" 49 | }, 50 | "Start": { 51 | "Lat": 0, 52 | "Lng": 0, 53 | "Time": "0001-01-01T00:00:00Z", 54 | "FixValidity": 0, 55 | "PressureAltitude": 0, 56 | "GNSSAltitude": 0, 57 | "IData": null, 58 | "NumSatellites": 0, 59 | "Description": "" 60 | }, 61 | "Turnpoints": null, 62 | "Finish": { 63 | "Lat": 0, 64 | "Lng": 0, 65 | "Time": "0001-01-01T00:00:00Z", 66 | "FixValidity": 0, 67 | "PressureAltitude": 0, 68 | "GNSSAltitude": 0, 69 | "IData": null, 70 | "NumSatellites": 0, 71 | "Description": "" 72 | }, 73 | "Landing": { 74 | "Lat": 0, 75 | "Lng": 0, 76 | "Time": "0001-01-01T00:00:00Z", 77 | "FixValidity": 0, 78 | "PressureAltitude": 0, 79 | "GNSSAltitude": 0, 80 | "IData": null, 81 | "NumSatellites": 0, 82 | "Description": "" 83 | }, 84 | "Description": "" 85 | }, 86 | "DGPSStationID": "", 87 | "Signature": "" 88 | } -------------------------------------------------------------------------------- /testdata/parse/parse-h-unknown-field.0.igc: -------------------------------------------------------------------------------- 1 | HFZZZaaa 2 | -------------------------------------------------------------------------------- /testdata/parse/parse-i-invalid-value-for-field-number.0.igc: -------------------------------------------------------------------------------- 1 | I0a 2 | -------------------------------------------------------------------------------- /testdata/parse/parse-i-wrong-size-with-fields.0.igc: -------------------------------------------------------------------------------- 1 | I02AAA0102BBB030 2 | -------------------------------------------------------------------------------- /testdata/parse/parse-i-wrong-size.0.igc: -------------------------------------------------------------------------------- 1 | I0 2 | -------------------------------------------------------------------------------- /testdata/parse/parse-j-invalid-value-for-field-number.0.igc: -------------------------------------------------------------------------------- 1 | J0a 2 | -------------------------------------------------------------------------------- /testdata/parse/parse-j-invalid-value-for-field-number.1.igc.golden: -------------------------------------------------------------------------------- 1 | { 2 | "Manufacturer": "", 3 | "UniqueID": "", 4 | "AdditionalData": "", 5 | "Date": "0001-01-01T00:00:00Z", 6 | "FixAccuracy": 0, 7 | "Pilot": "", 8 | "Crew": "", 9 | "GliderType": "", 10 | "GliderID": "", 11 | "GPSDatum": "", 12 | "FirmwareVersion": "", 13 | "HardwareVersion": "", 14 | "FlightRecorder": "", 15 | "GPS": "", 16 | "PressureSensor": "", 17 | "CompetitionID": "", 18 | "CompetitionClass": "", 19 | "Timezone": 0, 20 | "Points": null, 21 | "K": null, 22 | "Events": null, 23 | "Satellites": null, 24 | "Logbook": null, 25 | "Task": { 26 | "DeclarationDate": "0001-01-01T00:00:00Z", 27 | "Date": "0001-01-01T00:00:00Z", 28 | "Number": 0, 29 | "Takeoff": { 30 | "Lat": 0, 31 | "Lng": 0, 32 | "Time": "0001-01-01T00:00:00Z", 33 | "FixValidity": 0, 34 | "PressureAltitude": 0, 35 | "GNSSAltitude": 0, 36 | "IData": null, 37 | "NumSatellites": 0, 38 | "Description": "" 39 | }, 40 | "Start": { 41 | "Lat": 0, 42 | "Lng": 0, 43 | "Time": "0001-01-01T00:00:00Z", 44 | "FixValidity": 0, 45 | "PressureAltitude": 0, 46 | "GNSSAltitude": 0, 47 | "IData": null, 48 | "NumSatellites": 0, 49 | "Description": "" 50 | }, 51 | "Turnpoints": null, 52 | "Finish": { 53 | "Lat": 0, 54 | "Lng": 0, 55 | "Time": "0001-01-01T00:00:00Z", 56 | "FixValidity": 0, 57 | "PressureAltitude": 0, 58 | "GNSSAltitude": 0, 59 | "IData": null, 60 | "NumSatellites": 0, 61 | "Description": "" 62 | }, 63 | "Landing": { 64 | "Lat": 0, 65 | "Lng": 0, 66 | "Time": "0001-01-01T00:00:00Z", 67 | "FixValidity": 0, 68 | "PressureAltitude": 0, 69 | "GNSSAltitude": 0, 70 | "IData": null, 71 | "NumSatellites": 0, 72 | "Description": "" 73 | }, 74 | "Description": "" 75 | }, 76 | "DGPSStationID": "", 77 | "Signature": "" 78 | } -------------------------------------------------------------------------------- /testdata/parse/parse-j-wrong-size-with-fields.0.igc: -------------------------------------------------------------------------------- 1 | J02AAA0102BBB030 2 | -------------------------------------------------------------------------------- /testdata/parse/parse-j-wrong-size.0.igc: -------------------------------------------------------------------------------- 1 | J0 2 | -------------------------------------------------------------------------------- /testdata/parse/parse-k-invalid-date.0.igc: -------------------------------------------------------------------------------- 1 | K160271 2 | -------------------------------------------------------------------------------- /testdata/parse/parse-k-wrong-size-with-fields.0.igc: -------------------------------------------------------------------------------- 1 | K16027000090 2 | -------------------------------------------------------------------------------- /testdata/parse/parse-k-wrong-size.0.igc: -------------------------------------------------------------------------------- 1 | K16024 2 | -------------------------------------------------------------------------------- /testdata/phases/phases-short-flight-1.golden.igc: -------------------------------------------------------------------------------- 1 | [{"Type":3,"CirclingType":0,"Start":{"Lat":0.827064408630309,"Lng":0.08636238204718341,"Time":"2017-09-10T12:12:43Z","FixValidity":86,"PressureAltitude":1266,"GNSSAltitude":0,"IData":{"ENL":"002","FXA":"999","SIU":"00"},"NumSatellites":0,"Description":""},"StartIndex":0,"End":{"Lat":0.8272235244804491,"Lng":0.08691565142006562,"Time":"2017-09-10T12:12:51Z","FixValidity":65,"PressureAltitude":1273,"GNSSAltitude":1397,"IData":{"ENL":"003","FXA":"006","SIU":"04"},"NumSatellites":0,"Description":""},"EndIndex":2,"AvgVario":174.625,"TopVario":0,"AvgGndSpeed":1232.596782887029,"TopGndSpeed":0,"Distance":2.7391039619711752,"LD":1.9607043392778634,"Centroid":{"Lat":0.8271375872603298,"Lng":0.0866474298184727},"CellID":5182935931723710464},{"Type":5,"CirclingType":0,"Start":{"Lat":0.8272235244804491,"Lng":0.08691565142006562,"Time":"2017-09-10T12:12:51Z","FixValidity":65,"PressureAltitude":1273,"GNSSAltitude":1397,"IData":{"ENL":"003","FXA":"006","SIU":"04"},"NumSatellites":0,"Description":""},"StartIndex":2,"End":{"Lat":0.8274440177426177,"Lng":0.08700960831146465,"Time":"2017-09-10T12:18:03Z","FixValidity":65,"PressureAltitude":1811,"GNSSAltitude":1909,"IData":{"ENL":"001","FXA":"004","SIU":"08"},"NumSatellites":8,"Description":""},"EndIndex":80,"AvgVario":1.641025641025641,"TopVario":0,"AvgGndSpeed":95.39880663962374,"TopGndSpeed":0,"Distance":8.267896575434058,"LD":0,"Centroid":{"Lat":0.827333060206061,"Lng":0.08697571034950823},"CellID":5182907919947005952},{"Type":3,"CirclingType":0,"Start":{"Lat":0.8274440177426177,"Lng":0.08700960831146465,"Time":"2017-09-10T12:18:03Z","FixValidity":65,"PressureAltitude":1811,"GNSSAltitude":1909,"IData":{"ENL":"001","FXA":"004","SIU":"08"},"NumSatellites":8,"Description":""},"StartIndex":80,"End":{"Lat":0.826759848675836,"Lng":0.08678504261437471,"Time":"2017-09-10T12:26:11Z","FixValidity":65,"PressureAltitude":1911,"GNSSAltitude":2007,"IData":{"ENL":"005","FXA":"004","SIU":"10"},"NumSatellites":10,"Description":""},"EndIndex":202,"AvgVario":0.20081967213114754,"TopVario":0,"AvgGndSpeed":97.56761207145482,"TopGndSpeed":0,"Distance":13.225831858574988,"LD":134.95746794464273,"Centroid":{"Lat":0.8271082099293334,"Lng":0.08741972843080717},"CellID":5182907670838902784},{"Type":5,"CirclingType":0,"Start":{"Lat":0.826759848675836,"Lng":0.08678504261437471,"Time":"2017-09-10T12:26:11Z","FixValidity":65,"PressureAltitude":1911,"GNSSAltitude":2007,"IData":{"ENL":"005","FXA":"004","SIU":"10"},"NumSatellites":10,"Description":""},"StartIndex":202,"End":{"Lat":0.8268671864248335,"Lng":0.08684263847969052,"Time":"2017-09-10T12:27:07Z","FixValidity":65,"PressureAltitude":1958,"GNSSAltitude":2065,"IData":{"ENL":"003","FXA":"004","SIU":"10"},"NumSatellites":10,"Description":""},"EndIndex":216,"AvgVario":1.0357142857142858,"TopVario":0,"AvgGndSpeed":130.9313712638931,"TopGndSpeed":0,"Distance":2.0367102196605593,"LD":0,"Centroid":{"Lat":0.8267987970140933,"Lng":0.08684384756036166},"CellID":5182907404550930432},{"Type":3,"CirclingType":0,"Start":{"Lat":0.8268671864248335,"Lng":0.08684263847969052,"Time":"2017-09-10T12:27:07Z","FixValidity":65,"PressureAltitude":1958,"GNSSAltitude":2065,"IData":{"ENL":"003","FXA":"004","SIU":"10"},"NumSatellites":10,"Description":""},"StartIndex":216,"End":{"Lat":0.8262586482923049,"Lng":0.08576402500195804,"Time":"2017-09-10T12:32:19Z","FixValidity":65,"PressureAltitude":1649,"GNSSAltitude":1741,"IData":{"ENL":"002","FXA":"004","SIU":"10"},"NumSatellites":10,"Description":""},"EndIndex":294,"AvgVario":-1.0384615384615385,"TopVario":0,"AvgGndSpeed":117.01295389887623,"TopGndSpeed":0,"Distance":10.14112267123594,"LD":31.299761330975123,"Centroid":{"Lat":0.8267040101963131,"Lng":0.08598020354130528},"CellID":5182936876616515584},{"Type":5,"CirclingType":0,"Start":{"Lat":0.8262586482923049,"Lng":0.08576402500195804,"Time":"2017-09-10T12:32:19Z","FixValidity":65,"PressureAltitude":1649,"GNSSAltitude":1741,"IData":{"ENL":"002","FXA":"004","SIU":"10"},"NumSatellites":10,"Description":""},"StartIndex":294,"End":{"Lat":0.8263083901759867,"Lng":0.08581114889176186,"Time":"2017-09-10T12:35:47Z","FixValidity":65,"PressureAltitude":1956,"GNSSAltitude":2055,"IData":{"ENL":"001","FXA":"004","SIU":"09"},"NumSatellites":10,"Description":""},"EndIndex":346,"AvgVario":1.5096153846153846,"TopVario":0,"AvgGndSpeed":96.43182203441681,"TopGndSpeed":0,"Distance":5.571616384210749,"LD":0,"Centroid":{"Lat":0.8263043267862639,"Lng":0.08580373469859302},"CellID":5184349577259515904},{"Type":3,"CirclingType":0,"Start":{"Lat":0.8263083901759867,"Lng":0.08581114889176186,"Time":"2017-09-10T12:35:47Z","FixValidity":65,"PressureAltitude":1956,"GNSSAltitude":2055,"IData":{"ENL":"001","FXA":"004","SIU":"09"},"NumSatellites":10,"Description":""},"StartIndex":346,"End":{"Lat":0.8254773225638287,"Lng":0.08537947079010194,"Time":"2017-09-10T12:38:43Z","FixValidity":65,"PressureAltitude":1893,"GNSSAltitude":1993,"IData":{"ENL":"003","FXA":"004","SIU":"09"},"NumSatellites":9,"Description":""},"EndIndex":390,"AvgVario":-0.3522727272727273,"TopVario":0,"AvgGndSpeed":115.81301958019979,"TopGndSpeed":0,"Distance":5.661969846143101,"LD":91.32209429263067,"Centroid":{"Lat":0.825900933247082,"Lng":0.08560278451681509},"CellID":5184349345331281920},{"Type":5,"CirclingType":0,"Start":{"Lat":0.8254773225638287,"Lng":0.08537947079010194,"Time":"2017-09-10T12:38:43Z","FixValidity":65,"PressureAltitude":1893,"GNSSAltitude":1993,"IData":{"ENL":"003","FXA":"004","SIU":"09"},"NumSatellites":9,"Description":""},"StartIndex":390,"End":{"Lat":0.8254421250905802,"Lng":0.08539779674724789,"Time":"2017-09-10T12:39:31Z","FixValidity":65,"PressureAltitude":1915,"GNSSAltitude":2017,"IData":{"ENL":"005","FXA":"004","SIU":"09"},"NumSatellites":9,"Description":""},"EndIndex":402,"AvgVario":0.5,"TopVario":0,"AvgGndSpeed":103.49616819499889,"TopGndSpeed":0,"Distance":1.379948909266652,"LD":0,"Centroid":{"Lat":0.8254900092534753,"Lng":0.08535048385336667},"CellID":5184351020368527360},{"Type":3,"CirclingType":0,"Start":{"Lat":0.8254421250905802,"Lng":0.08539779674724789,"Time":"2017-09-10T12:39:31Z","FixValidity":65,"PressureAltitude":1915,"GNSSAltitude":2017,"IData":{"ENL":"005","FXA":"004","SIU":"09"},"NumSatellites":9,"Description":""},"StartIndex":402,"End":{"Lat":0.8248321325170082,"Lng":0.08548768120372559,"Time":"2017-09-10T12:41:23Z","FixValidity":65,"PressureAltitude":1746,"GNSSAltitude":1837,"IData":{"ENL":"000","FXA":"004","SIU":"09"},"NumSatellites":9,"Description":""},"EndIndex":430,"AvgVario":-1.6071428571428572,"TopVario":0,"AvgGndSpeed":130.56375554060833,"TopGndSpeed":0,"Distance":4.061983505707815,"LD":22.566575031710084,"Centroid":{"Lat":0.8251379638735756,"Lng":0.08539018080117418},"CellID":5184350788440293376},{"Type":5,"CirclingType":0,"Start":{"Lat":0.8248321325170082,"Lng":0.08548768120372559,"Time":"2017-09-10T12:41:23Z","FixValidity":65,"PressureAltitude":1746,"GNSSAltitude":1837,"IData":{"ENL":"000","FXA":"004","SIU":"09"},"NumSatellites":9,"Description":""},"StartIndex":430,"End":{"Lat":0.8247832632979523,"Lng":0.0854324124440791,"Time":"2017-09-10T12:41:43Z","FixValidity":65,"PressureAltitude":1720,"GNSSAltitude":1807,"IData":{"ENL":"011","FXA":"004","SIU":"09"},"NumSatellites":9,"Description":""},"EndIndex":435,"AvgVario":-1.5,"TopVario":0,"AvgGndSpeed":92.32849505886804,"TopGndSpeed":0,"Distance":0.512936083660378,"LD":0,"Centroid":{"Lat":0.8248028789029764,"Lng":0.08548379839781141},"CellID":5184351784872706048},{"Type":3,"CirclingType":0,"Start":{"Lat":0.8247832632979523,"Lng":0.0854324124440791,"Time":"2017-09-10T12:41:43Z","FixValidity":65,"PressureAltitude":1720,"GNSSAltitude":1807,"IData":{"ENL":"011","FXA":"004","SIU":"09"},"NumSatellites":9,"Description":""},"StartIndex":435,"End":{"Lat":0.8270449191203284,"Lng":0.08636878158777406,"Time":"2017-09-10T12:53:23Z","FixValidity":65,"PressureAltitude":456,"GNSSAltitude":529,"IData":{"ENL":"000","FXA":"003","SIU":"11"},"NumSatellites":10,"Description":""},"EndIndex":606,"AvgVario":-1.8257142857142856,"TopVario":0,"AvgGndSpeed":117.5879261288162,"TopGndSpeed":0,"Distance":22.86431896949204,"LD":17.890703419007856,"Centroid":{"Lat":0.8261213692785656,"Lng":0.08621341463841474},"CellID":5184349714698469376},{"Type":5,"CirclingType":0,"Start":{"Lat":0.8270449191203284,"Lng":0.08636878158777406,"Time":"2017-09-10T12:53:23Z","FixValidity":65,"PressureAltitude":456,"GNSSAltitude":529,"IData":{"ENL":"000","FXA":"003","SIU":"11"},"NumSatellites":10,"Description":""},"StartIndex":606,"End":{"Lat":0.8270449191203284,"Lng":0.08636878158777406,"Time":"2017-09-10T12:53:55Z","FixValidity":65,"PressureAltitude":456,"GNSSAltitude":531,"IData":{"ENL":"009","FXA":"003","SIU":"10"},"NumSatellites":10,"Description":""},"EndIndex":610,"AvgVario":0.0625,"TopVario":0,"AvgGndSpeed":0.601431763132787,"TopGndSpeed":0,"Distance":0.0053460601167358845,"LD":0,"Centroid":{"Lat":0.8270447736762282,"Lng":0.08636899291396825},"CellID":5182936051982794752},{"Type":3,"CirclingType":0,"Start":{"Lat":0.8270449191203284,"Lng":0.08636878158777406,"Time":"2017-09-10T12:53:55Z","FixValidity":65,"PressureAltitude":456,"GNSSAltitude":531,"IData":{"ENL":"009","FXA":"003","SIU":"10"},"NumSatellites":10,"Description":""},"StartIndex":610,"End":{"Lat":0,"Lng":0,"Time":"0001-01-01T00:00:00Z","FixValidity":0,"PressureAltitude":0,"GNSSAltitude":0,"IData":null,"NumSatellites":0,"Description":""},"EndIndex":0,"AvgVario":0,"TopVario":0,"AvgGndSpeed":0,"TopGndSpeed":0,"Distance":0,"LD":0,"Centroid":{"Lat":0,"Lng":0},"CellID":0}] -------------------------------------------------------------------------------- /testdata/simplify/simplify-short-flight-1.igc.golden: -------------------------------------------------------------------------------- 1 | {"Manufacturer":"FLA","UniqueID":"5HH","AdditionalData":"","Date":"2017-08-09T00:00:00Z","Site":"","FixAccuracy":500,"Pilot":"Dijon Planeurs CDVV","PilotBirth":"0001-01-01T00:00:00Z","Crew":"Dijon Planeurs CDVV","GliderType":"DG 500","GliderID":"F-CIED","Observation":"","GPSDatum":"WGS84","FirmwareVersion":"Flarm-IGC06.09","HardwareVersion":"Flarm-IGC06","SoftwareVersion":"","Specification":"","FlightRecorder":"Flarm-IGC","GPS":"u-blox:LEA-4P,16,8191","GNSSModel":"","PressureModel":"","PressureSensor":"Intersema MS5534B,8191","AltimeterPressure":0,"CompetitionID":"","CompetitionClass":"","Timezone":0,"MOPSensor":"","ID":"","Points":[{"Lat":0.827064408630309,"Lng":0.08636238204718341,"Time":"2017-09-10T12:12:43Z","FixValidity":86,"PressureAltitude":1266,"GNSSAltitude":0,"IData":{"ENL":"002","FXA":"999","SIU":"00"},"NumSatellites":0,"Description":""},{"Lat":0.8272107253992678,"Lng":0.08693252293616822,"Time":"2017-09-10T12:12:47Z","FixValidity":65,"PressureAltitude":1263,"GNSSAltitude":1390,"IData":{"ENL":"002","FXA":"009","SIU":"04"},"NumSatellites":0,"Description":""},{"Lat":0.8273387162110807,"Lng":0.08696364797449545,"Time":"2017-09-10T12:14:59Z","FixValidity":65,"PressureAltitude":1502,"GNSSAltitude":1596,"IData":{"ENL":"001","FXA":"004","SIU":"08"},"NumSatellites":0,"Description":""},{"Lat":0.8274771789984056,"Lng":0.08704538756113053,"Time":"2017-09-10T12:17:47Z","FixValidity":65,"PressureAltitude":1786,"GNSSAltitude":1882,"IData":{"ENL":"001","FXA":"004","SIU":"08"},"NumSatellites":8,"Description":""},{"Lat":0.8273573330564353,"Lng":0.08785987454539454,"Time":"2017-09-10T12:20:11Z","FixValidity":65,"PressureAltitude":1719,"GNSSAltitude":1807,"IData":{"ENL":"001","FXA":"004","SIU":"10"},"NumSatellites":8,"Description":""},{"Lat":0.8272002534237558,"Lng":0.08755124215600021,"Time":"2017-09-10T12:21:47Z","FixValidity":65,"PressureAltitude":1737,"GNSSAltitude":1825,"IData":{"ENL":"007","FXA":"004","SIU":"10"},"NumSatellites":8,"Description":""},{"Lat":0.8267290145257173,"Lng":0.08740114384032871,"Time":"2017-09-10T12:24:23Z","FixValidity":65,"PressureAltitude":1873,"GNSSAltitude":1965,"IData":{"ENL":"006","FXA":"004","SIU":"10"},"NumSatellites":10,"Description":""},{"Lat":0.8267999912486318,"Lng":0.08675159047037814,"Time":"2017-09-10T12:25:59Z","FixValidity":65,"PressureAltitude":1888,"GNSSAltitude":1986,"IData":{"ENL":"003","FXA":"004","SIU":"10"},"NumSatellites":10,"Description":""},{"Lat":0.8268255894109943,"Lng":0.08690168878604966,"Time":"2017-09-10T12:26:59Z","FixValidity":65,"PressureAltitude":1996,"GNSSAltitude":2097,"IData":{"ENL":"002","FXA":"004","SIU":"10"},"NumSatellites":10,"Description":""},{"Lat":0.8268933663636134,"Lng":0.08582016642623051,"Time":"2017-09-10T12:29:07Z","FixValidity":65,"PressureAltitude":1854,"GNSSAltitude":1946,"IData":{"ENL":"007","FXA":"004","SIU":"09"},"NumSatellites":10,"Description":""},{"Lat":0.8262164695020484,"Lng":0.08566628656384635,"Time":"2017-09-10T12:31:55Z","FixValidity":65,"PressureAltitude":1630,"GNSSAltitude":1719,"IData":{"ENL":"000","FXA":"004","SIU":"10"},"NumSatellites":10,"Description":""},{"Lat":0.8263002453061441,"Lng":0.08578351451193864,"Time":"2017-09-10T12:33:07Z","FixValidity":65,"PressureAltitude":1729,"GNSSAltitude":1822,"IData":{"ENL":"004","FXA":"004","SIU":"09"},"NumSatellites":10,"Description":""},{"Lat":0.8254619054887695,"Lng":0.08533118334746342,"Time":"2017-09-10T12:38:55Z","FixValidity":65,"PressureAltitude":1931,"GNSSAltitude":2030,"IData":{"ENL":"003","FXA":"004","SIU":"09"},"NumSatellites":9,"Description":""},{"Lat":0.8247815179687004,"Lng":0.08545481083614637,"Time":"2017-09-10T12:41:39Z","FixValidity":65,"PressureAltitude":1727,"GNSSAltitude":1813,"IData":{"ENL":"005","FXA":"004","SIU":"09"},"NumSatellites":9,"Description":""},{"Lat":0.8259680509718478,"Lng":0.0857794420770173,"Time":"2017-09-10T12:45:31Z","FixValidity":65,"PressureAltitude":1532,"GNSSAltitude":1621,"IData":{"ENL":"002","FXA":"004","SIU":"10"},"NumSatellites":9,"Description":""},{"Lat":0.8265137572513047,"Lng":0.08716232462101414,"Time":"2017-09-10T12:48:31Z","FixValidity":65,"PressureAltitude":1048,"GNSSAltitude":1128,"IData":{"ENL":"001","FXA":"003","SIU":"10"},"NumSatellites":10,"Description":""},{"Lat":0.8266426207277435,"Lng":0.08677893396199272,"Time":"2017-09-10T12:49:39Z","FixValidity":65,"PressureAltitude":907,"GNSSAltitude":989,"IData":{"ENL":"040","FXA":"003","SIU":"11"},"NumSatellites":10,"Description":""},{"Lat":0.8269090743268814,"Lng":0.08643743120501916,"Time":"2017-09-10T12:50:59Z","FixValidity":65,"PressureAltitude":638,"GNSSAltitude":715,"IData":{"ENL":"002","FXA":"003","SIU":"11"},"NumSatellites":10,"Description":""},{"Lat":0.8271519659811173,"Lng":0.08647902821885836,"Time":"2017-09-10T12:51:51Z","FixValidity":65,"PressureAltitude":533,"GNSSAltitude":605,"IData":{"ENL":"002","FXA":"003","SIU":"11"},"NumSatellites":10,"Description":""},{"Lat":0.8270440464557023,"Lng":0.08636907247598273,"Time":"2017-09-10T12:52:39Z","FixValidity":65,"PressureAltitude":456,"GNSSAltitude":523,"IData":{"ENL":"035","FXA":"003","SIU":"10"},"NumSatellites":10,"Description":""},{"Lat":0.8270786621525336,"Lng":0.08638652576850267,"Time":"2017-09-10T12:57:47Z","FixValidity":65,"PressureAltitude":458,"GNSSAltitude":529,"IData":{"ENL":"000","FXA":"003","SIU":"11"},"NumSatellites":11,"Description":""}],"K":null,"Events":null,"Satellites":[{"Time":"0000-01-01T12:17:06Z","Ids":["26","16","21","07","18","10","08","27"]},{"Time":"0000-01-01T12:22:06Z","Ids":["26","16","21","07","18","10","15","08","27","20"]},{"Time":"0000-01-01T12:27:06Z","Ids":["26","16","21","07","20","18","10","15","08","27"]},{"Time":"0000-01-01T12:32:06Z","Ids":["26","16","21","07","20","18","10","15","08","27"]},{"Time":"0000-01-01T12:37:06Z","Ids":["26","16","21","07","18","10","15","08","27"]},{"Time":"0000-01-01T12:42:06Z","Ids":["26","16","21","07","18","10","15","08","27"]},{"Time":"0000-01-01T12:47:06Z","Ids":["26","16","21","07","18","10","15","08","30","27"]},{"Time":"0000-01-01T12:52:06Z","Ids":["26","16","21","07","18","11","10","15","08","27"]},{"Time":"0000-01-01T12:57:06Z","Ids":["26","16","21","07","18","11","10","15","08","30","27"]}],"Logbook":["FLA12124502DAWciK_WVHu\u003cvHT]mF[GSFM\u003eW\u003eX?","FLA12124502Av`LNL`Xp_JqCuiP@sNrfsxcjcmb","FLA12124502=AXdfP#T%q\u003c#Vht=Mf;gsfm%w%x_","FLA121247 STEALTH OFF","FLA121247 NOTRACK OFF","FLA121247ID 2 DDB1CA","FLA121247OB","FLA12124707OBSTEXP","FLA12124707DEVNO Flarm-IGC06-935810288","FLA12124707BUILD d4ec337","FLA12124707RANGE 3000","FLA12124707ACFT 1","FLA12124707FREQ 100","FLA12124707CFLAGS 00","FLA12124707RFTX 1","FLA12124707MISC 00","FLA12124707LOGINT 4","FLA12124707NMEAOUT1 1","FLA12124707BAUD1 2","FLA121247EE0BDffywIHA?A?A?A?A?rsNQrssrrsut","FLA121247EE1A?rstutursA?A?A?A?srsrA?rssrVV","FLA121247EE2A?vwA?rsA?GFjjjj`aA?rsrsrsA?A?","FLA121247EE3dersrs","FLA121250011","FLA12125702Av`LN@jBuiTfK]mO?c?cwbir#u[t","FLA12130102Av`LNfT#RItGj\u003c@br;g;M?\u003cOFOIN","FLA12130303vwruQLNMUqqvjw","FLA12130702Av`LNdW_=Q#KfXrZjsOs_mn]sZt[","FLA12130702Av`LN]NfnZOcN`axh`D`ugby`y_x","FLA12131102DAWciLZR\u003cP]dN`W=MSoSGRAJDMCL","FLA12131202Av`LNAjBIWbY#Jx[kwKwisvekblc","FLA12131502=AXdf[PhFVc%TboqaKwKIS\u003cOIPFQ","FLA12131502=AXdfvDlscVKiWOP@xLxis#ofoin","FLA12131602DAWciaOgviTSaO]`pG[GSFM\u003eW\u003eX?","FLA12131902Av`LNAgOpcVZWiMUEeAetfcxax%y","FLA12132202=AXdf?cK`tI?uC_hxUqUBX?LELBM","FLA12132702=AXdfcBj[wB@rD:qa=i=WEJAW\u003eX?","FLA12133002=AXdfWr:nbW:xF]YIc?co]ripioh","FLA12133502Av`LNdmEw[NFl:eP@Ad@N\u003cAJCJDK","FLA12134202=AXdfav\u003eeo:iK]A]maD`n#shngqf","FLA12134402DAWci#BjQ=pEp\u003euVFZFZn[xcmdje","FLA12134702DAWciVpHVExR_QKtdB%BVCP;R;U:","FLA12135402=AXdfW:rJHubP%\u003cQArNrhr]ngnho","FLA12135502Av`LNx#Thk\u003e:xFWDTf:fIRWDMDJE","FLA12140002DAWciHr:n[NWZLY\u003cL[G[oZyblekd","FLA12140303wvpoehbia@@G;F","FLA12140502=AXdfN?w_yD#Vh;\u003eNaEaYBM\u003eXAW@","FLA12140602=AXdfxhPX\u003ekXZLYRByMyCX?LELBM","FLA12141002DAWciQw?\u003cLaMiW#iy@d@LARIPIOH","FLA12141502=AXdfG\u003evdp=kFxw\u003eNb\u003ebrin]r[uZ","FLA12142002=AXdflr:DSfdN`rRBpTpCW@KBKEJ","FLA12142302Av`LN[fNoZOGm;@_oI]Isgby`y_x","FLA12142702=AXdfyoGwdYY[MXn%d@dP\u003cSHNGQF","FLA12143002=AXdf`hPwOZdN`J=MMyMP\u003cSHQHNI","FLA12143402DAWci]BjhuH\u003erDx%nd@dxen]t]s#","FLA12143902Av`LNYEmF`MSaOFFVXlXydir[r#s","FLA12144502DAWcig:rBP]eQ_UK;kWk_jir[r#s","FLA12144702Av`LNeyAf;n`Rdc_o:f:FSVELEKD","FLA12145302DAWcia@xq`MxWijCSjVj%khsZs]r","FLA12145602=AXdfwbJi:o=xF%SCh\u003chxejaxaw`","FLA12145802Av`LNSDlrP]PeS]=MkWko_Zqhqgp","FLA12150202=AXdfBRZOuHy\u003cjVueRnR=MBY@Y?X","FLA12150303vwru`]_#d;;D@E","FLA12150602DAWciqPhygRPoA`CSWkWCV=NGNHO","FLA12151102=AXdf%x@LxEfK]xbrrNrXIN=T=S\u003c","FLA12151502Av`LNw`Xc:oS%P=XHJvJPA\u003cOFOIN","FLA12152702Av`LNQ@x]wBWZL;FVwKwVDIR;R\u003cS","FLA121530AZNSTAT 0 0","FLA12153102Av`LNL;sdo:KfXGo_AeA`jo#r[uZ","FLA12153202DAWci[\u003evV:okM[u;KoSo[nev_v`w","FLA12154202DAWciwS[RCvtRdChx\u003eb\u003eJ?TGQHNI","FLA12155302DAWci:]U[k\u003eiHvW?OSoSGRAJCJDK","FLA12155702DAWcixX`ETivWi_sc_C_k%ufpioh","FLA12160303vwruSVTWOnnymx","FLA12160902DAWciEiQmcVcCu?CSxLxdyZqhqgp","FLA12161302DAWcibIqEK%Pp\u003e@CSxLxdyZqgnho","FLA12161802DAWciR:rim@pP%kIYsOsgrajcjdk","FLA121620RFC 433 314 10 0 0","FLA12162202DAWciphPneXiIwSueFZFRGL?Y@VA","FLA12163802DAWciuX`o_JsSe`m]?c?K\u003eUFOFPG","FLA12164502DAWciFhPDN[sSe?UEpTp#qby_v`w","FLA12165402DAWcivU]BLalLZMYIqUq]pcxax%y","FLA12165902DAWci%w?nhU=]KRZjD`DXEN=S:T;","FLA12170303vwqnWRXSKkktpu","FLA12170502DAWciR\u003ct;WbYrDgL\u003cd@dxen]t]s#","FLA12171202DAWciniQdn;%=kpVF@d@LARIOFPG","FLA12171402DAWciGOg]lAgDro_opTp#qby`y_x","FLA12171802DAWci?V%W=pWtBDYID`DXEN=S:T;","FLA12171802DAWcicIq[yDYrD:QAAeAM@SHQHNI","FLA12172302DAWciA[S_sFOl:NDTYmYEX;PFOIN","FLA12174602DAWci`r:BOZdGyMm]C_CWBQ:S:T;","FLA12175302DAWciAS[hk\u003eoM[\u003ceuAeAM@SHNGQF","FLA12180202DAWcihnF?Ti\u003c%PL?Og;gsfm%w%x_","FLA12180303vwqnSVTWOrrmyl","FLA12180802DAWciR\u003ctmhUpJ#U\u003eNg;gsfm%xaw`","FLA12182402DAWcijcK\u003c@m\u003c%PU:JESDXEN=T=S\u003c","FLA12183102DAWciPjBBDyyRd@UE]H#p]vekblc","FLA12190303vwst`]_#d@@G;F","FLA12193202Av`LNYKcyRf]:lRSCRDSTIFU\u003cU;T","FLA12193702Av`LNtqI?#PWxF\u003e\u003eNAO@?JM\u003eXAW@","FLA12193902Av`LNumEdHtIgYCCSESDBWXCJCMB","FLA121946AZNSTAT 0 0","FLA12195502Av`LNohPQo;Lj\u003cV\u003cL@NAALK@V?Y\u003e","FLA12200303vwstUXRYQrrmyl","FLA12200902Av`LNRu=h;oIhVQXHH]ISFIR;R\u003cS","FLA12201402Av`LNuRZrQ]HiWXJ:OrN%nqZt]s#","FLA122036RFC 945 359 11 0 0","FLA12205502DAWcicEmHfRKk=:iyuPthu%mdmcl","FLA12205802Av`LN@ZR[;oFeS_qaaD`ycdw%wav","FLA12205902DAWci`?wCeYeFx\u003cJ::g;O:YBLEKD","FLA12205902DAWciCdLa?k?#J\u003eP@=h\u003cP=VELEKD","FLA12210202Av`LNLoGcCwTwIN@PSnR;QN=S:T;","FLA12210303vwstMPJQYllsor","FLA12210702DAWci:[SeHtYrDlVFP\u003eQ=PCX\u003eWAV","FLA12212202Av`LNIhPZ@l_\u003cjRqaXmY:OP;R;U:","FLA12212402DAWciMx@U]QxSexfvuPthu%mdmcl","FLA12212902DAWcih?w:sGWtBKAQB_CWBQ:T=S\u003c","FLA12213402DAWcinW_v?ktTbiue\u003ci=Q\u003cWDMDJE","FLA12214802DAWciXfN%VbOl:%o_k]j%khs]tZu","FLA12214802DAWci%QiV%JnM[]l#lZmalgt]tZu","FLA12215802Av`LN[JbV_KWtBYBRf;g#qn]sZt[","FLA12215802DAWciv\u003ctlDx_\u003cjajZNsOIX;PFOIN","FLA12220303vwstEHBIAeeZf[","FLA12220802DAWciu;snGsbIw\u003cK;=K\u003cRCP;R;U:","FLA12220802Av`LNs\u003evmDx_\u003cjIVFHVIUHGT=T:U","FLA12221202DAWcit;sKbVVuC%n%OANHY:QGNHO","FLA12221302Av`LN`V%XaMRyGSp`OAN:OP;U\u003cR=","FLA12221302Av`LNQfN]ThUwIB`p\u003eP?K\u003eAJCJDK","FLA12221602DAWcikCkT#PVuC%CSdre\u003eODW\u003eWAV","FLA12221802Av`LNJbJiQ]UwILyi#j]q#_lbkej","FLA12222002DAWcioFny\u003ejEgYn%nTqUufm%w%x_","FLA12222502Av`LNOMNLcWnLZREU_B%j_#ohqgp","FLA12223902=AXdfbFnE@l:oAm`pJwKP@WDMDJE","FLA12224302=AXdfyW_`eYPZLlaqYlXSCL?Y@VA","FLA12230303vwstLQKPXmmrns","FLA12232902DAWcirbiQhTeHvFaqCUBVCP;R;U:","FLA12233302DAWci@PKqIuCfXVo_WIVBW\u003cOIPFQ","FLA12233302DAWciSCH\u003csGxUc:csIWHTIJAXAW@","FLA12233702DAWcifwtqGsIdR?gwm[l`mfu[r#s","FLA12234702DAWciLHCIo;A[MFUEVkWCV=NGNHO","FLA12235202DAWcioibpFrnLZ=K;N@O;NEV@Y?X","FLA12235302DAWcibmn:tHHbTjXHygxdyZqhqgp","FLA12235802DAWci\u003cSXpDxrXfd=Mfxgsfm%xaw`","FLA12240102DAWci#uv#WcnLZoTDh=iuhk`y`va","FLA122402AZNSTAT 0 0","FLA12240303vwst\u003e;A:B%%i]h","FLA12240702DAWciqJQqBvoM[dueOrN:ODWAX\u003eY","FLA12240802DAWcigCH@rFwRdK;K:L;O:YBKBLC","FLA12241502DAWcigBIo\u003ejQl:wN\u003eserfs`kelbm","FLA12241602DAWciiDG_O[FcUSyiGYFRGL?V?Y\u003e","FLA12242602DAWciIefRgSsVh?[kserfs`kelbm","FLA12242602DAWcioKP@l@[\u003epCgwucthu%mdmcl","FLA12243002DAWci[?\u003c[P#\u003e[MQn%[mZn[xcmdje","FLA12243102DAWciJqjVdX_:l#;KL:MALGT=T:U","FLA12244102DAWciHbiQWbKn@uRBYGXDY:QGNHO","FLA12244402DAWcibHCXLaUxFuRBYGXDY:QHQGP","FLA12244902DAWcikbid%K_:lqO?M;L@MFU;R\u003cS","FLA12245202DAWciLDGvwB@ZLa\u003eN@NAM@SHQHNI","FLA122452RFC 1457 496 13 0 0","FLA12250303vwstehbiaBB=I\u003c","FLA12250802DAWci?YRNGruVhiFVkVj%khs]tZu","FLA12251102DAWcir#_qfSYrDrVF`Eam`shqhni","FLA12252802DAWci#yrucVpJ#xTD`Eam`shngqf","FLA12252802DAWciiloJ\u003cq#\u003epkO?b?cwbqZsZt[","FLA12255802=AXdf[_#psFXfXCaqg:fO=RIPIOH","FLA12260002DAWciCMN:hUWrDtdtg:frgl_y`va","FLA12260303vwtsUXRYQwwptq","FLA12261202=AXdf?\u003c?WN[q?qJHXoSoj`wdjcmb","FLA12261302DAWci`:AnYd\u003c`NAAQvJvbw#ofoin","FLA12261902DAWciZsxE#QTxFLN\u003eZFZn[xcmdje","FLA12262402DAWci#ry\u003eLa]Aob_oVjVBW\u003cOFOIN","FLA12262802DAWcijfef?jxTbIp`\u003eb\u003eJ?TGQHNI","FLA12263602DAWcinefoRgB]KkSCAeAM@SHQHNI","FLA12263702=AXdfqFndZOFwI\u003cbryMyl%ybkblc","FLA12264002DAWciY=\u003eZGrTk=Baqf:frgl_y`va","FLA12264802=AXdfS#TdWcIyGGJ:=i=WEJAW\u003eX?","FLA12264902DAWciX?\u003cUlA]Btuuei=iuhk`y`va","FLA12265302DAWciY?\u003cUFsYn@ehxqUq]pcx%wav","FLA12265802=AXdfhU]yHtUfXZhxjVjft[pipfq","FLA12270202=AXdfpYaN_KcXf:ue]I]ycl_y`va","FLA12270303vwstbfdg_\u003e\u003eI=H","FLA12270802DAWciP\u003e=RlAEiW#AQ%B%j_tgngqf","FLA12271302DAWciNA:Sp=;_QlTDh\u003chtijaw%x_","FLA12271302DAWci_ol\u003eeXVrDTl#H#HTIJAXAW@","FLA12271602=AXdfQpHPXegTbe=MMxLFT;PIPFQ","FLA12272002=AXdfJjB%iTM%P]EUf;gm_xcmdje","FLA12272802=AXdfNlDUN[UgYjL\u003c@NAXBM\u003eW\u003eX?","FLA12272902DAWcik[`gOZtXf=[koanZodwax%y","FLA12272902DAWciO@;#TilP%vWG?Q\u003eJ?TGNGQF","FLA12273202=AXdf_w?LVcFtBhIYTBU\u003cNIR\u003cU;T","FLA12274002DAWcialovGr%;mmK;J\u003cK?JIR\u003cU;T","FLA12275102DAWciHRYg%h#\u003epWxh:g;O:YBKBLC","FLA12280303vwst;\u003e\u003c?Gffae`","FLA12280402DAWciwol::DMoAA%n_q%j_tgqhni","FLA122818AZNSTAT 0 0","FLA12285002=AXdfCxAfFstGy=cs?b\u003eIR=NGNHO","FLA12285402=AXdfgT]Op=EvHnRByLxo#shngqf","FLA12290303vwst:?=\u003eF[[dad","FLA122908RFC 1967 662 17 0 0","FLA12293602=AXdfonFv@mO_Q\u003eO?N@O\u003eOFU\u003cU;T","FLA12294102=AXdfuoG]Rg%OaTBRoRnM?VEKBLC","FLA12300303vwstMPJQYmmror","FLA12300502=AXdf@MeP_JdRd]HX=K\u003cn_xcjcmb","FLA12301202=AXdfZmEwGrFxFRp`q_pP:UFPIOH","FLA12301802=AXdflZRsFsnAo?N\u003eZG[XDK@Y@VA","FLA12302202=AXdfds;\u003co:=j\u003co%nVkW#pgtZs]r","FLA12303102=AXdfV@xC;EYfXMvfvhwk[tgngqf","FLA12304302=AXdfdFnAj?k\u003cjubrSnRXBM\u003eXAW@","FLA12304702=AXdfDMeh`fptqn`pRoS%jev_v`w","FLA12305202=AXdf:`XxnxYMXueutbuk_xcmdje","FLA12305302=AXdf?[Si#b#i#N\u003eNESDwbm%w%x_","FLA12305802=AXdf#\u003ctg#bkvksP@WIVWBM\u003eXAW@","FLA12310102=AXdfRqI:I?c%cL\u003cL:L;QAVELEKD","FLA12310303vwstOJPKSFFADA","FLA12311502=AXdfC[SvqwrpuwhxYlXVDK@V?Y\u003e","FLA12311902=AXdfBZRdZdykvwcsTqUj%ybkblc","FLA12314102=AXdfhQi%c]e%c_l#\u003ec?ix_lbkej","FLA12314102=AXdf_V%e[eXJW`xhB_CPAVELEKD","FLA12320303vwtsXUWTL\u003e\u003eI\u003cI","FLA12322202DAWciD=?hGAmM[kGWDaEYDO\u003cU\u003cR=","FLA12322802DAWcie#%?ZdVvHZ\u003eNoSo[nev`y_x","FLA123234AZNSTAT 0 0","FLA12330303vwts%[aZb;;DAD","FLA123324RFC 2479 935 21 0 0","FLA12334002Av`LNkvtrNXpJ#dM=uQuitwdmdje","FLA12334702Av`LN?CIE_i:`N?aq=i=Q\u003c?LBKEJ","FLA12335802Av`LNAGEkJTRxFRFVi=im_Zqhqgp","FLA12340303vwstUXRYQ==B?B","FLA12340402Av`LNwqkrPVPj\u003cHN\u003evJvguxcmdje","FLA12342702Av`LNZVUO;EmP%BAQMxLISVELEKD","FLA12343002DAWcifMNrf`XuCFp`wJv]ohsZs]r","FLA12343602Av`LNcUVJAGlQ_uTD#H#vdir#u[t","FLA12343802DAWciQ_#JAGeHv]AQoSodvajdmcl","FLA12343902=AXdf?t\u003c_LarpuBhxWkWufqZt]s#","FLA12344102=AXdfMgO[Q#G=HF_oNrNhs#ofoin","FLA12345102=AXdft\u003evqyomwjbtdwJvSHO\u003cR;U:","FLA12345202=AXdfdNfmukKVK%qaaD`RIN=T=S\u003c","FLA12345302Av`LNUibevpvTbIVFC%BJ@=NGNHO","FLA12345902Av`LNYbixe[[Ao_xhD`Dajo#r[uZ","FLA12350102Av`LNqXSctjjP%BBR#H#@KN=T=S\u003c","FLA12350303vwstQLNMUqqvkv","FLA12350302DAWcitMNWH\u003eWuCcdt\u003ch\u003cgt[pipfq","FLA12350702DAWciUkpN?IWuC[`pI]IZqfu[r#s","FLA12350902=AXdfZPhJ_JUPURK;tPt;PGT:S=R","FLA12351602=AXdftNfL`MymxFqa?c?]nir[r#s","FLA12351902DAWciMolTI?Nl:eO?[G[rin]t]s#","FLA12352202=AXdfHfNo=p;G:?_oKwKk`wdjcmb","FLA12352302Av`LNs[`GUK?]KXyiEaEVEHS=T:U","FLA12352402DAWciYA:hrlZ@nCdtXlX:QFU;R\u003cS","FLA12353002Av`LN=UVIZdA[MxJ:mYm@LQ:S:T;","FLA12353602Av`LNLnmqQWqK];O?tPtq]`kelbm","FLA12360303vwstLQKPXjjupu","FLA12361502=AXdfqMejjt]i#rQAWIVL?XCJCMB","FLA12362002=AXdf`\u003ctFI?%Q_tO?L:MVEJAW\u003eX?","FLA123650AZNSTAT 0 0","FLA12365202=AXdf\u003eZRcsmWJWRqaq_prin]t]s#","FLA12365602=AXdfWs;VH\u003exmx[HXHVI;PGT:S=R","FLA12370303vwstKNLOWvvqtq","FLA12370302=AXdfoOgg?Imxm@P@b?c@MBY@Y?X","FLA12371002=AXdf\u003cbJH]crorwL\u003cM;LM@WDJCMB","FLA12371902=AXdfCv\u003eWoyRORiyi;f:vdk`y`va","FLA12372302=AXdfZLdPyoB?BxhxJwKguZqgnho","FLA12372302=AXdftBjWnxC\u003eCo_oUpTakdw%wav","FLA12372702=AXdfLZR`jtB?B#m]?b\u003esin]sZt[","FLA12372802=AXdf_Phftj?B?TBRsNr\u003eLCXAX\u003eY","FLA123740RFC 2991 1205 24 0 0","FLA12380303vwstYTVUM??H=H","FLA12385102DAWci\u003cqkH\u003eImHvyRBiwhN?XCJCMB","FLA12385102Av`LNdYSd[dMhVYrbIWHudir[r#s","FLA12385602DAWciVciH\u003eIlIwiHX@O@yho#r[uZ","FLA12385602Av`LNvCI`g`WZL:[kctc]lqZt]s#","FLA12390303vwstcfdg_llsns","FLA12400303vwst\u003e;A:BXXORO","FLA12410303vwstvrxskaaf[f","FLA124106AZNSTAT 0 0","FLA124156RFC 3503 1681 30 0 0","FLA12420303vwstUXRYQ;;DAD","FLA12421102=AXdfaT#`lrWJWXcsrdswgp[u#r]","FLA12421502=AXdffLdre[\u003eC\u003eYJ:TBUsin]t]s#","FLA12424502=AXdfvMeDSM]h]]gwMxLXHO\u003cR;U:","FLA12424502=AXdfWmEVF@F\u003cIJXHYGXduZqhqgp","FLA12425602=AXdfiBjMGAe_bZhxMxLBY@KELBM","FLA12425902=AXdfZr:FYOf#i#gwUpTUIN=T=S\u003c","FLA12430303vwst]`Zai;;DAD","FLA12430502=AXdfT:rQ\u003cBi[fu=MtQuLAVEKBLC","FLA12430502=AXdfkdLdyoD\u003eC#TD[FZalcxax%y","FLA12431102=AXdfuZRALRRQTtqa#j]oZufpioh","FLA12431202=AXdfbmEbukwlyb]mxfyK:UFOFPG","FLA12433202=AXdfew?OH\u003ec`ejvfMxLZqfu[r#s","FLA12434002=AXdf\u003e?wT;EqroUO?ZG[ugp[r[uZ","FLA12434402=AXdfghP%vp@C\u003exjZB_CM?XCMDJE","FLA12440303vwstRWUVNnnyly","FLA12440302=AXdfaV_pf`pxmLVFK=J`jdw%wav","FLA12441202=AXdfgKbZ`fAI\u003c[dtser%nhs]tZu","FLA12450303vwtsMPJQYyynsn","FLA124522AZNSTAT 0 0","FLA12460303vwts\u003e;A:Bcc#i#","FLA124612RFC 4015 1795 30 0 0","FLA12470303vwtsLQKPXyynsn","FLA12480303utwxhegd#==B?B","FLA12484602DAWciD[`dh%%]hlCSc\u003ebwgp[r[uZ","FLA12485902DAWciYxsQKU\u003c\u003cI@%nrOs\u003eMDWAX\u003eY","FLA12490002DAWciQqj\u003eVP\u003e\u003eC\u003eaquPtj%wdmdje","FLA12490303utwxidfe]\u003c\u003cC\u003eC","FLA12490502DAWciPpk@TJyyl?]mvKwk_vekblc","FLA12490502DAWciA`[lf`??BKqab?cCVAJCJDK","FLA12493002DAWcimWTBZdVWJvSCXFYBY\u003eMCJDK","FLA12493602DAWcii\u003c?yKUCE@cFVFXGM@WDMDJE","FLA124938AZNSTAT 0 0","FLA12500303utvyidfe]II\u003eC\u003e","FLA12500402DAWci`DGCjtuadjL\u003cUCT\u003eMBY?V@W","FLA12501402DAWciwJQbSM%roRaqn`ouejaxaw`","FLA12501802DAWci\u003echuE;:WJy;KL:MWGP;U\u003cR=","FLA12501802DAWciGZaTe[v[fBp``na=LCXAX\u003eY","FLA12502202DAWcisNMwF@_roLfv[mZ\u003eOHS=T:U","FLA12502302DAWcipSXFwqBORZXHN@O_mby`y_x","FLA12502702DAWcipQJ\u003eoyCNS%SCP\u003eQakdwax%y","FLA125028RFC 4527 1887 31 0 0","FLA12504302DAWcihCHkrmOE@cJ:=K\u003cIY\u003eMDMCL","FLA12504802DAWciLnm@jtji#iRBFXGCS\u003cOIPFQ","FLA12510303utvya#%]e??H=H","FLA12513002DAWcinyrPZdCKVVK;J\u003cKTIN=T=S\u003c","FLA12514302DAWcif[`M[edmx]SC\u003ec?\u003eOHS=T:U","FLA12520303utvy]`ZaiHH?B?","FLA12521002DAWci=BIjju\u003eXMTDTKvJ@PGT=T:U","FLA12521402DAWcibxs``gavkwgwE`DVFQ:T=S\u003c","FLA12521502DAWcio]%tujmb_iyiJwK:KDW\u003eWAV","FLA12522002DAWciDWTB@GOH=\u003cL\u003c%C_BX?LBKEJ","FLA12523902DAWci;MNLPWBMXleu;f:DT;PIPFQ","FLA12524402DAWciFVUFC\u003cPG:fo_;f:AQFU;R\u003cS","FLA12524602DAWci;KPrujR=H#ue:g;TEJAXAW@","FLA12525202DAWciUBIbc#pgZFO?e@dKAVEKBLC","FLA125258010","FLA12530303utvyMPJylLLSNS","FLA12531102DAWciwibEFA%ylXAQ@NALAVELEKD","FLA12531502DAWciM:A]%iFQTpiyhvitin]sZt[","FLA12531802DAWciWGDGB=ZupT=M\u003cJ=RCL?V?Y\u003e","FLA12532402DAWcidtw%afCLYmdtesdIS\u003cOIPFQ","FLA12534202DAWcijib%]b=ROsZj[mZPAVELEKD","FLA12534902DAWcicqjJLS%ylXAQ@NAHR;PFOIN","FLA125354AZNSTAT 0 0","FLA12540303utvyokqROnnyly","FLA125444RFC 5039 1922 33 0 0","FLA12550303utvy_[axpQQVKV","FLA12560303utvybfd[cAAF;F","FLA12570303utvyRVTLTnnyly","FLA12573702DAWciVvuj%i=TQOM=OAN?MBY@Y?X"],"Task":{"DeclarationDate":"0001-01-01T00:00:00Z","Date":"0001-01-01T00:00:00Z","Number":0,"Takeoff":{"Lat":0,"Lng":0,"Time":"0001-01-01T00:00:00Z","FixValidity":0,"PressureAltitude":0,"GNSSAltitude":0,"IData":null,"NumSatellites":0,"Description":""},"Start":{"Lat":0,"Lng":0,"Time":"0001-01-01T00:00:00Z","FixValidity":0,"PressureAltitude":0,"GNSSAltitude":0,"IData":null,"NumSatellites":0,"Description":""},"Turnpoints":null,"Finish":{"Lat":0,"Lng":0,"Time":"0001-01-01T00:00:00Z","FixValidity":0,"PressureAltitude":0,"GNSSAltitude":0,"IData":null,"NumSatellites":0,"Description":""},"Landing":{"Lat":0,"Lng":0,"Time":"0001-01-01T00:00:00Z","FixValidity":0,"PressureAltitude":0,"GNSSAltitude":0,"IData":null,"NumSatellites":0,"Description":""},"Description":""},"DGPSStationID":"","Signature":"2A01203953D6563B6C5B93E733ADB945E7C7438C0000128A4D0BE110F2626904112EA000D8476BC6537C0000"} --------------------------------------------------------------------------------