Compare commits

...

No commits in common. "master" and "WIP" have entirely different histories.
master ... WIP

86 changed files with 12350 additions and 2288 deletions

View File

@ -1,2 +0,0 @@
[build]
rustflags = ["-C", "target-cpu=native"]

40
.chglog/CHANGELOG.tpl.md Normal file
View File

@ -0,0 +1,40 @@
{{ if .Versions -}}
## [Unreleased]
{{ if .Unreleased.CommitGroups -}}
{{ range .Unreleased.CommitGroups -}}
### {{ .Title }}
{{ range .Commits -}}
- {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }} [{{.Hash.Short}}]({{ $.Info.RepositoryURL }}/commit/{{ .Hash.Short }})
{{ end }}
{{ end -}}
{{ end -}}
{{ end -}}
{{ range .Versions }}
## {{ if .Tag.Previous }}[{{ .Tag.Name }}]{{ else }}{{ .Tag.Name }}{{ end }} - {{ datetime "2006-01-02" .Tag.Date }}
{{ range .CommitGroups -}}
### {{ .Title }}
{{ range .Commits -}}
- {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }} [{{.Hash.Short}}]({{ $.Info.RepositoryURL }}/commit/{{ .Hash.Short }})
{{ end }}
{{ end -}}
{{- if .NoteGroups -}}
{{ range .NoteGroups -}}
### {{ .Title }}
{{ range .Notes }}
{{ .Body }}
{{ end }}
{{ end -}}
{{ end -}}
{{ end -}}
{{- if .Versions }}
[Unreleased]: {{ $.Info.RepositoryURL }}/compare/{{ $latest := index .Versions 0 }}{{ $latest.Tag.Name }}...HEAD
{{ range .Versions -}}
{{ if .Tag.Previous -}}
[{{ .Tag.Name }}]: {{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }}
{{ end -}}
{{ end -}}
{{ end -}}

34
.chglog/config.yml Normal file
View File

@ -0,0 +1,34 @@
style: gitlab
template: CHANGELOG.tpl.md
info:
title: CHANGELOG
repository_url: https://gitlab.com/Earthnuker/ed_lrr
options:
commits:
filters:
Type:
- feat
- fix
- perf
- refactor
- misc
- other
- docs
commit_groups:
title_maps:
feat: Features
fix: Bug Fixes
perf: Performance Improvements
refactor: Code Refactoring
misc: Miscellaneous
other: Other
docs: Documentation
header:
pattern: "^(\\w*)(?:\\(([\\w\\$\\.\\-\\*\\s]*)\\))?\\:\\s(.*)$"
pattern_maps:
- Type
- Scope
- Subject
notes:
keywords:
- BREAKING CHANGE

1
.env Normal file
View File

@ -0,0 +1 @@
FLASK_APP="ed_lrr_gui.web:app"

29
.gitignore vendored
View File

