WIP, to be cleaned and merged

This commit is contained in:
Daniel S. 2020-02-06 00:23:23 +01:00
parent 314adbeb1d
commit d1e3152a83
30 changed files with 1498 additions and 248 deletions

39
benchmark.py Normal file
View File

@ -0,0 +1,39 @@
from datetime import datetime
import time
import json
import statistics as stats
from pprint import pprint
import os
import _ed_lrr
NUM_LOOPS=5
results={}
def time_run(w,s,file="benchmark.json", loops=None):
global results,NUM_LOOPS
if loops is None:
loops=NUM_LOOPS
for _ in range(loops):
t_start = time.time()
ret = _ed_lrr.route(s,48,None,'bfs',True,False,False,False,0.0,None,r"D:\devel\rust\ED_LRR\stars.csv",w,lambda *args,**kwargs: None)
t_end = time.time()
results.setdefault(w,[]).append({'ret':len(ret),'time':t_end - t_start})
with open(file, "w") as of:
json.dump(results, of,indent=2)
t_start = datetime.today()
for w in [1,2,4,7,8,0]:
time_run(w,['Ix','72'])
print("Benchmark took:", datetime.today() - t_start)
for workers,results in results.items():
t_total=sum([res['time'] for res in results])/len(results)
avg_len=sum([res['ret'] for res in results])/len(results)
times.append([int(workers),timedelta(seconds=t_total),avg_len])
for k,v,l in sorted(times,key=lambda rec:rec[1]):
print(k,v,l)

17
celery_rabbitmq_setup.ps1 Normal file
View File

@ -0,0 +1,17 @@
#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
$env:FORKED_BY_MULTIPROCESSING=1
Write-Host starting Celery
celery -A celery_test worker -E -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/

9
celery_test.py Normal file
View File

@ -0,0 +1,9 @@
from celery import Celery
import _ed_lrr
app = Celery('ed_lrr',backend = 'db+sqlite:///ed_lrr_results.sqlite', broker='pyamqp://ed_lrr:ed_lrr@localhost/ed_lrr')
@app.task(bind=True)
def route(self,hops,jmp_range):
def callback(state):
self.update_state(state="PROGRESS", meta=state)
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)

18
celery_worker.py Normal file
View File

@ -0,0 +1,18 @@
import time
from celery_test import route
jobs = [route.delay(["Ix", "Colonia"], 48), route.delay(["Colonia", "Sol"], 48)]
while True:
for job in jobs:
if job.ready():
print([job, job.state, len(job.info)])
else:
print([job, job.state, job.info])
print("="*10)
time.sleep(1)
# 02c77491-9abd-4a88-ab2c-acdf2981086b
# d56b0ca8-067d-45a6-be9b-bb9e74f196cd
# 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/

View File

