432 lines
13 KiB
Python
432 lines
13 KiB
Python
# -*- 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 (0.0=BFS, 1.0=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()
|