@ -1,9 +1,28 @@
/target
rust/target
rust/.history/
**/*.rs.bk
*.tmp
*.idx
.vscode/**
*.csv
*.router
dumps/*.json
plot.py
*.tmp
*.idx
plot.py
*.tmp
*.idx
*.pyd
__pycache__
*.egg-info
build
*.pdf
.history
.tox
pip-wheel-metadata
.eggs/
dist/
installer/Output/
workspace.code-workspace
ed_lrr_gui/web/jobs.db
ed_lrr_gui/web/ed_lrr_web_ui.db
__version__.py
.nox/
dist_vis.py

View File

@ -1,26 +0,0 @@
# This file is a template, and might need editing before it works on your project.
# Official language image. Look for the different tagged releases at:
# https://hub.docker.com/r/library/rust/tags/
image: "rust:latest"
# Optional: Pick zero or more services to be used on all builds.
# Only needed when using a docker container to run your tests in.
# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-a-service
# services:
# - mysql:latest
# - redis:latest
# - postgres:latest
# Optional: Install a C compiler, cmake and git into the container.
# You will often need this when you (or any of your dependencies) depends on C code.
before_script:
- apt-get update -yqq
- apt-get install -yqq --no-install-recommends build-essential
- rustup update nightly
- rustup default nightly
# Use cargo to test the project
test:cargo:
script:
- rustc --version && cargo --version # Print version info for debugging
- cargo build --release

264
CHANGELOG.md Normal file
View File

@ -0,0 +1,264 @@
## [Unreleased]
## [v0.14.0] - 2019-10-23
### Features
- **tests:** Add pytest.ini for pytest-qt [eb43ca8](https://gitlab.com/Earthnuker/ed_lrr/commit/eb43ca8)
## [v0.13.0] - 2019-10-23
### Documentation
- **readme:** Update README with more infos [634af75](https://gitlab.com/Earthnuker/ed_lrr/commit/634af75)
### Features
- **setup.py:** Update add dependencies for HTTP API [45c11da](https://gitlab.com/Earthnuker/ed_lrr/commit/45c11da)
## [v0.12.0] - 2019-10-23
### Features
- **tox:** Add more python versions to test against [3115f9d](https://gitlab.com/Earthnuker/ed_lrr/commit/3115f9d)
## [v0.11.0] - 2019-10-23
### Features
- **html_export:** Add basic HTML-Export using Jinja2 template [bb2ee8c](https://gitlab.com/Earthnuker/ed_lrr/commit/bb2ee8c)
## [v0.10.0] - 2019-10-23
### Features
- **route_progress:** minor change to progress dialog message [35fc135](https://gitlab.com/Earthnuker/ed_lrr/commit/35fc135)
## [v0.9.4] - 2019-10-23
### Bug Fixes
- **config:** Add missing defaults [30dbd24](https://gitlab.com/Earthnuker/ed_lrr/commit/30dbd24)
### Documentation
- Update CHANGELOG.md and README.md [38acfc7](https://gitlab.com/Earthnuker/ed_lrr/commit/38acfc7)
## [v0.9.3] - 2019-09-28
### Features
- **config:** impelemt save and load to GUI [65fe131](https://gitlab.com/Earthnuker/ed_lrr/commit/65fe131)
## [v0.9.2] - 2019-09-28
### Features
- **GUI:** implement preprocessing [f34d37a](https://gitlab.com/Earthnuker/ed_lrr/commit/f34d37a)
## [v0.9.1] - 2019-09-28
### Documentation
- Update TODO list in readme [4663d4e](https://gitlab.com/Earthnuker/ed_lrr/commit/4663d4e)
### Features
- **installer:** switch from LZMA to LZMA2 [3ee952e](https://gitlab.com/Earthnuker/ed_lrr/commit/3ee952e)
## [v0.9.0] - 2019-09-28
### Features
- **build scripts:** skip .history folder when building UI files [92bd1b1](https://gitlab.com/Earthnuker/ed_lrr/commit/92bd1b1)
## [v0.8.1] - 2019-09-28
### Features
- **config:** update config format [9ec4687](https://gitlab.com/Earthnuker/ed_lrr/commit/9ec4687)
## [v0.8.0] - 2019-09-28
### Features
- **UI:** made dropdowns non-editable [37f5547](https://gitlab.com/Earthnuker/ed_lrr/commit/37f5547)
### Miscellaneous
- Alsways compile in release mode [e9bcd3e](https://gitlab.com/Earthnuker/ed_lrr/commit/e9bcd3e)
## [v0.7.1] - 2019-09-28
### Features
- **config:** finish integrating new configuration system [a8d6a32](https://gitlab.com/Earthnuker/ed_lrr/commit/a8d6a32)
## [v0.7.0] - 2019-09-28
### Features
- **Router:** finish implementing route computation progress display [93f655c](https://gitlab.com/Earthnuker/ed_lrr/commit/93f655c)
### Miscellaneous
- Add VSCode workspace config to gitignore [a91fa73](https://gitlab.com/Earthnuker/ed_lrr/commit/a91fa73)
- formatting [f78ec66](https://gitlab.com/Earthnuker/ed_lrr/commit/f78ec66)
- **CI:** rename main executable before building installer [dc0e3fc](https://gitlab.com/Earthnuker/ed_lrr/commit/dc0e3fc)
## [v0.6.2] - 2019-09-28
### Features
- **GUI:** Integrate route computation Job [f4e6bd3](https://gitlab.com/Earthnuker/ed_lrr/commit/f4e6bd3)
## [v0.6.1] - 2019-09-28
### Features
- **GUI:** Integrate new config system [22ca2d2](https://gitlab.com/Earthnuker/ed_lrr/commit/22ca2d2)
### Miscellaneous
- re-export config from main GUI module [0d89106](https://gitlab.com/Earthnuker/ed_lrr/commit/0d89106)
## [v0.6.0] - 2019-09-28
### Features
- **cli:** Add config editor [8b0b56f](https://gitlab.com/Earthnuker/ed_lrr/commit/8b0b56f)
## [v0.5.1] - 2019-09-28
### Features
- **router:** Implement route computation job with progress dualog [2c000da](https://gitlab.com/Earthnuker/ed_lrr/commit/2c000da)
### Miscellaneous
- **setup.py:** Pull version info from git, unify scripts (one script for GUI and CLI) [a630851](https://gitlab.com/Earthnuker/ed_lrr/commit/a630851)
## [v0.5.0] - 2019-09-28
### Documentation
- Update README.md [66267e7](https://gitlab.com/Earthnuker/ed_lrr/commit/66267e7)
### Features
- **installer:** Download EDSM dumps after install [4237b30](https://gitlab.com/Earthnuker/ed_lrr/commit/4237b30)
### Miscellaneous
- Update icon generator and regenerate icon [7838480](https://gitlab.com/Earthnuker/ed_lrr/commit/7838480)
## [v0.4.1] - 2019-09-28
### Features
- **router:** Start implementing route pruning support [88a0378](https://gitlab.com/Earthnuker/ed_lrr/commit/88a0378)
## [v0.4.0] - 2019-09-28
### Features
- **config:** Update config system to use profig [b1143c3](https://gitlab.com/Earthnuker/ed_lrr/commit/b1143c3)
## [v0.3.0] - 2019-09-28
### Features
- **build system:** Remove build.py (switched to tox), add output to build_gui.py [fb3a130](https://gitlab.com/Earthnuker/ed_lrr/commit/fb3a130)
## [v0.2.0] - 2019-09-28
### Features
- **router:** Add dialog to display computed route [d498746](https://gitlab.com/Earthnuker/ed_lrr/commit/d498746)
## [v0.1.12] - 2019-09-21
### Bug Fixes
- switch inno setup version in appveyor [fe3534e](https://gitlab.com/Earthnuker/ed_lrr/commit/fe3534e)
## [v0.1.11] - 2019-09-21
### Bug Fixes
- typo in appveyor.yml [09e6f0a](https://gitlab.com/Earthnuker/ed_lrr/commit/09e6f0a)
## [v0.1.10] - 2019-09-21
### Bug Fixes
- disable confirmation for conda install [eaddb18](https://gitlab.com/Earthnuker/ed_lrr/commit/eaddb18)
## [v0.1.9] - 2019-09-21
### Bug Fixes
- add missing conda channel [6d9f1ef](https://gitlab.com/Earthnuker/ed_lrr/commit/6d9f1ef)
## [v0.1.8] - 2019-09-21
### Bug Fixes
- add missing dependencies to appveyor.yml [e8beb55](https://gitlab.com/Earthnuker/ed_lrr/commit/e8beb55)
## [v0.1.7] - 2019-09-20
### Bug Fixes
- fix nuikta call to clean build directory [fa485be](https://gitlab.com/Earthnuker/ed_lrr/commit/fa485be)
## [v0.1.6] - 2019-09-20
### Bug Fixes
- fix nuikta call to download without confirmation [11efe30](https://gitlab.com/Earthnuker/ed_lrr/commit/11efe30)
## [v0.1.5] - 2019-09-20
### Bug Fixes
- fixed appveyor.yml [59390fe](https://gitlab.com/Earthnuker/ed_lrr/commit/59390fe)
## [v0.1.4] - 2019-09-20
### Bug Fixes
- fixed tox.ini [6bb7e1e](https://gitlab.com/Earthnuker/ed_lrr/commit/6bb7e1e)
### Documentation
- small wording change [3779911](https://gitlab.com/Earthnuker/ed_lrr/commit/3779911)
## [v0.1.3] - 2019-08-31
### Bug Fixes
- **setup:** fix setup script to include subdirectories [4a392d9](https://gitlab.com/Earthnuker/ed_lrr/commit/4a392d9)
### Documentation
- Insert page break after table of contents [f027e02](https://gitlab.com/Earthnuker/ed_lrr/commit/f027e02)
- Update documentation to include basic description [714741a](https://gitlab.com/Earthnuker/ed_lrr/commit/714741a)
- Rename `doc` folder to `docs`, update outline [fb54bda](https://gitlab.com/Earthnuker/ed_lrr/commit/fb54bda)
- Add skeleton for documentation [dbc6f35](https://gitlab.com/Earthnuker/ed_lrr/commit/dbc6f35)
### Miscellaneous
- Update changelog template to make conversion to PDF easier [87550a9](https://gitlab.com/Earthnuker/ed_lrr/commit/87550a9)
- **formatting:** ran `cargo fmt` and `cargo clippy`, fixed all warnings [fb3f79b](https://gitlab.com/Earthnuker/ed_lrr/commit/fb3f79b)
## [v0.1.2] - 2019-08-05
### Bug Fixes
- **router:** Fixed some syntax errors created by botched merge [4b14643](https://gitlab.com/Earthnuker/ed_lrr/commit/4b14643)
## [v0.1.1] - 2019-08-05
### Features
- **GUI:** Implement route plotting and fuzzy search [c290d5e](https://gitlab.com/Earthnuker/ed_lrr/commit/c290d5e)
## [v0.1.0] - 2019-07-22
### Features
- **GUI:** Add Download functionality, update Rust code, update Python code [ec3972b](https://gitlab.com/Earthnuker/ed_lrr/commit/ec3972b)
## v0.0.0 - 2019-07-15
[Unreleased]: https://gitlab.com/Earthnuker/ed_lrr/compare/v0.14.0...HEAD
[v0.14.0]: https://gitlab.com/Earthnuker/ed_lrr/compare/v0.13.0...v0.14.0
[v0.13.0]: https://gitlab.com/Earthnuker/ed_lrr/compare/v0.12.0...v0.13.0
[v0.12.0]: https://gitlab.com/Earthnuker/ed_lrr/compare/v0.11.0...v0.12.0
[v0.11.0]: https://gitlab.com/Earthnuker/ed_lrr/compare/v0.10.0...v0.11.0
[v0.10.0]: https://gitlab.com/Earthnuker/ed_lrr/compare/v0.9.4...v0.10.0
[v0.9.4]: https://gitlab.com/Earthnuker/ed_lrr/compare/v0.9.3...v0.9.4
[v0.9.3]: https://gitlab.com/Earthnuker/ed_lrr/compare/v0.9.2...v0.9.3
[v0.9.2]: https://gitlab.com/Earthnuker/ed_lrr/compare/v0.9.1...v0.9.2
[v0.9.1]: https://gitlab.com/Earthnuker/ed_lrr/compare/v0.9.0...v0.9.1
[v0.9.0]: https://gitlab.com/Earthnuker/ed_lrr/compare/v0.8.1...v0.9.0
[v0.8.1]: https://gitlab.com/Earthnuker/ed_lrr/compare/v0.8.0...v0.8.1
[v0.8.0]: https://gitlab.com/Earthnuker/ed_lrr/compare/v0.7.1...v0.8.0
[v0.7.1]: https://gitlab.com/Earthnuker/ed_lrr/compare/v0.7.0...v0.7.1
[v0.7.0]: https://gitlab.com/Earthnuker/ed_lrr/compare/v0.6.2...v0.7.0
[v0.6.2]: https://gitlab.com/Earthnuker/ed_lrr/compare/v0.6.1...v0.6.2
[v0.6.1]: https://gitlab.com/Earthnuker/ed_lrr/compare/v0.6.0...v0.6.1
[v0.6.0]: https://gitlab.com/Earthnuker/ed_lrr/compare/v0.5.1...v0.6.0
[v0.5.1]: https://gitlab.com/Earthnuker/ed_lrr/compare/v0.5.0...v0.5.1
[v0.5.0]: https://gitlab.com/Earthnuker/ed_lrr/compare/v0.4.1...v0.5.0
[v0.4.1]: https://gitlab.com/Earthnuker/ed_lrr/compare/v0.4.0...v0.4.1
[v0.4.0]: https://gitlab.com/Earthnuker/ed_lrr/compare/v0.3.0...v0.4.0
[v0.3.0]: https://gitlab.com/Earthnuker/ed_lrr/compare/v0.2.0...v0.3.0
[v0.2.0]: https://gitlab.com/Earthnuker/ed_lrr/compare/v0.1.12...v0.2.0
[v0.1.12]: https://gitlab.com/Earthnuker/ed_lrr/compare/v0.1.11...v0.1.12
[v0.1.11]: https://gitlab.com/Earthnuker/ed_lrr/compare/v0.1.10...v0.1.11
[v0.1.10]: https://gitlab.com/Earthnuker/ed_lrr/compare/v0.1.9...v0.1.10
[v0.1.9]: https://gitlab.com/Earthnuker/ed_lrr/compare/v0.1.8...v0.1.9
[v0.1.8]: https://gitlab.com/Earthnuker/ed_lrr/compare/v0.1.7...v0.1.8
[v0.1.7]: https://gitlab.com/Earthnuker/ed_lrr/compare/v0.1.6...v0.1.7
[v0.1.6]: https://gitlab.com/Earthnuker/ed_lrr/compare/v0.1.5...v0.1.6
[v0.1.5]: https://gitlab.com/Earthnuker/ed_lrr/compare/v0.1.4...v0.1.5
[v0.1.4]: https://gitlab.com/Earthnuker/ed_lrr/compare/v0.1.3...v0.1.4
[v0.1.3]: https://gitlab.com/Earthnuker/ed_lrr/compare/v0.1.2...v0.1.3
[v0.1.2]: https://gitlab.com/Earthnuker/ed_lrr/compare/v0.1.1...v0.1.2
[v0.1.1]: https://gitlab.com/Earthnuker/ed_lrr/compare/v0.1.0...v0.1.1
[v0.1.0]: https://gitlab.com/Earthnuker/ed_lrr/compare/v0.0.0...v0.1.0

825
Cargo.lock generated
View File

@ -1,825 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "aho-corasick"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "ansi_term"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "atty"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)",
"termion 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "autocfg"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "bincode"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"autocfg 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
"byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.94 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "bitflags"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "block-buffer"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"block-padding 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
"byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
"generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "block-padding"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "bstr"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"regex-automata 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.94 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "byte-tools"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "byteorder"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "cfg-if"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "clap"
version = "2.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
"atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
"bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
"textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "clicolors-control"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "cloudabi"
version = "0.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "console"
version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
"clicolors-control 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"encode_unicode 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)",
"parking_lot 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 1.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
"termios 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "csv"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bstr 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"csv-core 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
"ryu 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.94 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "csv-core"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "digest"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "ed_lrr"
version = "0.1.0"
dependencies = [
"bincode 1.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
"byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
"csv 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)",
"humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"indicatif 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
"permutohedron 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
"rstar 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.94 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 1.0.94 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)",
"sha3 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)",
"structopt 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "either"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "encode_unicode"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "fnv"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "fuchsia-cprng"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "generic-array"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"typenum 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "heck"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"unicode-segmentation 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "humantime"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "indicatif"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"console 0.7.7 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"number_prefix 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"parking_lot 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 1.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "itertools"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"either 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "itoa"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "keccak"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "lazy_static"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "libc"
version = "0.2.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "lock_api"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"scopeguard 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "memchr"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "num-traits"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"autocfg 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "number_prefix"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "numtoa"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "opaque-debug"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "parking_lot"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"lock_api 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"parking_lot_core 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "parking_lot_core"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
"cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
"redox_syscall 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)",
"rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"smallvec 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "pdqselect"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "permutohedron"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "proc-macro2"
version = "0.4.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "quick-error"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "quote"
version = "0.6.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rand"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"autocfg 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)",
"rand_chacha 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"rand_hc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"rand_isaac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"rand_jitter 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
"rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
"rand_pcg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"rand_xorshift 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rand_chacha"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"autocfg 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
"rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rand_core"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rand_core"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "rand_hc"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rand_isaac"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rand_jitter"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)",
"rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rand_os"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
"fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)",
"rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rand_pcg"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"autocfg 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
"rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rand_xorshift"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rdrand"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "redox_syscall"
version = "0.1.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "redox_termios"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"redox_syscall 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "regex"
version = "1.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"aho-corasick 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)",
"memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"regex-syntax 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)",
"thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"utf8-ranges 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "regex-automata"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "regex-syntax"
version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"ucd-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rstar"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"itertools 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
"num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"pdqselect 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.94 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rustc_version"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "ryu"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "ryu"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "scopeguard"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "semver"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "semver-parser"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "serde"
version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"serde_derive 1.0.94 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "serde_derive"
version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 0.15.39 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "serde_json"
version = "1.0.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)",
"ryu 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.94 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "sha3"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"block-buffer 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)",
"byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"digest 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
"keccak 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"opaque-debug 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "smallvec"
version = "0.6.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "strsim"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "structopt"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)",
"structopt-derive 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "structopt-derive"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 0.15.39 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "syn"
version = "0.15.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "termion"
version = "1.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)",
"numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"redox_syscall 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)",
"redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "termios"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "textwrap"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "thread_local"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "typenum"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "ucd-util"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "unicode-segmentation"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "unicode-width"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "unicode-xid"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "utf8-ranges"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "vec_map"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "winapi"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[metadata]
"checksum aho-corasick 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e6f484ae0c99fec2e858eb6134949117399f222608d84cadb3f58c1f97c2364c"
"checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
"checksum atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652"
"checksum autocfg 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "0e49efa51329a5fd37e7c79db4621af617cd4e3e5bc224939808d076077077bf"
"checksum bincode 1.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "9f04a5e50dc80b3d5d35320889053637d15011aed5e66b66b37ae798c65da6f7"
"checksum bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3d155346769a6855b86399e9bc3814ab343cd3d62c7e985113d46a0ec3c281fd"
"checksum block-buffer 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b"
"checksum block-padding 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "6d4dc3af3ee2e12f3e5d224e5e1e3d73668abbeb69e566d361f7d5563a4fdf09"
"checksum bstr 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "6cc0572e02f76cb335f309b19e0a0d585b4f62788f7d26de2a13a836a637385f"
"checksum byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
"checksum byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5"
"checksum cfg-if 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "b486ce3ccf7ffd79fdeb678eac06a9e6c09fc88d33836340becb8fffe87c5e33"
"checksum clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9"
"checksum clicolors-control 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "73abfd4c73d003a674ce5d2933fca6ce6c42480ea84a5ffe0a2dc39ed56300f9"
"checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
"checksum console 0.7.7 (registry+https://github.com/rust-lang/crates.io-index)" = "8ca57c2c14b8a2bf3105bc9d15574aad80babf6a9c44b1058034cdf8bd169628"
"checksum csv 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a54cd62557f353f140b42305fb4efcff2ae08e32fbabaf5b0929423000febb63"
"checksum csv-core 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "9b5cadb6b25c77aeff80ba701712494213f4a8418fcda2ee11b6560c3ad0bf4c"
"checksum digest 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "05f47366984d3ad862010e22c7ce81a7dbcaebbdfb37241a620f8b6596ee135c"
"checksum either 1.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5527cfe0d098f36e3f8839852688e63c8fff1c90b2b405aef730615f9a7bcf7b"
"checksum encode_unicode 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "90b2c9496c001e8cb61827acdefad780795c42264c137744cae6f7d9e3450abd"
"checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3"
"checksum fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
"checksum generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec"
"checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205"
"checksum humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3ca7e5f2e110db35f93b837c81797f3714500b81d517bf20c431b16d3ca4f114"
"checksum indicatif 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2c60da1c9abea75996b70a931bba6c750730399005b61ccd853cee50ef3d0d0c"
"checksum itertools 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5b8467d9c1cebe26feb08c640139247fac215782d35371ade9a2136ed6085358"
"checksum itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f"
"checksum keccak 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "67c21572b4949434e4fc1e1978b99c5f77064153c59d998bf13ecd96fb5ecba7"
"checksum lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14"
"checksum libc 0.2.58 (registry+https://github.com/rust-lang/crates.io-index)" = "6281b86796ba5e4366000be6e9e18bf35580adf9e63fbe2294aadb587613a319"
"checksum lock_api 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ed946d4529956a20f2d63ebe1b69996d5a2137c91913fe3ebbeff957f5bca7ff"
"checksum memchr 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2efc7bc57c883d4a4d6e3246905283d8dae951bb3bd32f49d6ef297f546e1c39"
"checksum num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "6ba9a427cfca2be13aa6f6403b0b7e7368fe982bfa16fccc450ce74c46cd9b32"
"checksum number_prefix 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "dbf9993e59c894e3c08aa1c2712914e9e6bf1fcbfc6bef283e2183df345a4fee"
"checksum numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef"
"checksum opaque-debug 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "93f5bb2e8e8dec81642920ccff6b61f1eb94fa3020c5a325c9851ff604152409"
"checksum parking_lot 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fa7767817701cce701d5585b9c4db3cdd02086398322c1d7e8bf5094a96a2ce7"
"checksum parking_lot_core 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cb88cb1cb3790baa6776844f968fea3be44956cf184fa1be5a03341f5491278c"
"checksum pdqselect 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4ec91767ecc0a0bbe558ce8c9da33c068066c57ecc8bb8477ef8c1ad3ef77c27"
"checksum permutohedron 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b687ff7b5da449d39e418ad391e5e08da53ec334903ddbb921db208908fc372c"
"checksum proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)" = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759"
"checksum quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9274b940887ce9addde99c4eee6b5c44cc494b182b97e73dc8ffdcb3397fd3f0"
"checksum quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)" = "faf4799c5d274f3868a4aae320a0a182cbd2baee377b378f080e16a23e9d80db"
"checksum rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca"
"checksum rand_chacha 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef"
"checksum rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b"
"checksum rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d0e7a549d590831370895ab7ba4ea0c1b6b011d106b5ff2da6eee112615e6dc0"
"checksum rand_hc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4"
"checksum rand_isaac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08"
"checksum rand_jitter 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b"
"checksum rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071"
"checksum rand_pcg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44"
"checksum rand_xorshift 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c"
"checksum rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2"
"checksum redox_syscall 0.1.54 (registry+https://github.com/rust-lang/crates.io-index)" = "12229c14a0f65c4f1cb046a3b52047cdd9da1f4b30f8a39c5063c8bae515e252"
"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76"
"checksum regex 1.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "0b2f0808e7d7e4fb1cb07feb6ff2f4bc827938f24f8c2e6a3beb7370af544bdd"
"checksum regex-automata 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "3ed09217220c272b29ef237a974ad58515bde75f194e3ffa7e6d0bf0f3b01f86"
"checksum regex-syntax 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)" = "9d76410686f9e3a17f06128962e0ecc5755870bb890c34820c7af7f1db2e1d48"
"checksum rstar 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "dd08ae4f9661517777346592956ea6cdbba2895a28037af7daa600382f4b4001"
"checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
"checksum ryu 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "b96a9549dc8d48f2c283938303c4b5a77aa29bfbc5b54b084fb1630408899a8f"
"checksum ryu 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c92464b447c0ee8c4fb3824ecc8383b81717b9f1e74ba2e72540aef7b9f82997"
"checksum scopeguard 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b42e15e59b18a828bbf5c58ea01debb36b9b096346de35d941dcb89009f24a0d"
"checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
"checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
"checksum serde 1.0.94 (registry+https://github.com/rust-lang/crates.io-index)" = "076a696fdea89c19d3baed462576b8f6d663064414b5c793642da8dfeb99475b"
"checksum serde_derive 1.0.94 (registry+https://github.com/rust-lang/crates.io-index)" = "ef45eb79d6463b22f5f9e16d283798b7c0175ba6050bc25c1a946c122727fe7b"
"checksum serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)" = "5a23aa71d4a4d43fdbfaac00eff68ba8a06a51759a89ac3304323e800c4dd40d"
"checksum sha3 0.8.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dd26bc0e7a2e3a7c959bc494caf58b72ee0c71d67704e9520f736ca7e4853ecf"
"checksum smallvec 0.6.10 (registry+https://github.com/rust-lang/crates.io-index)" = "ab606a9c5e214920bb66c458cd7be8ef094f813f20fe77a54cc7dbfff220d4b7"
"checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
"checksum structopt 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)" = "16c2cdbf9cc375f15d1b4141bc48aeef444806655cd0e904207edc8d68d86ed7"
"checksum structopt-derive 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)" = "53010261a84b37689f9ed7d395165029f9cc7abb9f56bbfe86bee2597ed25107"
"checksum syn 0.15.39 (registry+https://github.com/rust-lang/crates.io-index)" = "b4d960b829a55e56db167e861ddb43602c003c7be0bee1d345021703fac2fb7c"
"checksum termion 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6a8fb22f7cde82c8220e5aeacb3258ed7ce996142c77cba193f203515e26c330"
"checksum termios 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "72b620c5ea021d75a735c943269bb07d30c9b77d6ac6b236bc8b5c496ef05625"
"checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
"checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b"
"checksum typenum 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "612d636f949607bdf9b123b4a6f6d966dedf3ff669f7f045890d3a4a73948169"
"checksum ucd-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "535c204ee4d8434478593480b8f86ab45ec9aae0e83c568ca81abf0fd0e88f86"
"checksum unicode-segmentation 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1967f4cdfc355b37fd76d2a954fb2ed3871034eb4f26d60537d88795cfc332a9"
"checksum unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "882386231c45df4700b275c7ff55b6f3698780a650026380e72dabe76fa46526"
"checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
"checksum utf8-ranges 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "9d50aa7650df78abf942826607c62468ce18d9019673d4a2ebe1865dbb96ffde"
"checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a"
"checksum winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "f10e386af2b13e47c89e7236a7a14a086791a2b88ebad6df9bf42040195cf770"
"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"

View File

@ -1,26 +0,0 @@
[package]
name = "ed_lrr"
version = "0.1.0"
authors = ["Daniel Seiller <earthnuker@gmail.com>"]
edition = "2018"
repository = "https://gitlab.com/Earthnuker/ed_lrr.git"
license = "WTFPL"
[profile.release]
# debug=true
[dependencies]
csv = "1.1.0"
serde = "1.0.94"
serde_derive = "1.0.94"
rstar = {version="0.4.0",features=["serde"]}
humantime = "1.2.0"
structopt = "0.2.18"
permutohedron = "0.2.4"
serde_json = "1.0.39"
indicatif = "0.11.0"
fnv = "1.0.6"
bincode = "1.1.4"
sha3 = "0.8.2"
byteorder = "1.3.2"

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Daniel Seiller
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

4
MANIFEST.in Normal file
View File

@ -0,0 +1,4 @@
include rust/Cargo.toml
include rust/.cargo/config
recursive-include rust/src *
recursive-include ed_lrr_gui *

173
README.md
View File

@ -1,20 +1,153 @@
# Elite: Dangerous Long Range Router (Rust Version)
## Usage:
1. download `bodies.json` and `systemsWithCoordinates.json` from https://www.edsm.net/en/nightly-dumps/ and place them in the `dumps` folder
2. run `cargo +nightly install --path .` or `cargo +nightly install --git https://gitlab.com/Earthnuker/ed_lrr.git`
3. run `ed_lrr_pp --bodies dumps/bodies.json --systems dumps/systemsWithCoordinates.json`
- Alternatively run `process.py` in the `dumps` folder
4. run `ed_lrr --help`
## Dependencies
- Working nightly Rust Compiler (tested with `rustc 1.37.0-nightly (5d8f59f4b 2019-06-04)`)
- ~8GB of free RAM
- Optional:
- Python 3.7
- Pandas
- uJSON
# Elite: Dangerous Long-range router
## Features
- Five different routing algorithms:
- **Breadth-first search:**, always finds the shortest route but is quite slow
- **Bidirectional BFS:**: should give similar route quality to BFS but take less time (not yet implemented)
- **A-Star:** has an adjustable tradeoff between speed and quality
- **Greedy search:** always picks the next reachable star that's closest to the destination, very fast but very poor quality routes
- **Economic:** computes route with lowest possible fuel consumption (WIP)
- Nice GUI! (made with PyQt5)
- Two themes! (Dark and Light)
- Uses data from [EDSM](https://edsm.net/) for star data
- Only routes through scoopable systems, no more running out of fuel! (assuming you have a fuel scoop)
- Take fuel consumption into account and add refueling stops along the route (Not yet implemented)
- Precomputing of BFS routing graphs for:
- near-instant routing from your home system to any destination!
- near-instant routing any source back to your-home system (WIP)!
- near-instant routing between any pair of systems (Not yet implemented, see TODO)
- Routing code written in Rust, so it's quite speedy!
- Export routes as HTML, JSON, CSV and SVG (WIP)
- Automagically copy next jump destination into system clipboard (works by monitoring the player journal file) (Not yet implemented)
- (optional) Web interface with job queue for route computations and multi-user support
# Installation
## Prerequisites
- Python:
- conda ([Miniconda](https://docs.conda.io/en/latest/miniconda.html)/[Anaconda](https://www.anaconda.com/distribution/))
- [nox](https://nox.thea.codes/en/stable/index.html)
- Inno Setup Compiler
- Download from [here](http://www.jrsoftware.org/isdl.php)
- or install via [scoop](https://scoop.sh/) `scoop install inno-setup`)
- Visual Studio 2019
- nightly rust compiler (`x86_64-pc-windows-msvc`)
## Building an installer
(Assuming `conda` is in your `PATH`)
1. Run `nox -k build`
2. Grab the installer from `installer/Output/`
## Manual installation
(Assuming `conda` is in your `PATH`)
1. Start a Visual Studio 2019 x64 command prompt
2. Run the following commands:
```
conda install pycrypto ujson
pip install setuptools_rust
pip install .[all]
```
then you can run `ed_lrr -h` from your command prompt to get help
# Notes on FSD Fuel consumption and jump range
FSD Fuel consumption ([E:D Wiki](https://elite-dangerous.fandom.com/wiki/Frame_Shift_Drive#Hyperspace_Fuel_Equation)): $$Fuel = 0.001 \cdot l \cdot \left(\frac{dist \cdot \left(m_{fuel} +
m_{ship}\right)}{boost \cdot m_{opt}}\right)^{p}$$
Solving for $dist$ gives the jump range (in Ly) for a given amount of fuel (in tons) as: $$dist = \frac{boost \cdot m_{opt} \cdot \left(\frac{1000.0 \cdot \min\left(f_{max}, m_{fuel}\right)}{l}\right)^{\frac{1}{p}}}{m_{fuel} + m_{ship}}$$
Assuming $f_{max}$ tons of available fuel gives us the maximum jump range for a single jump as: $$dist_{max} = \frac{boost \cdot m_{opt} \cdot \left(\frac{1000.0 \cdot f_{max}}{l}\right)^{\frac{1}{p}}}{f_{max} + m_{ship}}$$
Since the guardian FSD booster increases the maximum jump range by $B_g$ Ly we can calculate a correction factor for the fuel consumption as: $$0.001 \cdot l \cdot \left(\frac{boost^{2} \cdot m_{opt} \cdot \left(\frac{1000.0 \cdot \min\left(f_{max}, m_{fuel}\right)}{l}\right)^{\frac{1}{p}}}{B_{g} \cdot \left(m_{fuel} + m_{ship}\right) + boost^{2} \cdot m_{opt} \cdot \left(\frac{1000.0 \cdot \min\left(f_{max}, m_{fuel}\right)}{l}\right)^{\frac{1}{p}}}\right)^{p}$$
Incorporating $e_{fuel}$ into the distance equation yields $$dist = \frac{boost \cdot m_{opt} \cdot \left(\frac{1000.0 \cdot e_{fuel} \cdot \min\left(f_{max}, m_{fuel}\right)}{l}\right)^{\frac{1}{p}}}{m_{fuel} + m_{ship}}$$
Expanding $e_{fuel}$ yields $$dist = \frac{boost \cdot m_{opt} \cdot \left(1.0 \cdot \left(\frac{boost^{2} \cdot m_{opt} \cdot \left(\frac{1000.0 \cdot \min\left(f_{max}, m_{fuel}\right)}{l}\right)^{\frac{1}{p}}}{B_{g} \cdot \left(m_{fuel} + m_{ship}\right) + boost^{2} \cdot m_{opt} \cdot \left(\frac{1000.0 \cdot \min\left(f_{max}, m_{fuel}\right)}{l}\right)^{\frac{1}{p}}}\right)^{p} \cdot \min\left(f_{max}, m_{fuel}\right)\right)^{\frac{1}{p}}}{m_{fuel} + m_{ship}}$$
Finally, Expanding $dist_{max}$ yields the full equation as $$dist = \frac{boost \cdot m_{opt} \cdot \left(\frac{1000000.0 \cdot \left(\frac{boost^{2} \cdot m_{opt} \cdot \left(\frac{1000.0 \cdot \min\left(f_{max}, m_{fuel}\right)}{l}\right)^{\frac{1}{p}}}{B_{g} \cdot \left(m_{fuel} + m_{ship}\right) + boost^{2} \cdot m_{opt} \cdot \left(\frac{1000.0 \cdot \min\left(f_{max}, m_{fuel}\right)}{l}\right)^{\frac{1}{p}}}\right)^{- p} \cdot \min\left(f_{max}, m_{fuel}\right)}{l^{2}}\right)^{\frac{1}{p}}}{m_{fuel} + m_{ship}}$$
Where:
- $Fuel$ is the fuel needed to jump (in tons)
- $l$ is the linear constant of your FSD (depends on the rating)
- $p$ is the power constant of your FSD (depends on the class)
- $m_{ship}$ is the mass of your ship (including cargo)
- $m_{fuel}$ is the amount of fuel in tons currently stored in your tanks
- $m_{opt}$ is the optimized mass of your FSD (in tons)
- $f_{max}$ is the maximum amount of fuel your FSD can use per jump
- $boost$ is the "boost factor" of your FSD (1.0 when jumping normally, 1.5 when supercharged by a white dwarf, 4.0 for a neutron star, etc)
- $dist$ is the distance you can jump with a given fuel amount
- $dist_{max}$ is the maximum distance you can jump (when $m_{fuel}=f_{max}$)
- $B_{g}$ is the amount of Ly added by your Guardian FSD Booster
- $e_{fuel}$ is the efficiency increase added by the Guardian FSD Booster
# Fuel multiplier (CMDR jonburnage)
$$F_f = f_{max} \cdot \left(\frac{B_{g} \cdot m_{ship}}{m_{opt}} + l \cdot \left(\frac{f_{max}}{l}\right)^{\frac{1}{p}}\right)^{- p}$$
# Fuel multiplier (Spansh)
$$F_f = 0.001 \cdot l \cdot \left(\frac{boost^{2} \cdot m_{opt} \cdot \left(\frac{1000.0 \cdot \min\left(f_{max}, m_{fuel}\right)}{l}\right)^{\frac{1}{p}}}{\left(B_{g} + \frac{boost^{2} \cdot m_
{opt} \cdot \left(\frac{1000.0 \cdot \min\left(f_{max}, m_{fuel}\right)}{l}\right)^{\frac{1}{p}}}{m_{fuel} + m_{ship}}\right) \cdot \left(m_{fuel} + m_{ship}\right)}\right)^{p}$$
$$ F_f = 0.001 \cdot l \cdot \left(\frac{boost^{2} \cdot m_{opt} \cdot \left(\frac{1000.0 \cdot \min\left(f_{max}, m_{fuel}\right)}{l}\right)^{\frac{1}{p}}}{B_{g} \cdot \left(m_{fuel} + m_{ship}\right) + boost^{2} \cdot m_{opt} \cdot \left(\frac{1000.0 \cdot \min\left(f_{max}, m_{fuel}\right)}{l}\right)^{\frac{1}{p}}}\right)^{p}$$
$$dist = \frac{boost \cdot m_{opt} \cdot \left(1.0 \cdot \left(\frac{boost^{2} \cdot m_{opt} \cdot \left(\frac{1000.0 \cdot \min\left(f_{max}, m_{fuel}\right)}{l}\right)^{\frac{1}{p}}}{B_{g} \cdot \left(m_{fuel} + m_{ship}\right) + boost^{2} \cdot m_{opt} \cdot \left(\frac{1000.0 \cdot \min\left(f_{max}, m_{fuel}\right)}{l}\right)^{\frac{1}{p}}}\right)^{p} \cdot \min\left(f_{max}, m_{fuel}\right)\right)^{\frac{1}{p}}}{m_{fuel} + m_{ship}}$$
# Misc Stuff
---
$$dist = \frac{- B_{g} \cdot m_{fuel} - B_{g} \cdot m_{ship} + boost \cdot m_{opt} \cdot \left(\frac{1000.0 \cdot e_{fuel} \cdot \min\left(f_{max}, m_{fuel}\right)}{l}\right)^{\frac{1}{p}}}{m_{fuel} + m_{ship}}$$
$$e_{fuel} = \frac{l \cdot \left(\frac{10.0^{- \frac{3.0}{p}} \cdot \left(B_{g} + dist\right) \cdot \left(m_{fuel} + m_{ship}\right)}{boost \cdot m_{opt}}\right)^{p}}{\min\left(f_{max}, m_{fuel}\right)}$$
---
# TODO
## Routing
- Implement Neutron Mode
- Filter for neutron stars, plot route, then plot "fine" router between waypoints
- What Jump-Range to use for neutron route? `max_range*4`?
- Implement Bidir BFS
- Optimized All-Pairs BFS for graph precomputation
- Take fuel consumption into account (WIP)
- Guardian Booster support (Done?)
- Economic routing
- Custom weights and filtering for routing
## GUI
- Implement estimate time to completion display for route computation and preprocessing (Done?)
- Export route as:
- JSON
- HTML (WIP)
- CSV
- SVG
## Installer
- Update PATH from installer
## Preprocessing
- Build index over `systemsWithCoordinates.json` instead of loading it into RAM (reuse modified `LineCache` from `router.rs`)
- Finish `galaxy.jsonl` preprocessor
- Implement Python interface to preprocessor
## Misc
- Luigi based Task queue for distributed routing
- Full route tree computation
- overlap elimination

32
appveyor.yml Normal file
View File

@ -0,0 +1,32 @@
image: Visual Studio 2019
platform: x64
branches:
only:
- pyqt_gui
- WIP
build: off
environment:
VCVARS: C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvarsall.bat
VCVARSARG: x64
MINICONDA: C:\Miniconda3-x64
INNO: C:\Program Files (x86)\Inno Setup 6
matrix:
- PY_VERSION: "3.5"
- PY_VERSION: "3.6"
- PY_VERSION: "3.7"
- PY_VERSION: "3.8"
install:
- set PATH=%MINICONDA%\\Library\\bin;%MINICONDA%\\Scripts;%USERPROFILE%\\.cargo\\bin;%PATH%
- set PATH=%INNO%;%PATH%
- appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe
- rustup-init -y --default-toolchain nightly --default-host x86_64-pc-windows-msvc
- if defined VCVARS call "%VCVARS%" %VCVARSARG%
- conda activate
- conda update -y -n base -c defaults conda
- pip install nox
test_script:
- cmd: nox -s test-%PY_VERSION%
- ps: Get-ChildItem .\installer\Output\*.exe | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name }
- ps: Get-ChildItem .\dist\* | % { Push-AppveyorArtifact $_.FullName -FileName $_.Name.Insert($_.Name.Length,".zip") }

18
build_gui.py Normal file
View File

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
from PyQt5 import uic
import os
ui_path = os.path.dirname(os.path.abspath(__file__))
for root, folders, files in os.walk(ui_path):
if "site-packages" in root:
continue
if ".history" in folders:
folders.remove(".history")
for file in files:
file = os.path.join(root, file)
outfile, ext = os.path.splitext(file)
if ext == ".ui":
outfile = outfile + ".py"
print(file, "->", outfile)
with open(outfile, "w") as fh_out:
uic.compileUi(file, fh_out, from_imports=True)

View File

@ -0,0 +1,16 @@
#RABBITMQ
rabbitmqctl stop_app
rabbitmqctl reset
rabbitmqctl start_app
rabbitmqctl add_user ed_lrr ed_lrr
rabbitmqctl add_vhost ed_lrr
rabbitmqctl set_user_tags ed_lrr ed_lrr
rabbitmqctl set_permissions -p ed_lrr ed_lrr ".*" ".*" ".*"
rabbitmqctl set_permissions guest ".*" ".*" ".*"
rabbitmqctl set_permissions -p ed_lrr guest ".*" ".*" ".*"
Write-Host RabbitMQ setup done
#Celery
Write-Host starting Celery
celery worker -l info
#celery -A celery_test flower --presistent --broker=pyamqp://ed_lrr:ed_lrr@localhost/ed_lrr --broker_api=http://ed_lrr:ed_lrr@localhost:15672/api/

32
celery/celery_test.py Normal file
View File

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
from celery import Celery
import _ed_lrr
import os
os.environ.setdefault("CELERY_CONFIG_MODULE", "celeryconfig")
app = Celery("ed_lrr")
app.config_from_envvar("CELERY_CONFIG_MODULE")
@app.task(bind=True)
def route(self, hops, jmp_range):
def callback(state):
print("PRC: ", state.get("prc_done", 0.0))
self.update_state(state="PROGRESS", meta=state)
self.update_state(state="RUNNING", meta={})
return _ed_lrr.route(
hops,
jmp_range,
None,
"bfs",
True,
False,
False,
False,
0.0,
None,
r"C:\Users\Earthnuker\AppData\Local\ED_LRR\data\stars.csv",
0,
callback,
)

13
celery/celery_worker.py Normal file
View File

@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
from celery_test import route
import sys
if len(sys.argv) > 1:
job = route.AsyncResult(sys.argv[1])
if job.ready():
print([job, job.state, len(job.info), len(job.result)])
else:
print([job, job.state, job.info, job.result])
exit(0)
jobs = [route.delay(["Ix", "Colonia"], 48)]
print(jobs)

18
celery/celeryconfig.py Normal file
View File

@ -0,0 +1,18 @@
import os
os.environ["FORKED_BY_MULTIPROCESSING"] = "1"
broker_url = "pyamqp://ed_lrr:ed_lrr@localhost/ed_lrr"
broker_api = "http://guest:guest@localhost:15672/api/"
imports = ("celery_test",)
result_backend = "file://celery_results/"
result_persistent = True
task_track_started = True
task_time_limit = 60 * 60
result_extended = True
result_expires = None
worker_direct = True
worker_max_tasks_per_child = 10
worker_max_memory_per_child = 4 * 1024 * 1024 # 4GB
worker_state_db = "ed_lrr.state"
worker_send_task_events = True
worker_log_color = True

11
docs/.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,11 @@
{
"spellright.language": [
"de",
"en"
],
"spellright.documentTypes": [
"markdown",
"latex",
"plaintext"
]
}

48
docs/Makefile Normal file
View File

@ -0,0 +1,48 @@
MD = $(wildcard src/*.md)
DOTS = $(wildcard src/*.dot)
ASYS = $(wildcard src/*.asy)
PYS = $(wildcard src/img_*.py)
PDFS = $(MD:src/%.md=out/%.pdf)
IMG_PDFS = $(ASYS:src/%.asy=img/%.pdf) $(PYS:src/img_%.py=img/%.pdf) $(DOTS:src/%.dot=img/%.pdf)
IMGS = $(IMG_PDFS)
TEMPLATE = eisvogel
PDF_ENGINE = xelatex
PANDOC = pandoc
PANDOC_OPTIONS = -F panflute -F pandoc-citeproc --pdf-engine=$(PDF_ENGINE) --template $(TEMPLATE) -N --standalone --listings
GRAPHVIZ = dot
GRAPHVIZ_OPTIONS = -Tpdf
ASY = asy
ASY_OPTIONS = -noV -f pdf
PYTHON = python
PYTHON_OPTIONS =
mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST)))
current_dir := $(notdir $(patsubst %/,%,$(dir $(mkfile_path))))
.PHONY: clean all default
all: $(PDFS)
default: all
out/%.pdf: src/%.md $(IMGS) Makefile
$(PANDOC) $(PANDOC_OPTIONS) -o $@ $<
img/%.pdf: src/%.dot
$(GRAPHVIZ) $(GRAPHVIZ_OPTIONS) -o $@ $<
img/%.pdf: src/img_%.py
$(PYTHON) $(PYTHON_OPTIONS) $< $@
img/%.pdf: src/%.asy
$(ASY) $(ASY_OPTIONS) -o $@ $<
watch:
watchexec -w src -w data -w filters -w Makefile make all
clean:
-rm $(PDFS) $(IMGS)

158
docs/filters/multifilter.py Normal file
View File

@ -0,0 +1,158 @@
# -*- coding: utf-8 -*-
import contextlib
import csv
import datetime
import hashlib
import io
import os
import re
import subprocess as SP
import sys
import tempfile
from functools import partial
import panflute as pf
from dateutil.parser import parse as dateparse
from jinja2 import Environment, PackageLoader, Template, select_autoescape
from panflute import *
def remove_pound(elem, doc):
if type(elem) == Str:
return Str(elem.text.lstrip("#"))
def fix_color(elem, doc):
if type(elem) == MetaMap:
for k in elem.content:
if k.endswith("-color"):
elem[k] = elem[k].walk(remove_pound)
def update_date(elem, doc):
if type(elem) == MetaMap:
datefmt = doc.get_metadata("datefmt", "%Y-%m-%d")
today = datetime.date.today().strftime(datefmt)
date = dateparse(doc.get_metadata("date", today)).date()
elem["date"] = MetaInlines(Str(date.strftime(datefmt)))
return elem
def csv_table(elem, doc):
if type(elem) == Para and len(elem.content) == 1 and type(elem.content[0]) == Image:
elem = elem.content[0]
ext = os.path.splitext(elem.url)[1][1:]
if ext == "csv":
caption = elem.content
has_header = elem.attributes.get("has-header", "false").lower() == "true"
with open(elem.url) as f:
reader = csv.reader(f)
body = []
for row in reader:
cells = [TableCell(Plain(Str(x))) for x in row]
body.append(TableRow(*cells))
header = body.pop(0) if has_header else None
ret = Table(*body, header=header, caption=caption)
return ret
def code_refs(elem, doc):
if type(elem) == Cite:
label = elem.content[0]
if type(label) == Str:
label = label.text
filename = re.findall(r"^\[@lst:(.*)\]$", label) or [None]
if filename[0] in doc.inc_files:
return [
RawInline(
"\\hyperref[{}]{{{}}}".format(filename[0], filename[0]),
format="tex",
)
]
def include_code(elem, doc):
if type(elem) == CodeBlock:
if "include" in elem.attributes:
filepath = elem.attributes.pop("include")
filename = os.path.split(filepath)[-1]
try:
elem.text += elem.text + open(filepath, encoding="utf-8").read()
elem.attributes["caption"] = filename
doc.inc_files.append(filename)
except Exception as e:
elem.text += "Error: {}".format(e)
return [RawBlock("\\label{{{}}}".format(filename), format="tex"), elem]
def py_eval(options, data, element, doc):
out_buffer = io.StringIO()
with contextlib.redirect_stdout(out_buffer):
exec(data, doc.pyenv)
out_buffer.seek(0)
return convert_text(out_buffer.read())
def jinja_py_filt(doc, file):
env = {}
code = open(file, encoding="utf-8").read()
exec(code, env)
return env["main"](doc)
def prepare(doc):
doc.inc_files = []
doc.env = Environment()
doc.pyenv = {}
filters = {"py": partial(jinja_py_filt, doc)}
doc.env.filters.update(filters)
def process_templates(elem, doc):
if type(elem) == CodeBlock:
if elem.classes == ["@"]:
args = {"meta": doc.get_metadata()}
return convert_text(doc.env.from_string(elem.text).render(args))
def yaml_filt(elem, doc):
tags = {"eval": py_eval}
return yaml_filter(elem, doc, tags=tags, strict_yaml=True)
def checkboxes(elem, doc):
if type(elem) in [Para, Plain]:
val = re.findall(r"^\[([xX]|\ )\] (.*)$", stringify(elem))
if val:
val = val[0][0].lower() == "x"
else:
return elem
marker = {
True: RawInline("$\\boxtimes$", format="latex"),
False: RawInline("$\\square$", format="latex"),
}[val]
cont = []
if val:
cont += elem.content[2:]
else:
cont += elem.content[4:]
return Plain(marker, Space, *cont)
return elem
def main(doc=None):
f = [
process_templates,
update_date,
csv_table,
include_code,
fix_color,
code_refs,
yaml_filt,
checkboxes,
]
return run_filters(f, prepare=prepare, doc=doc)
if __name__ == "__main__":
main()

92
docs/src/ed-lrr.md Normal file
View File

@ -0,0 +1,92 @@
---
# Metadata
title: ED_LRR
author:
- Daniel Seiller <earthnuker@gmail.com>
subtitle: 'Elite Dangerous: Long-Range Router'
# Formating
toc: true
lang: en
colorlinks: true
papersize: a4
numbersections: true
#Panflute options
panflute-filters: [multifilter]
panflute-path: 'filters'
#Template options
titlepage: true
toc-own-page: false
---
\pagebreak
# Implementation
## `stars.csv` format
### Columns
| Name | Content |
| --------- | ------------------------------------------------------------ |
| id | unique ID-Number (not equal to id or id64, just a sequential number) |
| star_type | Type of Star |
| system | Name of System |
| body | Name of Star |
| mult | Jump Range Multiplier (1.5 for White Dwarfs, 4.0 for Neutron Stars) |
| distance | Distance from arrival in Ls |
| x,y,z | Position in Galactic Coordinates with Sol at (0,0,0) |
## `stars.idx` format
`bincode` serialized data:
- **[u64]**: List of byte offset for records (entry 0=first recod, entry 1= second record, etc)
## Routing Algorithms
### Breadth-First Search (BFS)
Standard Breadth-First Search, always finds the shortest route
### A*-Search
Modified A*-Search with adjustable "greediness". Priority Queue weighted by $\text{number of jumps from start system} + (\text{estimated number of jumps to target system} * \text{greediness})$
A greediness of 0 is equivalent to BFS and a greediness of $\infty$ is equivalent to Greedy-Search
### Greedy-Search
Priority Queue weighted by minimum distance to target, prefers systems with high multiplier (Neutron Stars and White Dwarfs)
## Optimizations
### Ellipse elimination
Only consider systems within an ellipsoid with source and destination as the foci, the width of the ellipsoid is adjustable
## Routing Graphs
### File format
`bincode` serialized data:
- *bool* **primary**: flag to indicate that graph only includes primary stars
- *f32* **range**: jump range for routing graph
- *[u8]* **file_hash**: sha3 hash of `stars.csv` from which graph was generated
- *String* **path**: path to `stars.csv` from which graph was generated
- *FnvHashMap* **graph**: Hashmap mapping systems to the systems from which they can be rached, traversed from destination system backwards to source to reconstruct route
# Usage
<!--
TODO: Add screenshots
-->
## Preprocessing Data
## Plotting a Route
# [Changelog](https://gitlab.com/Earthnuker/ed_lrr/blob/pyqt_gui/CHANGELOG.md)

83
docs/src/img_out.py Normal file
View File

@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
import heapq
import sys
import numpy as np
import pylab as PL
from scipy.spatial.ckdtree import cKDTree
exit()
def vec(a, b):
return b - a
def bfs(points):
return
def in_ellipse(p, f1, f2, r, offset=0):
df = ((f1 - f2) ** 2).sum(0) ** 0.5
d_f1 = ((p - f1) ** 2).sum(1) ** 0.5
d_f2 = ((p - f2) ** 2).sum(1) ** 0.5
return (d_f1 + d_f2) < (df * (1 + r))
num_points = 100000
p_orig = np.random.normal(0, 10, size=(num_points, 2))
tree = cKDTree(p_orig)
f1 = np.array([0, -30])
f2 = -f1 # np.random.normal(0, 20, (3,))
# r = 2 ** ((n / cnt) - cnt)
mask = in_ellipse(p_orig, f1, f2, 0.1)
p = p_orig[mask]
p_orig = p_orig[~mask]
colors = np.random.random(p.shape[0])
fig = PL.gcf()
PL.scatter(
p_orig[:, 0],
p_orig[:, 1],
marker=".",
s=0.2,
edgecolor="None",
c=[(0.0, 0.0, 0.0)],
alpha=0.75,
rasterized=True,
)
PL.scatter(
p[:, 0],
p[:, 1],
marker="s",
s=0.2,
edgecolor="None",
c=colors,
rasterized=True
)
PL.plot(f1[0], f1[1], "r.", label="Source")
PL.plot(f2[0], f2[1], "g.", label="Destination")
max_v = max(
p_orig[:, 0].max(),
p_orig[:, 1].max(),
f1[0], f1[1],
f2[0], f2[1]
) + 2
min_v = min(
p_orig[:, 0].min(),
p_orig[:, 1].min(),
f1[0], f1[1],
f2[0], f2[1]
) - 2
PL.xlim(min_v, max_v)
PL.ylim(min_v, max_v)
PL.legend()
PL.savefig(sys.argv[1], dpi=1200)

View File

@ -1,180 +0,0 @@
import ujson as json
from tqdm import tqdm
from pprint import pprint
import itertools as ITT
import os
import sys
import csv
import sqlite3
import pandas as pd
from urllib.parse import urljoin
def is_scoopable(entry):
first = entry.type.split()[0]
return first == "Neutron" or first == "White" or first in "KGBFOAM"
def get_mult(name):
try:
first = name.split()[0]
except:
return 1
if first == "Neutron":
return 4
if first == "White":
return 1.5
return 1
def dict_factory(cursor, row):
d = {}
for idx, col in enumerate(cursor.description):
d[col[0]] = row[idx]
return d
def blocks(files, size=65536):
while True:
b = files.read(size)
if not b:
break
yield b
def getlines(f, fn, show_progbar=False):
f.seek(0, 2)
size = f.tell()
f.seek(0)
progbar = tqdm(
desc="Processing " + fn,
total=size,
unit="b",
unit_scale=True,
unit_divisor=1024,
ascii=True,
leave=True,
disable=(not show_progbar),
)
buffer = []
for block in blocks(f):
progbar.n = f.tell()
progbar.update(0)
if buffer:
buffer += (buffer.pop(0) + block).splitlines(keepends=True)
else:
buffer += block.splitlines(keepends=True)
while buffer and buffer[0].endswith("\n"):
try:
yield json.loads(buffer.pop(0).strip().rstrip(","))
except ValueError:
pass
while buffer:
try:
yield json.loads(buffer.pop(0).strip().rstrip(","))
except ValueError:
pass
def process_file(fn, show_progbar=False):
with open(fn, "r") as f:
for line in tqdm(
getlines(f, fn, show_progbar),
desc=fn,
unit=" lines",
unit_scale=True,
ascii=True,
leave=True,
disable=(not show_progbar),
):
yield line
if not (
os.path.isfile("bodies.json") and os.path.isfile("systemsWithCoordinates.json")
):
exit(
"Please download bodies.json and systemsWithCoordinates.json from https://www.edsm.net/en/nightly-dumps/"
)
if not os.path.isfile("stars.jl"):
print("Filtering for Stars")
with open("stars.jl", "w") as neut:
for body in process_file("bodies.json", True):
T = body.get("type") or ""
if "Star" in T:
neut.write(json.dumps(body) + "\n")
def load_systems(load=False):
load = not os.path.isfile("systems.db")
cache = sqlite3.connect("systems.db")
cache.row_factory = dict_factory
c = cache.cursor()
if load:
print("Caching Systems")
c.execute("DROP TABLE IF EXISTS systems")
c.execute(
"CREATE TABLE systems (id64 int primary key, name text, x real, y real, z real)"
)
cache.commit()
recs = []
for system in process_file("systemsWithCoordinates.json", True):
rec = [
system["id64"],
system["name"],
system["coords"]["x"],
system["coords"]["y"],
system["coords"]["z"],
]
recs.append(rec)
if len(recs) % 1024 * 1024 == 0:
c.executemany("INSERT INTO systems VALUES (?,?,?,?,?)", recs)
recs.clear()
c.executemany("INSERT INTO systems VALUES (?,?,?,?,?)", recs)
cache.commit()
return cache, c
if not os.path.isfile("stars.csv"):
cache, cur = load_systems()
rows = []
with open("stars.csv", "w", newline="") as sys_csv:
csv_writer = csv.writer(sys_csv, dialect="excel")
for neut in process_file("stars.jl", True):
cur.execute(
"SELECT * FROM systems WHERE id64==?", (neut.get("systemId64"),)
)
system = cur.fetchone()
if not system:
continue
row = [
neut["systemId64"],
neut["subType"],
neut["name"],
get_mult(neut["subType"]),
system["x"],
system["y"],
system["z"],
]
rows.append(row)
if len(rows) > 1024:
csv_writer.writerows(rows)
rows.clear()
csv_writer.writerows(rows)
print()
cache.close()
if not os.path.isfile("stars.csv"):
tqdm.pandas(ascii=True, leave=True)
print("Loading data...")
data = pd.read_csv(
"stars.csv",
encoding="utf-8",
names=["id", "type", "name", "mult", "x", "y", "z"],
)
print("Cleaning data...")
data.type.fillna("Unknown", inplace=True)
data.drop_duplicates("id", inplace=True)
print("Writing CSV...")
data.to_csv("stars.csv", header=False, index=False)

View File

@ -1,2 +0,0 @@
https://www.edsm.net/dump/systemsWithCoordinates.json
https://www.edsm.net/dump/bodies.json

6
ed_lrr_gui/__init__.py Normal file
View File

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
from _ed_lrr import *
from .preprocess import Preprocessor
from .router import Router
from .config import cfg

440
ed_lrr_gui/__main__.py Normal file
View File

@ -0,0 +1,440 @@
# -*- coding: utf-8 -*-
import sys
import multiprocessing as MP
import queue
import ctypes
import os
from datetime import datetime
import click
from tqdm import tqdm
import requests as RQ
from urllib.parse import urljoin
from ed_lrr_gui import Router, Preprocessor, cfg
from _ed_lrr import PyRouter
from dotenv import load_dotenv
load_dotenv()
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
stars_path = os.path.join(cfg["folders.data_dir"], "stars.csv")
for folder in cfg["history.stars_csv_path"]:
file = os.path.join(folder, "stars.csv")
if os.path.isfile(file):
stars_path = file
break
systems_path = os.path.join(cfg["folders.data_dir"], "systemsWithCoordinates.json")
for file in cfg["history.systems_path"]:
if os.path.isfile(file):
systems_path = file
break
bodies_path = os.path.join(cfg["folders.data_dir"], "bodies.json")
for file in cfg["history.bodies_path"][::-1]:
if os.path.isfile(file):
bodies_path = file
break
@click.group(invoke_without_command=True, context_settings=CONTEXT_SETTINGS)
@click.pass_context
@click.version_option()
def main(ctx):
"Elite: Dangerous long range router, command line interface."
MP.freeze_support()
if ctx.invoked_subcommand != "config":
os.makedirs(cfg["folders.data_dir"], exist_ok=True)
if ctx.invoked_subcommand==None:
click.forward(gui_main)
return
@main.command()
@click.option("--port", "-p", help="Port to bind to", type=int, default=3777)
@click.option("--host", "-h", help="Address to bind to", type=str, default="0.0.0.0")
@click.option("--debug", "-d", is_flag=True, help="Run using debug server")
def web(host, port, debug):
"Run web interface."
from gevent import monkey
monkey.patch_all()
from gevent.pywsgi import WSGIServer
from ed_lrr_gui.web import app
with app.test_client() as c:
c.get("/") # Force before_first_request hook to run
if debug:
app.debug = True
app.run(host=host, port=port, debug=True)
else:
print("Listening on {}:{}".format(host, port))
server = WSGIServer((host, port), app)
server.serve_forever()
@main.command()
@click.argument("option", default=None, required=False)
@click.argument("value", default=None, required=False)
def config(option, value):
"""Change configuration.
If "key" and "value" are both omitted the current configuration is printed
"""
def print_config(key):
default = cfg.section(key).default()
comment = cfg.section(key).comment
value = cfg[key]
is_default = value == default
if (
isinstance(value, list)
and all(isinstance(element, str) for element in value)
and len(value) != 0
):
value = "[{}]".format(", ".join(map("'{}'".format, value)))
key = click.style("{}".format(key), fg="cyan")
value = click.style("{}".format(value), fg="green")
default = click.style("{}".format(default), fg="blue")
comment = click.style("{}".format(comment), fg="yellow")
if is_default:
print("{}: {} # {}".format(key, default, comment))
else:
print("{}: {} (default: {}) # {}".format(key, value, default, comment))
if option is None and value is None:
click.secho("Config path: {}".format(cfg.sources[0]), bold=True)
print()
for key in cfg:
print_config(key)
return
if value is None:
if option in cfg:
print_config(option)
else:
print("Invalid option:", option)
return
cfg[option] = value
cfg.sync()
return
@main.command()
def explore():
"Open file manager in data folder."
click.launch(cfg["folders.data_dir"], locate=True)
@main.command()
@click.option("--debug", help="Enable debug output", is_flag=True)
def gui(debug):
"Run the ED LRR GUI (default)."
import ed_lrr_gui.gui as ED_LRR_GUI
if (not debug) and os.name == "nt":
ctypes.windll.kernel32.FreeConsole()
sys.stdin = open("NUL", "rt")
sys.stdout = open("NUL", "wt")
sys.stderr = open("NUL", "wt")
sys.exit(ED_LRR_GUI.main())
@main.command()
@click.option(
"--url",
"-u",
help="Base URL",
default="https://www.edsm.net/dump/",
show_default=True,
)
@click.option(
"--folder",
"-f",
help="Target folder for downloads",
default=cfg["folders.data_dir"],
type=click.Path(exists=True, dir_okay=True, file_okay=False),
show_default=True,
)
def download(url, folder):
"Download EDSM dumps."
os.makedirs(folder, exist_ok=True)
for file_name in ["systemsWithCoordinates.json", "bodies.json"]:
download_url = urljoin(url, file_name)
download_path = os.path.join(folder, file_name)
if os.path.isfile(download_path):
try:
if not click.confirm(
"{} already exissts, overwrite?".format(file_name)
):
continue
except click.Abort:
exit("Canceled!")
size = RQ.head(download_url, headers={"Accept-Encoding": "None"})
size.raise_for_status()
size = int(size.headers.get("Content-Length", 0))
with tqdm(
total=size,
desc="{}".format(file_name),
unit="b",
unit_divisor=1024,
unit_scale=True,
ascii=True,
smoothing=0,
) as pbar:
with open(download_path, "wb") as of:
resp = RQ.get(
download_url, stream=True, headers={"Accept-Encoding": "gzip"}
)
for chunk in resp.iter_content(1024 * 1024):
of.write(chunk)
pbar.update(len(chunk))
click.pause()
@main.command()
@click.option(
"--systems",
"-s",
default=systems_path,
metavar="<path>",
help="Path to systemsWithCoordinates.json",
type=click.Path(exists=True, dir_okay=False),
show_default=True,
)
@click.option(
"--bodies",
"-b",
default=bodies_path,
metavar="<path>",
help="Path to bodies.json",
type=click.Path(exists=True, dir_okay=False),
show_default=True,
)
@click.option(
"--output",
"-o",
default=stars_path,
metavar="<path>",
help="Path to stars.csv",
type=click.Path(exists=False, dir_okay=False),
show_default=True,
)
def preprocess(systems, bodies, output):
"Preprocess EDSM dumps."
with click.progressbar(
length=100, label="", show_percent=True, item_show_func=lambda v: v, width=50
) as pbar:
preproc = Preprocessor(systems, bodies, output)
preproc.start()
state = {}
pstate = {}
while not (preproc.queue.empty() and not preproc.is_alive()):
try:
event = preproc.queue.get(True, 0.1)
state.update(event)
if state != pstate:
prc = (state["status"]["done"] / state["status"]["total"]) * 100
pbar.pos = prc
pbar.update(0)
pbar.current_item = state["status"]["message"]
pstate = state.copy()
except queue.Empty:
pass
pbar.pos = 100
pbar.update(0)
print(state.get("result"))
print("DONE!")
click.pause()
@main.command()
@click.option(
"--path",
"-i",
required=True,
metavar="<path>",
help="Path to stars.csv",
default=stars_path,
type=click.Path(exists=True, dir_okay=False),
show_default=True,
)
@click.option(
"--precomp_file",
"-pf",
metavar="<path>",
help="Precomputed routing graph to use",
type=click.Path(exists=True, dir_okay=False),
)
@click.option(
"--range",
"-r",
default=cfg["route.range"],
metavar="<float>",
help="Jump range (Ly)",
type=click.FloatRange(min=0),
show_default=True,
)
@click.option(
"--prune",
"-d",
default=(cfg["route.prune.steps"], cfg["route.prune.min_improvement"]),
metavar="<n> <m>",
help="Prune search branches",
nargs=2,
type=click.Tuple([click.IntRange(min=0), click.FloatRange(min=0)]),
show_default=True,
)
@click.option(
"--permute",
"-p",
type=click.Choice(["all", "keep_first", "keep_last", "keep_both"]),
default=None,
help="Permute hops to find shortest route",
show_default=True,
)
@click.option(
"--primary/--no-primary",
"+ps/-ps",
is_flag=True,
default=cfg["route.primary"],
help="Only route through primary stars",
show_default=True,
)
@click.option(
"--factor",
"-g",
metavar="<float>",
default=cfg["route.greediness"],
help="Greedyness factor for A-Star",
show_default=True,
)
@click.option(
"--mode",
"-m",
default=cfg["route.mode"],
help="Search mode",
type=click.Choice(["bfs", "bfs_old", "a-star", "greedy"]),
show_default=True,
)
@click.option(
"--workers",
"-w",
metavar="<int>",
default=1,
help="Number of worker threads (more is not always better)",
show_default=True,
)
@click.argument("systems", nargs=-1)
def route(**kwargs):
"Compute a route."
if len(kwargs["systems"]) < 2:
exit("Need at least two systems to plot a route")
if kwargs["prune"] == (0, 0):
kwargs["prune"] = None
def to_string(state):
if state:
return "{prc_done:.2f}% [N:{depth} | Q:{queue_size} | D:{d_rem:.2f} Ly | S: {n_seen} ({prc_seen:.2f}%)] {system}".format(
**state
)
keep_first, keep_last = {
"all": (False, False),
"keep_first": (True, False),
"keep_last": (False, True),
"keep_both": (True, True),
None: (False, False),
}[kwargs["permute"]]
print("Resolving systems...")
t = datetime.today()
matches = find_sys(kwargs["systems"], kwargs["path"])
kwargs["systems"] = [str(matches[key][1]["id"]) for key in kwargs["systems"]]
print("Done in", datetime.today() - t)
args = [
kwargs["systems"],
kwargs["range"],
kwargs["prune"],
kwargs["mode"],
kwargs["primary"],
kwargs["permute"] != None,
keep_first,
keep_last,
kwargs["factor"],
None,
kwargs["path"],
kwargs["workers"],
]
with click.progressbar(
length=100,
label="Computing route",
show_percent=False,
item_show_func=to_string,
width=50,
) as pbar:
router = Router(*args)
t = datetime.today()
router.start()
state = {}
pstate = {}
while not (router.queue.empty() and router.is_alive() == False):
try:
event = router.queue.get(True, 0.1)
state.update(event)
if state != pstate:
pbar.current_item = state.get("status")
if pbar.current_item:
pbar.pos = pbar.current_item["prc_done"]
pbar.update(0)
pstate = state.copy()
except queue.Empty:
pass
pbar.pos = 100
pbar.update(0)
for n, jump in enumerate(state.get("return", []), 1):
jump["n"] = n
if jump["body"].find(jump["system"]) == -1:
jump["where"] = "[{body}] in [{system}]".format(**jump)
else:
jump["where"] = "[{body}]".format(**jump)
if jump["distance"] > 0:
print("({n}) {where}: {star_type} ({distance} Ls)".format(**jump))
else:
print("({n}) {where}: {star_type}".format(**jump))
print("Done in", datetime.today() - t)
@main.command()
@click.option(
"--path",
"-i",
required=True,
help="Path to stars.csv",
default=stars_path,
type=click.Path(exists=True, dir_okay=False),
show_default=True,
)
@click.option(
"--range", "-r", required=True, help="Jump range (Ly)", type=click.FloatRange(min=0)
)
@click.option("--primary", "-ps", help="Only route through primary stars")
@click.option(
"--output",
"-o",
required=True,
help="Output path",
default="./stars.idx",
type=click.Path(exists=False, dir_okay=False),
show_default=True,
)
@click.argument("systems", nargs=-1)
def precompute(*args, **kwargs):
"Precompute routing graph"
print("PreComp:", args, kwargs)
raise NotImplementedError
def gui_main():
return gui(False)
if __name__ == "__main__":
main()

65
ed_lrr_gui/config.py Normal file
View File

@ -0,0 +1,65 @@
# -*- coding: utf-8 -*-
import pathlib
from collections import namedtuple
import profig
import appdirs
import os
config_dir = pathlib.Path(appdirs.user_config_dir("ED_LRR", ""))
config_dir.mkdir(parents=True, exist_ok=True)
config_file = config_dir / "config.ini"
config_file.touch()
config_dir = str(config_dir)
cfg = profig.Config(str(config_file), strict=True)
cfg.init(
"history.bodies_url",
["https://www.edsm.net/dump/bodies.json"],
"path_list",
comment="history of bodies.json urls",
)
cfg.init(
"history.systems_url",
["https://www.edsm.net/dump/systemsWithCoordinates.json"],
"path_list",
comment="history of systems.json urls",
)
cfg.init(
"history.bodies_path",
[os.path.join(config_dir, "data", "bodies.json")],
"path_list",
comment="history of bodies.json download paths",
)
cfg.init(
"history.systems_path",
[os.path.join(config_dir, "data", "systemsWithCoordinates.json")],
"path_list",
comment="history of systems.json download paths",
)
cfg.init(
"history.stars_csv_path",
[os.path.join(config_dir, "data", "stars.csv")],
"path_list",
comment="history of paths for stars.csv",
)
cfg.init("route.range", 7.56, comment="jump range")
cfg.init("route.primary", False, comment="only route through primary stars")
cfg.init("route.mode", "bfs", comment="routing mode")
cfg.init(
"route.prune.min_improvement",
10.0,
comment="path needs to improve by at least (jump_range*min_improvement) in route.prune.steps",
)
cfg.init("route.prune.steps", 5, comment="number of steps before path gets pruned")
cfg.init("route.greediness", 0.5, comment="A* greediness")
cfg.init("folders.data_dir", os.path.join(config_dir, "data"), comment="Data directory")
cfg.init("GUI.theme", "dark", comment="GUI theme to use")
cfg.init("web.port", 3777, comment="Port to bind to")
cfg.init("web.host", "0.0.0.0", comment="Address to bind to")
cfg.init("web.debug", False, comment="Run using debug server")
cfg.sync()

View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from .main import main

484
ed_lrr_gui/gui/ed_lrr.py Normal file
View File

@ -0,0 +1,484 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'D:\devel\rust\ed_lrr_gui\ed_lrr_gui\gui\ed_lrr.ui'
#
# Created by: PyQt5 UI code generator 5.14.1
#
# WARNING! All changes made in this file will be lost!
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_ED_LRR(object):
def setupUi(self, ED_LRR):
ED_LRR.setObjectName("ED_LRR")
ED_LRR.setEnabled(True)
ED_LRR.resize(577, 500)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed
)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(ED_LRR.sizePolicy().hasHeightForWidth())
ED_LRR.setSizePolicy(sizePolicy)
ED_LRR.setMinimumSize(QtCore.QSize(577, 500))
ED_LRR.setMaximumSize(QtCore.QSize(577, 500))
ED_LRR.setStyleSheet("")
ED_LRR.setDocumentMode(False)
ED_LRR.setTabShape(QtWidgets.QTabWidget.Rounded)
self.centralwidget = QtWidgets.QWidget(ED_LRR)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred
)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.centralwidget.sizePolicy().hasHeightForWidth()
)
self.centralwidget.setSizePolicy(sizePolicy)
self.centralwidget.setObjectName("centralwidget")
self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget)
self.verticalLayout.setObjectName("verticalLayout")
self.tabs = QtWidgets.QTabWidget(self.centralwidget)
self.tabs.setEnabled(True)
self.tabs.setAutoFillBackground(False)
self.tabs.setTabPosition(QtWidgets.QTabWidget.North)
self.tabs.setTabShape(QtWidgets.QTabWidget.Rounded)
self.tabs.setElideMode(QtCore.Qt.ElideNone)
self.tabs.setTabsClosable(False)
self.tabs.setTabBarAutoHide(False)
self.tabs.setObjectName("tabs")
self.tab_download = QtWidgets.QWidget()
self.tab_download.setObjectName("tab_download")
self.formLayout = QtWidgets.QFormLayout(self.tab_download)
self.formLayout.setObjectName("formLayout")
self.lbl_bodies_dl = QtWidgets.QLabel(self.tab_download)
self.lbl_bodies_dl.setObjectName("lbl_bodies_dl")
self.formLayout.setWidget(
1, QtWidgets.QFormLayout.LabelRole, self.lbl_bodies_dl
)
self.lbl_systems_dl = QtWidgets.QLabel(self.tab_download)
self.lbl_systems_dl.setObjectName("lbl_systems_dl")
self.formLayout.setWidget(
3, QtWidgets.QFormLayout.LabelRole, self.lbl_systems_dl
)
self.inp_bodies_dl = QtWidgets.QComboBox(self.tab_download)
self.inp_bodies_dl.setEditable(True)
self.inp_bodies_dl.setInsertPolicy(QtWidgets.QComboBox.InsertAtTop)
self.inp_bodies_dl.setObjectName("inp_bodies_dl")
self.formLayout.setWidget(
1, QtWidgets.QFormLayout.FieldRole, self.inp_bodies_dl
)
self.inp_systems_dl = QtWidgets.QComboBox(self.tab_download)
self.inp_systems_dl.setEditable(True)
self.inp_systems_dl.setInsertPolicy(QtWidgets.QComboBox.InsertAtTop)
self.inp_systems_dl.setObjectName("inp_systems_dl")
self.formLayout.setWidget(
3, QtWidgets.QFormLayout.FieldRole, self.inp_systems_dl
)
self.gridLayout = QtWidgets.QGridLayout()
self.gridLayout.setObjectName("gridLayout")
self.inp_bodies_dest_dl = QtWidgets.QComboBox(self.tab_download)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed
)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.inp_bodies_dest_dl.sizePolicy().hasHeightForWidth()
)
self.inp_bodies_dest_dl.setSizePolicy(sizePolicy)
self.inp_bodies_dest_dl.setEditable(False)
self.inp_bodies_dest_dl.setInsertPolicy(QtWidgets.QComboBox.InsertAtTop)
self.inp_bodies_dest_dl.setObjectName("inp_bodies_dest_dl")
self.gridLayout.addWidget(self.inp_bodies_dest_dl, 0, 0, 1, 1)
self.btn_bodies_dest_browse_dl = QtWidgets.QPushButton(self.tab_download)
self.btn_bodies_dest_browse_dl.setObjectName("btn_bodies_dest_browse_dl")
self.gridLayout.addWidget(self.btn_bodies_dest_browse_dl, 0, 1, 1, 1)
self.formLayout.setLayout(2, QtWidgets.QFormLayout.FieldRole, self.gridLayout)
self.gridLayout_2 = QtWidgets.QGridLayout()
self.gridLayout_2.setObjectName("gridLayout_2")
self.btn_systems_dest_browse_dl = QtWidgets.QPushButton(self.tab_download)
self.btn_systems_dest_browse_dl.setObjectName("btn_systems_dest_browse_dl")
self.gridLayout_2.addWidget(self.btn_systems_dest_browse_dl, 0, 1, 1, 1)
self.inp_systems_dest_dl = QtWidgets.QComboBox(self.tab_download)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed
)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.inp_systems_dest_dl.sizePolicy().hasHeightForWidth()
)
self.inp_systems_dest_dl.setSizePolicy(sizePolicy)
self.inp_systems_dest_dl.setEditable(False)
self.inp_systems_dest_dl.setInsertPolicy(QtWidgets.QComboBox.InsertAtTop)
self.inp_systems_dest_dl.setObjectName("inp_systems_dest_dl")
self.gridLayout_2.addWidget(self.inp_systems_dest_dl, 0, 0, 1, 1)
self.formLayout.setLayout(4, QtWidgets.QFormLayout.FieldRole, self.gridLayout_2)
self.btn_download = QtWidgets.QPushButton(self.tab_download)
self.btn_download.setObjectName("btn_download")
self.formLayout.setWidget(5, QtWidgets.QFormLayout.LabelRole, self.btn_download)
self.label = QtWidgets.QLabel(self.tab_download)
self.label.setObjectName("label")
self.formLayout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.label)
self.label_2 = QtWidgets.QLabel(self.tab_download)
self.label_2.setObjectName("label_2")
self.formLayout.setWidget(4, QtWidgets.QFormLayout.LabelRole, self.label_2)
self.tabs.addTab(self.tab_download, "")
self.tab_preprocess = QtWidgets.QWidget()
self.tab_preprocess.setObjectName("tab_preprocess")
self.formLayout_3 = QtWidgets.QFormLayout(self.tab_preprocess)
self.formLayout_3.setObjectName("formLayout_3")
self.lbl_bodies_pp = QtWidgets.QLabel(self.tab_preprocess)
self.lbl_bodies_pp.setObjectName("lbl_bodies_pp")
self.formLayout_3.setWidget(
0, QtWidgets.QFormLayout.LabelRole, self.lbl_bodies_pp
)
self.gr_bodies_pp = QtWidgets.QGridLayout()
self.gr_bodies_pp.setObjectName("gr_bodies_pp")
self.btn_bodies_browse_pp = QtWidgets.QPushButton(self.tab_preprocess)
self.btn_bodies_browse_pp.setObjectName("btn_bodies_browse_pp")
self.gr_bodies_pp.addWidget(self.btn_bodies_browse_pp, 0, 1, 1, 1)
self.inp_bodies_pp = QtWidgets.QComboBox(self.tab_preprocess)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed
)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.inp_bodies_pp.sizePolicy().hasHeightForWidth()
)
self.inp_bodies_pp.setSizePolicy(sizePolicy)
self.inp_bodies_pp.setEditable(False)
self.inp_bodies_pp.setInsertPolicy(QtWidgets.QComboBox.InsertAtTop)
self.inp_bodies_pp.setObjectName("inp_bodies_pp")
self.gr_bodies_pp.addWidget(self.inp_bodies_pp, 0, 0, 1, 1)
self.formLayout_3.setLayout(
0, QtWidgets.QFormLayout.FieldRole, self.gr_bodies_pp
)
self.lbl_systems_pp = QtWidgets.QLabel(self.tab_preprocess)
self.lbl_systems_pp.setObjectName("lbl_systems_pp")
self.formLayout_3.setWidget(
1, QtWidgets.QFormLayout.LabelRole, self.lbl_systems_pp
)
self.gr_systems_pp = QtWidgets.QGridLayout()
self.gr_systems_pp.setObjectName("gr_systems_pp")
self.btn_systems_browse_pp = QtWidgets.QPushButton(self.tab_preprocess)
self.btn_systems_browse_pp.setObjectName("btn_systems_browse_pp")
self.gr_systems_pp.addWidget(self.btn_systems_browse_pp, 0, 1, 1, 1)
self.inp_systems_pp = QtWidgets.QComboBox(self.tab_preprocess)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed
)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.inp_systems_pp.sizePolicy().hasHeightForWidth()
)
self.inp_systems_pp.setSizePolicy(sizePolicy)
self.inp_systems_pp.setEditable(False)
self.inp_systems_pp.setInsertPolicy(QtWidgets.QComboBox.InsertAtTop)
self.inp_systems_pp.setObjectName("inp_systems_pp")
self.gr_systems_pp.addWidget(self.inp_systems_pp, 0, 0, 1, 1)
self.formLayout_3.setLayout(
1, QtWidgets.QFormLayout.FieldRole, self.gr_systems_pp
)
self.lbl_out_pp = QtWidgets.QLabel(self.tab_preprocess)
self.lbl_out_pp.setObjectName("lbl_out_pp")
self.formLayout_3.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.lbl_out_pp)
self.gr_out_grid_pp = QtWidgets.QGridLayout()
self.gr_out_grid_pp.setObjectName("gr_out_grid_pp")
self.btn_out_browse_pp = QtWidgets.QPushButton(self.tab_preprocess)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed
)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.btn_out_browse_pp.sizePolicy().hasHeightForWidth()
)
self.btn_out_browse_pp.setSizePolicy(sizePolicy)
self.btn_out_browse_pp.setObjectName("btn_out_browse_pp")
self.gr_out_grid_pp.addWidget(self.btn_out_browse_pp, 0, 1, 1, 1)
self.inp_out_pp = QtWidgets.QComboBox(self.tab_preprocess)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed
)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.inp_out_pp.sizePolicy().hasHeightForWidth())
self.inp_out_pp.setSizePolicy(sizePolicy)
self.inp_out_pp.setEditable(False)
self.inp_out_pp.setInsertPolicy(QtWidgets.QComboBox.InsertAtTop)
self.inp_out_pp.setObjectName("inp_out_pp")
self.gr_out_grid_pp.addWidget(self.inp_out_pp, 0, 0, 1, 1)
self.formLayout_3.setLayout(
2, QtWidgets.QFormLayout.FieldRole, self.gr_out_grid_pp
)
self.btn_preprocess = QtWidgets.QPushButton(self.tab_preprocess)
self.btn_preprocess.setObjectName("btn_preprocess")
self.formLayout_3.setWidget(
3, QtWidgets.QFormLayout.LabelRole, self.btn_preprocess
)
self.tabs.addTab(self.tab_preprocess, "")
self.tab_route = QtWidgets.QWidget()
self.tab_route.setObjectName("tab_route")
self.formLayout_2 = QtWidgets.QFormLayout(self.tab_route)
self.formLayout_2.setObjectName("formLayout_2")
self.lbl_sys_lst = QtWidgets.QLabel(self.tab_route)
self.lbl_sys_lst.setObjectName("lbl_sys_lst")
self.formLayout_2.setWidget(
0, QtWidgets.QFormLayout.LabelRole, self.lbl_sys_lst
)
self.gr_sys = QtWidgets.QGridLayout()
self.gr_sys.setObjectName("gr_sys")
self.btn_sys_lst_browse = QtWidgets.QPushButton(self.tab_route)
self.btn_sys_lst_browse.setObjectName("btn_sys_lst_browse")
self.gr_sys.addWidget(self.btn_sys_lst_browse, 0, 1, 1, 1)
self.inp_sys_lst = QtWidgets.QComboBox(self.tab_route)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed
)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.inp_sys_lst.sizePolicy().hasHeightForWidth())
self.inp_sys_lst.setSizePolicy(sizePolicy)
self.inp_sys_lst.setEditable(False)
self.inp_sys_lst.setInsertPolicy(QtWidgets.QComboBox.InsertAtTop)
self.inp_sys_lst.setFrame(True)
self.inp_sys_lst.setModelColumn(0)
self.inp_sys_lst.setObjectName("inp_sys_lst")
self.gr_sys.addWidget(self.inp_sys_lst, 0, 0, 1, 1)
self.formLayout_2.setLayout(0, QtWidgets.QFormLayout.FieldRole, self.gr_sys)
self.btn_add = QtWidgets.QPushButton(self.tab_route)
self.btn_add.setObjectName("btn_add")
self.formLayout_2.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.btn_add)
self.inp_sys = QtWidgets.QLineEdit(self.tab_route)
self.inp_sys.setObjectName("inp_sys")
self.formLayout_2.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.inp_sys)
self.btn_rm = QtWidgets.QPushButton(self.tab_route)
self.btn_rm.setObjectName("btn_rm")
self.formLayout_2.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.btn_rm)
self.gr_mode = QtWidgets.QGridLayout()
self.gr_mode.setObjectName("gr_mode")
self.rd_comp = QtWidgets.QRadioButton(self.tab_route)
self.rd_comp.setChecked(True)
self.rd_comp.setObjectName("rd_comp")
self.gr_mode.addWidget(self.rd_comp, 0, 1, 1, 1)
self.rd_precomp = QtWidgets.QRadioButton(self.tab_route)
self.rd_precomp.setObjectName("rd_precomp")
self.gr_mode.addWidget(self.rd_precomp, 0, 2, 1, 1)
self.formLayout_2.setLayout(3, QtWidgets.QFormLayout.FieldRole, self.gr_mode)
self.chk_permute = QtWidgets.QCheckBox(self.tab_route)
self.chk_permute.setObjectName("chk_permute")
self.formLayout_2.setWidget(
4, QtWidgets.QFormLayout.LabelRole, self.chk_permute
)
self.gridLayout_4 = QtWidgets.QGridLayout()
self.gridLayout_4.setObjectName("gridLayout_4")
self.chk_permute_keep_last = QtWidgets.QCheckBox(self.tab_route)
self.chk_permute_keep_last.setObjectName("chk_permute_keep_last")
self.gridLayout_4.addWidget(self.chk_permute_keep_last, 0, 3, 1, 1)
self.chk_permute_keep_first = QtWidgets.QCheckBox(self.tab_route)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed
)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.chk_permute_keep_first.sizePolicy().hasHeightForWidth()
)
self.chk_permute_keep_first.setSizePolicy(sizePolicy)
self.chk_permute_keep_first.setTristate(False)
self.chk_permute_keep_first.setObjectName("chk_permute_keep_first")
self.gridLayout_4.addWidget(self.chk_permute_keep_first, 0, 2, 1, 1)
self.lbl_keep = QtWidgets.QLabel(self.tab_route)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred
)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.lbl_keep.sizePolicy().hasHeightForWidth())
self.lbl_keep.setSizePolicy(sizePolicy)
self.lbl_keep.setObjectName("lbl_keep")
self.gridLayout_4.addWidget(self.lbl_keep, 0, 1, 1, 1)
self.formLayout_2.setLayout(
4, QtWidgets.QFormLayout.FieldRole, self.gridLayout_4
)
self.lst_sys = QtWidgets.QTreeWidget(self.tab_route)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding
)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.lst_sys.sizePolicy().hasHeightForWidth())
self.lst_sys.setSizePolicy(sizePolicy)
self.lst_sys.setMinimumSize(QtCore.QSize(0, 0))
self.lst_sys.setDragEnabled(True)
self.lst_sys.setDragDropOverwriteMode(False)
self.lst_sys.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove)
self.lst_sys.setDefaultDropAction(QtCore.Qt.MoveAction)
self.lst_sys.setAlternatingRowColors(True)
self.lst_sys.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.lst_sys.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
self.lst_sys.setHeaderHidden(True)
self.lst_sys.setObjectName("lst_sys")
self.lst_sys.headerItem().setText(0, "Name")
self.lst_sys.header().setVisible(False)
self.formLayout_2.setWidget(7, QtWidgets.QFormLayout.SpanningRole, self.lst_sys)
self.lbl_range = QtWidgets.QLabel(self.tab_route)
self.lbl_range.setObjectName("lbl_range")
self.formLayout_2.setWidget(8, QtWidgets.QFormLayout.LabelRole, self.lbl_range)
self.sb_range = QtWidgets.QDoubleSpinBox(self.tab_route)
self.sb_range.setObjectName("sb_range")
self.formLayout_2.setWidget(8, QtWidgets.QFormLayout.FieldRole, self.sb_range)
self.gr_opts = QtWidgets.QGridLayout()
self.gr_opts.setObjectName("gr_opts")
self.cmb_mode = QtWidgets.QComboBox(self.tab_route)
self.cmb_mode.setObjectName("cmb_mode")
self.cmb_mode.addItem("")
self.cmb_mode.addItem("")
self.cmb_mode.addItem("")
self.gr_opts.addWidget(self.cmb_mode, 0, 2, 1, 1)
self.lbl_greedyness = QtWidgets.QLabel(self.tab_route)
self.lbl_greedyness.setEnabled(True)
self.lbl_greedyness.setObjectName("lbl_greedyness")
self.gr_opts.addWidget(self.lbl_greedyness, 1, 1, 1, 1)
self.chk_primary = QtWidgets.QCheckBox(self.tab_route)
self.chk_primary.setObjectName("chk_primary")
self.gr_opts.addWidget(self.chk_primary, 0, 3, 1, 1)
self.sld_greedyness = QtWidgets.QSlider(self.tab_route)
self.sld_greedyness.setMaximum(100)
self.sld_greedyness.setPageStep(10)
self.sld_greedyness.setProperty("value", 50)
self.sld_greedyness.setOrientation(QtCore.Qt.Horizontal)
self.sld_greedyness.setTickPosition(QtWidgets.QSlider.TicksBelow)
self.sld_greedyness.setTickInterval(10)
self.sld_greedyness.setObjectName("sld_greedyness")
self.gr_opts.addWidget(self.sld_greedyness, 1, 2, 1, 2)
self.lbl_mode = QtWidgets.QLabel(self.tab_route)
self.lbl_mode.setObjectName("lbl_mode")
self.gr_opts.addWidget(self.lbl_mode, 0, 1, 1, 1)
self.formLayout_2.setLayout(9, QtWidgets.QFormLayout.SpanningRole, self.gr_opts)
self.btn_go = QtWidgets.QPushButton(self.tab_route)
self.btn_go.setFlat(False)
self.btn_go.setObjectName("btn_go")
self.formLayout_2.setWidget(10, QtWidgets.QFormLayout.LabelRole, self.btn_go)
self.tabs.addTab(self.tab_route, "")
self.tab_log = QtWidgets.QWidget()
self.tab_log.setObjectName("tab_log")
self.gridLayout_3 = QtWidgets.QGridLayout(self.tab_log)
self.gridLayout_3.setObjectName("gridLayout_3")
self.txt_log = QtWidgets.QTextEdit(self.tab_log)
self.txt_log.setEnabled(True)
self.txt_log.setFrameShadow(QtWidgets.QFrame.Sunken)
self.txt_log.setLineWidth(1)
self.txt_log.setReadOnly(True)
self.txt_log.setAcceptRichText(False)
self.txt_log.setObjectName("txt_log")
self.gridLayout_3.addWidget(self.txt_log, 0, 0, 1, 1)
self.tabs.addTab(self.tab_log, "")
self.verticalLayout.addWidget(self.tabs)
ED_LRR.setCentralWidget(self.centralwidget)
self.menu = QtWidgets.QMenuBar(ED_LRR)
self.menu.setGeometry(QtCore.QRect(0, 0, 577, 21))
self.menu.setObjectName("menu")
self.menu_file = QtWidgets.QMenu(self.menu)
self.menu_file.setObjectName("menu_file")
self.menuWindow = QtWidgets.QMenu(self.menu)
self.menuWindow.setObjectName("menuWindow")
self.menuStyle = QtWidgets.QMenu(self.menuWindow)
self.menuStyle.setObjectName("menuStyle")
ED_LRR.setMenuBar(self.menu)
self.bar_status = QtWidgets.QStatusBar(ED_LRR)
self.bar_status.setObjectName("bar_status")
ED_LRR.setStatusBar(self.bar_status)
self.menu_act_quit = QtWidgets.QAction(ED_LRR)
self.menu_act_quit.setObjectName("menu_act_quit")
self.actionA = QtWidgets.QAction(ED_LRR)
self.actionA.setObjectName("actionA")
self.actionB = QtWidgets.QAction(ED_LRR)
self.actionB.setObjectName("actionB")
self.menu_file.addAction(self.menu_act_quit)
self.menuWindow.addAction(self.menuStyle.menuAction())
self.menu.addAction(self.menu_file.menuAction())
self.menu.addAction(self.menuWindow.menuAction())
self.retranslateUi(ED_LRR)
self.tabs.setCurrentIndex(2)
self.menu_act_quit.triggered.connect(ED_LRR.close)
QtCore.QMetaObject.connectSlotsByName(ED_LRR)
ED_LRR.setTabOrder(self.rd_comp, self.cmb_mode)
ED_LRR.setTabOrder(self.cmb_mode, self.chk_primary)
ED_LRR.setTabOrder(self.chk_primary, self.sld_greedyness)
ED_LRR.setTabOrder(self.sld_greedyness, self.rd_precomp)
def retranslateUi(self, ED_LRR):
_translate = QtCore.QCoreApplication.translate
ED_LRR.setWindowTitle(
_translate("ED_LRR", "Elite: Dangerous Long Range Route Plotter")
)
self.lbl_bodies_dl.setText(_translate("ED_LRR", "bodies.json"))
self.lbl_systems_dl.setText(_translate("ED_LRR", "systemsWithCoordinates.json"))
self.inp_bodies_dl.setCurrentText(
_translate("ED_LRR", "https://www.edsm.net/dump/bodies.json")
)
self.inp_systems_dl.setCurrentText(
_translate(
"ED_LRR", "https://www.edsm.net/dump/systemsWithCoordinates.json"
)
)
self.btn_bodies_dest_browse_dl.setText(_translate("ED_LRR", "..."))
self.btn_systems_dest_browse_dl.setText(_translate("ED_LRR", "..."))
self.btn_download.setText(_translate("ED_LRR", "Download"))
self.label.setText(_translate("ED_LRR", "Download path"))
self.label_2.setText(_translate("ED_LRR", "Download path"))
self.tabs.setTabText(
self.tabs.indexOf(self.tab_download), _translate("ED_LRR", "Download")
)
self.lbl_bodies_pp.setText(_translate("ED_LRR", "bodies.json"))
self.btn_bodies_browse_pp.setText(_translate("ED_LRR", "..."))
self.lbl_systems_pp.setText(_translate("ED_LRR", "systemsWithCoordinates.json"))
self.btn_systems_browse_pp.setText(_translate("ED_LRR", "..."))
self.lbl_out_pp.setText(_translate("ED_LRR", "Output"))
self.btn_out_browse_pp.setText(_translate("ED_LRR", "..."))
self.btn_preprocess.setText(_translate("ED_LRR", "Preprocess"))
self.tabs.setTabText(
self.tabs.indexOf(self.tab_preprocess), _translate("ED_LRR", "Preprocess")
)
self.lbl_sys_lst.setText(_translate("ED_LRR", "System List"))
self.btn_sys_lst_browse.setText(_translate("ED_LRR", "..."))
self.btn_add.setText(_translate("ED_LRR", "Add"))
self.inp_sys.setPlaceholderText(_translate("ED_LRR", "System Name"))
self.btn_rm.setText(_translate("ED_LRR", "Remove"))
self.rd_comp.setText(_translate("ED_LRR", "Compute Route"))
self.rd_precomp.setText(_translate("ED_LRR", "Precompute Graph"))
self.chk_permute.setText(_translate("ED_LRR", "Permute"))
self.chk_permute_keep_last.setText(_translate("ED_LRR", "Last"))
self.chk_permute_keep_first.setText(_translate("ED_LRR", "First"))
self.lbl_keep.setText(_translate("ED_LRR", "Keep Endpoints:"))
self.lst_sys.headerItem().setText(1, _translate("ED_LRR", "Type"))
self.lbl_range.setText(_translate("ED_LRR", "Jump Range (Ly)"))
self.cmb_mode.setCurrentText(_translate("ED_LRR", "Breadth-First Search"))
self.cmb_mode.setItemText(0, _translate("ED_LRR", "Breadth-First Search"))
self.cmb_mode.setItemText(1, _translate("ED_LRR", "Greedy-Search"))
self.cmb_mode.setItemText(2, _translate("ED_LRR", "A*-Search"))
self.lbl_greedyness.setText(_translate("ED_LRR", "Greedyness Factor"))
self.chk_primary.setText(_translate("ED_LRR", "Primary Stars Only"))
self.lbl_mode.setText(_translate("ED_LRR", "Mode"))
self.btn_go.setText(_translate("ED_LRR", "GO!"))
self.tabs.setTabText(
self.tabs.indexOf(self.tab_route), _translate("ED_LRR", "Route")
)
self.tabs.setTabText(
self.tabs.indexOf(self.tab_log), _translate("ED_LRR", "Log")
)
self.menu_file.setTitle(_translate("ED_LRR", "File"))
self.menuWindow.setTitle(_translate("ED_LRR", "Window"))
self.menuStyle.setTitle(_translate("ED_LRR", "Style"))
self.menu_act_quit.setText(_translate("ED_LRR", "Quit"))
self.menu_act_quit.setShortcut(_translate("ED_LRR", "Ctrl+Q"))
self.actionA.setText(_translate("ED_LRR", "A"))
self.actionB.setText(_translate("ED_LRR", "B"))

710
ed_lrr_gui/gui/ed_lrr.ui Normal file
View File

@ -0,0 +1,710 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ED_LRR</class>
<widget class="QMainWindow" name="ED_LRR">
<property name="enabled">
<bool>true</bool>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>577</width>
<height>500</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>577</width>
<height>500</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>577</width>
<height>500</height>
</size>
</property>
<property name="windowTitle">
<string>Elite: Dangerous Long Range Route Plotter</string>
</property>
<property name="styleSheet">
<string notr="true"/>
</property>
<property name="documentMode">
<bool>false</bool>
</property>
<property name="tabShape">
<enum>QTabWidget::Rounded</enum>
</property>
<widget class="QWidget" name="centralwidget">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QTabWidget" name="tabs">
<property name="enabled">
<bool>true</bool>
</property>
<property name="autoFillBackground">
<bool>false</bool>
</property>
<property name="tabPosition">
<enum>QTabWidget::North</enum>
</property>
<property name="tabShape">
<enum>QTabWidget::Rounded</enum>
</property>
<property name="currentIndex">
<number>2</number>
</property>
<property name="elideMode">
<enum>Qt::ElideNone</enum>
</property>
<property name="tabsClosable">
<bool>false</bool>
</property>
<property name="tabBarAutoHide">
<bool>false</bool>
</property>
<widget class="QWidget" name="tab_download">
<attribute name="title">
<string>Download</string>
</attribute>
<layout class="QFormLayout" name="formLayout">
<item row="1" column="0">
<widget class="QLabel" name="lbl_bodies_dl">
<property name="text">
<string>bodies.json</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="lbl_systems_dl">
<property name="text">
<string>systemsWithCoordinates.json</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="inp_bodies_dl">
<property name="editable">
<bool>true</bool>
</property>
<property name="currentText">
<string>https://www.edsm.net/dump/bodies.json</string>
</property>
<property name="insertPolicy">
<enum>QComboBox::InsertAtTop</enum>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="inp_systems_dl">
<property name="editable">
<bool>true</bool>
</property>
<property name="currentText">
<string>https://www.edsm.net/dump/systemsWithCoordinates.json</string>
</property>
<property name="insertPolicy">
<enum>QComboBox::InsertAtTop</enum>
</property>
</widget>
</item>
<item row="2" column="1">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QComboBox" name="inp_bodies_dest_dl">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="editable">
<bool>false</bool>
</property>
<property name="insertPolicy">
<enum>QComboBox::InsertAtTop</enum>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QPushButton" name="btn_bodies_dest_browse_dl">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="4" column="1">
<layout class="QGridLayout" name="gridLayout_2">
<item row="0" column="1">
<widget class="QPushButton" name="btn_systems_dest_browse_dl">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QComboBox" name="inp_systems_dest_dl">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="editable">
<bool>false</bool>
</property>
<property name="insertPolicy">
<enum>QComboBox::InsertAtTop</enum>
</property>
</widget>
</item>
</layout>
</item>
<item row="5" column="0">
<widget class="QPushButton" name="btn_download">
<property name="text">
<string>Download</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Download path</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Download path</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_preprocess">
<attribute name="title">
<string>Preprocess</string>
</attribute>
<layout class="QFormLayout" name="formLayout_3">
<item row="0" column="0">
<widget class="QLabel" name="lbl_bodies_pp">
<property name="text">
<string>bodies.json</string>
</property>
</widget>
</item>
<item row="0" column="1">
<layout class="QGridLayout" name="gr_bodies_pp">
<item row="0" column="1">
<widget class="QPushButton" name="btn_bodies_browse_pp">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QComboBox" name="inp_bodies_pp">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="editable">
<bool>false</bool>
</property>
<property name="insertPolicy">
<enum>QComboBox::InsertAtTop</enum>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QLabel" name="lbl_systems_pp">
<property name="text">
<string>systemsWithCoordinates.json</string>
</property>
</widget>
</item>
<item row="1" column="1">
<layout class="QGridLayout" name="gr_systems_pp">
<item row="0" column="1">
<widget class="QPushButton" name="btn_systems_browse_pp">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QComboBox" name="inp_systems_pp">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="editable">
<bool>false</bool>
</property>
<property name="insertPolicy">
<enum>QComboBox::InsertAtTop</enum>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<widget class="QLabel" name="lbl_out_pp">
<property name="text">
<string>Output</string>
</property>
</widget>
</item>
<item row="2" column="1">
<layout class="QGridLayout" name="gr_out_grid_pp">
<item row="0" column="1">
<widget class="QPushButton" name="btn_out_browse_pp">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QComboBox" name="inp_out_pp">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="editable">
<bool>false</bool>
</property>
<property name="insertPolicy">
<enum>QComboBox::InsertAtTop</enum>
</property>
</widget>
</item>
</layout>
</item>
<item row="3" column="0">
<widget class="QPushButton" name="btn_preprocess">
<property name="text">
<string>Preprocess</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_route">
<attribute name="title">
<string>Route</string>
</attribute>
<layout class="QFormLayout" name="formLayout_2">
<item row="0" column="0">
<widget class="QLabel" name="lbl_sys_lst">
<property name="text">
<string>System List</string>
</property>
</widget>
</item>
<item row="0" column="1">
<layout class="QGridLayout" name="gr_sys">
<item row="0" column="1">
<widget class="QPushButton" name="btn_sys_lst_browse">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QComboBox" name="inp_sys_lst">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="editable">
<bool>false</bool>
</property>
<property name="insertPolicy">
<enum>QComboBox::InsertAtTop</enum>
</property>
<property name="frame">
<bool>true</bool>
</property>
<property name="modelColumn">
<number>0</number>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QPushButton" name="btn_add">
<property name="text">
<string>Add</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="inp_sys">
<property name="placeholderText">
<string>System Name</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QPushButton" name="btn_rm">
<property name="text">
<string>Remove</string>
</property>
</widget>
</item>
<item row="3" column="1">
<layout class="QGridLayout" name="gr_mode">
<item row="0" column="1">
<widget class="QRadioButton" name="rd_comp">
<property name="text">
<string>Compute Route</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QRadioButton" name="rd_precomp">
<property name="text">
<string>Precompute Graph</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="4" column="0">
<widget class="QCheckBox" name="chk_permute">
<property name="text">
<string>Permute</string>
</property>
</widget>
</item>
<item row="4" column="1">
<layout class="QGridLayout" name="gridLayout_4">
<item row="0" column="3">
<widget class="QCheckBox" name="chk_permute_keep_last">
<property name="text">
<string>Last</string>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QCheckBox" name="chk_permute_keep_first">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>First</string>
</property>
<property name="tristate">
<bool>false</bool>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="lbl_keep">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Keep Endpoints:</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="7" column="0" colspan="2">
<widget class="QTreeWidget" name="lst_sys">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="dragEnabled">
<bool>true</bool>
</property>
<property name="dragDropOverwriteMode">
<bool>false</bool>
</property>
<property name="dragDropMode">
<enum>QAbstractItemView::InternalMove</enum>
</property>
<property name="defaultDropAction">
<enum>Qt::MoveAction</enum>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="headerHidden">
<bool>true</bool>
</property>
<attribute name="headerVisible">
<bool>false</bool>
</attribute>
<column>
<property name="text">
<string notr="true">Name</string>
</property>
</column>
<column>
<property name="text">
<string>Type</string>
</property>
</column>
</widget>
</item>
<item row="8" column="0">
<widget class="QLabel" name="lbl_range">
<property name="text">
<string>Jump Range (Ly)</string>
</property>
</widget>
</item>
<item row="8" column="1">
<widget class="QDoubleSpinBox" name="sb_range"/>
</item>
<item row="9" column="0" colspan="2">
<layout class="QGridLayout" name="gr_opts">
<item row="0" column="2">
<widget class="QComboBox" name="cmb_mode">
<property name="currentText">
<string>Breadth-First Search</string>
</property>
<item>
<property name="text">
<string>Breadth-First Search</string>
</property>
</item>
<item>
<property name="text">
<string>Greedy-Search</string>
</property>
</item>
<item>
<property name="text">
<string>A*-Search</string>
</property>
</item>
</widget>
</item>
<item row="1" column="1">
<widget class="QLabel" name="lbl_greedyness">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>Greedyness Factor</string>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QCheckBox" name="chk_primary">
<property name="text">
<string>Primary Stars Only</string>
</property>
</widget>
</item>
<item row="1" column="2" colspan="2">
<widget class="QSlider" name="sld_greedyness">
<property name="maximum">
<number>100</number>
</property>
<property name="pageStep">
<number>10</number>
</property>
<property name="value">
<number>50</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="tickPosition">
<enum>QSlider::TicksBelow</enum>
</property>
<property name="tickInterval">
<number>10</number>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="lbl_mode">
<property name="text">
<string>Mode</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="10" column="0">
<widget class="QPushButton" name="btn_go">
<property name="text">
<string>GO!</string>
</property>
<property name="flat">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_log">
<attribute name="title">
<string>Log</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<widget class="QTextEdit" name="txt_log">
<property name="enabled">
<bool>true</bool>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
<property name="lineWidth">
<number>1</number>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
<property name="acceptRichText">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menu">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>577</width>
<height>21</height>
</rect>
</property>
<widget class="QMenu" name="menu_file">
<property name="title">
<string>File</string>
</property>
<addaction name="menu_act_quit"/>
</widget>
<widget class="QMenu" name="menuWindow">
<property name="title">
<string>Window</string>
</property>
<widget class="QMenu" name="menuStyle">
<property name="title">
<string>Style</string>
</property>
</widget>
<addaction name="menuStyle"/>
</widget>
<addaction name="menu_file"/>
<addaction name="menuWindow"/>
</widget>
<widget class="QStatusBar" name="bar_status"/>
<action name="menu_act_quit">
<property name="text">
<string>Quit</string>
</property>
<property name="shortcut">
<string>Ctrl+Q</string>
</property>
</action>
<action name="actionA">
<property name="text">
<string>A</string>
</property>
</action>
<action name="actionB">
<property name="text">
<string>B</string>
</property>
</action>
</widget>
<tabstops>
<tabstop>rd_comp</tabstop>
<tabstop>cmb_mode</tabstop>
<tabstop>chk_primary</tabstop>
<tabstop>sld_greedyness</tabstop>
<tabstop>rd_precomp</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>menu_act_quit</sender>
<signal>triggered()</signal>
<receiver>ED_LRR</receiver>
<slot>close()</slot>
<hints>
<hint type="sourcelabel">
<x>-1</x>
<y>-1</y>
</hint>
<hint type="destinationlabel">
<x>288</x>
<y>249</y>
</hint>
</hints>
</connection>
</connections>
</ui>

685
ed_lrr_gui/gui/main.py Normal file
View File

@ -0,0 +1,685 @@
# -*- coding: utf-8 -*-
import csv
import gzip
import multiprocessing as MP
import os
import pathlib
import queue
import sys
from sys import exit
from datetime import datetime, timedelta
from urllib.request import Request, urlopen
import _ed_lrr
import ed_lrr_gui
import requests as RQ
from ed_lrr_gui import Preprocessor, Router, cfg
from ed_lrr_gui.gui.ed_lrr import Ui_ED_LRR
from ed_lrr_gui.gui.widget_route import Ui_diag_route
from PyQt5.QtCore import QObject, Qt, QThread, QTimer, pyqtSignal
from PyQt5.QtGui import QColor, QPalette, QIcon
from PyQt5.QtWidgets import (
QAction,
QApplication,
QFileDialog,
QMainWindow,
QMessageBox,
QProgressDialog,
QTreeWidgetItem,
QLabel,
QDialog,
)
class ProgressDialog(QProgressDialog):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setWindowModality(Qt.WindowModal)
def sizeof_fmt(num, suffix="B"):
for unit in ["", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"]:
if abs(num) < 1024.0:
return "{:.02f} {}{}".format(num, unit, suffix)
num /= 1024.0
return "{:.02f} {}{}".format(num, "Yi", suffix)
def t_round(dt):
return dt - dt % timedelta(seconds=1)
class Job(QObject):
progress = pyqtSignal("PyQt_PyObject")
def __init__(self, app, main_window, cls, *args, **kwargs):
super().__init__()
self.job = cls(*args, **kwargs)
self.timer = QTimer(app)
self.app = app
self.main_window = main_window
self.timer.timeout.connect(self.interval)
self.timer.start(100)
self.last_val = None
self.progress_dialog = None
self.state = {}
self.setup_progress(self.handle_progess)
def setup_progress(self, handle_progess):
self.progress.connect(lambda *args, **kwargs: handle_progess(*args, **kwargs))
def handle_progess(self, *args, **kwargs):
raise NotImplementedError
def start(self):
if self.progress_dialog is None:
self.progress.connect(
lambda *args, **kwargs: print("PROGRESS:", *args, **kwargs)
)
self.started = datetime.today()
return self.job.start()
def cancel(self):
self.job.terminate()
self.job = None
def __bool__(self):
return not self.done
@property
def done(self):
if self.job:
return (self.job.is_alive() == False) and (self.job.queue.empty())
return True
def interval(self):
while self:
try:
res = self.job.queue.get(True, 0.1)
except queue.Empty:
return
if res == self.last_val:
continue
self.state.update(res)
self.progress.emit(self.state)
self.last_val = res
class PreprocessJob(Job):
def __init__(self, app, main_window, *args, **kwargs):
super().__init__(app, main_window, Preprocessor, *args, **kwargs)
self.progress_dialog = ProgressDialog("", "Cancel", 0, 0, self.main_window)
self.progress_dialog.setAutoClose(False)
self.progress_dialog.canceled.connect(self.cancel)
self.progress_dialog.show()
self.start()
def handle_progess(self, state):
sent = object()
if state.get("return", sent) != sent:
self.progress_dialog.close()
return
msg = "Processed: {}/{}".format(
sizeof_fmt(state["status"]["done"]), sizeof_fmt(state["status"]["total"])
)
state["status"]["prc_done"] = (
state["status"]["done"] / state["status"]["total"]
) * 100
title = "[{prc_done:.2f}%] Processing {file}".format(**state["status"])
self.progress_dialog.setMinimum(0)
self.progress_dialog.setMaximum(100 * 100)
self.progress_dialog.setWindowTitle(title)
self.progress_dialog.setLabelText(msg)
self.progress_dialog.setValue(int(state["status"]["prc_done"] * 100))
class RouterJob(Job):
def __init__(self, app, main_window, *args, **kwargs):
super().__init__(app, main_window, Router, *args, **kwargs)
self.progress_dialog = ProgressDialog("", "Cancel", 0, 0, self.main_window)
self.progress_dialog.setAutoClose(False)
self.progress_dialog.setLabelText("Loading data (this will take a bit) ...")
self.progress_dialog.setWindowTitle("Loading...")
self.progress_dialog.canceled.connect(self.cancel)
self.progress_dialog.show()
self.start()
self.state = {}
def handle_progess(self, state):
sent = object()
if state.get("return", sent) != sent:
print(state["return"])
self.progress_dialog.close()
route_win = WRoute(self.main_window, state["return"])
return
msg = "Depth: {depth}\nBody: {body}\nQueued: {queue_size}\nDistance remaining: {d_rem:.2f} Ly".format(
**state["status"]
)
title = "[{prc_done:.2f}%] Plotting route from [{from}] to [{to}]".format(
**state["status"]
)
self.progress_dialog.setMinimum(0)
self.progress_dialog.setMaximum(100 * 100)
self.progress_dialog.setWindowTitle(title)
self.progress_dialog.setLabelText(msg)
self.progress_dialog.setValue(int(state["status"]["prc_done"] * 100))
class DownloadThread(QThread):
progress = pyqtSignal("PyQt_PyObject")
def __init__(self, systems_url, systems_file, bodies_url, bodies_file):
super().__init__()
self.systems_url = systems_url
self.systems_file = systems_file
self.bodies_url = bodies_url
self.bodies_file = bodies_file
self.running = True
def __del__(self):
self.wait()
def stop(self):
self.running = False
def run(self):
dl_jobs = [
(self.systems_url, self.systems_file),
(self.bodies_url, self.bodies_file),
]
for url, dest in dl_jobs:
outfile = url.split("/")[-1]
size = RQ.head(url, headers={"Accept-Encoding": "None"})
size.raise_for_status()
size = int(size.headers.get("Content-Length", 0))
with open(dest, "wb") as of:
resp = RQ.get(url, stream=True)
for chunk in resp.iter_content(1024 * 1024):
of.write(chunk)
self.progress.emit(
{"done": of.tell(), "size": size, "outfile": outfile}
)
if not self.running:
return
class App(QApplication):
def __init__(self):
super().__init__(sys.argv)
self.setStyle("Fusion")
self.setup_styles()
def set_style(self, style):
print("LOAD:", style)
self.setPalette(self.styles[style])
def setup_styles(self):
self.styles = {}
styles = {
"Dark": {
"Window": QColor(53, 53, 53),
"WindowText": Qt.white,
"Base": QColor(15, 15, 15),
"AlternateBase": QColor(53, 53, 53),
"ToolTipBase": Qt.white,
"ToolTipText": Qt.white,
"Text": Qt.white,
"Button": QColor(53, 53, 53),
"ButtonText": Qt.white,
"BrightText": Qt.red,
"Highlight": QColor(255, 128, 0),
"HighlightedText": Qt.black,
}
}
for style, colors in styles.items():
palette = QPalette()
for entry, color in colors.items():
palette.setColor(getattr(QPalette, entry), color)
if color == Qt.darkGray:
palette.setColor(
QPalette.Disabled, getattr(QPalette, entry), QColor(53, 53, 53)
)
else:
palette.setColor(
QPalette.Disabled, getattr(QPalette, entry), Qt.darkGray
)
self.styles[style] = palette
self.styles["Light"] = self.style().standardPalette()
class WRoute(Ui_diag_route):
def __init__(self, main_window, hops):
super().__init__()
self.route = hops
dialog = QDialog(main_window)
self.setupUi(dialog)
for n, item in enumerate(hops):
if item["body"].startswith(item["system"]):
item["body"] = item["body"].replace(item["system"], "").strip()
item = QTreeWidgetItem(
self.lst_route,
[
str(n + 1),
item["system"],
"{body} ({star_type})".format(**item),
str(item["distance"]),
"<NotImplemented>", # Jump distance
],
)
item.setFlags(item.flags() & ~Qt.ItemIsDropEnabled)
dialog.exec_()
class ED_LRR(Ui_ED_LRR):
dl_thread = None
diag_prog = None
dl_started = None
system_found = pyqtSignal("PyQt_PyObject")
def __init__(self):
super().__init__()
self.current_job = None
def get_open_file(self, filetypes, callback=None):
fileName, _ = QFileDialog.getOpenFileName(
self.main_window,
"Open file",
cfg["folders.data_dir"],
filetypes,
options=QFileDialog.DontUseNativeDialog,
)
if callback:
return callback(fileName)
return fileName
def get_save_file(self, filetypes, callback=None):
fileName, _ = QFileDialog.getSaveFileName(
self.main_window,
"Save file",
cfg["folders.data_dir"],
filetypes,
options=QFileDialog.DontUseNativeDialog,
)
if callback:
return callback(fileName)
return fileName
def preprocess(self):
if self.current_job:
# ERROR
return
bodies_json = self.inp_bodies_pp.currentText()
systems_json = self.inp_systems_pp.currentText()
output_file = self.inp_out_pp.currentText()
self.current_job = PreprocessJob(
self.app, self.main_window, systems_json, bodies_json, output_file
)
def set_sys_lst(self, path):
if path not in cfg["history.stars_csv_path"]:
cfg["history.stars_csv_path"].append(path)
self.update_dropdowns()
def set_bodies_file(self, path):
if path not in cfg["history.bodies_path"]:
cfg["history.bodies_path"].append(path)
self.update_dropdowns()
def set_systems_file(self, path):
if path not in cfg["history.systems_path"]:
cfg["history.systems_path"].append(path)
self.update_dropdowns()
def update_dropdowns(self):
self.inp_systems_pp.clear()
self.inp_systems_dest_dl.clear()
for path in cfg["history.systems_path"][:]:
self.inp_systems_pp.addItem(path)
self.inp_systems_pp.setCurrentText(path)
self.inp_systems_dest_dl.addItem(path)
self.inp_systems_dest_dl.setCurrentText(path)
self.inp_bodies_pp.clear()
self.inp_bodies_dest_dl.clear()
for path in cfg["history.bodies_path"][:]:
self.inp_bodies_pp.addItem(path)
self.inp_bodies_pp.setCurrentText(path)
self.inp_bodies_dest_dl.addItem(path)
self.inp_bodies_dest_dl.setCurrentText(path)
self.inp_sys_lst.clear()
self.inp_out_pp.clear()
for path in cfg["history.stars_csv_path"]:
self.inp_sys_lst.addItem(path)
self.inp_sys_lst.setCurrentText(path)
self.inp_out_pp.addItem(path)
self.inp_out_pp.setCurrentText(path)
return
def log(self, *args):
t = datetime.today()
msg_t = "[{}] {}".format(t, str.format(*args))
self.txt_log.append(msg_t)
def set_comp_mode(self, _):
if self.rd_comp.isChecked():
comp_mode = "Compute Route"
self.btn_add.setText("Add")
if self.rd_precomp.isChecked():
comp_mode = "Precompute Graph"
self.btn_add.setText("Select")
self.log("COMP_MODE", comp_mode)
self.lst_sys.setEnabled(self.rd_comp.isChecked())
self.btn_rm.setEnabled(self.rd_comp.isChecked())
self.cmb_mode.setEnabled(self.rd_comp.isChecked())
self.chk_permute.setEnabled(self.rd_comp.isChecked())
self.lbl_keep.setEnabled(self.rd_comp.isChecked())
self.lbl_mode.setEnabled(self.rd_comp.isChecked())
self.chk_permute_keep_first.setEnabled(self.rd_comp.isChecked())
self.chk_permute_keep_last.setEnabled(self.rd_comp.isChecked())
self.set_route_mode(self.rd_precomp.isChecked() or None)
def set_route_mode(self, mode=None):
if mode == None:
mode = self.cmb_mode.currentText()
self.lbl_greedyness.setEnabled(mode == "A*-Search")
self.sld_greedyness.setEnabled(mode == "A*-Search")
def set_greedyness(self, value):
self.lbl_greedyness.setText("Greedyness Factor ({:.0%})".format(value / 100))
@property
def systems(self):
ret = []
for n in range(self.lst_sys.topLevelItemCount()):
ret.append(self.sys_to_dict(n))
return ret
def sys_to_dict(self, n):
header = [
self.lst_sys.headerItem().data(c, 0)
for c in range(self.lst_sys.headerItem().columnCount())
]
system = [
self.lst_sys.topLevelItem(n).data(c, 0)
for c in range(self.lst_sys.topLevelItem(n).columnCount())
]
ret = dict(zip(header, system))
ret["id"] = getattr(self.lst_sys.topLevelItem(n), "__id__", None)
ret.pop(None, None)
return ret
def error(self, msg):
QMessageBox.critical(self.main_window, "ED_LRR Error", msg)
def get_sys_list(self):
if not self.inp_sys_lst.currentText():
self.error("System list is required!")
return
path = pathlib.Path(self.inp_sys_lst.currentText())
if not path.exists():
self.error("System list does not exist, run download and preprocess first!")
return
return path
def compute_route(self):
self.bar_status.showMessage("Resolving systems...")
num_resolved = self.resolve_systems()
if num_resolved:
if (
QMessageBox.question(
self.main_window,
"ED_LRR",
"Resolved {} system(s), are the names correct?".format(
num_resolved
),
)
== QMessageBox.No
):
return
self.bar_status.clearMessage()
print(self.systems)
systems = [s["id"] for s in self.systems]
jump_range = self.sb_range.value()
if jump_range == 0:
self.error(
"Your jump range is set to zero, you're not going to get anywhere that way"
)
return
mode = self.cmb_mode.currentText()
primary = self.chk_primary.isChecked()
keep_first = self.chk_permute_keep_first.isChecked()
keep_last = self.chk_permute_keep_last.isChecked()
permute = self.chk_permute.isChecked()
greedyness = (
self.sld_greedyness.value() / 100
if self.sld_greedyness.isEnabled()
else None
)
path = self.get_sys_list()
if path is None:
return
precomp = None
path = str(path)
mode = {
"Breadth-First Search": "bfs",
"A*-Search": "astar",
"Greedy-Search": "greedy",
}[mode]
print(
systems,
jump_range,
None,
mode,
primary,
permute,
keep_first,
keep_last,
greedyness,
precomp,
path,
os.cpu_count() - 1,
)
if not self.current_job:
self.bar_status.showMessage("Computing Route...")
self.current_job = RouterJob(
self.app,
self.main_window,
systems,
jump_range,
None,
mode,
primary,
permute,
keep_first,
keep_last,
greedyness,
precomp,
path,
os.cpu_count() - 1,
)
else:
self.error("there is already a job running!")
def find_sys_by_names(self, names):
t_s = datetime.today()
if not self.get_sys_list():
return None
# TODO: start thread/subprocess
ret = _ed_lrr.find_sys(names, self.inp_sys_lst.currentText())
print("Took:", datetime.today() - t_s)
return ret
def resolve_systems(self):
# TODO: show spinner
names = []
nums = []
for n in range(self.lst_sys.topLevelItemCount()):
sys_id = getattr(self.lst_sys.topLevelItem(n), "__id__", None)
if sys_id is not None:
continue
names.append(self.sys_to_dict(n)["Name"])
nums.append(n)
if not names:
return 0
systems = self.find_sys_by_names(names)
if systems is None:
return
for i, name in zip(nums, names):
_, system = systems[name]
self.lst_sys.topLevelItem(i).setData(0, 0, system["system"])
self.lst_sys.topLevelItem(i).setData(1, 0, system["star_type"])
self.lst_sys.topLevelItem(i).__id__ = system["id"]
return len(names)
# diff, item = self.find_sys_by_name(name)
# print("Found", (diff, item))
def add_system(self):
name = self.inp_sys.text()
item = QTreeWidgetItem(self.lst_sys, [name, None])
item.resolved = False
item.setFlags(item.flags() & ~Qt.ItemIsDropEnabled)
def remove_system(self):
root = self.lst_sys.invisibleRootItem()
for item in self.lst_sys.selectedItems():
root.removeChild(item)
def dl_canceled(self):
if self.dl_thread:
print("Cancel!")
try:
self.dl_thread.progress.disconnect()
except TypeError:
pass
self.dl_thread.stop()
self.dl_thread.wait()
self.diag_prog.close()
self.dl_thread = None
self.diag_prog = None
self.dl_started = None
def handle_dl_progress(self, args):
filename = os.path.split(args["outfile"])[-1]
if self.diag_prog is None:
self.diag_prog = ProgressDialog("", "Cancel", 0, 1000, self.main_window)
if self.dl_thread:
self.diag_prog.canceled.connect(self.dl_canceled)
self.diag_prog.show()
t_elapsed = datetime.today() - self.dl_started
rate = args["done"] / t_elapsed.total_seconds()
remaining = (args["size"] - args["done"]) / rate
rate = round(rate, 2)
# print(rate, remaining)
try:
t_rem = timedelta(seconds=remaining)
except OverflowError:
t_rem = "-"
msg = "Downloading {} [{}/{}] ({}/s)\n[{}/{}]".format(
filename,
sizeof_fmt(args["done"]),
sizeof_fmt(args["size"]),
sizeof_fmt(rate),
t_round(t_elapsed),
t_round(t_rem),
)
self.diag_prog.setLabelText(msg)
self.diag_prog.setWindowTitle("Downloading EDSM Dumps")
self.diag_prog.setValue((args["done"] * 1000) // args["size"])
def run_download(self):
if self.dl_thread:
return
self.dl_started = datetime.today()
self.dl_thread = DownloadThread(
self.inp_systems_dl.currentText(),
self.inp_systems_dest_dl.currentText(),
self.inp_bodies_dl.currentText(),
self.inp_bodies_dest_dl.currentText(),
)
self.dl_thread.progress.connect(self.handle_dl_progress)
self.dl_thread.start()
print(".")
def update_permute_chk(self, state):
self.chk_permute_keep_first.setEnabled(state)
self.chk_permute_keep_last.setEnabled(state)
self.lbl_keep.setEnabled(state)
def setup_signals(self):
self.btn_download.clicked.connect(self.run_download)
self.set_greedyness(self.sld_greedyness.value())
self.cmb_mode.currentTextChanged.connect(self.set_route_mode)
self.rd_comp.toggled.connect(self.set_comp_mode)
self.rd_precomp.toggled.connect(self.set_comp_mode)
self.sld_greedyness.valueChanged.connect(self.set_greedyness)
self.btn_go.clicked.connect(self.compute_route)
self.btn_add.clicked.connect(self.add_system)
self.btn_rm.clicked.connect(self.remove_system)
self.btn_preprocess.clicked.connect(self.preprocess)
self.chk_permute.stateChanged.connect(self.update_permute_chk)
self.btn_out_browse_pp.clicked.connect(
lambda: self.get_save_file("CSV File (*.csv)", self.set_sys_lst)
)
self.btn_sys_lst_browse.clicked.connect(
lambda: self.get_open_file("CSV File (*.csv)", self.set_sys_lst)
)
self.btn_bodies_browse_pp.clicked.connect(
lambda: self.get_open_file("JSON File (*.json)", self.set_bodies_file)
)
self.btn_bodies_dest_browse_dl.clicked.connect(
lambda: self.get_save_file("JSON File (*.json)", self.set_bodies_file)
)
self.btn_systems_browse_pp.clicked.connect(
lambda: self.get_open_file("JSON File (*.json)", self.set_systems_file)
)
self.btn_systems_dest_browse_dl.clicked.connect(
lambda: self.get_save_file("JSON File (*.json)", self.set_systems_file)
)
def handle_close(self):
for key in [
"history.stars_csv_path",
"history.bodies_path",
"history.systems_path",
]:
for path in cfg[key][:]:
if not os.path.isfile(path):
cfg[key].remove(path)
cfg.write()
print("BYEEEEEE!")
def setup_styles(self, win, app):
for name in app.styles:
action = QAction(app)
action.setObjectName("action_load_style_" + name)
action.setText(name)
action.triggered.connect(lambda _, name=name: app.set_style(name))
self.menuStyle.addAction(action)
def setupUi(self, MainWindow, app):
super().setupUi(MainWindow)
self.update_dropdowns()
self.main_window = MainWindow
self.app = app
self.setup_signals()
self.lst_sys.setHeaderLabels(["Name", "Type"])
self.set_route_mode()
self.update_permute_chk(self.chk_permute.isChecked())
self.setup_styles(MainWindow, app)
def main():
MP.freeze_support()
app = App()
app.setWindowIcon(QIcon(r"D:\devel\rust\ed_lrr_gui\icon\icon.ico"))
MainWindow = QMainWindow()
MainWindow.setWindowIcon(QIcon(r"D:\devel\rust\ed_lrr_gui\icon\icon.ico"))
ui = ED_LRR()
ui.setupUi(MainWindow, app)
MainWindow.show()
ret = app.exec_()
ui.handle_close()
exit(ret)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,65 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'D:\devel\rust\ed_lrr_gui\ed_lrr_gui\gui\widget_route.ui'
#
# Created by: PyQt5 UI code generator 5.14.1
#
# WARNING! All changes made in this file will be lost!
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_diag_route(object):
def setupUi(self, diag_route):
diag_route.setObjectName("diag_route")
diag_route.setWindowModality(QtCore.Qt.WindowModal)
diag_route.resize(430, 300)
self.layout_main = QtWidgets.QGridLayout(diag_route)
self.layout_main.setObjectName("layout_main")
self.layout_top = QtWidgets.QGridLayout()
self.layout_top.setObjectName("layout_top")
self.lst_route = QtWidgets.QTreeWidget(diag_route)
self.lst_route.setProperty("showDropIndicator", False)
self.lst_route.setDefaultDropAction(QtCore.Qt.IgnoreAction)
self.lst_route.setAlternatingRowColors(True)
self.lst_route.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
self.lst_route.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerItem)
self.lst_route.setHorizontalScrollMode(
QtWidgets.QAbstractItemView.ScrollPerPixel
)
self.lst_route.setItemsExpandable(False)
self.lst_route.setAllColumnsShowFocus(False)
self.lst_route.setObjectName("lst_route")
self.layout_top.addWidget(self.lst_route, 0, 0, 1, 1)
self.layout_main.addLayout(self.layout_top, 0, 0, 1, 1)
self.layout_bottom = QtWidgets.QGridLayout()
self.layout_bottom.setObjectName("layout_bottom")
self.chk_copy = QtWidgets.QCheckBox(diag_route)
self.chk_copy.setObjectName("chk_copy")
self.layout_bottom.addWidget(self.chk_copy, 1, 0, 1, 1)
self.btn_close = QtWidgets.QPushButton(diag_route)
self.btn_close.setObjectName("btn_close")
self.layout_bottom.addWidget(self.btn_close, 1, 2, 1, 1)
self.btn_export = QtWidgets.QPushButton(diag_route)
self.btn_export.setObjectName("btn_export")
self.layout_bottom.addWidget(self.btn_export, 1, 1, 1, 1)
self.layout_main.addLayout(self.layout_bottom, 1, 0, 1, 1)
self.retranslateUi(diag_route)
QtCore.QMetaObject.connectSlotsByName(diag_route)
def retranslateUi(self, diag_route):
_translate = QtCore.QCoreApplication.translate
diag_route.setWindowTitle(_translate("diag_route", "Route"))
self.lst_route.headerItem().setText(0, _translate("diag_route", "Num"))
self.lst_route.headerItem().setText(1, _translate("diag_route", "System"))
self.lst_route.headerItem().setText(2, _translate("diag_route", "Body"))
self.lst_route.headerItem().setText(
3, _translate("diag_route", "Distance (Ls)")
)
self.chk_copy.setText(
_translate("diag_route", "Auto-copy next hop to clipboard")
)
self.btn_close.setText(_translate("diag_route", "Close"))
self.btn_export.setText(_translate("diag_route", "Export"))

View File

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>diag_route</class>
<widget class="QDialog" name="diag_route">
<property name="windowModality">
<enum>Qt::WindowModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>430</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Route</string>
</property>
<layout class="QGridLayout" name="layout_main">
<item row="0" column="0">
<layout class="QGridLayout" name="layout_top">
<item row="0" column="0">
<widget class="QTreeWidget" name="lst_route">
<property name="showDropIndicator" stdset="0">
<bool>false</bool>
</property>
<property name="defaultDropAction">
<enum>Qt::IgnoreAction</enum>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::NoSelection</enum>
</property>
<property name="verticalScrollMode">
<enum>QAbstractItemView::ScrollPerItem</enum>
</property>
<property name="horizontalScrollMode">
<enum>QAbstractItemView::ScrollPerPixel</enum>
</property>
<property name="itemsExpandable">
<bool>false</bool>
</property>
<property name="allColumnsShowFocus">
<bool>false</bool>
</property>
<column>
<property name="text">
<string>Num</string>
</property>
</column>
<column>
<property name="text">
<string>System</string>
</property>
</column>
<column>
<property name="text">
<string>Body</string>
</property>
</column>
<column>
<property name="text">
<string>Distance (Ls)</string>
</property>
</column>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<layout class="QGridLayout" name="layout_bottom">
<item row="1" column="0">
<widget class="QCheckBox" name="chk_copy">
<property name="text">
<string>Auto-copy next hop to clipboard</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="btn_close">
<property name="text">
<string>Close</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QPushButton" name="btn_export">
<property name="text">
<string>Export</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

47
ed_lrr_gui/html_export.py Normal file
View File

@ -0,0 +1,47 @@
# -*- coding: utf-8 -*-
import jinja2
import os
tmpl_path = os.path.join(os.path.dirname(__file__), "html_export_template.html.jinja2")
def dist(p1, p2):
s = 0
for c1, c2 in zip(p1, p2):
s += (c1 - c2) ** 2
return s ** 0.5
colors = {
"O": "#0000FF",
"B": "#140AF0",
"A": "#3C1EDC",
"F": "#EEEEEE",
"G": "#969646",
"K": "#B43C1E",
"M": "#FF280A",
"L": "#FF1E00",
"T": "#800000",
"Y": "#800000",
"White Dwarf": "#5D67EF",
"Neutron": "#99A0FF",
}
entries = []
prev = route[0]
num = 1
for hop in route[1:]:
prev["jump_dist"] = "{:.2f} Ly".format(dist(hop["pos"], prev["pos"]))
prev["num"] = num
prev["color"] = colors.get(prev["star_type"].split()[0], "#eee")
prev["distance"] = "{} Ls".format(prev["distance"])
entries.append(prev)
prev = hop
num += 1
prev["jump_dist"] = "0 Ly"
prev["distance"] = "{} Ls".format(prev["distance"])
prev["num"] = num
prev["color"] = colors.get(prev["star_type"].split()[0], "#eee")
entries.append(prev)
tmpl = jinja2.Template(open(tmpl_path).read())
open("route.html", "w").write(tmpl.render(route=entries))

View File

@ -0,0 +1,158 @@
<html>
<head>
<meta charset="utf-8"/>
<style>
h1 {
float: left;
}
body {
background: #222;
margin: 0px;
width: 100%;
height: 100%;
}
table {
border-collapse: collapse;
max-width: 50%;
float: right;
}
#graph {
border: 1px solid #eee;
float: left;
}
table,
td,
tr,th {
color: #eee;
margin: auto;
border: 1px solid #eee;
text-align: center;
}
/* D3 stuff */
.d3-tip {
line-height: 1;
font-weight: bold;
padding: 12px;
background: rgba(0, 0, 0, 0.8);
color: #fff;
border-radius: 2px;
}
/* Creates a small triangle extender for the tooltip */
.d3-tip:after {
box-sizing: border-box;
display: inline;
font-size: 10px;
width: 100%;
line-height: 1;
color: rgba(0, 0, 0, 0.8);
content: "\25BC";
position: absolute;
text-align: center;
}
/* Style northward tooltips differently */
.d3-tip.n:after {
margin: -1px 0 0 0;
top: 100%;
left: 0;
}
</style>
<script src="https://d3js.org/d3.v5.min.js"></script>
</head>
<body>
<div id="graph"></div>
<table>
<tr>
<th>Num</th>
<th>Body</th>
<th>Type</th>
<th>Distance to arrival</th>
<th>Jump range</th>
</tr>
{% for sys in route %}
<tr>
<td>{{sys.num}}</td>
<td>{{sys.body}}</td>
<td style="color: {{sys.color}}">{{sys.star_type}}</td>
<td>{{sys.distance}}</td>
<td>{{sys.jump_dist}}</td>
</tr>
{% endfor %}
</table>
<script type="text/javascript">
function dist(a,b) {
var sum=0;
for (var i=0;i<a.length;++i) {
sum+=Math.pow(a[i]-b[i],2)
}
return Math.pow(sum,0.5);
}
var width=512;
var height=512;
var route={{route|tojson}};
var vis=d3.select("#graph")
.append("svg").attr("viewBox", [0, 0, width, height]);;
vis.attr("width", width)
.attr("height", height);
var g=vis.append("g");
vis.call(d3.zoom()
.extent([[0, 0], [width, height]])
.on("zoom", () => {
g.attr("transform", d3.event.transform);
}));
var lines=[];
for (var i=0;i<route.length-1;++i) {
lines.push({
x1: route[i].pos[1],
x2: route[i+1].pos[1],
y1: -route[i].pos[2],
y2: -route[i+1].pos[2],
dist: dist(route[i].pos,route[i+1].pos),
color: ({
'#99A0FF':'#99A0FF', // Neutron star
'#5D67EF':'#5D67EF' // White dwarf
}[route[i].color]||'#eee')
})
}
g.selectAll(".line")
.data(lines)
.enter()
.append("line")
.attr("x1", (l) => l.x1 )
.attr("y1", (l) => l.y1 )
.attr("x2", (l) => l.x2 )
.attr("y2", (l) => l.y2 )
.style("stroke", (l) => l.color )
.style("stroke-width", 5)
.append("title")
.text((l) => Math.round(l.dist*100)/100 +" Ly");
g.selectAll("circle .nodes")
.data(route)
.enter()
.append("svg:circle")
.attr("class", "nodes")
.attr("cx", (d) => d.pos[1])
.attr("cy", (d) => -d.pos[2])
.attr("r", 10)
.attr("fill", (d) => d.color)
.append("title")
.text((d) => d.body+" ("+d.star_type+")")
</script>
</body>
</html>