@ -39,7 +39,7 @@ for file in cfg["history.bodies_path"][::-1]:
@click.pass_context
@click.version_option()
def main(ctx):
"Elite: Dangerous long range router, command line interface"
"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)
@ -49,12 +49,31 @@ def main(ctx):
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.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)
return
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
"""Change configuration.
If "key" and "value" are both omitted the current configuration is printed
"""
@ -98,14 +117,14 @@ def config(option, value):
@main.command()
def explore():
"Open file manager in data folder"
"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="Enable debug output", is_flag=True)
def gui(debug):
"Run the ED LRR GUI (default)"
"Run the ED LRR GUI (default)."
import ed_lrr_gui.gui as ED_LRR_GUI
if (not debug) and os.name == "nt":
@ -133,7 +152,7 @@ def gui(debug):
show_default=True,
)
def download(url, folder):
"Download EDSM dumps"
"Download EDSM dumps."
os.makedirs(folder, exist_ok=True)
for file_name in ["systemsWithCoordinates.json", "bodies.json"]:
download_url = urljoin(url, file_name)
@ -156,6 +175,7 @@ def download(url, folder):
unit_divisor=1024,
unit_scale=True,
ascii=True,
smoothing=0
) as pbar:
with open(download_path, "wb") as of:
resp = RQ.get(
@ -173,7 +193,7 @@ def download(url, folder):
"-s",
default=systems_path,
metavar="<path>",
help="Path to stars.csv",
help="Path to systemsWithCoordinates.json",
type=click.Path(exists=True, dir_okay=False),
show_default=True,
)
@ -196,7 +216,7 @@ def download(url, folder):
show_default=True,
)
def preprocess(systems, bodies, output):
"Preprocess EDSM dumps"
"Preprocess EDSM dumps."
with click.progressbar(
length=100, label="", show_percent=True, item_show_func=lambda v: v, width=50
) as pbar:
@ -289,12 +309,20 @@ def preprocess(systems, bodies, output):
"-m",
default=cfg["route.mode"],
help="Search mode",
type=click.Choice(["bfs", "a-star", "greedy"]),
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"
"Compute a route."
if len(kwargs["systems"]) < 2:
exit("Need at least two systems to plot a route")
if kwargs["prune"] == (0, 0):
@ -302,7 +330,7 @@ def route(**kwargs):
def to_string(state):
if state:
return "{prc_done:.2f}% [N:{depth} Q:{queue_size} D:{d_rem:.2f} Ly] {system}".format(
return "{prc_done:.2f}% [N:{depth} | Q:{queue_size} | D:{d_rem:.2f} Ly | S: {n_seen} ({prc_seen:.2f}%)] {system}".format(
**state
)
@ -330,6 +358,7 @@ def route(**kwargs):
kwargs["factor"],
None,
kwargs["path"],
kwargs["workers"]
]
with click.progressbar(
length=100,
@ -359,7 +388,7 @@ def route(**kwargs):
pbar.update(0)
for n, jump in enumerate(state.get("return", []), 1):
jump["n"] = n
if jump["body"].index(jump["system"]) == -1:
if jump["body"].find(jump["system"]) == -1:
jump["where"] = "[{body}] in [{system}]".format(**jump)
else:
jump["where"] = "[{body}]".format(**jump)
@ -397,6 +426,7 @@ def route(**kwargs):
def precompute(*args, **kwargs):
"Precompute routing graph"
print("PreComp:", args, kwargs)
raise NotImplementedError
if __name__ == "__main__":

View File

@ -55,5 +55,8 @@ cfg.init("folders.data_dir", os.path.join(config_dir, "data"), comment="Data dir
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

@ -2,7 +2,7 @@
# 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.13.1
# Created by: PyQt5 UI code generator 5.14.1
#
# WARNING! All changes made in this file will be lost!
@ -15,9 +15,7 @@ class Ui_ED_LRR(object):
ED_LRR.setObjectName("ED_LRR")
ED_LRR.setEnabled(True)
ED_LRR.resize(577, 500)
sizePolicy = QtWidgets.QSizePolicy(
QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed
)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(ED_LRR.sizePolicy().hasHeightForWidth())
@ -28,14 +26,10 @@ class Ui_ED_LRR(object):
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 = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.centralwidget.sizePolicy().hasHeightForWidth()
)
sizePolicy.setHeightForWidth(self.centralwidget.sizePolicy().hasHeightForWidth())
self.centralwidget.setSizePolicy(sizePolicy)
self.centralwidget.setObjectName("centralwidget")
self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget)
@ -55,39 +49,27 @@ class Ui_ED_LRR(object):
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.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.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.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.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 = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.inp_bodies_dest_dl.sizePolicy().hasHeightForWidth()
)
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)
@ -103,14 +85,10 @@ class Ui_ED_LRR(object):
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 = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.inp_systems_dest_dl.sizePolicy().hasHeightForWidth()
)
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)
@ -133,79 +111,57 @@ class Ui_ED_LRR(object):
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.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 = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.inp_bodies_pp.sizePolicy().hasHeightForWidth()
)
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.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.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 = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.inp_systems_pp.sizePolicy().hasHeightForWidth()
)
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.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 = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.btn_out_browse_pp.sizePolicy().hasHeightForWidth()
)
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 = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.inp_out_pp.sizePolicy().hasHeightForWidth())
@ -214,14 +170,10 @@ class Ui_ED_LRR(object):
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.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.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")
@ -229,18 +181,14 @@ class Ui_ED_LRR(object):
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.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 = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.inp_sys_lst.sizePolicy().hasHeightForWidth())
@ -273,44 +221,32 @@ class Ui_ED_LRR(object):
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.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 = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(
self.chk_permute_keep_first.sizePolicy().hasHeightForWidth()
)
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 = 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.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 = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.lst_sys.sizePolicy().hasHeightForWidth())
@ -417,27 +353,17 @@ class Ui_ED_LRR(object):
def retranslateUi(self, ED_LRR):
_translate = QtCore.QCoreApplication.translate
ED_LRR.setWindowTitle(
_translate("ED_LRR", "Elite: Dangerous Long Range Route Plotter")
)
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.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.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"))
@ -445,9 +371,7 @@ class Ui_ED_LRR(object):
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.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"))
@ -469,12 +393,8 @@ class Ui_ED_LRR(object):
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.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"))

