Skip to content
Commits on Source (4)
......@@ -2,110 +2,402 @@ import sys
import multiprocessing as MP
import queue
import ctypes
import os
from datetime import datetime
from math import floor
import click
from tqdm import tqdm
from click_default_group import DefaultGroup
import requests as RQ
from ed_lrr_gui import Router
from ed_lrr_gui import Preprocessor
import ed_lrr_gui.gui as ED_LRR_GUI
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
from urllib.parse import urljoin
from ed_lrr_gui import Router, Preprocessor, cfg
from _ed_lrr import find_sys
@click.group(invoke_without_command=True,context_settings=CONTEXT_SETTINGS)
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
stars_path = os.path.join(cfg["folders.data_dir"], "stars.csv")
for folder in cfg["history.out_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 is None:
ctx.invoke(gui)
return
return
@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="Debug print",is_flag=True)
@click.option("--debug", help="Debug print", is_flag=True)
def gui(debug):
"Run the ED LRR GUI (default)"
if not debug:
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.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("--systems","-s",help="Target path for systemsWithCoordinates.json",default="systemsWithCoordinates.json",show_default=True)
@click.option("--bodies","-b",help="Target path for bodies.json",default="bodies.json",show_default=True)
def download(*args,**kwargs):
@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"
print("Download:",args,kwargs)
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,
) 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()
def preprocess(*args,**kwargs):
@click.option(
"--systems",
"-s",
default=systems_path,
metavar="<path>",
help="Path to stars.csv",
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"
print("PreProcess:",ctx,args,kwargs)
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 preproc.is_alive() == False):
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.csv",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",required=True,metavar="<float>",help="Jump range (Ly)",type=click.FloatRange(min=0))
@click.option("--prune","-d",default=(0,0),metavar="<n> <m>",help="Prune search branches",nargs=2,type=click.Tuple([click.IntRange(min=0),click.FloatRange(min=0)]))
@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","-ps",is_flag=True,default=False,help="Only route through primary stars")
@click.option("--factor","-g",metavar="<float>",default=0.5,help="Greedyness factor for A-Star",show_default=True)
@click.option("--mode","-m",default="bfs",help="Search mode",type=click.Choice(["bfs","a-star","greedy"]),show_default=True)
@click.argument('systems',nargs=-1)
@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", "a-star", "greedy"]),
show_default=True,
)
@click.argument("systems", nargs=-1)
def route(**kwargs):
"Compute a route"
if kwargs['prune']==(0,0):
kwargs['prune']=None
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 "[{}] {}".format(state['depth'],state['system'])
keep_first,keep_last={
"all":(False,False),
"keep_first":(True,False),
"keep_last":(False,True),
"keep_both":(True,True),
None: (False,False)
}[kwargs['permute']]
args=[kwargs['systems'],kwargs['range'],kwargs['prune'],kwargs['mode'],kwargs['primary'],kwargs['permute']!=None,keep_first,keep_last,kwargs['factor'],None,kwargs['path']]
with click.progressbar(length=100,label="Computing route",show_percent=True,item_show_func=to_string,width=50) as pbar:
router=Router(*args)
return "{prc_done:.2f}% [N:{depth} Q:{queue_size} D:{d_rem:.2f} Ly] {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"],
]
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):
state = {}
pstate = {}
while not (router.queue.empty() and router.is_alive() == False):
try:
event = router.queue.get(True,0.1)
event = router.queue.get(True, 0.1)
state.update(event)
if state!=pstate:
pbar.current_item=state.get("status")
if state != pstate:
pbar.current_item = state.get("status")
if pbar.current_item:
pbar.pos=floor(pbar.current_item["prc_done"]*10)/10
pbar.pos = pbar.current_item["prc_done"]
pbar.update(0)
pstate=state
pstate = state.copy()
except queue.Empty:
pass
pbar.pos=100
pbar.pos = 100
pbar.update(0)
print(state.get("result"))
print("DONE!")
for n, jump in enumerate(state.get("return", []), 1):
jump["n"] = n
if jump["body"].index(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.csv",type=click.Path(exists=True,dir_okay=False),show_default=True )
@click.option("--precomp_file","-pc",help="Precomputed routing graph to use",type=click.Path(exists=True,dir_okay=False))
@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):
@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:",ctx,args,kwargs)
print("PreComp:", args, kwargs)
if __name__ == '__main__':
MP.freeze_support()
main()
\ No newline at end of file
if __name__ == "__main__":
main()
......@@ -11,10 +11,10 @@ from urllib.request import Request, urlopen
import _ed_lrr
import ed_lrr_gui
import ed_lrr_gui.config as cfg
import requests as RQ
from ed_lrr_gui import Preprocessor, Router
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_Route
from PyQt5.QtCore import QObject, Qt, QThread, QTimer, pyqtSignal
from PyQt5.QtGui import QColor, QPalette,QIcon
from PyQt5.QtWidgets import (
......@@ -25,9 +25,17 @@ from PyQt5.QtWidgets import (
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:
......@@ -40,12 +48,6 @@ def t_round(dt):
return dt - dt % timedelta(seconds=1)
class ProgressDialog(QProgressDialog):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setWindowModality(Qt.WindowModal)
class Job(QObject):
progress = pyqtSignal("PyQt_PyObject")
......@@ -59,13 +61,14 @@ class Job(QObject):
self.timer.start(100)
self.last_val = None
self.progress_dialog = None
self.handle_progess = None
self.state = {}
self.setup_progress(self.handle_progess)
def setup_progress(self, handle_progess):
self.progress.connect(
lambda *args, **kwargs: handle_progess(self, *args, **kwargs)
)
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:
......@@ -83,7 +86,7 @@ class Job(QObject):
return (self.job.is_alive() == False) and (self.job.queue.empty())
def interval(self):
while True:
while self.job:
try:
res = self.job.queue.get(True, 0.1)
except queue.Empty:
......@@ -95,6 +98,39 @@ class Job(QObject):
self.last_val = res
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...")
self.progress_dialog.setWindowTitle("Loading...")
self.progress_dialog.canceled.connect(self.cancel)
self.progress_dialog.show()
self.start()
def done(self,result):
return
def handle_progess(self, state):
if state.get('return') is not None:
self.done(state['return'])
self.progress_dialog.close()
WRoute(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")
......@@ -311,9 +347,10 @@ class ED_LRR(Ui_ED_LRR):
def route_progress(self, job, state):
print("RP:", job, state)
def run(self):
if not all(s["Type"] for s in self.systems):
self.error('Not all systens have been resolved, please click "Search All"')
def compute_route(self):
num_resolved=self.resolve_systems()
if num_resolved:
if QMessageBox.question(self.main_window,"ED_LRR","Resolved {} systems, are the names correct?".format(num_resolved))==QMessageBox.No:
return
print(self.systems)
systems = [str(s["id"]) for s in self.systems]
......@@ -349,11 +386,13 @@ class ED_LRR(Ui_ED_LRR):
path,
precomp,
)
route_job = self.new_job(
Router,
if self.current_job is None:
self.current_job = RouterJob(
self.app,
self.main_window,
systems,
jump_range,
0.1,
None,
mode,
primary,
permute,
......@@ -385,16 +424,24 @@ class ED_LRR(Ui_ED_LRR):
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 enumerate(names):
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))
......@@ -479,7 +526,7 @@ class ED_LRR(Ui_ED_LRR):
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.run)
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.chk_permute.stateChanged.connect(self.update_permute_chk)
......
......@@ -4,13 +4,6 @@ from datetime import datetime, timedelta
from multiprocessing import Process, Queue, freeze_support
import _ed_lrr
# from PyQt5.QtWidgets import QProgressDialog
# class RouteProgress(QProgressDialog):
# def __init__(self, *args, **kwargs):
# super().__init__(*args, **kwargs)
# self.setWindowModality(Qt.WindowModal)
class Router(Process):
......
......@@ -10,7 +10,7 @@ with open("README.md", "r") as fh:
setup(
name="ed_lrr_gui",
version="0.1.0",
version_format='{tag}.dev{commitcount}+{gitsha}',
author="Daniel Seiller",
author_email="earthnuker@gmail.com",
description="Elite: Dangerous long range route plotter",
......@@ -22,17 +22,15 @@ setup(
"_ed_lrr",
path="rust/Cargo.toml",
binding=Binding.PyO3,
strip=Strip.All,
strip=Strip.No,
native=True,
)
],
packages=find_packages(),
entry_points={
"console_scripts": [
"ed_lrr_gui_console = ed_lrr_gui.gui.__main__:main",
"ed_lrr = ed_lrr_gui.__main__:main",
],
"gui_scripts": ["ed_lrr_gui = ed_lrr_gui.gui.__main__:main"],
},
install_requires=[
"appdirs",
......@@ -41,8 +39,11 @@ setup(
"python-dateutil",
"pyperclip",
"click",
"tqdm",
"PyQt5",
"click-default-group"
"click-default-group",
"profig",
"colorama"
],
setup_requires=[
"setuptools",
......@@ -50,6 +51,7 @@ setup(
"wheel",
"pyinstaller",
"pytest-runner",
"setuptools-git-version"
],
tests_require=["pytest", "pytest-pep8", "pytest-cov"],
extras_require={
......