36
ed_lrr_gui/preprocess.py Normal file
View File

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
import queue
from collections import namedtuple
from datetime import datetime, timedelta
from multiprocessing import Process, Queue, freeze_support
import _ed_lrr
class Preprocessor(Process):
def __init__(self, *args, **kwargs):
super().__init__()
self.state = {}
self.queue = Queue()
self.daemon = True
self.args = args
self.kwargs = kwargs
self.kwargs["callback"] = self.callback
def callback(self, state):
self.queue.put({"status": state})
def run(self):
res = _ed_lrr.preprocess(*self.args, **self.kwargs)
self.queue.put({"result": res})
if __name__ == "__main__":
freeze_support()
r = Preprocessor(
r"D:\devel\rust\ED_LRR\dumps\systemsWithCoordinates.json",
r"D:\devel\rust\ED_LRR\dumps\bodies.json",
r"D:\devel\rust\ED_LRR\stars.csv",
)
for i, e in enumerate(r):
print(e)

42
ed_lrr_gui/router.py Normal file
View File

@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
import queue
from collections import namedtuple
from datetime import datetime, timedelta
from multiprocessing import Process, Queue, freeze_support
import _ed_lrr
class Router(Process):
def __init__(self, *args, **kwargs):
super().__init__()
self.state = {}
self.queue = Queue()
self.daemon = True
self.args = args
self.kwargs = kwargs
self.kwargs["callback"] = self.callback
def callback(self, state):
self.queue.put({"status": state})
def run(self):
print("Route(): ", self.args, self.kwargs)
route = _ed_lrr.route(*self.args, **self.kwargs)
self.queue.put({"return": route})
if __name__ == "__main__":
freeze_support()
r = Router(
["Ix", "Beagle Point"],
48,
"BFS",
False,
False,
None,
None,
r"D:\devel\rust\ED_LRR\stars.csv",
)
for e in r:
print(e)