View File

@ -8,7 +8,6 @@ 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
@ -436,7 +435,7 @@ class ED_LRR(Ui_ED_LRR):
return
self.bar_status.clearMessage()
print(self.systems)
systems = [str(s["id"]) for s in self.systems]
systems = [s["id"] for s in self.systems]
jump_range = self.sb_range.value()
if jump_range == 0:
self.error(
@ -466,13 +465,16 @@ class ED_LRR(Ui_ED_LRR):
print(
systems,
jump_range,
None,
mode,
primary,
permute,
(keep_first, keep_last),
keep_first,
keep_last,
greedyness,
path,
precomp,
path,
os.cpu_count()-1
)
if not self.current_job:
self.bar_status.showMessage("Computing Route...")
@ -490,6 +492,7 @@ class ED_LRR(Ui_ED_LRR):
greedyness,
precomp,
path,
os.cpu_count()-1
)
else:
self.error("there is already a job running!")

View File

@ -2,7 +2,7 @@
# 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.13.1
# Created by: PyQt5 UI code generator 5.14.1
#
# WARNING! All changes made in this file will be lost!
@ -25,9 +25,7 @@ class Ui_diag_route(object):
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.setHorizontalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
self.lst_route.setItemsExpandable(False)
self.lst_route.setAllColumnsShowFocus(False)
self.lst_route.setObjectName("lst_route")
@ -55,11 +53,7 @@ class Ui_diag_route(object):
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.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

@ -10,7 +10,6 @@ def dist(p1, p2):
s += (c1 - c2) ** 2
return s ** 0.5
colors = {
"O": "#0000FF",
"B": "#140AF0",

View File

@ -20,6 +20,7 @@ class Router(Process):
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})

View File

@ -0,0 +1 @@
from .app import app, templates, db

View File

@ -1,100 +0,0 @@
from flask import Flask, jsonify
import uuid
import json
from webargs import fields, validate
from webargs.flaskparser import use_args
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy_utils import Timestamp, generic_repr
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import scoped_session, sessionmaker, relationship, backref
from sqlalchemy.types import Float, String, Boolean
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///jobs.db"
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
db = SQLAlchemy(app)
@generic_repr
class Job(db.Model, Timestamp):
id = db.Column(db.String, default=lambda: str(uuid.uuid4()), primary_key=True)
jump_range = db.Column(db.Float, nullable=False)
mode = db.Column(db.String, default="bfs")
systems = db.Column(db.String)
permute = db.Column(db.String, default=None, nullable=True)
primary = db.Column(db.Boolean, default=False)
factor = db.Column(db.Float, default=0.5)
done = db.Column(db.DateTime, nullable=True, default=None)
started = db.Column(db.DateTime, nullable=True, default=None)
progress = db.Column(db.Float, default=0.0)
# ============================================================
@classmethod
def new(cls, **kwargs):
obj = cls(**kwargs)
db.session.add(obj)
db.session.commit()
print(obj)
return obj
@property
def dict(self):
ret = {}
for col in self.__table__.columns:
ret[col.name] = getattr(self, col.name)
ret["systems"] = json.loads(ret["systems"])
return ret
@dict.setter
def set_dict(self, *args, **kwargs):
raise NotImplementedError
db.create_all()
db.session.commit()
@app.errorhandler(422)
@app.errorhandler(400)
def handle_error(err):
headers = err.data.get("headers", None)
messages = err.data.get("messages", ["Invalid request."])
if headers:
return jsonify({"errors": messages}), err.code, headers
else:
return jsonify({"errors": messages}), err.code
@app.route("/route", methods=["GET", "POST"])
@use_args(
{
"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(["all", "keep_first", "keep_last", "keep_both"]),
),
"primary": fields.Boolean(missing=False),
"factor": fields.Float(missing=0.5),
}
)
def route(args):
args["systems"] = json.dumps(args["systems"])
for k, v in args.items():
print(k, v)
return jsonify({"id": Job.new(**args).id})
@app.route("/status/<uuid:job_id>")
def status(job_id):
job = db.session.query(Job).get_or_404(str(job_id))
return jsonify(job.dict)
if __name__ == "__main__":
app.run(host="0.0.0.0", port=3777, debug=True)

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