View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from .app import app, templates, db

721
ed_lrr_gui/web/app.py Normal file
View File

@ -0,0 +1,721 @@
# -*- coding: utf-8 -*-
from flask import (
Flask,
jsonify,
render_template,
redirect,
url_for,
send_from_directory,
request,
flash,
current_app,
)
from flask.cli import AppGroup
import uuid
import os
import click
from functools import wraps
from concurrent.futures.process import BrokenProcessPool
from datetime import datetime, timedelta
from webargs import fields, validate
from webargs.flaskparser import use_kwargs
from flask_executor import Executor
from flask_sqlalchemy import SQLAlchemy
from flask_bootstrap import Bootstrap
from flask_nav import Nav, register_renderer
from flask_nav.elements import Navbar, View
from flask_admin import Admin
from flask_admin.contrib.sqla import ModelView
from flask_wtf.csrf import CSRFProtect
from flask_login import (
LoginManager,
current_user,
logout_user,
UserMixin,
AnonymousUserMixin,
login_user,
login_required,
)
from flask_debugtoolbar import DebugToolbarExtension
from sqlalchemy_utils import generic_repr, JSONType, PasswordType, UUIDType
from sqlalchemy.orm import relationship
from sqlalchemy.types import DateTime
from jinja2.exceptions import TemplateNotFound
from .forms import RouteForm, LoginForm, RegisterForm, ChangePasswordForm
from .utils import prepare_route, BootsrapRenderer, is_safe_url
import _ed_lrr as ed_lrr
templates = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates")
app = Flask(__name__, template_folder=templates)
app.config.from_pyfile("config.py")
app.executor = executor = Executor(app)
app.db = db = SQLAlchemy(app)
app.bootstrap = bootstrap = Bootstrap(app)
app.csrf = csfr = CSRFProtect(app)
app.nav = nav = Nav(app)
app.login_manager = login_manager = LoginManager(app)
login_manager.login_view = "login"
login_manager.session_protection = "strong"
admin = Admin(app, name="ED_LRR", template_mode="bootstrap3")
app.debug = True
app.toolbar = toolbar = DebugToolbarExtension(app)
def wants_json_response():
return (
request.accept_mimetypes["application/json"]
>= request.accept_mimetypes["text/html"]
)
@app.errorhandler(422)
@app.errorhandler(400)
@app.errorhandler(500)
@app.errorhandler(404)
def handle_error(err):
if wants_json_response():
return jsonify(error=str(err), code=err.code), err.code
templates = ["error/{}.html".format(err.code), "error/default.html"]
try:
print(dir(err))
return render_template(templates, error=err), err.code
except TemplateNotFound:
return err.get_response()
def role_required(*roles):
def wrapper(fn):
@wraps(fn)
def decorated_view(*args, **kwargs):
if not current_user.is_authenticated():
return current_app.login_manager.unauthorized()
has_role = False
user = current_app.login_manager.reload_user()
for role in roles:
has_role |= user.has_role(role)
if not has_role:
return current_app.login_manager.unauthorized()
return fn(*args, **kwargs)
return decorated_view
return wrapper
@login_manager.user_loader
def load_user(user_name):
return User.query.get(user_name)
@login_manager.request_loader
def load_user_from_header(header_val):
for api_key in [request.args.get("api_key"), request.headers.get("X-API-Key")]:
if api_key:
user = User.query.filter_by(api_key=api_key).one_or_none()
if user:
return user
return None
return None
def left_nav():
links = [
View("Home", "index"),
View("Route", "route"),
View("Jobs", "status", job_id=None),
]
if current_user.has_role("admin") or current_user.has_role("worker_host"):
links.insert(2, View("Workers", "worker"))
return Navbar("E:D LRR", *links)
def right_nav():
links = [View("Login", "login"), View("Register", "register")]
if current_user.is_authenticated:
links = [View("Change Password", "change_password"), View("Logout", "logout")]
if current_user.has_role("admin"):
links = [View("Admin", "admin.index")] + links
return Navbar("", *links)
register_renderer(app, "bootstrap4", BootsrapRenderer)
nav.register_element("left_nav", left_nav)
nav.register_element("right_nav", right_nav)
def compute_route(args, kwargs):
return ed_lrr.route(*args, **kwargs)
class AnonymousUser(AnonymousUserMixin):
def has_role(self, role):
return False
@property
def roles(self):
return []
@roles.setter
def __set_roles(self, value):
raise NotImplementedError
login_manager.anonymous_user = AnonymousUser
@generic_repr
class Worker(db.Model):
id = db.Column(
UUIDType(binary=False, native=False), primary_key=True, default=uuid.uuid4
)
name = db.Column(db.String, unique=True)
current_job = db.Column(
UUIDType(binary=False, native=False),
db.ForeignKey("job.id"),
nullable=True,
default=None,
)
job = relationship("Job", backref="workers")
last_active = db.Column(DateTime, nullable=True, default=None)
owner_name = db.Column(
db.String, db.ForeignKey("user.name"), nullable=True, index=True
)
owner = relationship("User", backref="workers")
user_roles = db.Table(
"user_roles",
db.Column("user_name", db.String, db.ForeignKey("user.name"), primary_key=True),
db.Column("role_name", db.String, db.ForeignKey("role.name"), primary_key=True),
)
class Role(db.Model):
name = db.Column(db.String, unique=True, index=True, primary_key=True)
def __init__(self, name):
self.name = name
def __repr__(self):
return self.name
class User(db.Model, UserMixin):
name = db.Column(db.String, unique=True, index=True, primary_key=True)
is_active = db.Column(db.Boolean, default=False)
api_key = db.Column(
UUIDType(binary=False, native=False),
nullable=True,
default=uuid.uuid4,
index=True,
)
password = db.Column(PasswordType(schemes=["pbkdf2_sha512"], max_length=256))
created = db.Column(DateTime, default=datetime.today)
roles = db.relationship("Role", secondary="user_roles")
def add_roles(self, roles):
for role_name in roles:
role = Role.query.filter_by(name=role_name).one()
if role not in self.roles:
self.roles.append(role)
db.session.commit()
def has_role(self, role_name):
return (
Role.query.join(User.roles)
.filter(User.name == self.name, Role.name == role_name)
.count()
> 0
)
def reset_api_key(self):
self.api_key = uuid.uuid4()
db.session.add(self)
db.session.comiit()
def get_id(self):
return self.name
def __repr__(self):
return self.name
class Job(db.Model):
id = db.Column(
UUIDType(binary=False, native=False), primary_key=True, default=uuid.uuid4
)
user_name = db.Column(
db.String, db.ForeignKey("user.name"), nullable=True, index=True
)
func = db.Column(db.String)
args = db.Column(JSONType)
kwargs = db.Column(JSONType)
state = db.Column(JSONType, default={})
priority = db.Column(db.Integer, default=0, nullable=True)
created = db.Column(DateTime, default=datetime.today)
finished = db.Column(DateTime, nullable=True, default=None)
started = db.Column(DateTime, nullable=True, default=None)
last_update = db.Column(DateTime, nullable=True, default=None)
user = relationship("User", backref="jobs")
# ============================================================
def __repr__(self):
return str(self.id)
@property
def future(self):
fut = executor.futures._futures.get(self.id)
return fut
@property
def sort_key(self):
state_priorities = {
"Queued": 0,
"Starting": 1,
"Error": 1,
"Stalled": 1,
"Running": 1,
}
status_key = state_priorities.get(self.status[1], -1) + 1
user = 1 - int(self.user is not None)
return (user, -status_key, self.priority, self.created)
@property
def age(self):
dt = datetime.today() - self.created
return dt - dt % timedelta(seconds=1)
@classmethod
def get_next(cls):
for job in sorted(cls.query.all(), key=lambda v: v.sort_key):
if job.status[1] in ["Done"]:
continue
return job
return None
# return cls.query.
@property
def status(self):
# [
# ("primary", "Done"),
# ("danger", "Error"),
# ("info", "Stalled"),
# ("success", "Running"),
# ("secondary", "Starting"),
# ("warning", "Queued"),
# ]
# return states[self.id.int%len(states)]
if self.state.get("result"):
return ("primary", "Done")
if self.state.get("error"):
return ("danger", "Error")
if self.state.get("progress"):
if (datetime.today() - self.last_update).total_seconds() > (60 * 10):
return ("info", "Stalled")
return ("success", "Running")
if self.started is not None:
return ("secondary", "Starting")
return ("warning", "Queued")
@status.setter
def __set_status(self):
raise NotImplementedError
@property
def dict(self):
return {
"id": self.id,
"args": self.args,
"kwargs": self.kwargs,
"state": self.state,
"finished": self.finished,
"created": self.created,
"started": self.started,
}
@dict.setter
def __set_dict(self, value):
raise NotImplementedError
@property
def route(self):
try:
return prepare_route(self.state["result"])
except KeyError:
return None
@property
def t_rem(self):
if self.started is None:
return None
runtime = datetime.today() - self.started
try:
prc_done = self.state["progress"]["prc_done"]
if prc_done != 0:
t_rem = (runtime / prc_done) * (100 - prc_done)
return timedelta(seconds=round(t_rem.total_seconds(), 0))
return None
except KeyError:
return None
@t_rem.setter
def __set_t_rem(self, value):
raise NotImplementedError
@classmethod
def new(cls, func, args=None, kwargs=None):
args = args or ()
kwargs = kwargs or {}
job = cls(args=args, kwargs=kwargs, func=func.__qualname__)
job.__last_upd = 0.0
if current_user.is_authenticated:
job.user = current_user
db.session.add(job)
db.session.commit()
return job
def start(self):
global executor
self.state = {}
self.started = None
db.session.add(self)
db.session.commit()
args = self.args + [self.callback]
try:
future = executor.submit_stored(self.id, compute_route, args, self.kwargs)
except (BrokenProcessPool, RuntimeError) as e:
print("Error:", e)
print("Restarting Executor!")
executor = Executor(app)
future = executor.submit_stored(self.id, compute_route, args, self.kwargs)
future.add_done_callback(self.done)
def callback(self, cb_state):
try:
if self.started is None:
self.started = datetime.today()
if self.last_update is not None:
time_since_last_upd = (
datetime.today() - self.last_update
).total_seconds()
if time_since_last_upd < 5.0:
return
state = {}
state.update(self.state)
state.update({"progress": cb_state})
self.state = state
self.last_update = datetime.today()
db.session.add(self)
db.session.commit()
except Exception as e:
print(e)
def done(self, future):
print(self.id, "DONE")
state = {}
state.update(self.state)
executor.futures.pop(self.id)
exc = future.exception()
if exc:
state.update(
{"error": {"type": type(exc).__name__, "args": list(exc.args)}}
)
else:
state.update({"result": future.result()})
self.state = state
self.finished = datetime.now()
db.session.add(self)
db.session.commit()
class Ship(db.Model):
user_name = db.Column(
db.String, db.ForeignKey("user.name"), nullable=True, index=True, primary_key=True
)
user = relationship("User", backref="ships")
ship = db.Column(db.JSONType, nullable=False, index=True, primary_key = True)
db.create_all()
for role in ["admin", "user", "worker_host"]:
if Role.query.filter_by(name=role).one_or_none() is None:
db.session.add(Role(role))
def create_user(name, password, roles, active=False):
user = User.query.filter_by(name=name).one_or_none()
if user:
db.session.delete(user)
user = User(name=name, password=password, is_active=active)
user.add_roles(roles)
db.session.add(user)
db.session.commit()
return user
# create_user("admin", "admin", ["admin", "user"], True)
# create_user("user", "user", ["user"], True)
# create_user("host", "host", ["user", "worker_host"], True)
class SQLAView(ModelView):
column_exclude_list = ["password"]
column_editable_list = []
create_modal = True
edit_modal = True
can_view_details = True
column_display_pk = True
def is_accessible(self):
return current_user.is_authenticated and current_user.has_role("admin")
def inaccessible_callback(self, name, **kwargs):
return redirect(url_for("login"))
class UserView(SQLAView):
from wtforms import PasswordField
column_list = ("name", "active", "password", "api_key", "roles")
column_formatters = {
"password": lambda view, context, model, name: "",
"api_key": lambda view, context, model, name: model.api_key or "",
}
form_extra_fields = {"password": PasswordField("Password")}
class JobView(SQLAView):
# Job.id,Job.user,Job.func,Job.args,Job.kwargs,Job.state,Job.created,Job.finished,Job.started,Job.last_update
column_list = ("id", "status", "user", "created", "started", "finished")
column_formatters = {"status": lambda view, context, model, name: model.status[1]}
class WorkerView(SQLAView):
pass
# # Job.id,Job.user,Job.func,Job.args,Job.kwargs,Job.state,Job.created,Job.finished,Job.started,Job.last_update
# column_list = ("id", "status", "user", "created", "started", "finished")
# column_formatters = {
# "user": lambda view, context, model, name: model.user.name
# if model.user
# else "",
# "status": lambda view, context, model, name: model.status[1],
# }
admin.add_view(JobView(Job, db.session))
admin.add_view(UserView(User, db.session))
admin.add_view(SQLAView(Worker, db.session))
admin.add_view(SQLAView(Role, db.session))
def submit_job(func, *args, **kwargs):
job = Job.new(func, args, kwargs)
job.start()
return job.id
@app.route("/api/route", methods=["GET", "POST"])
@use_kwargs(
{
"jump_range": fields.Float(required=True),
"mode": fields.String(
missing="bfs", validate=validate.OneOf(["bfs", "greedy", "a-star"])
),
"systems": fields.DelimitedList(fields.String, required=True),
"permute": fields.String(
missing=None,
validate=validate.OneOf(
["off", "all", "keep_first", "keep_last", "keep_both"]
),
),
"primary": fields.Boolean(missing=False),
"factor": fields.Float(missing=0.5),
}
)
def api_route(_=None, **args):
if args["permute"] == "off":
args["permute"] = None
args["systems"] = [s.strip() for s in args["systems"]]
args = (
args["systems"],
args["jump_range"],
None,
args["mode"],
args["primary"],
args["permute"] is not None,
args["permute"] in ["keep_first", "keep_both"],
args["permute"] in ["keep_last", "keep_both"],
args["factor"],
None,
r"D:\devel\rust\ED_LRR\stars.csv",
app.config["ROUTE_WORKERS"],
)
return jsonify({"id": submit_job(ed_lrr.route, *args)})
@app.route("/api/status")
def api_status():
info = {"queued_jobs": len(executor.futures._futures)}
return jsonify(info)
@app.route("/api/whoami")
def api_whoami():
return jsonify({"name": current_user.name})
@app.route("/api/status/<uuid:job_id>")
def api_job_status(job_id):
job = Job.query.get_or_404(str(job_id))
return jsonify(job.dict)
@app.route("/static/<path:path>")
def send_static(path):
return send_from_directory("static", path)
@app.route("/route", methods=["GET", "POST"])
@login_required
def route():
form = RouteForm()
if form.validate_on_submit():
data = dict(form.data)
if data["permute"] == "off":
data["permute"] = None
del data["csrf_token"]
del data["submit"]
job = api_route(data)
return redirect(url_for("status", job_id=job.json["id"]))
return render_template("form.html", form=form, title="Plot Route")
@app.route("/status/", defaults={"job_id": None})
@app.route("/status/<uuid:job_id>")
@login_required
def status(job_id=None):
if job_id is not None:
job = Job.query.get_or_404(str(job_id))
return render_template("job.html", job=job)
return render_template("status.html", Job=Job, state=request.args.get("state"))
@app.route("/")
def index():
return render_template("index.html")
@app.route("/login", methods=["GET", "POST"])
def login():
if current_user.is_authenticated:
return redirect(url_for("index"))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(name=form.data["username"]).one_or_none()
if (user is None) or (user.password != form.data["password"]):
flash("Invalid credentials!", "danger")
return redirect(url_for("login"))
if not user.is_active:
flash("Account is deactivated!", "warning")
return redirect(url_for("login"))
login_user(user, remember=form.data["remember"])
next = request.args.get("next")
if not is_safe_url(next):
next = None
return redirect(next or url_for("status"))
return render_template("form.html", form=form, title="Login")
@app.route("/register", methods=["GET", "POST"])
def register():
form = RegisterForm()
if form.validate_on_submit():
if User.query.filter_by(name=form.data["username"]).one_or_none() is not None:
flash("Username already exists", "danger")
return render_template("form.html", form=form, title="Register")
user = User()
user.name = form.data["username"]
user.password = form.data["password"]
db.session.add(user)
db.session.commit()
login_user(user)
return redirect(url_for("status"))
return render_template("form.html", form=form, title="Register")
@app.route("/change_password", methods=["GET", "POST"])
def change_password():
if current_user.is_anonymous:
return redirect(url_for("index"))
form = ChangePasswordForm()
if form.validate_on_submit():
if form.data["old_password"] == current_user.password:
current_user.password = form.data["password"]
flash("Password changed!", "success")
else:
flash("Wrong password!", "danger")
return render_template("form.html", form=form, title="Register")
return redirect(url_for("status"))
return render_template("form.html", form=form, title="Register")
@app.route("/workers/", defaults={"worker_id": None})
@app.route("/workers/<uuid:worker_id>")
@login_required
def worker(worker_id):
return render_template("workers.html")
@app.route("/logout")
def logout():
logout_user()
return redirect(url_for("login"))
@app.before_first_request
def resume_jobs():
print("NEXT:", Job.get_next())
with app.test_request_context():
for job in Job.query.all():
if job.status[1] != "Done":
print("Restarting {} with state {}".format(job.id, job.status[1]))
job.start()
user_cli = AppGroup('user', help="Manage users")
job_cli = AppGroup('job', help="Manage Jobs")
worker_cli = AppGroup('worker', help="Manage Workers")
@app.cli.command("gevent")
def cmd_gevent():
return
@user_cli.command("create")
@click.argument("name")
@click.option("-i", "--inactive", help="Crate account as inactive", is_flag=True, default=False)
@click.option("-r", "--role", help="Assign role to account", default=["user"], multiple=True)
@click.password_option("-p", "--password", help="Password for user")
def cmd_create_user(name, role, password, inactive):
"Create a new user"
create_user(name, password, role, not inactive)
print("User created!")
app.cli.add_command(user_cli)
app.cli.add_command(job_cli)
app.cli.add_command(worker_cli)
if __name__ == "__main__":
app.run(host="127.0.0.1", port=3777, debug=True)

19
ed_lrr_gui/web/config.py Normal file
View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
import os
SECRET_KEY = "ED_LRR_WEBAPP"
SQLALCHEMY_DATABASE_URI = "sqlite:///ed_lrr_web_ui.db"
SQLALCHEMY_TRACK_MODIFICATIONS = False
ROUTE_WORKERS = 0
EXECUTOR_TYPE = "process"
EXECUTOR_MAX_WORKERS = os.cpu_count() - 1
EXECUTOR_FUTURES_MAX_LENGTH = 500
FLASK_ADMIN_SWATCH = "Darkly"
DEBUG_TB_TEMPLATE_EDITOR_ENABLED = True
MAIL_DEFAULT_SENDER = '"ED_LRR Admin" <ed_lrr@gmail.com>'

106
ed_lrr_gui/web/forms.py Normal file
View File

@ -0,0 +1,106 @@
# -*- coding: utf-8 -*-
from flask_wtf import FlaskForm
from wtforms import (
StringField,
PasswordField,
FieldList,
FloatField,
BooleanField,
SelectField,
SubmitField,
validators,
Field,
)
from wtforms.widgets.html5 import NumberInput
from wtforms.widgets import TextInput
from wtforms.validators import ValidationError
class StringListField(Field):
widget = TextInput()
def _value(self):
if self.data:
return u",".join(self.data)
else:
return u""
def process_formdata(self, valuelist):
if valuelist:
self.data = [x.strip() for x in valuelist[0].split(",")]
else:
self.data = []
class RouteForm(FlaskForm):
systems = StringListField("Systems", [validators.DataRequired()])
jump_range = FloatField(
"Jump Range (Ly)",
[validators.DataRequired(), validators.NumberRange(0, None)],
widget=NumberInput(min=0, step=0.1),
)
mode = SelectField(
"Routing Mode",
choices=[
("bfs", "Breadth-First Search"),
("greedy", "Greedy Search"),
("a-star", "A*-Search"),
],
)
permute = SelectField(
"Permutation Mode",
choices=[
("off", "Off"),
("keep_first", "Keep starting system"),
("keep_last", "Keep destination system"),
("keep_both", "Keep both endpoints"),
],
)
primary = BooleanField("Only route through primary stars")
factor = FloatField(
"Greedyness for A*-Search (%)",
[validators.NumberRange(0, 100)],
default=50,
widget=NumberInput(min=0, max=100, step=1),
)
priority = FloatField(
"Priority (0=max, 100=min)",
[validators.NumberRange(0, 100)],
default=0,
widget=NumberInput(min=0, max=100, step=1),
)
submit = SubmitField("GO!")
class LoginForm(FlaskForm):
username = StringField("Username", [validators.Required()])
password = PasswordField("Password", [validators.Required()])
remember = BooleanField("Remember me")
submit = SubmitField("Login")
class RegisterForm(FlaskForm):
username = StringField("Username", [validators.Required()])
password = PasswordField(
"Password",
[
validators.Required(),
validators.EqualTo("confirm", message="Passwords must match"),
],
)
confirm = PasswordField("Verify password", [validators.Required()])
submit = SubmitField("Login")
class ChangePasswordForm(FlaskForm):
old_password = PasswordField("Current Password", [validators.Required()])
password = PasswordField(
"Password",
[
validators.Required(),
validators.EqualTo("confirm", message="Passwords must match"),
],
)
confirm = PasswordField("Verify password", [validators.Required()])
submit = SubmitField("Change")

View File

@ -0,0 +1,23 @@
body,input,select,pre {
background-color: #222 !important;
color: #eee;
}
table {
line-height: 1;
}
.progress {
background-color: #444;
}
.progress-bar {
background-color: #f70;
}
.form-control {
color: #eee !important;
}
#graph {
border: 1px solid #eee;
width: 512px;
height: 512px;
}

View File

@ -0,0 +1,5 @@
{% extends 'admin/master.html' %}
{% block body %}
<p>Hello world</p>
{% endblock %}

View File