@ -0,0 +1,665 @@
from flask import (
Flask,
jsonify,
session,
render_template,
redirect,
url_for,
send_from_directory,
request,
flash,
current_app
)
from flask.json.tag import JSONTag
import uuid
import pickle
import os
import time
import random
import base64
import gevent
from functools import wraps
from concurrent.futures.process import BrokenProcessPool
from datetime import datetime, timedelta
from decimal import Decimal
from multiprocessing import Queue
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 werkzeug.http import HTTP_STATUS_CODES
from sqlalchemy_utils import generic_repr, JSONType, PasswordType, UUIDType
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import scoped_session, sessionmaker, relationship, backref
from sqlalchemy.types import Float, String, 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")
executor = Executor(app)
db = SQLAlchemy(app)
bootstrap = Bootstrap(app)
csrf = CSRFProtect(app)
nav = Nav(app)
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
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 not role 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
return ret
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 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):
states=[
("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 = dict()
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 = dict()
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()
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/")
@login_required
def worker():
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(Job.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()
if __name__ == "__main__":
app.run(host="127.0.0.1", port=3777, debug=True)

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

@ -0,0 +1,18 @@
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>'

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

@ -0,0 +1,104 @@
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 %}

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

@ -0,0 +1,73 @@
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

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

@ -0,0 +1,6 @@
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("_")
}

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

@ -0,0 +1,68 @@
use serde::Deserialize;
use serde_json::Result;
use serde_json;
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)]
struct Coords {
x: f32,
y: f32,
z: f32,
}
#[derive(Debug, Deserialize)]
struct Body {
name: String,
#[serde(rename = "type")]
body_type: String,
subType: String,
#[serde(rename = "distanceToArrival")]
distance: f32,
}
#[derive(Debug, Deserialize)]
struct System {
coords: Coords,
name: String,
bodies: Vec<Body>,
}
fn main() -> std::io::Result<()> {
better_panic::install();
let mut buffer = String::new();
let mut bz2_reader = std::process::Command::new("bzip2").args(
&["-d","-c",r#"E:\EDSM\galaxy.json.bz2"#]
).stdout(std::process::Stdio::piped())
.spawn()
.expect("Failed to spawn 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 let Ok(sys) = serde_json::from_str::<System>(&buffer) {
for b in &sys.bodies {
if b.body_type == "Star" {
count += 1;
if (count % 100_000) == 0 {
println!("{}: {:?}", count, b);
}
}
}
}
buffer.clear();
}
println!("Total: {}", count);
Ok(())
}

24
tests/test_ed_lrr.py Normal file
View File

@ -0,0 +1,24 @@
import pytest
stars_csv = "D:\\devel\\rust\\ED_LRR\\stars.csv"
@pytest.mark.dependency()
def test_import():
import _ed_lrr
@pytest.mark.dependency(depends=["test_import"])
def test_search_works():
import _ed_lrr
system_names = ["Ix", "Sol", "Colonia", "Sagittarius A*"]
systems = _ed_lrr.find_sys(system_names, stars_csv)
print(systems)
@pytest.mark.dependency(depends=["test_import"])
def test_zero_range_fails():
import _ed_lrr
# _ed_lrr.route()

18
tests/test_gui.py Normal file
View File

@ -0,0 +1,18 @@
import pytest
@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