@ -0,0 +1,48 @@
{% extends "bootstrap/base.html" %}
{% import "bootstrap/utils.html" as utils %}
{% block title %}Elite: Dangerous Long Range Router{% endblock %}
{% block scrips %}
{% endblock %}
{% block styles %}
{{super()}}
<link rel="stylesheet" href="{{url_for('static', filename='theme.css')}}">
{% endblock %}
{% block navbar %}
<nav class="navbar navbar-expand-lg navbar-dark" style="background-color: #222;">
<a class="navbar-brand" href="/">E:D LRR</a>
<ul class="navbar-nav mr-auto">
{{nav.left_nav.render(renderer='bootstrap4')}}
</ul>
<ul class="navbar-nav ml-auto">
{% if current_user.is_authenticated %}
<li class="nav-item">
<a class="nav-link">
Logged in as {{current_user.name}}
</a>
</li>
{% endif %}
{{nav.right_nav.render(renderer='bootstrap4')}}
</ul>
</nav>
{% endblock %}
{% block content %}
<div class="container">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category,message in messages %}
<div class="alert alert-{{category}}" role="{{category}}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
{# application content needs to be provided in the app_content block #}
{% block app_content %}{% endblock %}
</div>
{% endblock %}

View File

@ -0,0 +1,6 @@
{% extends "base.html" %}
{% block app_content %}
<h1>404 Not Found</h1>
<p><a href="{{ url_for('index') }}"><button type="button" class="btn btn-secondary">Back</button></a></p>
{% endblock %}

View File

@ -0,0 +1,16 @@
{% extends "base.html" %}
{% import "bootstrap/wtf.html" as wtf %}
{% block app_content %}
<h1>{{title}}</h1>
{% for field in form %}
{% for error in field.errors %}
<div class="alert alert-danger" role="danger">{{error}}</div>
{% endfor %}
{% endfor %}
<div class="row">
<div class="col-md-4">
{{ wtf.quick_form(form) }}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,10 @@
{% extends "base.html" %}
{% block app_content %}
<h1>E:D LRR</h1>
<div class="row">
<div class="col-md-4">
Number of Jobs: {{current_user.jobs|count}}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,131 @@
{% extends "base.html" %}
{% block app_content %}
<h1>Job Status <span class="badge badge-{{job.status[0]}}">{{ job.status[1] }}</span></h1>
<div class="row">
<div class="col-lg-0">
{% if job.state.error %}
<ul>
{% for err in job.state.error.args %}
<li>{{err}}</li>
{% endfor %}
</ul>
{% endif %}
{% if job.state.progress %}
<p class="lead">Routing from <b>{{ job.state.progress.from }}</b> to <b>{{ job.state.progress.to }}</b> using
{{ job.state.progress.mode }}</p>
<p>Current system: <b>{{ job.state.progress.system }}</b></p>
<p>Search queue size: <b>{{"{:,}".format(job.state.progress.queue_size) }}</b></p>
<p>Number of systems checked: <b>{{"{:,}".format(job.state.progress.n_seen) }}
({{job.state.progress.prc_seen|round(2)}} %)</b></p>
<p>Estimated time remaining: <b>{{job.t_rem}}</b></p>
<p>Search Depth: <b>{{job.state.progress.depth}}</b></p>
<div class="progress" style="width: 100%;">
<div class="progress-bar progress-bar-striped progress-bar-animated"
style="width: {{job.state.progress.prc_done}}%;" role="progressbar"
aria-valuenow="{{job.state.progress.prc_done|round(2)}}" aria-valuemin="0" aria-valuemax="100">
{{job.state.progress.prc_done|round(2)}}&nbsp;%
</div>
</div>
{% endif %}
{% if job.state.result %}
<h2>Result</h2>
<h3>Map</h3>
<div id="graph">
</div>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script type="text/javascript">
function dist(a, b) {
var sum = 0;
for (var i = 0; i < a.length; ++i) {
sum += Math.pow(a[i] - b[i], 2)
}
return Math.pow(sum, 0.5);
}
var width = 512;
var height = 512;
var route = {{job.route | tojson}};
var vis = d3.select("#graph")
.append("svg").attr("viewBox", [0, 0, width, height]);
vis.attr("width", width)
.attr("height", height);
var g = vis.append("g");
vis.call(d3.zoom()
.extent([
[0, 0],
[width, height]
])
.on("zoom", () => {
g.attr("transform", d3.event.transform);
}));
var lines = [];
for (var i = 0; i < route.length - 1; ++i) {
lines.push({
x1: route[i].pos[1],
x2: route[i + 1].pos[1],
y1: -route[i].pos[2],
y2: -route[i + 1].pos[2],
dist: dist(route[i].pos, route[i + 1].pos),
color: route[i].color || '#eee'
})
}
g.selectAll(".line")
.data(lines)
.enter()
.append("line")
.attr("x1", (l) => l.x1)
.attr("y1", (l) => l.y1)
.attr("x2", (l) => l.x2)
.attr("y2", (l) => l.y2)
.style("stroke", (l) => l.color)
.style("stroke-width", 5)
.append("title")
.text((l) => Math.round(l.dist * 100) / 100 + " Ly");
g.selectAll("circle .nodes")
.data(route)
.enter()
.append("svg:circle")
.attr("class", "nodes")
.attr("cx", (d) => d.pos[1])
.attr("cy", (d) => -d.pos[2])
.attr("r", 10)
.attr("fill", (d) => d.color)
.append("title")
.text((d) => d.body + " (" + d.star_type + ")")
</script>
<h3>Jumps</h3>
<table class="table table-striped table-bordered">
<thead>
<tr>
<th scope="col">Num</th>
<th scope="col">Body</th>
<th scope="col">Type</th>
<th scope="col">Distance from arrival</th>
<th scope="col">Jump distance</th>
</tr>
</thead>
{% for sys in job.route %}
<tr>
<th scope="row">{{sys.num}}</td>
<td>{{sys.body}}</td>
<td style="color: {{sys.color}}">{{sys.star_type}}</td>
<td>{{"{:,}".format(sys.distance)}} Ls</td>
<td>{{"{:,}".format(sys.jump_dist|round(2))}} Ly</td>
</tr>
{% endfor %}
<tr>
<th scope="row" colspan=4>Total Distance</th>
<td>{{"{:,}".format(job.route|sum(attribute='jump_dist')|round(2))}} Ly</td>
</tr>
</table>
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,87 @@
{% extends "base.html" %}
{% block app_content %}
{% if current_user.has_role('admin') %}
{% set jobs = Job.query.all() %}
{% else %}
{% set jobs = current_user.jobs %}
{% endif %}
<h1>System Status</h1>
<div class="row">
<h2>Overview</h2>
</div>
<div class="row">
<table class="table table-striped table-bordered" style="width: 1px;">
<tr>
<th>Status</th>
<th>Count</th>
</tr>
{% for group in (jobs|groupby('status')) %}
<tr>
<td>
<a href="?state={{group.grouper[1]}}">
<span class="badge badge-{{ group.grouper[0] }}">{{ group.grouper[1] }}</span>
</a>
</td>
<td>
{{group.list|count}}
</td>
<tr>
{% endfor %}
<tr>
<td>
<a href="?">
Total
</a>
</td>
<td>
{{jobs|count}}
</td>
</tr>
</table>
</div>
<div class="row">
<h2>Jobs</h2>
</div>
<div class="row">
<!-- Next: {{Job.next().id}} -->
<table class="table table-striped table-bordered" style="width: 100%;">
<thead>
<tr>
<th scope="col">ID</th>
<th scope="col">Systems</th>
<th scope="col">Status</th>
<th scope="col">User</th>
<th scope="col">Priority</th>
<th scope="col">Progess</th>
<th scope="col">ETC</th>
<th scope="col">Created</th>
</tr>
</thead>
{% for job in (jobs|sort(attribute='sort_key')) %}
{% if (state==None) or job.status[1]==state %}
<tr>
<td style="width: 1px; white-space: nowrap;"><a href="{{url_for('status',job_id=job.id)}}">{{job.id}}</a></td>
<td style="width: 1px; white-space: nowrap;">{{job.args[0]|join(', ')}}</td>
<td style="width: 1px; white-space: nowrap;"><span class="badge badge-{{job.status[0]}}">{{ job.status[1] }}</span></td>
<td style="width: 1px; white-space: nowrap;">{{job.user.name}}</td>
<td style="width: 1px; white-space: nowrap;">{{job.priority}}</td>
{% if job.state.progress %}
<td>
<div class="progress" style="width: 100%;">
<div class="progress-bar progress-bar-striped progress-bar-animated" style="width: {{job.state.progress.prc_done}}%;" role="progressbar" aria-valuenow="{{job.state.progress.prc_done|round(2)}}" aria-valuemin="0" aria-valuemax="100">
{{job.state.progress.prc_done|round(2)}}&nbsp;%
</div>
</div>
</td>
{% else %}
<td>Unknown</td>
{% endif %}
<td style="width: 1px; white-space: nowrap;">{{job.t_rem}}</td>
<td style="width: 1px; white-space: nowrap;">{{job.age}} ago</td>
</tr>
{% endif %}
{% endfor %}
</table>
</div>
{% endblock %}

View File

@ -0,0 +1,14 @@
{% extends "base.html" %}
{% block app_content %}
<h1>Workers</h1>
<div class="row">
<div class="col-md-4">
{% if current_user.is_authenticated %}
Hello {{current_user.name}}!
{% else %}
Nothing to see here!
{% endif %}
</div>
</div>
{% endblock %}

75
ed_lrr_gui/web/utils.py Normal file
View File

@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
from flask_nav.renderers import Renderer
from dominate import tags
from urllib.parse import urlparse, urljoin
from flask import request
def is_safe_url(target):
ref_url = urlparse(request.host_url)
test_url = urlparse(urljoin(request.host_url, target))
return test_url.scheme in ("http", "https") and ref_url.netloc == test_url.netloc
def dist(p1, p2):
s = 0
for c1, c2 in zip(p1, p2):
s += (c1 - c2) ** 2
return s ** 0.5
class BootsrapRenderer(Renderer):
def visit_Navbar(self, node):
sub = []
for item in node.items:
sub.append(self.visit(item))
return "".join([v.render() for v in sub])
def visit_View(self, node):
classes = ["nav-link"]
if node.active:
classes.append("active")
return tags.li(
tags.a(node.text, href=node.get_url(), cls=" ".join(classes)),
cls="nav-item",
)
def visit_Subgroup(self, node):
# almost the same as visit_Navbar, but written a bit more concise
return tags.div(node.title, *[self.visit(item) for item in node.items])
colors = {
"O": "#0000FF",
"B": "#140AF0",
"A": "#3C1EDC",
"F": "#EEEEEE",
"G": "#969646",
"K": "#B43C1E",
"M": "#FF280A",
"L": "#FF1E00",
"T": "#800000",
"Y": "#800000",
"White Dwarf": "#5D67EF",
"Neutron": "#99A0FF",
}
def prepare_route(route):
entries = []
prev = route[0]
num = 1
for hop in route[1:]:
prev["jump_dist"] = dist(hop["pos"], prev["pos"])
prev["num"] = num
prev["color"] = colors.get(prev["star_type"].split()[0], "#eee")
prev["distance"] = prev["distance"]
entries.append(prev)
prev = hop
num += 1
prev["jump_dist"] = 0
prev["distance"] = prev["distance"]
prev["num"] = num
prev["color"] = colors.get(prev["star_type"].split()[0], "#eee")
entries.append(prev)
return entries

7
ed_lrr_gui/web/worker.py Normal file
View File

@ -0,0 +1,7 @@
# -*- coding: utf-8 -*-
import requests as RQ
import _ed_lrr as ed_lrr
funcs = {
func: getattr(ed_lrr, func) for func in dir(ed_lrr) if not func.startswith("_")
}

BIN
icon/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

145
icon/make.py Normal file
View File

@ -0,0 +1,145 @@
# -*- coding: utf-8 -*-
import svgwrite
import random
from math import sin, cos, pi
import tsp as m_tsp
def dist(p1, p2):
return dist2(p1, p2) ** 0.5
def dist2(p1, p2):
diff = 0
for a, b in zip(p1, p2):
diff += (a - b) ** 2
return diff
def tsp(points):
res = []
for idx in m_tsp.tsp(points)[1]:
res.append(points[idx])
return res
def make_points(n, size, min_dist=0):
min_dist *= min_dist
points = []
while len(points) < n:
px, py = random.random(), random.random()
px *= size / 2
py *= size / 2
px += 70
py += 70
valid = True
for p in points:
if dist2(p, (px, py)) < min_dist:
valid = False
break
if valid:
points.append((px, py))
print("{}/{}".format(len(points), n))
return points
def ring_step(v):
return 10 + v * 10
def generate(seed, name=None, small=False):
sd = 1
if small:
sd = 2
random.seed(seed)
w = 2
max_rings = 3
num_points = 5
min_dist = 10 + 10 + 20 * (max_rings + 1)
base_r = 10
size = 1000
if name is None:
name = seed
dwg = svgwrite.Drawing(filename="out/{}.svg".format(name))
dwg.defs.add(dwg.style(".background { fill: #222; }"))
dwg.add(dwg.rect(size=("100%", "100%"), class_="background"))
print("Generating points...")
color = "#eee"
pos = make_points(num_points, size, min_dist=min_dist)
print("TSP...")
pos = tsp(pos)
for (x1, y1), (x2, y2) in zip(pos, pos[1:]):
if small:
x1 /= sd
x2 /= sd
y1 /= sd
y2 /= sd
dwg.add(dwg.line(
(x1, y1),
(x2, y2),
stroke_width=w,
stroke=color
))
for (px, py) in pos:
base_r = 3
if small:
base_r = 5
px /= sd
py /= sd
if random.random() > 0.8:
dwg.add(
dwg.circle(
(px, py),
r=base_r + random.random() * base_r,
stroke_width=w,
stroke="#0ae",
)
).fill("#0ae")
else:
dwg.add(
dwg.circle(
(px, py),
r=base_r + random.random() * base_r,
stroke_width=w,
stroke=color,
)
).fill(color)
r = base_r
for _ in range(random.randint(1, max_rings)):
if small:
random.random()
random.random()
random.random()
continue
r += ring_step(random.random())
ring_col = color
if random.random() > 0.75:
ring_col = "#ea0"
circ = dwg.add(dwg.circle(
(px, py),
r=r,
stroke_width=w,
stroke=ring_col
))
circ.fill(color, opacity=0)
d = random.random() * pi * 2
dx = cos(d)
dy = sin(d)
m = random.random()
moon = dwg.add(
dwg.circle(
(px + dx * r, py + dy * r),
r=2 + 2 * m,
stroke_width=w,
stroke=ring_col,
)
)
moon.fill(ring_col)
dwg.save()
seed = -4
generate(seed, "icon_1", small=False)
generate(seed, "icon_1_small", small=True)

2
icon/out/icon_1.svg Normal file
View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8" ?>
<svg baseProfile="full" height="100%" version="1.1" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css"><![CDATA[.background { fill: #222; }]]></style></defs><rect class="background" height="100%" width="100%" x="0" y="0" /><line stroke="#eee" stroke-width="2" x1="188.02404486871725" x2="103.25754783979495" y1="121.5830171153579" y2="270.7955072425374" /><line stroke="#eee" stroke-width="2" x1="103.25754783979495" x2="528.9775215438594" y1="270.7955072425374" y2="470.2261757479043" /><line stroke="#eee" stroke-width="2" x1="528.9775215438594" x2="452.58130125271924" y1="470.2261757479043" y2="180.96408784515882" /><line stroke="#eee" stroke-width="2" x1="452.58130125271924" x2="338.3400040874068" y1="180.96408784515882" y2="208.34132172072512" /><circle cx="188.02404486871725" cy="121.5830171153579" fill="#eee" r="3.3185498772945903" stroke="#eee" stroke-width="2" /><circle cx="188.02404486871725" cy="121.5830171153579" fill="#eee" fill-opacity="0" r="22.429593602379832" stroke="#eee" stroke-width="2" /><circle cx="173.80179554276944" cy="104.23901834695114" fill="#eee" r="2.52050830126476" stroke="#eee" stroke-width="2" /><circle cx="103.25754783979495" cy="270.7955072425374" fill="#eee" r="3.4944547782059745" stroke="#eee" stroke-width="2" /><circle cx="103.25754783979495" cy="270.7955072425374" fill="#eee" fill-opacity="0" r="19.2697560241313" stroke="#eee" stroke-width="2" /><circle cx="115.03444741085107" cy="255.5433554690071" fill="#eee" r="3.7601015027223985" stroke="#eee" stroke-width="2" /><circle cx="103.25754783979495" cy="270.7955072425374" fill="#eee" fill-opacity="0" r="30.13693855048052" stroke="#eee" stroke-width="2" /><circle cx="89.02212054970202" cy="244.23260689141011" fill="#eee" r="3.0119075514208307" stroke="#eee" stroke-width="2" /><circle cx="528.9775215438594" cy="470.2261757479043" fill="#eee" r="4.420763662755435" stroke="#eee" stroke-width="2" /><circle cx="528.9775215438594" cy="470.2261757479043" fill="#eee" fill-opacity="0" r="22.44577790309402" stroke="#ea0" stroke-width="2" /><circle cx="549.9596596985477" cy="462.25354606049257" fill="#ea0" r="3.680925358835544" stroke="#ea0" stroke-width="2" /><circle cx="452.58130125271924" cy="180.96408784515882" fill="#eee" r="3.8758250081323116" stroke="#eee" stroke-width="2" /><circle cx="452.58130125271924" cy="180.96408784515882" fill="#eee" fill-opacity="0" r="21.8231723987879" stroke="#ea0" stroke-width="2" /><circle cx="430.78831758434035" cy="179.8166052191519" fill="#ea0" r="2.827892086263464" stroke="#ea0" stroke-width="2" /><circle cx="452.58130125271924" cy="180.96408784515882" fill="#eee" fill-opacity="0" r="37.812297120687795" stroke="#eee" stroke-width="2" /><circle cx="472.57653937753463" cy="213.0570818761791" fill="#eee" r="2.6102231928654778" stroke="#eee" stroke-width="2" /><circle cx="452.58130125271924" cy="180.96408784515882" fill="#eee" fill-opacity="0" r="55.938220307034" stroke="#eee" stroke-width="2" /><circle cx="506.1669380410402" cy="197.01600427617765" fill="#eee" r="3.252701491079807" stroke="#eee" stroke-width="2" /><circle cx="338.3400040874068" cy="208.34132172072512" fill="#eee" r="4.603865384638267" stroke="#eee" stroke-width="2" /><circle cx="338.3400040874068" cy="208.34132172072512" fill="#eee" fill-opacity="0" r="20.00878719559634" stroke="#eee" stroke-width="2" /><circle cx="329.11968037233845" cy="190.58358550160054" fill="#eee" r="2.132876938772122" stroke="#eee" stroke-width="2" /><circle cx="338.3400040874068" cy="208.34132172072512" fill="#eee" fill-opacity="0" r="39.144105385654704" stroke="#eee" stroke-width="2" /><circle cx="301.84139133159863" cy="222.48742554279568" fill="#eee" r="2.3674072974299003" stroke="#eee" stroke-width="2" /></svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8" ?>
<svg baseProfile="full" height="100%" version="1.1" width="100%" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><style type="text/css"><![CDATA[.background { fill: #222; }]]></style></defs><rect class="background" height="100%" width="100%" x="0" y="0" /><line stroke="#eee" stroke-width="2" x1="94.01202243435863" x2="51.628773919897476" y1="60.79150855767895" y2="135.3977536212687" /><line stroke="#eee" stroke-width="2" x1="51.628773919897476" x2="264.4887607719297" y1="135.3977536212687" y2="235.11308787395214" /><line stroke="#eee" stroke-width="2" x1="264.4887607719297" x2="226.29065062635962" y1="235.11308787395214" y2="90.48204392257941" /><line stroke="#eee" stroke-width="2" x1="226.29065062635962" x2="169.1700020437034" y1="90.48204392257941" y2="104.17066086036256" /><circle cx="94.01202243435863" cy="60.79150855767895" fill="#eee" r="5.53091646215765" stroke="#eee" stroke-width="2" /><circle cx="51.628773919897476" cy="135.3977536212687" fill="#eee" r="6.358738772326545" stroke="#eee" stroke-width="2" /><circle cx="264.4887607719297" cy="235.11308787395214" fill="#0ae" r="9.400253756805997" stroke="#0ae" stroke-width="2" /><circle cx="226.29065062635962" cy="90.48204392257941" fill="#eee" r="6.236611100792434" stroke="#eee" stroke-width="2" /><circle cx="169.1700020437034" cy="104.17066086036256" fill="#eee" r="9.41158619939395" stroke="#eee" stroke-width="2" /></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

25
installer/ED_LRR.iss Normal file
View File

@ -0,0 +1,25 @@
[Setup]
AppName = "ED_LRR"
AppVersion ="0.1.0"
; WizardStyle = modern
DefaultDirName = {autopf}\ED_LRR
DefaultGroupName=ED_LRR
Compression = lzma2/ultra
SolidCompression = true
InternalCompressLevel = ultra
OutputBaseFilename="ED_LRR Setup"
ChangesEnvironment = true
[Files]
Source: "{#SourceFolder}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs
[Icons]
Name: "{group}\ED_LRR"; Filename: "{app}\ED_LRR.exe"; WorkingDir: "{app}"
Name: "{group}\Uninstall ED_LRR"; Filename: "{uninstallexe}"
[Tasks]
;Name: modifypath; Description: Add application directory to PATH; Flags: unchecked
[Run]
Filename: "{app}\ED_LRR.exe"; Parameters: "download"; StatusMsg: "Downloading EDSM dumps..."; Description: "Download EDSM dumps"; Flags: postinstall
Filename: "{app}\ED_LRR.exe"; Description: "Launch ED_LRR"; Flags: postinstall nowait skipifsilent unchecked

107
noxfile.py Normal file
View File

@ -0,0 +1,107 @@
import nox
from nox.logger import logger
import os
import shutil
to_append = []
path = os.environ.get("PATH", "").split(os.pathsep)
while True:
python = shutil.which("python", path=os.pathsep.join(path))
if python is None:
break
python = os.path.dirname(python)
for elem in path:
if elem.startswith(python):
path.remove(elem)
to_append.append(elem)
path += to_append
path = os.pathsep.join(path)
os.environ["PATH"] = path
versions = ["3.{}".format(s) for s in [6, 7, 8]]
versions += ["3"]
nox.options.keywords = "test"
@nox.session(venv_backend="conda")
def devenv(session):
"""Set up development environment"""
global path
location = os.path.abspath(session._runner.venv.location_name)
session.env["PATH"] = os.pathsep.join([location, path, location])
session.conda_install("pycrypto", "ujson")
session.install("--no-cache-dir", "-e",".[all]")
logger.warning(f'Devenv set up, now run "conda activate {location}"')
@nox.session(python=versions, venv_backend="conda")
def test(session):
"""Run the test suite."""
global path
location = os.path.abspath(session._runner.venv.location_name)
python = os.path.join(location, "python.exe")
# friendly_name = session._runner.friendly_name
cargo_args = ["--manifest-path", "./rust/Cargo.toml"]
session.env["PATH"] = os.pathsep.join([location, path, location])
# ================================================
session.run(python, "-VV", external=True)
session.run("cargo", "clean", *cargo_args, external=True)
session.run("cargo", "clean", *(cargo_args + ["--release"]), external=True)
session.run("cargo", "test", *cargo_args, external=True)
session.conda_install("pycrypto", "ujson")
session.install("--no-cache-dir", "setuptools_rust")
session.install("--no-cache-dir", ".[all]")
session.run("py.test", "-v", *(session.posargs or []))
if session.python:
session.notify(f"build-{session.python}")
@nox.session(python=versions, venv_backend="conda")
def build(session):
"Build installer and zip"
global path
location = session._runner.venv.location_name
python = os.path.join(location, "python.exe")
location = os.path.abspath(location)
cargo_args = ["--manifest-path", "./rust/Cargo.toml"]
session.env["PATH"] = os.pathsep.join([location, path, location])
# ================================================
session.run(python, "-VV", external=True)
session.run("cargo", "clean", *cargo_args, external=True)
session.run("cargo", "clean", *(cargo_args + ["--release"]), external=True)
session.conda_install("pycrypto", "ujson")
session.install("--no-cache-dir", ".[gui,web,build]")
session.run(
"pyinstaller",
"-y",
"--console",
"--noupx",
"--clean",
"--hidden-import",
"pkg_resources.py2_warn",
"--name",
f"ED_LRR_{session.python}",
"--specpath",
"build",
"ed_lrr_gui/__main__.py",
external=True,
silent=True,
)
if session.python:
source_path = os.path.abspath(f"./dist/ED_LRR_{session.python}")
else:
source_path = os.path.abspath(f"./dist/ED_LRR")
source_path = source_path.strip(os.sep)
session.run(
"iscc",
f'/FED_LRR_{session.python}',
f'/DSourceFolder={source_path}',
"installer/ED_LRR.iss",
external=True,
silent=True,
)

3
pyproject.toml Normal file
View File

@ -0,0 +1,3 @@
[build-system]
requires = ["setuptools", "wheel","setuptools_rust"]
build-backend = "setuptools.build_meta"

862
rust/Cargo.lock generated Normal file
View File

@ -0,0 +1,862 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "addr2line"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"gimli 0.21.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "adler32"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "aho-corasick"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "as-slice"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)",
"generic-array 0.13.2 (registry+https://github.com/rust-lang/crates.io-index)",
"stable_deref_trait 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"hermit-abi 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "autocfg"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "backtrace"
version = "0.3.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"addr2line 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)",
"cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)",
"miniz_oxide 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
"object 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)",
"rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "better-panic"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"backtrace 0.3.49 (registry+https://github.com/rust-lang/crates.io-index)",
"console 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "bincode"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.112 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "bitflags"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "block-buffer"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"block-padding 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
"generic-array 0.14.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "block-padding"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "bstr"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
"regex-automata 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.112 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "byte-tools"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "byteorder"
version = "1.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "cfg-if"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "chrono"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"num-integer 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)",
"num-traits 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)",
"time 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "clicolors-control"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "cloudabi"
version = "0.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "console"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"clicolors-control 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"encode_unicode 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 1.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
"termios 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "crossbeam-channel"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
"maybe-uninit 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "crossbeam-utils"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "csv"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bstr 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)",
"csv-core 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
"itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
"ryu 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.112 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "csv-core"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "ctor"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 1.0.31 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "derivative"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 1.0.31 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "dict_derive"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 1.0.31 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "digest"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"generic-array 0.14.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "ed_lrr"
version = "0.2.0"
dependencies = [
"better-panic 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"bincode 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
"chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
"crossbeam-channel 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
"csv 1.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
"derivative 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"dict_derive 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"fnv 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
"humantime 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"num_cpus 1.13.0 (registry+https://github.com/rust-lang/crates.io-index)",
"permutohedron 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
"pyo3 0.10.1 (git+https://github.com/PyO3/pyo3)",
"regex 1.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
"rstar 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.112 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.55 (registry+https://github.com/rust-lang/crates.io-index)",
"sha3 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
"strsim 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "encode_unicode"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "generic-array"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"typenum 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "generic-array"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"typenum 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "generic-array"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"typenum 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)",
"version_check 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "ghost"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 1.0.31 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "gimli"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "hash32"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "heapless"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"as-slice 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
"generic-array 0.13.2 (registry+https://github.com/rust-lang/crates.io-index)",
"hash32 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"stable_deref_trait 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "hermit-abi"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "humantime"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "indoc"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"indoc-impl 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
"proc-macro-hack 0.5.16 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "indoc-impl"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro-hack 0.5.16 (registry+https://github.com/rust-lang/crates.io-index)",
"proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 1.0.31 (registry+https://github.com/rust-lang/crates.io-index)",
"unindent 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "inventory"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"ctor 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)",
"ghost 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"inventory-impl 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "inventory-impl"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 1.0.31 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "itoa"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "keccak"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "libc"
version = "0.2.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "lock_api"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"scopeguard 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "maybe-uninit"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "memchr"
version = "2.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "miniz_oxide"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"adler32 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "num-integer"
version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"num-traits 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "num-traits"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "num_cpus"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"hermit-abi 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "object"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "opaque-debug"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "parking_lot"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"lock_api 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
"parking_lot_core 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "parking_lot_core"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
"cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)",
"redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)",
"smallvec 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "paste"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"paste-impl 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
"proc-macro-hack 0.5.16 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "paste-impl"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro-hack 0.5.16 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "pdqselect"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "permutohedron"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "proc-macro-hack"
version = "0.5.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "proc-macro2"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "pyo3"
version = "0.10.1"
source = "git+https://github.com/PyO3/pyo3#93608bf031442fe41e8c259b392c2d2d78dc7d6e"
dependencies = [
"indoc 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
"inventory 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)",
"parking_lot 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)",
"paste 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
"pyo3cls 0.10.1 (git+https://github.com/PyO3/pyo3)",
"unindent 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
"version_check 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "pyo3-derive-backend"
version = "0.10.1"
source = "git+https://github.com/PyO3/pyo3#93608bf031442fe41e8c259b392c2d2d78dc7d6e"
dependencies = [
"proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 1.0.31 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "pyo3cls"
version = "0.10.1"
source = "git+https://github.com/PyO3/pyo3#93608bf031442fe41e8c259b392c2d2d78dc7d6e"
dependencies = [
"pyo3-derive-backend 0.10.1 (git+https://github.com/PyO3/pyo3)",
"quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 1.0.31 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "quote"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "redox_syscall"
version = "0.1.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "regex"
version = "1.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"aho-corasick 0.7.10 (registry+https://github.com/rust-lang/crates.io-index)",
"memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
"regex-syntax 0.6.18 (registry+https://github.com/rust-lang/crates.io-index)",
"thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "regex-automata"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "regex-syntax"
version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "rstar"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"heapless 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)",
"num-traits 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)",
"pdqselect 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"smallvec 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rustc-demangle"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "ryu"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "scopeguard"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "serde"
version = "1.0.112"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"serde_derive 1.0.112 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "serde_derive"
version = "1.0.112"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
"syn 1.0.31 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "serde_json"
version = "1.0.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
"ryu 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.112 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "sha3"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"block-buffer 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
"byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"digest 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
"keccak 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "smallvec"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "stable_deref_trait"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "syn"
version = "1.0.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)",
"quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
"unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "termios"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "thread_local"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "time"
version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "typenum"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "unicode-xid"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "unindent"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "version_check"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "winapi"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[metadata]
"checksum addr2line 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a49806b9dadc843c61e7c97e72490ad7f7220ae249012fbda9ad0609457c0543"
"checksum adler32 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "567b077b825e468cc974f0020d4082ee6e03132512f207ef1a02fd5d00d1f32d"
"checksum aho-corasick 0.7.10 (registry+https://github.com/rust-lang/crates.io-index)" = "8716408b8bc624ed7f65d223ddb9ac2d044c0547b6fa4b0d554f3a9540496ada"
"checksum as-slice 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "37dfb65bc03b2bc85ee827004f14a6817e04160e3b1a28931986a666a9290e70"
"checksum atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
"checksum autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"
"checksum backtrace 0.3.49 (registry+https://github.com/rust-lang/crates.io-index)" = "05100821de9e028f12ae3d189176b41ee198341eb8f369956407fea2f5cc666c"
"checksum better-panic 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3d12a680cc74d8c4a44ee08be4a00dedf671b089c2440b2e3fdaa776cd468476"
"checksum bincode 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5753e2a71534719bf3f4e57006c3a4f0d2c672a4b676eec84161f763eca87dbf"
"checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
"checksum block-buffer 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "dbcf92448676f82bb7a334c58bbce8b0d43580fb5362a9d608b18879d12a3d31"
"checksum block-padding 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5"
"checksum bstr 0.2.13 (registry+https://github.com/rust-lang/crates.io-index)" = "31accafdb70df7871592c058eca3985b71104e15ac32f64706022c58867da931"
"checksum byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
"checksum byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
"checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
"checksum chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "80094f509cf8b5ae86a4966a39b3ff66cd7e2a3e594accec3743ff3fabeab5b2"
"checksum clicolors-control 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "90082ee5dcdd64dc4e9e0d37fbf3ee325419e39c0092191e0393df65518f741e"
"checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
"checksum console 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)" = "45e0f3986890b3acbc782009e2629dfe2baa430ac091519ce3be26164a2ae6c0"
"checksum crossbeam-channel 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "cced8691919c02aac3cb0a1bc2e9b73d89e832bf9a06fc579d4e71b68a2da061"
"checksum crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8"
"checksum csv 1.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "00affe7f6ab566df61b4be3ce8cf16bc2576bca0963ceb0955e45d514bf9a279"
"checksum csv-core 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90"
"checksum ctor 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)" = "39858aa5bac06462d4dd4b9164848eb81ffc4aa5c479746393598fd193afa227"
"checksum derivative 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cb582b60359da160a9477ee80f15c8d784c477e69c217ef2cdd4169c24ea380f"
"checksum dict_derive 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "9d7ee5676aa48357bd5e8bcf095167e6678837232a7b85bce424f46cd93a2c60"
"checksum digest 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
"checksum encode_unicode 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
"checksum fnv 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
"checksum generic-array 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec"
"checksum generic-array 0.13.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0ed1e761351b56f54eb9dcd0cfaca9fd0daecf93918e1cfc01c8a3d26ee7adcd"
"checksum generic-array 0.14.2 (registry+https://github.com/rust-lang/crates.io-index)" = "ac746a5f3bbfdadd6106868134545e684693d54d9d44f6e9588a7d54af0bf980"
"checksum ghost 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2a36606a68532b5640dc86bb1f33c64b45c4682aad4c50f3937b317ea387f3d6"
"checksum gimli 0.21.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bcc8e0c9bce37868955864dbecd2b1ab2bdf967e6f28066d65aaac620444b65c"
"checksum hash32 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d4041af86e63ac4298ce40e5cca669066e75b6f1aa3390fe2561ffa5e1d9f4cc"
"checksum heapless 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "73a8a2391a3bc70b31f60e7a90daa5755a360559c0b6b9c5cfc0fee482362dc0"
"checksum hermit-abi 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "b9586eedd4ce6b3c498bc3b4dd92fc9f11166aa908a914071953768066c67909"
"checksum humantime 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3c1ad908cc71012b7bea4d0c53ba96a8cba9962f048fa68d143376143d863b7a"
"checksum indoc 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "79255cf29f5711995ddf9ec261b4057b1deb34e66c90656c201e41376872c544"
"checksum indoc-impl 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "54554010aa3d17754e484005ea0022f1c93839aabc627c2c55f3d7b47206134c"
"checksum inventory 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "82d3f4b90287725c97b17478c60dda0c6324e7c84ee1ed72fb9179d0fdf13956"
"checksum inventory-impl 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "9092a4fefc9d503e9287ef137f03180a6e7d1b04c419563171ee14947c5e80ec"
"checksum itoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "b8b7a7c0c47db5545ed3fef7468ee7bb5b74691498139e4b3f6a20685dc6dd8e"
"checksum keccak 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "67c21572b4949434e4fc1e1978b99c5f77064153c59d998bf13ecd96fb5ecba7"
"checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
"checksum libc 0.2.71 (registry+https://github.com/rust-lang/crates.io-index)" = "9457b06509d27052635f90d6466700c65095fdf75409b3fbdd903e988b886f49"
"checksum lock_api 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75"
"checksum maybe-uninit 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
"checksum memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400"
"checksum miniz_oxide 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435"
"checksum num-integer 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b"
"checksum num-traits 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)" = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611"
"checksum num_cpus 1.13.0 (registry+https://github.com/rust-lang/crates.io-index)" = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3"
"checksum object 0.20.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1ab52be62400ca80aa00285d25253d7f7c437b7375c4de678f5405d3afe82ca5"
"checksum opaque-debug 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c"
"checksum parking_lot 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e"
"checksum parking_lot_core 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d58c7c768d4ba344e3e8d72518ac13e259d7c7ade24167003b8488e10b6740a3"
"checksum paste 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)" = "026c63fe245362be0322bfec5a9656d458d13f9cfb1785d1b38458b9968e8080"
"checksum paste-impl 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)" = "7b9281a268ec213237dcd2aa3c3d0f46681b04ced37c1616fd36567a9e6954b0"
"checksum pdqselect 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4ec91767ecc0a0bbe558ce8c9da33c068066c57ecc8bb8477ef8c1ad3ef77c27"
"checksum permutohedron 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b687ff7b5da449d39e418ad391e5e08da53ec334903ddbb921db208908fc372c"
"checksum proc-macro-hack 0.5.16 (registry+https://github.com/rust-lang/crates.io-index)" = "7e0456befd48169b9f13ef0f0ad46d492cf9d2dbb918bcf38e01eed4ce3ec5e4"
"checksum proc-macro2 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)" = "beae6331a816b1f65d04c45b078fd8e6c93e8071771f41b8163255bbd8d7c8fa"
"checksum pyo3 0.10.1 (git+https://github.com/PyO3/pyo3)" = "<none>"
"checksum pyo3-derive-backend 0.10.1 (git+https://github.com/PyO3/pyo3)" = "<none>"
"checksum pyo3cls 0.10.1 (git+https://github.com/PyO3/pyo3)" = "<none>"
"checksum quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37"
"checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84"
"checksum regex 1.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "9c3780fcf44b193bc4d09f36d2a3c87b251da4a046c87795a0d35f4f927ad8e6"
"checksum regex-automata 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "ae1ded71d66a4a97f5e961fd0cb25a5f366a42a41570d16a763a69c092c26ae4"
"checksum regex-syntax 0.6.18 (registry+https://github.com/rust-lang/crates.io-index)" = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8"
"checksum rstar 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7b305196ce78c78c37c47890cfde452a14f8a5e7d5516314e69b062b5ed132c2"
"checksum rustc-demangle 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783"
"checksum ryu 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
"checksum scopeguard 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
"checksum serde 1.0.112 (registry+https://github.com/rust-lang/crates.io-index)" = "736aac72d1eafe8e5962d1d1c3d99b0df526015ba40915cb3c49d042e92ec243"
"checksum serde_derive 1.0.112 (registry+https://github.com/rust-lang/crates.io-index)" = "bf0343ce212ac0d3d6afd9391ac8e9c9efe06b533c8d33f660f6390cc4093f57"
"checksum serde_json 1.0.55 (registry+https://github.com/rust-lang/crates.io-index)" = "ec2c5d7e739bc07a3e73381a39d61fdb5f671c60c1df26a130690665803d8226"
"checksum sha3 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b859cf80317bb4ab6b29422f3d77de357ef60a0e0b3dedf28469d2b11d098968"
"checksum smallvec 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c7cb5678e1615754284ec264d9bb5b4c27d2018577fd90ac0ceb578591ed5ee4"
"checksum stable_deref_trait 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8"
"checksum strsim 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
"checksum syn 1.0.31 (registry+https://github.com/rust-lang/crates.io-index)" = "b5304cfdf27365b7585c25d4af91b35016ed21ef88f17ced89c7093b43dba8b6"
"checksum termios 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6f0fcee7b24a25675de40d5bb4de6e41b0df07bc9856295e7e2b3a3600c400c2"
"checksum thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14"
"checksum time 0.1.43 (registry+https://github.com/rust-lang/crates.io-index)" = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438"
"checksum typenum 1.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33"
"checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c"
"checksum unindent 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "63f18aa3b0e35fed5a0048f029558b1518095ffe2a0a31fb87c93dece93a4993"
"checksum version_check 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
"checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6"
"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"

42
rust/Cargo.toml Normal file
View File

@ -0,0 +1,42 @@
[package]
name = "ed_lrr"
version = "0.2.0"
authors = [ "Daniel Seiller <earthnuker@gmail.com>",]
edition = "2018"
repository = "https://gitlab.com/Earthnuker/ed_lrr.git"
license = "MIT"
[lib]
crate-type = [ "cdylib",]
name = "_ed_lrr"
[dependencies]
csv = "1.1.3"
humantime = "2.0.1"
permutohedron = "0.2.4"
serde_json = "1.0.55"
fnv = "1.0.7"
bincode = "1.2.1"
sha3 = "0.9.0"
byteorder = "1.3.4"
strsim = "0.10.0"
rstar = "0.8.0"
crossbeam-channel = "0.4.2"
better-panic = "0.2.0"
derivative = "2.1.1"
dict_derive = "0.2.0"
num_cpus = "1.13.0"
regex = "1.3.9"
chrono = "0.4.11"
[dependencies.pyo3]
git = "https://github.com/PyO3/pyo3"
features = [ "extension-module",]
[dependencies.serde]
version = "1.0.112"
features = [ "derive",]
[profile.release]
codegen-units = 1
lto = true

19
rust/cargo_check.py Normal file
View File

@ -0,0 +1,19 @@
import toml
import subprocess as SP
import os
def set_version(rev=None):
with open("Cargo.toml") as fh:
cargo = toml.loads(fh.read())
cargo["dependencies"]["pyo3"]["rev"] = rev
if rev is None:
del cargo["dependencies"]["pyo3"]["rev"]
with open("Cargo.toml", "w") as fh:
toml.dump(cargo, fh)
for commit in open("ch.txt").readlines():
set_version(commit.strip())
if os.system("cargo check") == 0:
break

2203
rust/ch.txt Normal file

File diff suppressed because it is too large Load Diff

19
rust/rustup_check.py Normal file
View File

@ -0,0 +1,19 @@
import subprocess as SP
import os
from datetime import timedelta
from datetime import datetime
dt = timedelta(days=1)
d = datetime.today().date()
while True:
toolchain="nightly-{}".format(d.isoformat())
ret = os.system(
f"rustup default {toolchain}"
)
if ret==0:
os.system("cargo +{} clean".format(toolchain))
if os.system("cargo +{} check".format(toolchain))==0:
print(d)
break
d = d - dt

231
rust/src/common.rs Normal file
View File

@ -0,0 +1,231 @@
use crate::route::Router;
use dict_derive::IntoPyObject;
use pyo3::conversion::ToPyObject;
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyTuple};
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::collections::HashMap;
use std::path::PathBuf;
pub fn get_fsd_booster_info(class: usize) -> Result<f32, String> {
// Data from https://elite-dangerous.fandom.com/wiki/Guardian_Frame_Shift_Drive_Booster
let ret = match class {
0 => 0.0,
1 => 4.0,
2 => 6.0,
3 => 7.75,
4 => 9.25,
5 => 10.5,
_ => return Err(format!("Invalid Guardian booster class: {}", class)),
};
return Ok(ret);
}
pub fn get_fsd_info(rating: usize, class: usize) -> Result<HashMap<String, f32>, String> {
let mut ret = HashMap::new();
// Data from https://elite-dangerous.fandom.com/wiki/Frame_Shift_Drive#Specifications
let (opt_mass, max_fuel) = match (class, rating) {
(2, 1) => (48.0, 0.6),
(2, 2) => (54.0, 0.6),
(2, 3) => (60.0, 0.6),
(2, 4) => (75.0, 0.8),
(2, 5) => (90.0, 0.9),
(3, 1) => (80.0, 1.2),
(3, 2) => (90.0, 1.2),
(3, 3) => (100.0, 1.2),
(3, 4) => (125.0, 1.5),
(3, 5) => (150.0, 1.8),
(4, 1) => (280.0, 2.0),
(4, 2) => (315.0, 2.0),
(4, 3) => (350.0, 2.0),
(4, 4) => (438.0, 2.5),
(4, 5) => (525.0, 3.0),
(5, 1) => (560.0, 3.3),
(5, 2) => (630.0, 3.3),
(5, 3) => (700.0, 3.3),
(5, 4) => (875.0, 4.1),
(5, 5) => (1050.0, 5.0),
(6, 1) => (960.0, 5.3),
(6, 2) => (1080.0, 5.3),
(6, 3) => (1200.0, 5.3),
(6, 4) => (1500.0, 6.6),
(6, 5) => (1800.0, 8.0),
(7, 1) => (1440.0, 8.5),
(7, 2) => (1620.0, 8.5),
(7, 3) => (1800.0, 8.5),
(7, 4) => (2250.0, 10.6),
(7, 5) => (2700.0, 12.8),
(r, c) => return Err(format!("Invalid FSD Type: Rating: {}, Class: {}", r, c)),
};
ret.insert("FSDOptimalMass".to_owned(), opt_mass);
ret.insert("MaxFuel".to_owned(), max_fuel);
return Ok(ret);
}
pub fn get_mult(star_type: &str) -> f32 {
if star_type.contains("White Dwarf") {
return 1.5;
}
if star_type.contains("Neutron") {
return 4.0;
}
1.0
}
pub enum SysEntry {
ID(u32),
Name(String),
}
impl SysEntry {
pub fn parse(s: &str) -> Self {
if let Ok(n) = s.parse() {
SysEntry::ID(n)
} else {
SysEntry::Name(s.to_owned())
}
}
}
pub fn find_matches(
path: &PathBuf,
names: Vec<String>,
exact: bool,
) -> Result<HashMap<String, (f64, Option<System>)>, String> {
let mut best: HashMap<String, (f64, Option<System>)> = HashMap::new();
if names.is_empty() {
return Ok(best);
}
for name in &names {
best.insert(name.to_string(), (0.0, None));
}
let mut reader = match csv::ReaderBuilder::new().from_path(path) {
Ok(rdr) => rdr,
Err(e) => {
return Err(format!("Error opening {}: {}", path.to_str().unwrap(), e));
}
};
let systems = reader.deserialize::<SystemSerde>();
for sys in systems {
let sys = sys.unwrap();
for name in &names {
best.entry(name.clone()).and_modify(|ent| {
if (exact) && (&sys.system == name) {
*ent = (1.0, Some(sys.clone().build()))
} else {
let d1 = strsim::normalized_levenshtein(&sys.system, &name);
let d2 = strsim::normalized_levenshtein(&sys.body, &name);
if d1 > ent.0 {
*ent = (d1, Some(sys.clone().build()))
} else if d2 > ent.0 {
*ent = (d2, Some(sys.clone().build()))
}
}
});
}
}
Ok(best)
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct TreeNode {
pub id: u32,
pub pos: [f32; 3],
pub mult: f32,
}
impl TreeNode {
pub fn get(&self, router: &Router) -> Option<System> {
let mut cache = router.cache.as_ref().unwrap().lock().unwrap();
cache.get(self.id)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, IntoPyObject)]
pub struct SystemSerde {
pub id: u32,
pub star_type: String,
pub system: String,
pub body: String,
pub mult: f32,
pub distance: f32,
pub x: f32,
pub y: f32,
pub z: f32,
}
impl SystemSerde {
pub fn build(self) -> System {
System {
id: self.id,
star_type: self.star_type,
system: self.system,
body: self.body,
mult: self.mult,
distance: self.distance,
pos: [self.x, self.y, self.z],
}
}
pub fn to_node(&self) -> TreeNode {
TreeNode {
id: self.id,
pos: [self.x, self.y, self.z],
mult: self.mult,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct System {
pub id: u32,
pub star_type: String,
pub system: String,
pub body: String,
pub mult: f32,
pub distance: f32,
pub pos: [f32; 3],
}
impl ToPyObject for System {
fn to_object(&self, py: Python) -> PyObject {
let pos = PyTuple::new(py, self.pos.iter());
let elem = PyDict::new(py);
elem.set_item("star_type", self.star_type.clone()).unwrap();
elem.set_item("system", self.system.clone()).unwrap();
elem.set_item("body", self.body.clone()).unwrap();
elem.set_item("distance", self.distance).unwrap();
elem.set_item("mult", self.mult).unwrap();
elem.set_item("id", self.id).unwrap();
elem.set_item("pos", pos).unwrap();
elem.to_object(py)
}
}
impl System {
pub fn to_node(&self) -> TreeNode {
TreeNode {
id: self.id,
pos: self.pos,
mult: self.mult,
}
}
}
impl Ord for System {
fn cmp(&self, other: &Self) -> Ordering {
self.id.cmp(&other.id)
}
}
impl PartialOrd for System {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}

289
rust/src/data.json Normal file
View File

@ -0,0 +1,289 @@
{
"timestamp": "2019-09-25T21:29:51Z",
"event": "Loadout",
"Ship": "asp",
"ShipID": 0,
"ShipName": "Nightmaregreen_N",
"ShipIdent": "NMGR_N",
"HullValue": 6144793,
"ModulesValue": 33042643,
"HullHealth": 1.000000,
"UnladenMass": 347.200012,
"CargoCapacity": 0,
"MaxJumpRange": 56.372398,
"FuelCapacity": {
"Main": 64.000000,
"Reserve": 0.630000
},
"Rebuy": 1959374,
"Modules": [
{
"Slot": "ShipCockpit",
"Item": "asp_cockpit",
"On": true,
"Priority": 1,
"Health": 1.000000
},
{
"Slot": "CargoHatch",
"Item": "modularcargobaydoor",
"On": true,
"Priority": 0,
"Health": 1.000000
},
{
"Slot": "TinyHardpoint1",
"Item": "hpt_heatsinklauncher_turret_tiny",
"On": true,
"Priority": 0,
"AmmoInClip": 1,
"AmmoInHopper": 2,
"Health": 1.000000
},
{
"Slot": "TinyHardpoint2",
"Item": "hpt_heatsinklauncher_turret_tiny",
"On": true,
"Priority": 0,
"AmmoInClip": 1,
"AmmoInHopper": 2,
"Health": 1.000000
},
{
"Slot": "TinyHardpoint3",
"Item": "hpt_heatsinklauncher_turret_tiny",
"On": true,
"Priority": 0,
"AmmoInClip": 1,
"AmmoInHopper": 2,
"Health": 1.000000
},
{
"Slot": "TinyHardpoint4",
"Item": "hpt_heatsinklauncher_turret_tiny",
"On": true,
"Priority": 0,
"AmmoInClip": 1,
"AmmoInHopper": 2,
"Health": 1.000000
},
{
"Slot": "PaintJob",
"Item": "paintjob_asp_operator_red",
"On": true,
"Priority": 1,
"Health": 1.000000
},
{
"Slot": "Armour",
"Item": "asp_armour_grade1",
"On": true,
"Priority": 1,
"Health": 1.000000
},
{
"Slot": "PowerPlant",
"Item": "int_powerplant_size5_class2",
"On": true,
"Priority": 1,
"Health": 1.000000
},
{
"Slot": "MainEngines",
"Item": "int_engine_size4_class2",
"On": true,
"Priority": 0,
"Health": 1.000000
},
{
"Slot": "FrameShiftDrive",
"Item": "int_hyperdrive_size5_class5",
"On": true,
"Priority": 0,
"Health": 1.000000,
"Engineering": {
"Engineer": "Felicity Farseer",
"EngineerID": 300100,
"BlueprintID": 128673694,
"BlueprintName": "FSD_LongRange",
"Level": 5,
"Quality": 1.000000,
"ExperimentalEffect": "special_fsd_heavy",
"ExperimentalEffect_Localised": "Mass Manager",
"Modifiers": [
{
"Label": "Mass",
"Value": 26.000000,
"OriginalValue": 20.000000,
"LessIsGood": 1
},
{
"Label": "Integrity",
"Value": 93.840004,
"OriginalValue": 120.000000,
"LessIsGood": 0
},
{
"Label": "PowerDraw",
"Value": 0.690000,
"OriginalValue": 0.600000,
"LessIsGood": 1
},
{
"Label": "FSDOptimalMass",
"Value": 1692.599976,
"OriginalValue": 1050.000000,
"LessIsGood": 0
}
]
}
},
{
"Slot": "LifeSupport",
"Item": "int_lifesupport_size4_class2",
"On": true,
"Priority": 0,
"Health": 1.000000
},
{
"Slot": "PowerDistributor",
"Item": "int_powerdistributor_size4_class2",
"On": true,
"Priority": 0,
"Health": 1.000000
},
{
"Slot": "Radar",
"Item": "int_sensors_size5_class2",
"On": true,
"Priority": 0,
"Health": 1.000000
},
{
"Slot": "FuelTank",
"Item": "int_fueltank_size5_class3",
"On": true,
"Priority": 1,
"Health": 1.000000
},
{
"Slot": "Decal1",
"Item": "decal_explorer_starblazer",
"On": true,
"Priority": 1,
"Health": 1.000000
},
{
"Slot": "Decal2",
"Item": "decal_explorer_starblazer",
"On": true,
"Priority": 1,
"Health": 1.000000
},
{
"Slot": "Decal3",
"Item": "decal_explorer_starblazer",
"On": true,
"Priority": 1,
"Health": 1.000000
},
{
"Slot": "ShipName0",
"Item": "nameplate_shipname_white",
"On": true,
"Priority": 1,
"Health": 1.000000
},
{
"Slot": "ShipName1",
"Item": "nameplate_shipname_white",
"On": true,
"Priority": 1,
"Health": 1.000000
},
{
"Slot": "ShipID0",
"Item": "nameplate_shipid_white",
"On": true,
"Priority": 1,
"Health": 1.000000
},
{
"Slot": "ShipID1",
"Item": "nameplate_shipid_white",
"On": true,
"Priority": 1,
"Health": 1.000000
},
{
"Slot": "Slot01_Size6",
"Item": "int_fuelscoop_size6_class5",
"On": true,
"Priority": 0,
"Health": 1.000000
},
{
"Slot": "Slot02_Size5",
"Item": "int_fueltank_size5_class3",
"On": true,
"Priority": 1,
"Health": 1.000000
},
{
"Slot": "Slot03_Size3",
"Item": "int_repairer_size3_class5",
"On": false,
"Priority": 0,
"Health": 1.000000
},
{
"Slot": "Slot04_Size3",
"Item": "int_shieldgenerator_size3_class2",
"On": true,
"Priority": 0,
"Health": 1.000000
},
{
"Slot": "Slot05_Size3",
"Item": "int_buggybay_size2_class2",
"On": true,
"Priority": 0,
"Health": 1.000000
},
{
"Slot": "Slot06_Size2",
"Item": "int_detailedsurfacescanner_tiny",
"On": true,
"Priority": 0,
"Health": 1.000000
},
{
"Slot": "Slot07_Size2",
"Item": "int_dockingcomputer_standard",
"On": true,
"Priority": 0,
"Health": 1.000000
},
{
"Slot": "Slot08_Size1",
"Item": "int_supercruiseassist",
"On": true,
"Priority": 0,
"Health": 1.000000
},
{
"Slot": "PlanetaryApproachSuite",
"Item": "int_planetapproachsuite",
"On": true,
"Priority": 1,
"Health": 1.000000
},
{
"Slot": "VesselVoice",
"Item": "voicepack_eden",
"On": true,
"Priority": 1,
"Health": 1.000000
}
]
}

193
rust/src/data_guardian.json Normal file
View File

@ -0,0 +1,193 @@
{
"timestamp": "2019-09-25T21:29:51Z",
"event": "Loadout",
"Ship": "asp",
"ShipName": "Nightmaregreen_G",
"ShipIdent": "NMGR_G",
"HullValue": 6144793,
"ModulesValue": 33181682,
"UnladenMass": 348.500061,
"CargoCapacity": 0,
"MaxJumpRange": 60.164637,
"FuelCapacity": {
"Main": 64,
"Reserve": 0.63
},
"Rebuy": 1966323,
"Modules": [
{
"Slot": "CargoHatch",
"Item": "modularcargobaydoor",
"On": true,
"Priority": 0
},
{
"Slot": "TinyHardpoint1",
"Item": "hpt_heatsinklauncher_turret_tiny",
"On": true,
"Priority": 0,
"Value": 3071
},
{
"Slot": "TinyHardpoint2",
"Item": "hpt_heatsinklauncher_turret_tiny",
"On": true,
"Priority": 0,
"Value": 3071
},
{
"Slot": "TinyHardpoint3",
"Item": "hpt_heatsinklauncher_turret_tiny",
"On": true,
"Priority": 0,
"Value": 3071
},
{
"Slot": "TinyHardpoint4",
"Item": "hpt_heatsinklauncher_turret_tiny",
"On": true,
"Priority": 0,
"Value": 3071
},
{
"Slot": "Armour",
"Item": "asp_armour_grade1",
"On": true,
"Priority": 1,
"Value": 0
},
{
"Slot": "PowerPlant",
"Item": "int_powerplant_size5_class2",
"On": true,
"Priority": 1,
"Value": 140523
},
{
"Slot": "MainEngines",
"Item": "int_engine_size4_class2",
"On": true,
"Priority": 0,
"Value": 52325
},
{
"Slot": "FrameShiftDrive",
"Item": "int_hyperdrive_size5_class5",
"On": true,
"Priority": 0,
"Value": 4478716,
"Engineering": {
"BlueprintName": "FSD_LongRange",
"Level": 5,
"Quality": 1,
"ExperimentalEffect": "special_fsd_heavy",
"Modifiers": [
{
"Label": "Mass",
"Value": 26.000061,
"OriginalValue": 20
},
{
"Label": "Integrity",
"Value": 93.839832,
"OriginalValue": 120
},
{
"Label": "PowerDraw",
"Value": 0.690001,
"OriginalValue": 0.6
},
{
"Label": "FSDOptimalMass",
"Value": 1692.58667,
"OriginalValue": 1050
}
]
}
},
{
"Slot": "LifeSupport",
"Item": "int_lifesupport_size4_class2",
"On": true,
"Priority": 0,
"Value": 24895
},
{
"Slot": "PowerDistributor",
"Item": "int_powerdistributor_size4_class2",
"On": true,
"Priority": 0,
"Value": 24895
},
{
"Slot": "Radar",
"Item": "int_sensors_size5_class2",
"On": true,
"Priority": 0,
"Value": 69709
},
{
"Slot": "FuelTank",
"Item": "int_fueltank_size5_class3",
"On": true,
"Priority": 1,
"Value": 85776
},
{
"Slot": "Slot01_Size6",
"Item": "int_fuelscoop_size6_class5",
"On": true,
"Priority": 0,
"Value": 25240068
},
{
"Slot": "Slot02_Size5",
"Item": "int_fueltank_size5_class3",
"On": true,
"Priority": 1,
"Value": 85776
},
{
"Slot": "Slot03_Size3",
"Item": "int_repairer_size3_class5",
"On": false,
"Priority": 0,
"Value": 2302911
},
{
"Slot": "Slot04_Size3",
"Item": "int_shieldgenerator_size3_class2",
"On": true,
"Priority": 0,
"Value": 16506
},
{
"Slot": "Slot05_Size3",
"Item": "int_buggybay_size2_class2",
"On": true,
"Priority": 0,
"Value": 18954
},
{
"Slot": "Slot06_Size2",
"Item": "int_detailedsurfacescanner_tiny",
"On": true,
"Priority": 0,
"Value": 219375
},
{
"Slot": "Slot07_Size2",
"Item": "int_dockingcomputer_standard",
"On": true,
"Priority": 0,
"Value": 3949
},
{
"Slot": "Slot08_Size1",
"Item": "int_guardianfsdbooster_size1",
"On": true,
"Priority": 0,
"Value": 405020
}
]
}

196
rust/src/edsm.rs Normal file
View File

@ -0,0 +1,196 @@
use crate::common::get_mult;
use crate::common::SystemSerde;
use fnv::FnvHashMap;
use pyo3::prelude::*;
use serde::Deserialize;
use serde_json::Result;
use std::fs::File;
use std::io::Seek;
use std::io::{BufRead, BufReader, BufWriter, SeekFrom};
use std::path::PathBuf;
use std::str;
use std::time::Instant;
#[derive(Debug, Deserialize)]
#[allow(non_snake_case)]
struct Body {
name: String,
subType: String,
#[serde(rename = "type")]
body_type: String,
systemId: i32,
systemId64: i64,
#[serde(rename = "distanceToArrival")]
distance: f32,
}
#[derive(Debug, Deserialize)]
struct Coords {
x: f32,
y: f32,
z: f32,
}
#[derive(Debug, Deserialize)]
struct System {
id: i32,
id64: i64,
name: String,
coords: Coords,
}
#[derive(Debug)]
pub struct PreprocessState {
pub file: String,
pub message: String,
pub total: u64,
pub done: u64,
pub count: usize,
}
fn process(
path: &PathBuf,
func: &mut dyn for<'r> FnMut(&'r str) -> (),
callback: &dyn Fn(&PreprocessState) -> PyResult<PyObject>,
) -> std::io::Result<()> {
let mut buffer = String::new();
let fh = File::open(path)?;
let total_size = fh.metadata()?.len();
let mut t_last = Instant::now();
let mut reader = BufReader::new(fh);
let mut state = PreprocessState {
file: path.to_str().unwrap().to_owned(),
total: total_size,
done: 0,
count: 0,
message: format!("Processing {} ...", path.to_str().unwrap()),
};
println!("Loading {} ...", path.to_str().unwrap());
while let Ok(n) = reader.read_line(&mut buffer) {
if n == 0 {
break;
}
buffer = buffer.trim_end().trim_end_matches(|c| c == ',').to_string();
if !buffer.is_empty() {
func(&buffer);
}
let pos = reader.seek(SeekFrom::Current(0)).unwrap();
state.done = pos;
state.count += 1;
if t_last.elapsed().as_millis() > 100 {
callback(&state)?;
t_last = Instant::now();
}
buffer.clear();
}
Ok(())
}
fn process_systems(
path: &PathBuf,
callback: &dyn Fn(&PreprocessState) -> PyResult<PyObject>,
) -> FnvHashMap<i32, System> {
let mut ret = FnvHashMap::default();
process(
path,
&mut |line| {
let sys_res: Result<System> = serde_json::from_str(&line);
if let Ok(sys) = sys_res {
ret.insert(sys.id, sys);
} else {
eprintln!("\nError parsing: {}\n\t{:?}\n", line, sys_res.unwrap_err());
}
},
callback,
)
.unwrap();
ret
}
pub fn build_index(path: &PathBuf) -> std::io::Result<()> {
let mut wtr = BufWriter::new(File::create(path.with_extension("idx"))?);
let mut idx: Vec<u64> = Vec::new();
let mut records = (csv::Reader::from_path(path)?).into_deserialize::<SystemSerde>();
loop {
idx.push(records.reader().position().byte());
if records.next().is_none() {
break;
}
}
bincode::serialize_into(&mut wtr, &idx).unwrap();
Ok(())
}
fn process_bodies(
path: &PathBuf,
out_path: &PathBuf,
systems: &mut FnvHashMap<i32, System>,
callback: &dyn Fn(&PreprocessState) -> PyResult<PyObject>,
) -> std::io::Result<()> {
println!(
"Processing {} into {} ...",
path.to_str().unwrap(),
out_path.to_str().unwrap(),
);
let mut n: u32 = 0;
let mut wtr = csv::Writer::from_path(out_path)?;
process(
path,
&mut |line| {
if !line.contains("Star") {
return;
}
let body_res: Result<Body> = serde_json::from_str(&line);
if let Ok(body) = body_res {
if !body.body_type.contains("Star") {
return;
}
if let Some(sys) = systems.get(&body.systemId) {
let sub_type = body.subType;
let mult = get_mult(&sub_type);
let sys_name = sys.name.clone();
let rec = SystemSerde {
id: n,
star_type: sub_type,
system: sys_name,
body: body.name,
mult,
distance: body.distance,
x: sys.coords.x,
y: sys.coords.y,
z: sys.coords.z,
};
wtr.serialize(rec).unwrap();
n += 1;
};
} else {
eprintln!("\nError parsing: {}\n\t{:?}\n", line, body_res.unwrap_err());
}
},
callback,
)
.unwrap();
println!("Total Systems: {}", n);
systems.clear();
Ok(())
}
pub fn preprocess_files(
bodies: &PathBuf,
systems: &PathBuf,
out_path: &PathBuf,
callback: &dyn Fn(&PreprocessState) -> PyResult<PyObject>,
) -> std::io::Result<()> {
if !out_path.exists() {
let mut systems = process_systems(systems, &callback);
process_bodies(bodies, out_path, &mut systems, &callback)?;
} else {
println!(
"File '{}' exists, not overwriting it",
out_path.to_str().unwrap()
);
}
println!("Building index...");
println!("Index result: {:?}", build_index(&out_path));
Ok(())
}

113
rust/src/galaxy.rs Normal file
View File

@ -0,0 +1,113 @@
use serde::{Deserialize, Serialize};
use std::fs::File;
use std::io::{BufRead, BufReader, BufWriter};
use std::str;
#[derive(Debug, Clone, Serialize)]
pub struct SystemSerde {
pub id: u32,
pub star_type: String,
pub system: String,
pub body: String,
pub mult: f32,
pub distance: f32,
pub x: f32,
pub y: f32,
pub z: f32,
}
fn get_mult(star_type: &str) -> f32 {
if star_type.contains("White Dwarf") {
return 1.5;
}
if star_type.contains("Neutron") {
return 4.0;
}
1.0
}
#[derive(Debug, Deserialize)]
struct Coords {
x: f32,
y: f32,
z: f32,
}
#[derive(Debug, Deserialize)]
struct Body {
name: String,
#[serde(rename = "type")]
body_type: String,
#[serde(rename = "subType")]
sub_type: String,
#[serde(rename = "distanceToArrival")]
distance: f32,
}
#[derive(Debug, Deserialize)]
struct System {
coords: Coords,
name: String,
bodies: Vec<Body>,
}
pub fn process_galaxy_dump(path: &str) -> std::io::Result<()> {
let fh = File::create("stars.csv")?;
let mut wtr = csv::Writer::from_writer(BufWriter::new(fh));
let mut buffer = String::new();
let mut bz2_reader = std::process::Command::new("7z")
.args(&["x", "-so", path])
.stdout(std::process::Stdio::piped())
.spawn()
.unwrap_or_else(|err| {
eprintln!("Failed to run 7z: {}", err);
eprintln!("Falling back to bzip2");
std::process::Command::new("bzip2")
.args(&["-d", "-c", path])
.stdout(std::process::Stdio::piped())
.spawn()
.expect("Failed to execute bzip2!")
});
let mut reader = BufReader::new(
bz2_reader
.stdout
.as_mut()
.expect("Failed to open stdout of child process"),
);
let mut count = 0;
while let Ok(n) = reader.read_line(&mut buffer) {
if n == 0 {
break;
}
buffer = buffer
.trim()
.trim_end_matches(|c| c == ',')
.trim()
.to_string();
if !buffer.contains("Star") {
continue;
};
if let Ok(sys) = serde_json::from_str::<System>(&buffer) {
for b in &sys.bodies {
if b.body_type == "Star" {
let s = SystemSerde {
id: count,
star_type: b.sub_type.clone(),
distance: b.distance,
mult: get_mult(&b.sub_type),
body: b.name.clone(),
system: sys.name.clone(),
x: sys.coords.x,
y: sys.coords.y,
z: sys.coords.z,
};
wtr.serialize(s)?;
count += 1;
}
}
}
buffer.clear();
}
println!("Total: {}", count);
Ok(())
}

169
rust/src/journal.rs Normal file
View File

@ -0,0 +1,169 @@
use crate::common::get_fsd_info;
use crate::ship::Ship;
use regex::Regex;
use serde::Deserialize;
use std::collections::HashMap;
#[derive(Clone, Debug, PartialEq, Deserialize)]
pub struct Event {
#[serde(flatten)]
pub event: EventData,
}
#[serde(tag = "event")]
#[derive(Clone, Debug, PartialEq, Deserialize)]
pub enum EventData {
Loadout(Loadout),
#[serde(other)]
Unknown,
}
#[serde(rename_all = "PascalCase")]
#[derive(Clone, Debug, PartialEq, Deserialize)]
pub struct Modifier {
label: String,
value: f32,
}
#[serde(rename_all = "PascalCase")]
#[derive(Clone, Debug, PartialEq, Deserialize)]
pub struct Engineering {
modifiers: Vec<Modifier>,
}
#[derive(Clone, Debug, PartialEq, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Module {
engineering: Option<Engineering>,
item: String,
slot: String,
}
#[derive(Clone, Debug, PartialEq, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct FuelCapacity {
main: f32,
reserve: f32,
}
#[derive(Clone, Debug, PartialEq, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct Loadout {
#[serde(rename = "timestamp")]
timestamp: String,
ship: String,
ship_name: String,
ship_ident: String,
fuel_capacity: FuelCapacity,
unladen_mass: f32,
modules: Vec<Module>,
}
impl Engineering {
fn to_hashmap(&self) -> HashMap<String, f32> {
let mut h = HashMap::new();
for v in &self.modifiers {
h.insert(v.label.clone(), v.value);
}
return h;
}
}
impl Loadout {
fn get_booster(&self) -> Option<usize> {
self.modules
.iter()
.cloned()
.filter_map(|m| {
let Module { item, .. } = m;
if item.starts_with("int_guardianfsdbooster") {
return item
.chars()
.last()
.unwrap()
.to_digit(10)
.map(|v| v as usize);
}
return None;
})
.next()
}
fn get_fsd(&self) -> Option<(String, Option<Engineering>)> {
self.modules
.iter()
.cloned()
.filter_map(|m| {
let Module {
slot,
engineering,
item,
} = m;
if slot == "FrameShiftDrive" {
return Some((item, engineering));
}
return None;
})
.next()
}
pub fn try_into_ship(self) -> Result<Ship, String> {
let fsd = self.get_fsd().ok_or("No FSD found!")?;
let booster = self.get_booster().unwrap_or(0);
let fsd_type = Regex::new(r"^int_hyperdrive_size(\d+)_class(\d+)$")
.unwrap()
.captures(&fsd.0);
let fsd_type: (usize, usize) = fsd_type
.map(|m| {
let s = m.get(1)?.as_str().to_owned().parse().ok()?;
let c = m.get(2)?.as_str().to_owned().parse().ok()?;
return Some((c, s));
})
.flatten()
.ok_or(format!("Invalid FSD found: {}", &fsd.0))?;
let eng = fsd
.1
.map(|eng| eng.to_hashmap())
.unwrap_or_else(HashMap::new);
let mut fsd_info = get_fsd_info(fsd_type.0, fsd_type.1)?;
let fsd_type = (
"_EDCBA"
.chars()
.nth(fsd_type.0)
.ok_or(format!("Invalid FSD found: {}", &fsd.0))?,
fsd_type.1 as u8,
);
fsd_info.extend(eng);
let max_fuel = fsd_info
.get("MaxFuel")
.ok_or(format!("Unknwon MaxFuelPerJump for FSD: {}", &fsd.0))?;
let opt_mass = fsd_info
.get("FSDOptimalMass")
.ok_or(format!("Unknwon FSDOptimalMass for FSD: {}", &fsd.0))?;
return Ship::new(
self.ship_name,
self.ship_ident,
self.ship,
self.unladen_mass,
self.fuel_capacity.main,
self.fuel_capacity.main,
fsd_type,
*max_fuel,
*opt_mass,
booster,
);
}
}
impl Event {
pub fn get_loadout(self) -> Option<Loadout> {
if let Event {
event: EventData::Loadout(loadout),
} = self
{
return Some(loadout);
}
None
}
}

484
rust/src/lib.rs Normal file
View File

@ -0,0 +1,484 @@
// #![deny(warnings)]
#![allow(dead_code, clippy::needless_return, clippy::too_many_arguments)]
pub mod common;
pub mod edsm;
pub mod galaxy;
pub mod journal;
pub mod route;
pub mod ship;
extern crate derivative;
use crate::common::{find_matches, SysEntry};
use crate::route::{Router, SearchState};
use crate::ship::Ship;
use pyo3::exceptions::*;
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyList, PyTuple};
use pyo3::PyObjectProtocol;
use std::path::PathBuf;
#[pyclass(dict)]
#[derive(Debug)]
#[text_signature = "(callback, /)"]
struct PyRouter {
router: Router,
primary_only: bool,
stars_path: String,
}
#[pymethods]
impl PyRouter {
#[new]
#[args(callback = "None")]
fn new(callback: Option<PyObject>, py: Python<'static>) -> PyResult<Self> {
Ok(PyRouter {
router: Router::new(Box::new(
move |state: &SearchState| {
match callback.as_ref() {
Some(cb) => cb.call(py, (state.clone(),), None),
None => Ok(py.None()),
}
}
)),
primary_only: false,
stars_path: String::from(""),
})
}
#[text_signature = "(ship, /)"]
fn set_ship(&mut self, py: Python, ship: &PyShip) -> PyResult<PyObject> {
self.router.set_ship(ship.ship.clone());
Ok(py.None())
}
#[args(primary_only = "false")]
#[text_signature = "(path, primary_only, /)"]
fn load(
&mut self,
path: String,
primary_only: bool,
py: Python,
) -> PyResult<PyObject> {
self.stars_path = path;
self.primary_only = primary_only;
Ok(py.None())
}
#[args(greedyness = "0.5", num_workers = "0", beam_width = "0")]
#[text_signature = "(hops, range, greedyness, beam_width, num_workers, /)"]
fn route(
&mut self,
hops: &PyList,
range: Option<f32>,
greedyness: f32,
beam_width: usize,
num_workers: usize,
py: Python,
) -> PyResult<PyObject> {
let route_res = self
.router
.load(&PathBuf::from(self.stars_path.clone()), self.primary_only);
if let Err(err_msg) = route_res {
return Err(PyErr::new::<ValueError, _>(err_msg));
};
let mut sys_entries: Vec<SysEntry> = Vec::new();
for hop in hops {
if let Ok(id) = hop.extract() {
sys_entries.push(SysEntry::ID(id));
} else {
sys_entries.push(SysEntry::parse(hop.extract()?));
}
}
println!("Resolving systems...");
let ids: Vec<u32> = match resolve(&sys_entries, &self.router.path) {
Ok(ids) => ids,
Err(err_msg) => {
return Err(PyErr::new::<ValueError, _>(err_msg));
}
};
match self
.router
.compute_route(&ids, range, greedyness, beam_width, num_workers)
{
Ok(route) => {
let py_route: Vec<_> = route.iter().map(|hop| hop.to_object(py)).collect();
Ok(py_route.to_object(py))
}
Err(err_msg) => Err(PyErr::new::<RuntimeError, _>(err_msg)),
}
}
#[args(hops = "*")]
#[text_signature = "(sys_1, sys_2, ..., /)"]
fn resolve_systems(&self, hops: &PyTuple, py: Python) -> PyResult<PyObject> {
let mut sys_entries: Vec<SysEntry> = Vec::new();
for hop in hops {
if let Ok(id) = hop.extract() {
sys_entries.push(SysEntry::ID(id));
} else {
sys_entries.push(SysEntry::parse(hop.extract()?));
}
}
println!("Resolving systems...");
let stars_path = PathBuf::from(self.stars_path.clone());
let ids: Vec<u32> = match resolve(&sys_entries, &stars_path) {
Ok(ids) => ids,
Err(err_msg) => {
return Err(PyErr::new::<ValueError, _>(err_msg));
}
};
let ret: Vec<(_, u32)> = hops.into_iter().zip(ids.into_iter()).collect();
Ok(PyDict::from_sequence(py, ret.to_object(py))?.to_object(py))
}
#[staticmethod]
fn preprocess_edsm() -> PyResult<()> {
todo!("Implement EDSM Preprocessor")
}
#[staticmethod]
fn preprocess_galaxy() -> PyResult<()> {
todo!("Implement galaxy.json Preprocessor")
}
}
#[pyproto]
impl PyObjectProtocol for PyRouter {
fn __str__(&self) -> PyResult<String> {
Ok(format!("{:?}", &self))
}
fn __repr__(&self) -> PyResult<String> {
Ok(format!("{:?}", &self))
}
}
fn resolve(entries: &[SysEntry], path: &PathBuf) -> Result<Vec<u32>, String> {
let mut names: Vec<String> = Vec::new();
let mut ids: Vec<u32> = Vec::new();
let mut ret: Vec<u32> = Vec::new();
for ent in entries {
match ent {
SysEntry::Name(name) => names.push(name.to_owned()),
SysEntry::ID(id) => ids.push(*id),
}
}
if !path.exists() {
return Err(format!(
"Source file \"{:?}\" does not exist!",
path.display()
));
}
let name_ids = find_matches(path, names, false)?;
for ent in entries {
match ent {
SysEntry::Name(name) => {
let ent_res = name_ids
.get(&name.to_owned())
.ok_or(format!("System {} not found", name))?;
let sys = ent_res
.1
.as_ref()
.ok_or(format!("System {} not found", name))?;
if ent_res.0 < 0.75 {
println!(
"WARNING: {} match to {} with low confidence ({:.2}%)",
name,
sys.system,
ent_res.0 * 100.0
);
}
ret.push(sys.id);
}
SysEntry::ID(id) => ret.push(*id),
}
}
Ok(ret)
}
#[pyclass(dict)]
#[derive(Debug)]
struct PyShip {
ship: Ship,
}
#[pyproto]
impl PyObjectProtocol for PyShip {
fn __str__(&self) -> PyResult<String> {
Ok(format!("{:?}", &self.ship))
}
fn __repr__(&self) -> PyResult<String> {
Ok(format!("{:?}", &self.ship))
}
}
#[pymethods]
impl PyShip {
#[staticmethod]
fn from_loadout(py: Python, loadout: &str) -> PyResult<PyObject> {
match Ship::new_from_json(loadout) {
Ok(ship) => Ok((PyShip { ship }).into_py(py)),
Err(err_msg) => Err(PyErr::new::<ValueError, _>(err_msg)),
}
}
#[staticmethod]
fn from_journal(py: Python) -> PyResult<PyObject> {
let mut ship = match Ship::new_from_journal() {
Ok(ship) => ship,
Err(err_msg) => {
return Err(PyErr::new::<ValueError, _>(err_msg));
}
};
let ships: Vec<(PyObject, PyObject)> = ship
.drain()
.map(|(k, v)| {
let k_py = k.to_object(py);
let v_py = (PyShip { ship: v }).into_py(py);
(k_py, v_py)
})
.collect();
Ok(PyDict::from_sequence(py, ships.to_object(py))?.to_object(py))
}
fn to_dict(&self, py: Python) -> PyResult<PyObject> {
self.ship.to_object(py)
}
#[text_signature = "(dist, /)"]
fn fuel_cost(&self, _py: Python, dist: f32) -> PyResult<f32> {
Ok(self.ship.fuel_cost(dist))
}
#[text_signature = "(/)"]
fn range(&self, _py: Python) -> PyResult<f32> {
Ok(self.ship.range())
}
#[text_signature = "(/)"]
fn max_range(&self, _py: Python) -> PyResult<f32> {
Ok(self.ship.max_range())
}
#[text_signature = "(dist, /)"]
fn make_jump(&mut self, dist: f32, _py: Python) -> PyResult<Option<f32>> {
Ok(self.ship.make_jump(dist))
}
#[text_signature = "(dist, /)"]
fn can_jump(&self, dist: f32, _py: Python) -> PyResult<bool> {
Ok(self.ship.can_jump(dist))
}
#[args(fuel_amount = "None")]
#[text_signature = "(fuel_amount, /)"]
fn refuel(&mut self, fuel_amount: Option<f32>, _py: Python) -> PyResult<()> {
if let Some(fuel) = fuel_amount {
self.ship.fuel_mass = (self.ship.fuel_mass + fuel).min(self.ship.fuel_capacity)
} else {
self.ship.fuel_mass = self.ship.fuel_capacity;
}
Ok(())
}
#[text_signature = "(factor, /)"]
fn boost(&mut self, factor: f32, _py: Python) -> PyResult<()> {
self.ship.boost(factor);
Ok(())
}
}
#[pymodule]
pub fn _ed_lrr(_py: Python, m: &PyModule) -> PyResult<()> {
better_panic::install();
m.add_class::<PyRouter>()?;
m.add_class::<PyShip>()?;
/*
#[pyfn(m, "get_ships_from_journal")]
fn get_ships_from_journal(py: Python) -> PyResult<PyObject> {
let ship = match Ship::new_from_journal() {
Ok(ship) => ship,
Err(err_msg) => {
return Err(PyErr::new::<ValueError, _>(err_msg));
}
};
let ships: Vec<(_,_)> = ship.iter().map(|(k,v)| (k.to_object(py),v.to_object(py))).collect();
Ok(PyDict::from_sequence(py, ships.to_object(py))?.to_object(py))
}
#[pyfn(m, "get_ships_from_loadout")]
fn get_ship_from_loadout(py: Python, loadout: &str) -> PyResult<PyObject> {
let ship = match Ship::new_from_json(loadout) {
Ok(ship) => ship,
Err(err_msg) => {
return Err(PyErr::new::<ValueError, _>(err_msg));
}
};
Ok(ship.to_object(py))
}
*/
Ok(())
}
/*
/// Preprocess bodies.json and systemsWithCoordinates.json into stars.csv
#[pyfn(m, "preprocess")]
#[text_signature = "(infile_systems, infile_bodies, outfile, callback, /)"]
fn ed_lrr_preprocess(
py: Python<'static>,
infile_systems: String,
infile_bodies: String,
outfile: String,
callback: PyObject,
) -> PyResult<PyObject> {
use preprocess::*;
let state = PyDict::new(py);
let state_dict = PyDict::new(py);
callback.call(py, (state_dict,), None).unwrap();
let callback_wrapped = move |state: &PreprocessState| {
// println!("SEND: {:?}",state);
state_dict.set_item("file", state.file.clone())?;
state_dict.set_item("total", state.total)?;
state_dict.set_item("count", state.count)?;
state_dict.set_item("done", state.done)?;
state_dict.set_item("message", state.message.clone())?;
callback.call(py, (state_dict,), None)
};
preprocess_files(
&PathBuf::from(infile_bodies),
&PathBuf::from(infile_systems),
&PathBuf::from(outfile),
&callback_wrapped,
)
.unwrap();
Ok(state.to_object(py))
}
/// Find system by name
#[pyfn(m, "find_sys")]
#[text_signature = "(sys_names, sys_list_path, /)"]
fn find_sys(py: Python, sys_names: Vec<String>, sys_list: String) -> PyResult<PyObject> {
let path = PathBuf::from(sys_list);
match find_matches(&path, sys_names, false) {
Ok(vals) => {
let ret = PyDict::new(py);
for (key, (diff, sys)) in vals {
let ret_dict = PyDict::new(py);
if let Some(val) = sys {
let pos = PyList::new(py, val.pos.iter());
ret_dict.set_item("star_type", val.star_type.clone())?;
ret_dict.set_item("system", val.system.clone())?;
ret_dict.set_item("body", val.body.clone())?;
ret_dict.set_item("distance", val.distance)?;
ret_dict.set_item("pos", pos)?;
ret_dict.set_item("id", val.id)?;
ret.set_item(key, (diff, ret_dict).to_object(py))?;
}
}
Ok(ret.to_object(py))
}
Err(e) => Err(PyErr::new::<ValueError, _>(e)),
}
}
/// Compute a Route using the suplied parameters
#[pyfn(m, "route")]
#[text_signature = "(hops, range, mode, primary, permute, keep_first, keep_last, greedyness, precomp, path, num_workers, callback, /)"]
#[allow(clippy::too_many_arguments)]
fn py_route(
py: Python<'static>,
hops: Vec<&str>,
range: f32,
mode: String,
primary: bool,
permute: bool,
keep_first: bool,
keep_last: bool,
greedyness: Option<f32>,
precomp: Option<String>,
path: String,
num_workers: Option<usize>,
callback: PyObject,
) -> PyResult<PyObject> {
use route::*;
let num_workers = num_workers.unwrap_or(1);
let mode = match Mode::parse(&mode) {
Ok(val) => val,
Err(e) => {
return Err(PyErr::new::<ValueError, _>(e));
}
};
let state_dict = PyDict::new(py);
{
let cb_res = callback.call(py, (state_dict,), None);
if cb_res.is_err() {
println!("Error: {:?}", cb_res);
}
}
let callback_wrapped = move |state: &SearchState| {
state_dict.set_item("mode", state.mode.clone())?;
state_dict.set_item("system", state.system.clone())?;
state_dict.set_item("body", state.body.clone())?;
state_dict.set_item("depth", state.depth)?;
state_dict.set_item("queue_size", state.queue_size)?;
state_dict.set_item("d_rem", state.d_rem)?;
state_dict.set_item("d_total", state.d_total)?;
state_dict.set_item("prc_done", state.prc_done)?;
state_dict.set_item("n_seen", state.n_seen)?;
state_dict.set_item("prc_seen", state.prc_seen)?;
state_dict.set_item("from", state.from.clone())?;
state_dict.set_item("to", state.to.clone())?;
let cb_res = callback.call(py, (state_dict,), None);
if cb_res.is_err() {
println!("Error: {:?}", cb_res);
}
cb_res
};
let hops: Vec<SysEntry> = (hops.iter().map(|v| SysEntry::from_str(&v)).collect::<Result<Vec<SysEntry>,_>>())?;
println!("Resolving systems...");
let hops: Vec<u32> = match resolve(&hops, &PathBuf::from(&path)) {
Ok(ids) => ids,
Err(err_msg) => {
return Err(PyErr::new::<ValueError, _>(err_msg));
}
};
let opts = RouteOpts {
systems: hops,
range: Some(range),
file_path: PathBuf::from(path),
precomp_file: precomp.map(PathBuf::from),
callback: Box::new(callback_wrapped),
mode,
factor: greedyness,
precompute: false,
permute,
keep_first,
keep_last,
primary,
workers: num_workers,
};
match route(opts) {
Ok(Some(route)) => {
let hops = route.iter().map(|hop| {
let pos = PyList::new(py, hop.pos.iter());
let elem = PyDict::new(py);
elem.set_item("star_type", hop.star_type.clone()).unwrap();
elem.set_item("system", hop.system.clone()).unwrap();
elem.set_item("body", hop.body.clone()).unwrap();
elem.set_item("distance", hop.distance).unwrap();
elem.set_item("pos", pos).unwrap();
elem
});
let lst = PyList::new(py, hops);
Ok(lst.to_object(py))
}
Ok(None) => Ok(py.None()),
Err(e) => Err(PyErr::new::<ValueError, _>(e)),
}
}
*/

1358
rust/src/route.rs Normal file

File diff suppressed because it is too large Load Diff

271
rust/src/ship.rs Normal file
View File

@ -0,0 +1,271 @@
use crate::common::get_fsd_booster_info;
use crate::journal::*;
use pyo3::conversion::ToPyObject;
use pyo3::prelude::*;
use pyo3::types::PyDict;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FSD {
pub rating_val: f32,
pub class_val: f32,
pub opt_mass: f32,
pub max_fuel: f32,
pub boost: f32,
pub guardian_booster: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Ship {
pub name: String,
pub ident: String,
pub ship_type: String,
pub base_mass: f32,
pub fuel_mass: f32,
pub fuel_capacity: f32,
pub fsd: FSD,
}
impl Ship {
pub fn new(
name: String,
ident: String,
ship_type: String,
base_mass: f32,
fuel_mass: f32,
fuel_capacity: f32,
fsd_type: (char, u8),
max_fuel: f32,
opt_mass: f32,
guardian_booster: usize,
) -> Result<Self, String> {
let rating_val: f32 = match fsd_type.0 {
'A' => 12.0,
'B' => 10.0,
'C' => 8.0,
'D' => 10.0,
'E' => 11.0,
err => {
return Err(format!("Invalid rating: {}", err));
}
};
if fsd_type.1 < 2 || fsd_type.1 > 8 {
return Err(format!("Invalid class: {}", fsd_type.1));
};
if guardian_booster!=0 {
return Err("Guardian booster not yet implemented!".to_owned())
}
let ret = Self {
name,
ident,
ship_type,
fuel_capacity,
fuel_mass,
base_mass,
fsd: FSD {
rating_val,
class_val: 2.0 + (0.15 * ((fsd_type.1 - 2) as f32)),
opt_mass,
max_fuel,
boost: 1.0,
guardian_booster: get_fsd_booster_info(guardian_booster)?,
},
};
Ok(ret)
}
pub fn new_from_json(data: &str) -> Result<Self, String> {
match serde_json::from_str::<Event>(&data) {
Ok(Event {
event: EventData::Unknown,
}) => {
return Err(format!("Invalid Loadout event: {}", data));
}
Ok(ev) => {
if let Some(loadout) = ev.get_loadout() {
return loadout.try_into_ship();
} else {
return Err(format!("Invalid Loadout event: {}", data));
}
}
Err(msg) => {
return Err(format!("{}", msg));
}
};
}
pub fn new_from_journal() -> Result<HashMap<String, Self>, String> {
let mut ret = HashMap::new();
let re = Regex::new(r"^Journal\.\d{12}\.\d{2}\.log$").unwrap();
let mut journals: Vec<PathBuf> = Vec::new();
let mut userprofile = PathBuf::from(std::env::var("Userprofile").unwrap());
userprofile.push("Saved Games");
userprofile.push("Frontier Developments");
userprofile.push("Elite Dangerous");
if let Ok(iter) = userprofile.read_dir() {
for entry in iter {
if let Ok(entry) = entry {
if re.is_match(&entry.file_name().to_string_lossy()) {
journals.push(entry.path());
};
}
}
}
journals.sort();
for journal in &journals {
let mut fh = BufReader::new(File::open(journal).unwrap());
let mut line = String::new();
while let Ok(n) = fh.read_line(&mut line) {
if n == 0 {
break;
}
match serde_json::from_str::<Event>(&line) {
Ok(Event {
event: EventData::Unknown,
}) => {}
Ok(ev) => {
if let Some(loadout) = ev.get_loadout() {
let mut ship = loadout.try_into_ship()?;
if ship.name == "" {
ship.name = "<NO NAME>".to_owned();
}
let key = format!(
"[{}] {} ({})",
ship.ident,
ship.name,
ship.ship_type.to_ascii_lowercase()
);
ret.insert(key, ship);
}
}
Err(_) => {}
};
line.clear();
}
}
if ret.is_empty() {
return Err("No ships loaded!".to_owned());
}
return Ok(ret);
}
pub fn can_jump(&self, d: f32) -> bool {
self.fuel_cost(d) <= self.fsd.max_fuel.min(self.fuel_mass)
}
pub fn boost(&mut self, boost: f32) {
self.fsd.boost = boost;
}
pub fn refuel(&mut self) {
self.fuel_mass = self.fuel_capacity;
}
pub fn make_jump(&mut self, d: f32) -> Option<f32> {
let cost = self.fuel_cost(d);
if cost > self.fsd.max_fuel.min(self.fuel_mass) {
return None;
}
self.fuel_mass -= cost;
self.fsd.boost = 1.0;
Some(cost)
}
fn jump_range(&self, fuel: f32, booster: bool) -> f32 {
let mass = self.base_mass + fuel;
let mut fuel = self.fsd.max_fuel.min(fuel);
if booster {
fuel *= self.boost_fuel_mult();
}
let opt_mass = self.fsd.opt_mass * self.fsd.boost;
return opt_mass * ((1000.0 * fuel) / self.fsd.rating_val).powf(self.fsd.class_val.recip())
/ mass;
}
pub fn max_range(&self) -> f32 {
return self.jump_range(self.fsd.max_fuel, true);
}
pub fn range(&self) -> f32 {
return self.jump_range(self.fuel_mass, true);
}
fn boost_fuel_mult(&self) -> f32 {
if self.fsd.guardian_booster == 0.0 {
return 1.0;
}
let base_range = self.jump_range(self.fuel_mass, false); // current range without booster
return ((base_range + self.fsd.guardian_booster) / base_range).powf(self.fsd.class_val);
}
pub fn fuel_cost(&self, d: f32) -> f32 {
if d == 0.0 {
return 0.0;
}
let mass = self.base_mass + self.fuel_mass;
let opt_mass = self.fsd.opt_mass * self.fsd.boost;
let base_cost = (d * mass) / opt_mass;
return (self.fsd.rating_val * 0.001 * base_cost.powf(self.fsd.class_val))
/ self.boost_fuel_mult();
}
}
/*
#[derive(Debug,Clone, Serialize, Deserialize, ToPyObject)]
pub struct FSD {
pub rating_val: f32,
pub class_val: f32,
pub opt_mass: f32,
pub max_fuel: f32,
pub boost: f32,
pub guardian_booster: f32,
}
#[derive(Debug,Clone, Serialize, Deserialize, ToPyObject)]
pub struct Ship {
pub name: String,
pub ident: String,
pub ship_type: String,
pub base_mass: f32,
pub fuel_mass: f32,
pub fuel_capacity: f32,
pub fsd: FSD,
}
*/
impl FSD {
pub fn to_object(&self, py: Python) -> PyResult<PyObject> {
let elem = PyDict::new(py);
elem.set_item("rating_val", self.rating_val)?;
elem.set_item("class_val", self.class_val)?;
elem.set_item("opt_mass", self.opt_mass)?;
elem.set_item("max_fuel", self.max_fuel)?;
elem.set_item("boost", self.boost)?;
elem.set_item("guardian_booster", self.guardian_booster)?;
Ok(elem.to_object(py))
}
}
impl Ship {
pub fn to_object(&self, py: Python) -> PyResult<PyObject> {
let elem = PyDict::new(py);
elem.set_item("name", self.name.clone())?;
elem.set_item("ident", self.ident.clone())?;
elem.set_item("ship_type", self.ship_type.clone())?;
elem.set_item("base_mass", self.base_mass)?;
elem.set_item("fuel_mass", self.fuel_mass)?;
elem.set_item("fuel_capacity", self.fuel_capacity)?;
elem.set_item("fsd", self.fsd.to_object(py)?)?;
Ok(elem.to_object(py))
}
}

11
setup.cfg Normal file
View File

@ -0,0 +1,11 @@
[flake8]
exclude = .nox,.git,__pycache__,build,dist,.history,.eggs
verbose = 1
[tool:pytest]
qt_api = pyqt5
console_output_style = progress
addopts = --benchmark-skip
testpaths = tests
python_files = test_*.py
python_functions = test_*

116
setup.py Normal file
View File

@ -0,0 +1,116 @@
# -*- coding: utf-8 -*-
from setuptools import find_packages, setup
from setuptools_rust import Binding, RustExtension, Strip
with open('README.md', 'r') as fh:
long_description = fh.read()
extras_require = {
'build': ['pyinstaller', 'pywin32'],
'test': [
'pytest',
'pytest-cov',
'pytest-dependency',
'pytest-benchmark[histogram]',
'pytest-metadata',
'pytest-flake8',
'pytest-flask',
'pytest-mock',
'pytest-flask-sqlalchemy',
'pytest-steps',
'pytest-xdist',
'flake8-bugbear',
'flake8-comprehensions',
'cohesion',
'hypothesis',
'flaky'
],
'dev': [
'black; python_version >= "3.6"',
'jinja2',
'tsp',
'flake8',
'flake8-bugbear',
'flake8-comprehensions',
'cohesion',
'pre-commit',
'ipython',
'flask-konch',
'setuptools_rust'
],
'gui': ['PyQt5', 'pyperclip'],
'web': [
'flask',
'gevent',
'webargs',
'flask-executor',
'flask-wtf',
'flask-user',
'flask-debugtoolbar',
'flask-bootstrap4',
'flask-sqlalchemy',
'flask-nav',
'flask-admin',
'sqlalchemy_utils[password]',
'python-dotenv',
],
}
extras_require['all'] = sorted(set(sum(extras_require.values(), [])))
setup(
use_scm_version={'write_to': '__version__.py'},
name='ed_lrr_gui',
author='Daniel Seiller',
author_email='earthnuker@gmail.com',
description='Elite: Dangerous long range route plotter',
long_description=long_description,
long_description_content_type='text/markdown',
url='https://gitlab.com/Earthnuker/ed_lrr/-/tree/pyqt_gui',
rust_extensions=[
RustExtension(
'_ed_lrr',
path='rust/Cargo.toml',
binding=Binding.PyO3,
strip=Strip.No,
debug=False,
native=True,
quiet=True,
)
],
packages=find_packages(),
entry_points={
'console_scripts': ['ed_lrr = ed_lrr_gui.__main__:main'],
'gui_scripts': ['ed_lrr_gui = ed_lrr_gui.__main__:gui_main']
},
install_requires=[
'appdirs',
'PyYAML',
'requests',
'python-dateutil',
'click',
'tqdm',
'click-default-group',
'profig',
'ujson',
'colorama',
'svgwrite',
],
setup_requires=['setuptools', 'setuptools-rust',
'setuptools-scm', 'wheel'],
dependency_links=['https://github.com/Nuitka/Nuitka/archive/develop.zip'],
extras_require=extras_require,
classifiers=[
'License :: OSI Approved :: MIT License',
'Programming Language :: Rust',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: Implementation :: CPython',
'Operating System :: Windows',
'Operating System :: Linux',
],
include_package_data=True,
zip_safe=False,
)

View File

@ -1,38 +0,0 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemSerde {
pub id: u32,
pub star_type: String,
pub system: String,
pub body: String,
pub mult: f32,
pub distance: u32,
pub x: f32,
pub y: f32,
pub z: f32,
}
impl SystemSerde {
pub fn build(&self) -> System {
System {
id: self.id,
star_type: self.star_type.clone(),
system: self.system.clone(),
body: self.body.clone(),
mult: self.mult,
distance: self.distance,
pos: [self.x, self.y, self.z],
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct System {
pub id: u32,
pub star_type: String,
pub system: String,
pub body: String,
pub mult: f32,
pub distance: u32,
pub pos: [f32; 3],
}

View File

@ -1,3 +0,0 @@
pub mod common;
pub mod preprocess;
pub mod route;

View File

@ -1,28 +0,0 @@
use ed_lrr::preprocess::{preprocess_files, PreprocessOpts};
use ed_lrr::route::{route, RouteOpts};
use humantime::format_duration;
use std::time::Instant;
use structopt::StructOpt;
#[derive(Debug, StructOpt)]
#[structopt(
name = "ed_lrr",
about = "Elite: Dangerous Long-Range Router",
rename_all = "snake_case"
)]
enum Opts {
/// Plots a route through multiple systems
Route(RouteOpts),
/// Preprocess EDSM Dump
Preprocess(PreprocessOpts),
}
fn main() -> std::io::Result<()> {
let t_start = Instant::now();
let opts = Opts::from_args();
let ret = match opts {
Opts::Route(opts) => route(opts),
Opts::Preprocess(opts) => preprocess_files(opts),
};
println!("Total time: {}", format_duration(t_start.elapsed()));
ret
}

View File

@ -1,220 +0,0 @@
use crate::common::SystemSerde;
use fnv::FnvHashMap;
use humantime::format_duration;
use indicatif::{ProgressBar, ProgressStyle};
use serde::Deserialize;
use serde_json::Result;
use std::fs::File;
use std::io::Seek;
use std::io::{BufRead, BufReader, BufWriter, SeekFrom};
use std::path::PathBuf;
use std::str;
use std::time::Instant;
use structopt::StructOpt;
#[derive(Debug, StructOpt)]
pub struct PreprocessOpts {
#[structopt(short, long = "bodies")]
/// Path to bodies.json
pub bodies: PathBuf,
#[structopt(short, long = "systems")]
/// Path to systemsWithCoordinates.json
pub systems: PathBuf,
#[structopt(default_value = "stars")]
/// outfile prefix
pub prefix: String,
}
#[derive(Debug, Deserialize)]
#[allow(non_snake_case)]
struct Body {
name: String,
subType: String,
#[serde(rename = "type")]
body_type: String,
systemId: i32,
systemId64: i64,
#[serde(rename = "distanceToArrival")]
distance: u32,
}
#[derive(Debug, Deserialize)]
struct Coords {
x: f32,
y: f32,
z: f32,
}
#[derive(Debug, Deserialize)]
struct System {
id: i32,
id64: i64,
name: String,
coords: Coords,
date: String,
}
#[derive(Debug, StructOpt)]
#[structopt(
name = "ed_lrr_pp",
about = "Preprocessor for Elite: Dangerous Long-Range Router",
rename_all = "snake_case"
)]
/// Preprocess data for ed_lrr
struct Opt {
#[structopt(short, long = "bodies")]
/// Path to bodies.json
bodies: PathBuf,
#[structopt(short, long = "systems")]
/// Path to systemsWithCoordinates.json
systems: PathBuf,
#[structopt(default_value = "stars")]
/// outfile prefix
prefix: String,
}
fn get_mult(star_type: &str) -> f32 {
if star_type.contains("White Dwarf") {
return 1.5;
}
if star_type.contains("Neutron") {
return 4.0;
}
1.0
}
fn process(path: &PathBuf, func: &mut dyn for<'r> FnMut(&'r str) -> ()) -> std::io::Result<()> {
let mut cnt = 0;
let mut buffer = String::new();
let t_start = Instant::now();
let fh = File::open(path)?;
let prog_bar = ProgressBar::new(fh.metadata()?.len());
prog_bar.set_style(
ProgressStyle::default_bar()
.template(
"[{elapsed_precise}/{eta_precise}]{spinner} [{wide_bar}] {binary_bytes}/{binary_total_bytes} ({percent}%)",
)
.progress_chars("#9876543210 ")
.tick_chars("/-\\|"),
);
prog_bar.set_draw_delta(1024 * 1024);
let mut reader = BufReader::new(fh);
println!("Loading {} ...", path.to_str().unwrap());
while let Ok(n) = reader.read_line(&mut buffer) {
if n == 0 {
break;
}
buffer = buffer.trim_end().trim_end_matches(|c| c == ',').to_string();
if !buffer.is_empty() {
func(&buffer);
}
prog_bar.set_position(reader.seek(SeekFrom::Current(0)).unwrap());
cnt += 1;
buffer.clear();
}
prog_bar.finish_and_clear();
println!(
"Processed {} lines in {} ...",
cnt,
format_duration(t_start.elapsed())
);
Ok(())
}
fn process_systems(path: &PathBuf) -> FnvHashMap<i64, System> {
let mut ret = FnvHashMap::default();
process(path, &mut |line| {
let sys_res: Result<System> = serde_json::from_str(&line);
if let Ok(sys) = sys_res {
ret.insert(sys.id64, sys);
} else {
eprintln!("\nError parsing: {}\n\t{:?}\n", line, sys_res.unwrap_err());
}
})
.unwrap();
ret
}
fn build_index(path: &PathBuf) -> std::io::Result<()> {
let mut wtr = BufWriter::new(File::create(path.with_extension("idx"))?);
let mut idx: Vec<u64> = Vec::new();
let mut records = (csv::Reader::from_path(path)?).into_deserialize::<SystemSerde>();
loop {
idx.push(records.reader().position().byte());
if records.next().is_none() {
break;
}
}
bincode::serialize_into(&mut wtr, &idx).unwrap();
Ok(())
}
fn process_bodies(
path: &PathBuf,
out_prefix: &str,
systems: &mut FnvHashMap<i64, System>,
) -> std::io::Result<()> {
let out_path = PathBuf::from(format!("{}.csv", out_prefix));
println!(
"Processing {} into {} ...",
path.to_str().unwrap(),
out_path.to_str().unwrap(),
);
let mut n: u32 = 0;
let mut wtr = csv::Writer::from_path(out_path)?;
process(path, &mut |line| {
if !line.contains("Star") {
return;
}
let body_res: Result<Body> = serde_json::from_str(&line);
if let Ok(body) = body_res {
if !body.body_type.contains("Star") {
return;
}
if let Some(sys) = systems.get(&body.systemId64) {
let sub_type = body.subType;
let mult = get_mult(&sub_type);
let sys_name = sys.name.clone();
let mut body_name = body.name.replace(&sys_name, "").trim().to_string();
if body_name == sys_name {
body_name = "".to_string();
}
let rec = SystemSerde {
id: n,
star_type: sub_type,
system: sys_name,
body: body_name,
mult,
distance: body.distance,
x: sys.coords.x,
y: sys.coords.y,
z: sys.coords.z,
};
wtr.serialize(rec).unwrap();
n += 1;
};
} else {
eprintln!("\nError parsing: {}\n\t{:?}\n", line, body_res.unwrap_err());
}
})
.unwrap();
println!("Total Systems: {}", n);
systems.clear();
Ok(())
}
pub fn preprocess_files(opts: PreprocessOpts) -> std::io::Result<()> {
let out_path = PathBuf::from(format!("{}.csv", &opts.prefix));
if !out_path.exists() {
let mut systems = process_systems(&opts.systems);
process_bodies(&opts.bodies, &opts.prefix, &mut systems)?;
} else {
println!(
"File '{}' exists, not overwriting it",
out_path.to_str().unwrap()
);
}
println!("Building index...");
println!("Index result: {:?}", build_index(&out_path));
Ok(())
}

View File

@ -1,913 +0,0 @@
use core::cmp::Ordering;
use csv::StringRecord;
use fnv::{FnvHashMap, FnvHashSet};
use humantime::format_duration;
use permutohedron::LexicalPermutation;
use rstar::{PointDistance, RTree, RTreeObject, AABB};
use sha3::{Digest, Sha3_256};
use std::collections::VecDeque;
use std::fs::File;
use std::hash::{Hash, Hasher};
use std::io::Seek;
use std::io::{BufRead, BufReader, BufWriter, Write};
use std::path::PathBuf;
use std::str::FromStr;
use std::time::Instant;
use structopt::StructOpt;
use crate::common::{System, SystemSerde};
#[derive(Debug, StructOpt)]
pub struct RouteOpts {
#[structopt(short, long = "range")]
/// Jump Range
pub range: Option<f32>,
#[structopt(
parse(from_os_str),
short = "i",
long = "path",
default_value = "./stars.csv"
)]
/// Path to stars.csv
///
/// Generate using ed_lrr_pp
pub file_path: PathBuf,
#[structopt(
parse(from_os_str),
long = "precomp_file",
conflicts_with = "precompute"
)]
/// Path to precomputed route graph
///
/// Generate using --precompute option
pub precomp_file: Option<PathBuf>,
#[structopt(
short = "c",
long = "precompute",
conflicts_with = "full_permute",
conflicts_with = "permute",
conflicts_with = "precomp_file"
)]
/// Precompute all routes for the specified jump range starting at the specified system and write the result to {system}_{range}.bin
pub precompute: bool,
#[structopt(short = "p", long = "permute", conflicts_with = "full_permute")]
/// Permute intermediate hops to find shortest route
pub permute: bool,
#[structopt(short = "o", long = "primary")]
/// Only route through the primary star of a system
pub primary: bool,
#[structopt(short = "f", long = "full_permute", conflicts_with = "permute")]
/// Permute all hops to find shortest route
pub full_permute: bool,
#[structopt(short = "g", long = "factor")]
/// Greedyness factor for A-Star (0=BFS, inf=Greedy)
pub factor: Option<f32>,
#[structopt(
short,
long,
raw(possible_values = "&[\"bfs\", \"greedy\",\"astar\"]"),
default_value = "bfs"
)]
/// Search mode
///
/**
- BFS is guaranteed to find the shortest route but very slow
- Greedy is a lot faster but will probably not find the shortest route
- A-Star is a good middle ground between speed and accuracy
*/
pub mode: Mode,
/// Systems to route through
pub systems: Vec<String>,
}
#[derive(Debug)]
pub enum Mode {
BFS,
Greedy,
AStar,
}
impl FromStr for Mode {
type Err = String;
fn from_str(s: &str) -> Result<Mode, String> {
match s {
"bfs" => Ok(Mode::BFS),
"greedy" => Ok(Mode::Greedy),
"astar" => Ok(Mode::AStar),
_ => Err("Invalid Mode".to_string()),
}
}
}
fn dist2(p1: &[f32; 3], p2: &[f32; 3]) -> f32 {
let dx = p1[0] - p2[0];
let dy = p1[1] - p2[1];
let dz = p1[2] - p2[2];
dx * dx + dy * dy + dz * dz
}
fn dist(p1: &[f32; 3], p2: &[f32; 3]) -> f32 {
dist2(p1, p2).sqrt()
}
fn fcmp(a: f32, b: f32) -> Ordering {
match (a, b) {
(x, y) if x.is_nan() && y.is_nan() => Ordering::Equal,
(x, _) if x.is_nan() => Ordering::Greater,
(_, y) if y.is_nan() => Ordering::Less,
(..) => a.partial_cmp(&b).unwrap(),
}
}
impl System {
pub fn dist2(&self, p: &[f32; 3]) -> f32 {
dist2(&self.pos, p)
}
pub fn distp(&self, p: &System) -> f32 {
dist(&self.pos, &p.pos)
}
pub fn distp2(&self, p: &System) -> f32 {
self.dist2(&p.pos)
}
}
impl PartialEq for System {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Eq for System {}
impl Hash for System {
fn hash<H: Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
impl RTreeObject for System {
type Envelope = AABB<[f32; 3]>;
fn envelope(&self) -> Self::Envelope {
AABB::from_point(self.pos)
}
}
impl PointDistance for System {
fn distance_2(&self, point: &[f32; 3]) -> f32 {
self.dist2(&point)
}
}
fn hash_file(path: &PathBuf) -> Vec<u8> {
let mut hash_reader = BufReader::new(File::open(path).unwrap());
let mut hasher = Sha3_256::new();
std::io::copy(&mut hash_reader, &mut hasher).unwrap();
hasher.result().iter().copied().collect()
}
struct LineCache {
cache: Vec<u64>,
file: BufReader<File>,
header: Option<StringRecord>,
}
impl LineCache {
pub fn new(path: &PathBuf) -> std::io::Result<Self> {
let idx_path = path.with_extension("idx");
let t_load = Instant::now();
println!("Loading {}", path.to_str().unwrap());
let mut idx_reader = BufReader::new(File::open(idx_path)?);
let cache = match bincode::deserialize_from(&mut idx_reader) {
Ok(value) => value,
err => err.unwrap(),
};
let mut reader = BufReader::new(File::open(path)?);
let header = Self::read_record(&mut reader);
let ret = Self {
file: reader,
cache,
header,
};
println!("Done in {}!", format_duration(t_load.elapsed()));
Ok(ret)
}
fn read_record(reader: &mut BufReader<File>) -> Option<StringRecord> {
let mut line = String::new();
reader.read_line(&mut line).ok()?;
let v: Vec<_> = line.trim_end().split(',').collect();
let rec = StringRecord::from(v);
Some(rec)
}
pub fn get(&mut self, id: u32) -> Option<System> {
let pos = self.cache[id as usize];
self.file.seek(std::io::SeekFrom::Start(pos)).unwrap();
let rec = Self::read_record(&mut self.file).unwrap();
let sys: SystemSerde = rec.deserialize(self.header.as_ref()).unwrap();
Some(sys.build())
}
}
pub struct Router {
tree: RTree<System>,
scoopable: FnvHashSet<u32>,
pub route_tree: Option<FnvHashMap<u32, u32>>,
cache: Option<LineCache>,
range: f32,
primary_only: bool,
path: PathBuf,
}
impl Router {
pub fn new(path: &PathBuf, range: f32, primary_only: bool) -> Self {
let mut scoopable = FnvHashSet::default();
let mut reader = csv::ReaderBuilder::new()
.from_path(path)
.unwrap_or_else(|e| {
println!("Error opening {}: {}", path.to_str().unwrap(), e);
std::process::exit(1);
});
let t_load = Instant::now();
println!("Loading {}...", path.to_str().unwrap());
let systems: Vec<System> = reader
.deserialize::<SystemSerde>()
.map(|res| res.unwrap())
.filter(|sys| {
if primary_only {
sys.distance == 0
} else {
true
}
})
.map(|sys| {
if sys.mult > 1.0f32 {
scoopable.insert(sys.id);
} else {
for c in "KGBFOAM".chars() {
if sys.star_type.starts_with(c) {
scoopable.insert(sys.id);
break;
}
}
}
sys.build()
})
.collect();
println!("Building RTree...");
let ret = Self {
tree: RTree::bulk_load(systems),
scoopable,
route_tree: None,
range,
primary_only,
cache: LineCache::new(path).ok(),
path: path.clone(),
};
println!(
"{} Systems loaded in {}",
ret.tree.size(),
format_duration(t_load.elapsed())
);
ret
}
pub fn from_file(filename: &PathBuf) -> (PathBuf, Self) {
let t_load = Instant::now();
let mut reader = BufReader::new(File::open(&filename).unwrap());
println!("Loading {}", filename.to_str().unwrap());
let (primary, range, file_hash, path, route_tree): (
bool,
f32,
Vec<u8>,
String,
FnvHashMap<u32, u32>,
) = bincode::deserialize_from(&mut reader).unwrap();
let path = PathBuf::from(path);
println!("Done in {}!", format_duration(t_load.elapsed()));
if hash_file(&path) != file_hash {
panic!("File hash mismatch!")
}
let cache = LineCache::new(&path).ok();
(
path.clone(),
Self {
tree: RTree::default(),
scoopable: FnvHashSet::default(),
route_tree: Some(route_tree),
range,
cache,
primary_only: primary,
path,
},
)
}
fn closest(&self, point: &[f32; 3]) -> &System {
self.tree.nearest_neighbor(point).unwrap()
}
fn points_in_sphere(&self, center: &[f32; 3], radius: f32) -> impl Iterator<Item = &System> {
self.tree.locate_within_distance(*center, radius * radius)
}
fn neighbours(&self, sys: &System, r: f32) -> impl Iterator<Item = &System> {
self.points_in_sphere(&sys.pos, sys.mult * r)
}
fn valid(&self, sys: &System) -> bool {
self.scoopable.contains(&sys.id)
}
pub fn best_name_multiroute(
&self,
waypoints: &[String],
range: f32,
full: bool,
mode: Mode,
factor: f32,
) -> Vec<System> {
let mut best_score: f32 = std::f32::MAX;
let mut waypoints = waypoints.to_owned();
let mut best_permutation_waypoints = waypoints.to_owned();
let first = waypoints.first().cloned();
let last = waypoints.last().cloned();
let t_start = Instant::now();
println!("Finding best permutation of hops...");
while waypoints.prev_permutation() {}
loop {
let c_first = waypoints.first().cloned();
let c_last = waypoints.last().cloned();
if full || ((c_first == first) && (c_last == last)) {
let mut total_d = 0.0;
for pair in waypoints.windows(2) {
match pair {
[src, dst] => {
let (mut src, dst) =
(self.name_to_systems(&src), self.name_to_systems(&dst));
src.sort_by_key(|&p| (p.mult * 10.0) as u8);
let src = src.last().unwrap();
let dst = dst.last().unwrap();
total_d += src.distp2(dst);
}
_ => panic!("Invalid routing parameters!"),
}
}
if total_d < best_score {
best_score = total_d;
best_permutation_waypoints = waypoints.to_owned();
}
}
if !waypoints.next_permutation() {
break;
}
}
println!("Done in {}!", format_duration(t_start.elapsed()));
println!("Best permutation: {:?}", best_permutation_waypoints);
self.name_multiroute(&best_permutation_waypoints, range, mode, factor)
}
pub fn name_multiroute(
&self,
waypoints: &[String],
range: f32,
mode: Mode,
factor: f32,
) -> Vec<System> {
let mut coords = Vec::new();
for p_name in waypoints {
let mut p_l = self.name_to_systems(p_name);
p_l.sort_by_key(|&p| (p.mult * 10.0) as u8);
let p = p_l.last().unwrap();
coords.push((p_name, p.pos));
}
self.multiroute(coords.as_slice(), range, mode, factor)
}
pub fn multiroute(
&self,
waypoints: &[(&String, [f32; 3])],
range: f32,
mode: Mode,
factor: f32,
) -> Vec<System> {
let mut route: Vec<System> = Vec::new();
for pair in waypoints.windows(2) {
match *pair {
[src, dst] => {
let block = match mode {
Mode::BFS => self.route_bfs(&src, &dst, range),
Mode::Greedy => self.route_greedy(&src, &dst, range),
Mode::AStar => self.route_astar(&src, &dst, range, factor),
};
if route.is_empty() {
for sys in block.iter() {
route.push(sys.clone());
}
} else {
for sys in block.iter().skip(1) {
route.push(sys.clone());
}
}
}
_ => panic!("Invalid routing parameters!"),
}
}
route
}
fn name_to_systems(&self, name: &str) -> Vec<&System> {
for sys in &self.tree {
if sys.system == name {
return self.neighbours(&sys, 0.0).collect();
}
}
eprintln!("System not found: \"{}\"", name);
std::process::exit(1);
}
pub fn route_astar(
&self,
src: &(&String, [f32; 3]),
dst: &(&String, [f32; 3]),
range: f32,
factor: f32,
) -> Vec<System> {
if factor == 0.0 {
return self.route_bfs(src, dst, range);
}
println!("Running A-Star with greedy factor of {}", factor);
let (src_name, src) = src;
let (dst_name, dst) = dst;
let start_sys = self.closest(src);
let goal_sys = self.closest(dst);
{
let d = dist(src, dst);
println!("Plotting route from {} to {}...", src_name, dst_name);
println!(
"Jump Range: {} Ly, Distance: {} Ly, Theoretical Jumps: {}",
range,
d,
d / range
);
}
let total = self.tree.size() as f32;
let mut prev = FnvHashMap::default();
let mut seen = FnvHashSet::default();
let t_start = Instant::now();
let mut found = false;
let mut maxd = 0;
let mut queue: Vec<(usize, usize, &System)> = Vec::new();
queue.push((
0, // depth
(start_sys.distp(goal_sys) / range) as usize, // h
&start_sys,
));
seen.insert(start_sys.id);
while !(queue.is_empty() || found) {
while let Some((depth, _, sys)) = queue.pop() {
if depth > maxd {
maxd = depth;
print!(
"[{}] Depth: {}, Queue: {}, Seen: {} ({:.02}%) \r",
format_duration(t_start.elapsed()),
depth,
queue.len(),
seen.len(),
((seen.len() * 100) as f32) / total
);
std::io::stdout().flush().unwrap();
}
if sys.id == goal_sys.id {
found = true;
break;
}
queue.extend(
self.neighbours(&sys, range)
.filter(|&nb| (self.valid(nb) || (nb.id == goal_sys.id)))
.filter(|&nb| seen.insert(nb.id))
.map(|nb| {
prev.insert(nb.id, sys);
let d_g = (nb.distp(goal_sys) / range) as usize;
(depth + 1, d_g, nb)
}),
);
queue.sort_by(|b, a| {
let (a_0, a_1) = (a.0 as f32, a.1 as f32);
let (b_0, b_1) = (b.0 as f32, b.1 as f32);
let v_a = a_0 + a_1 * factor;
let v_b = b_0 + b_1 * factor;
fcmp(v_a, v_b)
});
// queue.reverse();
}
}
println!();
println!();
if !found {
eprintln!("No route from {} to {} found!", src_name, dst_name);
return Vec::new();
}
let mut v: Vec<System> = Vec::new();
let mut curr_sys = goal_sys;
loop {
v.push(curr_sys.clone());
match prev.get(&curr_sys.id) {
Some(sys) => curr_sys = *sys,
None => {
break;
}
}
}
v.reverse();
v
}
pub fn route_greedy(
&self,
src: &(&String, [f32; 3]),
dst: &(&String, [f32; 3]),
range: f32,
) -> Vec<System> {
println!("Running Greedy-Search");
let (src_name, src) = src;
let (dst_name, dst) = dst;
let start_sys = self.closest(src);
let goal_sys = self.closest(dst);
{
let d = dist(src, dst);
println!("Plotting route from {} to {}...", src_name, dst_name);
println!(
"Jump Range: {} Ly, Distance: {} Ly, Theoretical Jumps: {}",
range,
d,
d / range
);
}
let total = self.tree.size() as f32;
let mut prev = FnvHashMap::default();
let mut seen = FnvHashSet::default();
let t_start = Instant::now();
let mut found = false;
let mut maxd = 0;
let mut queue: Vec<(f32, f32, usize, &System)> = Vec::new();
queue.push((-goal_sys.mult, start_sys.distp2(goal_sys), 0, &start_sys));
seen.insert(start_sys.id);
while !(queue.is_empty() || found) {
while let Some((_, _, depth, sys)) = queue.pop() {
if depth > maxd {
maxd = depth;
print!(
"[{}] Depth: {}, Queue: {}, Seen: {} ({:.02}%) \r",
format_duration(t_start.elapsed()),
depth,
queue.len(),
seen.len(),
((seen.len() * 100) as f32) / total
);
std::io::stdout().flush().unwrap();
}
if sys.id == goal_sys.id {
found = true;
break;
}
queue.extend(
self.neighbours(&sys, range)
.filter(|&nb| (self.valid(nb) || (nb.id == goal_sys.id)))
.filter(|&nb| seen.insert(nb.id))
.map(|nb| {
prev.insert(nb.id, sys);
(-nb.mult, nb.distp2(goal_sys), depth + 1, nb)
}),
);
queue.sort_by(|a, b| fcmp(a.0, b.0).then(fcmp(a.1, b.1)));
queue.reverse();
}
}
println!();
println!();
if !found {
eprintln!("No route from {} to {} found!", src_name, dst_name);
return Vec::new();
}
let mut v: Vec<System> = Vec::new();
let mut curr_sys = goal_sys;
loop {
v.push(curr_sys.clone());
match prev.get(&curr_sys.id) {
Some(sys) => curr_sys = *sys,
None => {
break;
}
}
}
v.reverse();
v
}
pub fn precompute(&mut self, src: &str) {
let mut sys_l = self.name_to_systems(src);
sys_l.sort_by_key(|&sys| (sys.mult * 10.0) as u8);
let sys = sys_l.last().unwrap();
println!("Precomputing routes starting at {} ...", sys.system);
let total = self.tree.size() as f32;
let mut prev = FnvHashMap::default();
let mut seen = FnvHashSet::default();
let t_start = Instant::now();
let mut depth = 0;
let mut queue: VecDeque<(usize, &System)> = VecDeque::new();
let mut queue_next: VecDeque<(usize, &System)> = VecDeque::new();
queue.push_front((0, &sys));
seen.insert(sys.id);
while !queue.is_empty() {
print!(
"[{}] Depth: {}, Queue: {}, Seen: {} ({:.02}%) \r",
format_duration(t_start.elapsed()),
depth,
queue.len(),
seen.len(),
((seen.len() * 100) as f32) / total
);
std::io::stdout().flush().unwrap();
while let Some((d, sys)) = queue.pop_front() {
queue_next.extend(
self.neighbours(&sys, self.range)
// .filter(|&nb| self.valid(nb))
.filter(|&nb| seen.insert(nb.id))
.map(|nb| {
prev.insert(nb.id, sys.id);
(d + 1, nb)
}),
);
}
std::mem::swap(&mut queue, &mut queue_next);
depth += 1;
}
self.route_tree = Some(prev);
let ofn = format!(
"{}_{}{}.router",
src.replace("*", "").replace(" ", "_"),
self.range,
if self.primary_only { "_primary" } else { "" }
);
println!("\nSaving to {}", ofn);
let mut out_fh = BufWriter::new(File::create(&ofn).unwrap());
// (range, path, route_tree)
let data = (
self.primary_only,
self.range,
hash_file(&self.path),
String::from(self.path.to_str().unwrap()),
self.route_tree.as_ref().unwrap(),
);
bincode::serialize_into(&mut out_fh, &data).unwrap();
}
fn get_systems_by_ids(&mut self, path: &PathBuf, ids: &[u32]) -> FnvHashMap<u32, System> {
println!("Processing {}", path.to_str().unwrap());
let mut ret = FnvHashMap::default();
if let Some(c) = &mut self.cache.as_mut() {
let mut missing = false;
for id in ids {
match c.get(*id) {
Some(sys) => {
ret.insert(*id, sys);
}
None => {
println!("ID {} not found in cache", id);
missing = true;
break;
}
}
}
if !missing {
return ret;
}
}
let mut reader = csv::ReaderBuilder::new()
.from_path(path)
.unwrap_or_else(|e| {
println!("Error opening {}: {}", path.to_str().unwrap(), e);
std::process::exit(1);
});
reader
.deserialize::<SystemSerde>()
.map(|res| res.unwrap())
.filter(|sys| ids.contains(&sys.id))
.map(|sys| {
ret.insert(sys.id, sys.build());
})
.last()
.unwrap_or_else(|| {
eprintln!("Error: No systems matching {:?} found!", ids);
std::process::exit(1);
});
ret
}
fn get_system_by_name(path: &PathBuf, name: &str) -> System {
let mut reader = csv::ReaderBuilder::new()
.from_path(path)
.unwrap_or_else(|e| {
eprintln!("Error opening {}: {}", path.to_str().unwrap(), e);
std::process::exit(1);
});
let sys = reader
.deserialize::<SystemSerde>()
.map(|res| res.unwrap())
.find(|sys| sys.system == name)
.unwrap_or_else(|| {
eprintln!("Error: System '{}' not found!", name);
std::process::exit(1);
});
sys.build()
}
pub fn route_to(&mut self, dst: &str, systems_path: &PathBuf) -> Vec<System> {
let prev = self.route_tree.as_ref().unwrap();
let dst = Self::get_system_by_name(&systems_path, dst);
if !prev.contains_key(&dst.id) {
eprintln!("System-ID {} not found", dst.id);
std::process::exit(1);
};
let mut v_ids: Vec<u32> = Vec::new();
let mut v: Vec<System> = Vec::new();
let mut curr_sys: u32 = dst.id;
loop {
v_ids.push(curr_sys);
match prev.get(&curr_sys) {
Some(sys_id) => curr_sys = *sys_id,
None => {
break;
}
}
}
v_ids.reverse();
let id_map = self.get_systems_by_ids(&systems_path, &v_ids);
for sys_id in v_ids {
let sys = id_map.get(&sys_id).unwrap();
v.push(sys.clone())
}
v
}
pub fn route_bfs(
&self,
src: &(&String, [f32; 3]),
dst: &(&String, [f32; 3]),
range: f32,
) -> Vec<System> {
println!("Running BFS");
let (src_name, src) = src;
let (dst_name, dst) = dst;
let start_sys = self.closest(src);
let goal_sys = self.closest(dst);
{
let d = dist(src, dst);
println!("Plotting route from {} to {}...", src_name, dst_name);
println!(
"Jump Range: {} Ly, Distance: {} Ly, Theoretical Jumps: {}",
range,
d,
d / range
);
}
let total = self.tree.size() as f32;
let mut prev = FnvHashMap::default();
let mut seen = FnvHashSet::default();
let t_start = Instant::now();
let mut depth = 0;
let mut found = false;
let mut queue: VecDeque<(usize, &System)> = VecDeque::new();
let mut queue_next: VecDeque<(usize, &System)> = VecDeque::new();
queue.push_front((0, &start_sys));
seen.insert(start_sys.id);
while !(queue.is_empty() || found) {
print!(
"[{}] Depth: {}, Queue: {}, Seen: {} ({:.02}%) \r",
format_duration(t_start.elapsed()),
depth,
queue.len(),
seen.len(),
((seen.len() * 100) as f32) / total
);
std::io::stdout().flush().unwrap();
while let Some((d, sys)) = queue.pop_front() {
if sys.id == goal_sys.id {
found = true;
break;
}
queue_next.extend(
self.neighbours(&sys, range)
.filter(|&nb| (self.valid(nb) || (nb.id == goal_sys.id)))
.filter(|&nb| seen.insert(nb.id))
.map(|nb| {
prev.insert(nb.id, sys);
(d + 1, nb)
}),
);
}
std::mem::swap(&mut queue, &mut queue_next);
depth += 1;
}
println!();
println!();
if !found {
eprintln!("No route from {} to {} found!", src_name, dst_name);
return Vec::new();
}
let mut v: Vec<System> = Vec::new();
let mut curr_sys = goal_sys;
loop {
v.push(curr_sys.clone());
match prev.get(&curr_sys.id) {
Some(sys) => curr_sys = *sys,
None => {
break;
}
}
}
v.reverse();
v
}
}
pub fn route(opts: RouteOpts) -> std::io::Result<()> {
if opts.systems.is_empty() {
if opts.precomp_file.is_some() {
eprintln!("Error: Please specify exatly one system");
} else if opts.precompute {
eprintln!("Error: Please specify at least one system");
} else {
eprintln!("Error: Please specify at least two systems");
}
std::process::exit(1);
}
let mut path = opts.file_path;
let mut router: Router = if opts.precomp_file.is_some() {
let ret = Router::from_file(&opts.precomp_file.clone().unwrap());
path = ret.0;
ret.1
} else {
Router::new(&path, opts.range.unwrap(), opts.primary)
};
if opts.precompute {
for sys in opts.systems {
router.route_tree = None;
router.precompute(&sys);
}
std::process::exit(0);
}
let t_route = Instant::now();
let route = if router.route_tree.is_some() {
router.route_to(opts.systems.first().unwrap(), &path)
} else if opts.permute || opts.full_permute {
router.best_name_multiroute(
&opts.systems,
opts.range.unwrap(),
opts.full_permute,
opts.mode,
opts.factor.unwrap_or(1.0),
)
} else {
router.name_multiroute(
&opts.systems,
opts.range.unwrap(),
opts.mode,
opts.factor.unwrap_or(1.0),
)
};
println!("Route computed in {}\n", format_duration(t_route.elapsed()));
if route.is_empty() {
eprintln!("No route found!");
return Ok(());
}
let mut total: f32 = 0.0;
for (sys1, sys2) in route.iter().zip(route.iter().skip(1)) {
let dist = sys1.distp(sys2);
total += dist;
println!(
"{} [{}] ({},{},{}) [{} Ls]: {:.2} Ly",
sys1.system,
sys1.star_type,
sys1.pos[0],
sys1.pos[1],
sys1.pos[2],
sys1.distance,
dist
);
}
let sys = route.iter().last().unwrap();
println!(
"{} [{}] ({},{},{}) [{} Ls]: {:.2} Ly",
sys.system, sys.star_type, sys.pos[0], sys.pos[1], sys.pos[2], sys.distance, 0.0
);
println!("Total: {:.2} Ly ({} Jumps)", total, route.len());
Ok(())
}

116
tests/conftest.py Normal file
View File

@ -0,0 +1,116 @@
import pytest
import random
import csv
from tempfile import mkstemp
from pathlib import Path
def get_mult(star_type):
if star_type.startswith("Neutron"):
return 4.0
if star_type.startswith("White Dwarf"):
return 1.5
return 1.0
def gen_pos(p_distrib):
p = []
for v in p_distrib:
p.append(random.triangular(-v, v))
return p
def make_stars(num, p_distrib):
star_types = [
"A (Blue-White) Star",
"A (Blue-White super giant) Star",
"B (Blue-White) Star",
"B (Blue-White super giant) Star",
"Black Hole",
"CJ Star",
"CN Star",
"C Star",
"F (White) Star",
"F (White super giant) Star",
"G (White-Yellow) Star",
"G (White-Yellow super giant) Star",
"Herbig Ae/Be Star",
"K (Yellow-Orange giant) Star",
"K (Yellow-Orange) Star",
"L (Brown dwarf) Star",
"M (Red dwarf) Star",
"M (Red giant) Star",
"M (Red super giant) Star",
"MS-type Star",
"Neutron Star",
"O (Blue-White) Star",
"star_type",
"S-type Star",
"Supermassive Black Hole",
"T (Brown dwarf) Star",
"T Tauri Star",
"White Dwarf (DAB) Star",
"White Dwarf (DA) Star",
"White Dwarf (DAV) Star",
"White Dwarf (DAZ) Star",
"White Dwarf (DB) Star",
"White Dwarf (DBV) Star",
"White Dwarf (DBZ) Star",
"White Dwarf (DC) Star",
"White Dwarf (DCV) Star",
"White Dwarf (DQ) Star",
"White Dwarf (D) Star",
"Wolf-Rayet C Star",
"Wolf-Rayet NC Star",
"Wolf-Rayet N Star",
"Wolf-Rayet O Star",
"Wolf-Rayet Star",
"Y (Brown dwarf) Star",
]
id_n = 0
while id_n < num:
name = "System {}".format(id_n)
body = "System {} Star {}".format(id_n, 0)
distance = 0
star_type = random.choice(star_types)
mult = get_mult(star_type)
x, y, z = gen_pos(p_distrib)
s_type = random.choice(star_types)
record = [id_n, s_type, name, body, mult, distance]
record.extend((x, y, z))
yield record
id_n += 1
for sub_id in range(random.randint(0, 4)):
star_type = random.choice(star_types)
mult = get_mult(star_type)
distance = random.randint(100, 10000)
body = "System {} Star {}".format(id_n, sub_id + 1)
s_type = random.choice(star_types)
record = [id_n, s_type, name, body, mult, distance]
record.extend((x, y, z))
yield record
id_n += 1
@pytest.fixture(scope="module")
def stars_path():
num_stars = int(1e7)
p_distrib = [5000, 5000, 500]
tmpfile, filename = mkstemp(suffix=".csv", prefix="stars_", text=True)
filename = Path(filename)
tmpfile = open(tmpfile, "w", encoding="utf-8")
fields = ["id", "star_type", "system", "body", "mult"]
fields += ["distance", "x", "y", "z"]
csv_writer = csv.DictWriter(tmpfile, fields)
rows = (dict(zip(fields, row)) for row in make_stars(num_stars, p_distrib))
csv_writer.writeheader()
csv_writer.writerows(rows)
tmpfile.close()
sys_ids = random.sample(range(num_stars), k=10)
rand_sys = list(map("System {}".format, sys_ids))
yield str(filename.resolve()), rand_sys
if filename.exists():
filename.unlink()
idx = filename.with_suffix(".idx")
if idx.exists():
idx.unlink()

45
tests/test_benchmark.py Normal file
View File

@ -0,0 +1,45 @@
import pytest
import os
from math import log2, ceil
def resolve_systems(r, *names):
ret = []
mapping = r.resolve_systems(*names)
for name in names:
ret.append(mapping.get(name))
return ret
@pytest.fixture(scope="module")
def ed_lrr_router(stars_path):
stars_path, systems = stars_path
from ed_lrr_gui import PyRouter
r = PyRouter(lambda status: None)
r.load(stars_path)
system_ids = resolve_systems(r, *systems)
return r, system_ids
argvalues = [(0, 0)]
argvalues += [(2 ** n, 0) for n in range(17)]
argvalues += [(0, g) for g in (0.25, 0.5, 0.75, 1)]
ids = []
for width, greedyness in argvalues:
ids.append("beam_width:{}-greedyness:{}".format(width, greedyness))
n_workers = [0]
n_workers += [2**n for n in range(ceil(log2(os.cpu_count()))+1)]
@pytest.mark.parametrize("workers", n_workers,
ids=lambda v: "workers:{}".format(v))
@pytest.mark.parametrize(argnames=("width", "greedyness"),
argvalues=argvalues, ids=ids)
@pytest.mark.parametrize("r_range", [48.0], ids=lambda v: "range:{}".format(v))
def test_benchmark(benchmark, ed_lrr_router,
r_range, workers, greedyness, width):
r, system_ids = ed_lrr_router
args = system_ids, r_range, greedyness, width, workers
benchmark(r.route, *args)

113
tests/test_ed_lrr.py Normal file
View File

@ -0,0 +1,113 @@
# -*- coding: utf-8 -*-
import random
import pytest
import os
from flaky import flaky
from pytest import approx
from glob import glob
def dist(a, b, m=2):
assert len(a) == len(b)
s = 0
for c1, c2 in zip(a, b):
s += (c1 - c2) ** 2
return s ** (1 / m)
@pytest.fixture(scope="module")
def py_router(stars_path):
from ed_lrr_gui import PyRouter
stars_path, names = stars_path
router = PyRouter(lambda status: None)
router.load(stars_path)
resolved_systems = router.resolve_systems(*names)
for name in names:
err = "Failed to resolve {}".format(name)
assert name in resolved_systems, err
yield router, resolved_systems
def idf(name):
return lambda val: "{}:{}".format(name, val)
# class Test_PyShip(object):
# folder = os.path.dirname(__file__)
# ships = glob(os.path.join(folder,"data","ships","*.json"))
# ship_data=[]
# ship_ids=[]
# for ship in ships:
# with open(ship,"r",encoding="utf-8") as fh:
# ship_data.append(fh.read())
# ship_ids.append(os.path.splitext(os.path.basename(ship))[0])
# @pytest.mark.parametrize("ship_data", ship_data, ids=ship_ids)
# def test_ship_from_json(self, ship_data):
# import json
# from _ed_lrr import PyShip
# loadout = json.loads(ship_data)
# ship = PyShip.from_loadout(ship_data)
# ship_dict = ship.to_dict()
# booster = ship_dict.get("fsd", {}).get("guardian_booster")
# assert booster is not None
# rel = None
# if booster != approx(0.0):
# rel = 0.01
# assert ship.max_range() == approx(loadout["MaxJumpRange"], rel)
class Test_PyRouter(object): # noqa: H601
beam_widths = [256, 512, 1024, 0]
greedyness = [0, 0.5, 1]
n_workers = [0, 1, 2, os.cpu_count()]
ranges = [30, 50]
@pytest.mark.dependency()
@flaky(max_runs=10, min_passes=5)
def test_load_and_resolve(self, stars_path):
stars_path, names = stars_path
from ed_lrr_gui import PyRouter
router = PyRouter(lambda status: None)
router.load(stars_path)
resolved_systems = router.resolve_systems(*names)
for name in names:
err = "Failed to resolve {}".format(name)
assert name in resolved_systems, err
@pytest.mark.dependency(depends=["Test_ED_LRR::test_load_and_resolve"])
@pytest.mark.parametrize(
"greedyness", greedyness, ids=lambda v: "greedyness:{}".format(v)
)
@flaky(max_runs=10, min_passes=5)
def test_zero_range_fails(self, py_router, greedyness):
r, resolved_systems = py_router
waypoints = random.sample(list(resolved_systems.values()), k=2)
err = pytest.raises(RuntimeError, r.route, waypoints, 0, greedyness)
err.match(r"No route from .* to .* found!")
@pytest.mark.dependency(depends=["Test_ED_LRR::test_load_and_resolve"])
@flaky(max_runs=10, min_passes=2)
@pytest.mark.parametrize("workers", n_workers, ids=idf("workers"))
@pytest.mark.parametrize("jump_range", ranges, ids=idf("range"))
@pytest.mark.parametrize("greedyness", greedyness, ids=idf("greedyness"))
@pytest.mark.parametrize("beam_width", beam_widths, ids=idf("beam_width"))
def test_route(self, py_router, jump_range, workers, greedyness, beam_width):
r, resolved_systems = py_router
waypoints = random.choices(list(resolved_systems.values()), k=2)
args = waypoints, jump_range, greedyness, beam_width, workers
err = "Failed to route for waypoints: {}".format(waypoints)
route = r.route(*args)
assert len(route) != 0, err
del r
hops = [h["id"] for h in route]
for wp in waypoints:
assert wp in hops, "System id {} not found in route".format(wp)
for a, b in zip(route, route[1:]):
d = dist(a["pos"], b["pos"])
msg = "jump from {} to {} seems impossible".format(a["system"], b["system"])
assert d <= jump_range * a["mult"], msg

22
tests/test_gui.py Normal file
View File

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# import pytest
# class Test_GUI(object):
# pass
# @pytest.mark.dependency()
# def test_import():
# import ed_lrr_gui
# from ed_lrr_gui.main import main
# import ed_lrr_gui.gui as ED_LRR_GUI
# @pytest.mark.dependency(depends=["test_import"])
# def test_search_works():
# import ed_lrr_gui
# @pytest.mark.dependency(depends=["test_import"])
# def test_zero_range_fails():
# import ed_lrr_gui

21
tests/test_web.py Normal file
View File

@ -0,0 +1,21 @@
import pytest
@pytest.fixture
def app():
from ed_lrr_gui.web import app as flask_app
yield flask_app
@pytest.fixture
def db():
from ed_lrr_gui.web import db as flask_db
yield flask_db
class Test_DB(object): # noqa: H601
pass
class Test_Webapp(object): # noqa: H601
pass