Add search_resource_async for visualisation
This commit is contained in:
parent
459bef9c45
commit
6c0f1dcbce
|
@ -5,7 +5,7 @@ Tool for helping with factory planning.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Tested are Python 3.11, PySide2 5.15.11 and Qt 5.15.11. The later two are necessary for the visualisation. Install all Python requirements by using
|
Tested are Python 3.11, Firefox 122, SQLite 3.45.1, PySide2 5.15.11 and Qt 5.15.11. The later two are necessary for the visualisation. Install all Python requirements by using
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
pipenv --site-packages sync
|
pipenv --site-packages sync
|
||||||
|
@ -39,6 +39,5 @@ Thanks to [NodeGraphQt](https://github.com/jchanvfx/NodeGraphQt) a graph base vi
|
||||||
The visualisation window can be opened with e.g.
|
The visualisation window can be opened with e.g.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
pipenv run fetch 'Plastic'
|
|
||||||
pipenv run vis 'Plastic'
|
pipenv run vis 'Plastic'
|
||||||
```
|
```
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
from concurrent.futures import Future
|
from concurrent.futures import Future
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
import click
|
import click
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
|
@ -18,7 +20,7 @@ from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from factorygame.data.common import chose_resource, resource_needs_update, chose_recipe
|
from factorygame.data.common import chose_resource, resource_needs_update, chose_recipe
|
||||||
from factorygame.data.sfp import SatisfactoryPlus, DEFAULT_IGNORE_FACTORIES
|
from factorygame.data.sfp import SatisfactoryPlus, DEFAULT_IGNORE_FACTORIES
|
||||||
from .models import Recipe, Resource, ResourceFlow
|
from .models import Recipe, Resource, ResourceFlow, Base
|
||||||
|
|
||||||
WORLD_INPUT_PORT_NAME = "World Input"
|
WORLD_INPUT_PORT_NAME = "World Input"
|
||||||
WORLD_OUTPUT_PORT_NAME = "World Output"
|
WORLD_OUTPUT_PORT_NAME = "World Output"
|
||||||
|
@ -105,7 +107,44 @@ class AsyncResourceFetcher(QThread):
|
||||||
resource = session.scalars(Resource.by_label(self.resource_label)).one()
|
resource = session.scalars(Resource.by_label(self.resource_label)).one()
|
||||||
resource = data_provider.update_resource_recipes(session=session, resource=resource)
|
resource = data_provider.update_resource_recipes(session=session, resource=resource)
|
||||||
if self.debug:
|
if self.debug:
|
||||||
print("Updated resource in separate thread", resource)
|
print("AsyncResourceFetcher updated resource in separate thread", resource)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncResourceFinder(QThread):
|
||||||
|
def __init__(self, parent, resource_label: str, engine: sqlalchemy.Engine, prompt: Callable, debug: bool):
|
||||||
|
super().__init__(parent=parent)
|
||||||
|
self.resource_label = resource_label
|
||||||
|
self.engine = engine
|
||||||
|
self.debug = debug
|
||||||
|
self.prompt = prompt
|
||||||
|
self.result = Future()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
try:
|
||||||
|
with (
|
||||||
|
Session(self.engine) as session,
|
||||||
|
SatisfactoryPlus(ignore_factories=DEFAULT_IGNORE_FACTORIES, debug=self.debug) as data_provider,
|
||||||
|
):
|
||||||
|
search_result_future = data_provider.search_for_resource(
|
||||||
|
session=session, search=self.resource_label, prompt=self.prompt
|
||||||
|
)
|
||||||
|
search_result = search_result_future.result(timeout=60)
|
||||||
|
if search_result is None:
|
||||||
|
if self.debug:
|
||||||
|
print("AsyncResourceFinder search completed, but no result")
|
||||||
|
self.result.set_result(None)
|
||||||
|
return
|
||||||
|
|
||||||
|
resource, exists_in_db = search_result
|
||||||
|
if not exists_in_db or resource_needs_update(resource):
|
||||||
|
resource = data_provider.update_resource_recipes(session=session, resource=resource)
|
||||||
|
if self.debug:
|
||||||
|
print("AsyncResourceFinder updated resource", resource)
|
||||||
|
self.result.set_result(resource)
|
||||||
|
session.commit()
|
||||||
|
except:
|
||||||
|
self.result.set_exception(sys.exception())
|
||||||
|
|
||||||
|
|
||||||
class NodeSlider(NodeBaseWidget):
|
class NodeSlider(NodeBaseWidget):
|
||||||
|
@ -410,6 +449,7 @@ class GraphController(QObject):
|
||||||
self.debug = debug
|
self.debug = debug
|
||||||
|
|
||||||
self.engine = create_engine("sqlite:///file.db", echo=debug)
|
self.engine = create_engine("sqlite:///file.db", echo=debug)
|
||||||
|
Base.metadata.create_all(bind=self.engine)
|
||||||
self.graph = NodeGraph(parent=self)
|
self.graph = NodeGraph(parent=self)
|
||||||
self.graph.widget.resize(1280, 720)
|
self.graph.widget.resize(1280, 720)
|
||||||
self.graph.register_node(Machine)
|
self.graph.register_node(Machine)
|
||||||
|
@ -421,26 +461,26 @@ class GraphController(QObject):
|
||||||
|
|
||||||
self.graph.port_connected.connect(self.on_port_connected)
|
self.graph.port_connected.connect(self.on_port_connected)
|
||||||
|
|
||||||
|
self.prompt_dialog = QInputDialog(parent=self.graph.widget)
|
||||||
|
self.prompt_dialog.setStyleSheet(generate_fgbg_stylesheet())
|
||||||
|
self.prompt_dialog.setModal(True)
|
||||||
|
self._dialog_future = Future()
|
||||||
|
self.prompt_dialog.rejected.connect(lambda: self._dialog_future.set_result(None))
|
||||||
|
self.prompt_dialog.accepted.connect(lambda: self._dialog_future.set_result(self.prompt_dialog.textValue()))
|
||||||
|
|
||||||
|
self.loading_dialog = QDialog(parent=self.graph.widget, f=(Qt.Dialog | Qt.ToolTip))
|
||||||
|
self.loading_dialog.setStyleSheet(generate_fgbg_stylesheet(0.5))
|
||||||
|
self.loading_dialog.setModal(True)
|
||||||
|
self.loading_dialog.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||||
|
label = QLabel("Loading…", self.loading_dialog)
|
||||||
|
label.setAlignment(Qt.AlignCenter)
|
||||||
|
|
||||||
def add_machine_from_search(self, search: str):
|
def add_machine_from_search(self, search: str):
|
||||||
recipe_selected_future = Future()
|
recipe_selected_future = Future()
|
||||||
|
|
||||||
def resource_selected_cb(resource_future: Future[Resource | None]):
|
def resource_found_db(resource: Resource | None):
|
||||||
resource = resource_future.result()
|
|
||||||
if resource is None:
|
if resource is None:
|
||||||
# FIXME: use concurrent resource searching/fetching
|
return
|
||||||
# ret = data_provider.search_for_resource(session=session, search=search)
|
|
||||||
ret = None
|
|
||||||
if ret is None:
|
|
||||||
print("Resource not found")
|
|
||||||
recipe_selected_future.set_result(None)
|
|
||||||
return
|
|
||||||
|
|
||||||
resource, exists_in_db = ret
|
|
||||||
if not exists_in_db:
|
|
||||||
print("Resource unknown")
|
|
||||||
recipe_selected_future.set_result(None)
|
|
||||||
return
|
|
||||||
assert resource is not None
|
|
||||||
|
|
||||||
def chose_recipe_async():
|
def chose_recipe_async():
|
||||||
with Session(self.engine) as session:
|
with Session(self.engine) as session:
|
||||||
|
@ -454,10 +494,13 @@ class GraphController(QObject):
|
||||||
else:
|
else:
|
||||||
chose_recipe_async()
|
chose_recipe_async()
|
||||||
|
|
||||||
def select_recipe_async():
|
def resource_selected_or_search_cb(resource_future: Future[Resource | None]):
|
||||||
with Session(self.engine) as session:
|
resource = resource_future.result()
|
||||||
resource_future = chose_resource(session=session, resource_label=search, prompt=self.dialog_prompt)
|
if resource is None:
|
||||||
resource_future.add_done_callback(resource_selected_cb)
|
search_future = self.search_resource_async(resource_label=search)
|
||||||
|
search_future.add_done_callback(lambda fut: resource_found_db(fut.result()))
|
||||||
|
else:
|
||||||
|
resource_found_db(resource)
|
||||||
|
|
||||||
def recipe_selected_cb(recipe_future: Future[Recipe | None]):
|
def recipe_selected_cb(recipe_future: Future[Recipe | None]):
|
||||||
recipe = recipe_future.result()
|
recipe = recipe_future.result()
|
||||||
|
@ -473,7 +516,9 @@ class GraphController(QObject):
|
||||||
self.graph.center_on([self.global_input, recipe_machine, self.global_output])
|
self.graph.center_on([self.global_input, recipe_machine, self.global_output])
|
||||||
|
|
||||||
recipe_selected_future.add_done_callback(recipe_selected_cb)
|
recipe_selected_future.add_done_callback(recipe_selected_cb)
|
||||||
select_recipe_async()
|
with Session(self.engine) as session:
|
||||||
|
resource_future = chose_resource(session=session, resource_label=search, prompt=self.dialog_prompt)
|
||||||
|
resource_future.add_done_callback(resource_selected_or_search_cb)
|
||||||
|
|
||||||
def show(self):
|
def show(self):
|
||||||
self.graph.widget.show()
|
self.graph.widget.show()
|
||||||
|
@ -597,44 +642,59 @@ class GraphController(QObject):
|
||||||
debug=self.debug,
|
debug=self.debug,
|
||||||
)
|
)
|
||||||
fetch_future = Future()
|
fetch_future = Future()
|
||||||
self.dialog_loading(fetch_future)
|
self.loading_dialog.show()
|
||||||
|
fetch_future.add_done_callback(lambda fut: self.loading_dialog.hide())
|
||||||
fetcher_thread.finished.connect(lambda: fetch_future.set_result(None))
|
fetcher_thread.finished.connect(lambda: fetch_future.set_result(None))
|
||||||
fetcher_thread.start()
|
fetcher_thread.start()
|
||||||
return fetch_future
|
return fetch_future
|
||||||
|
|
||||||
|
def search_resource_async(self, resource_label) -> Future[Resource]:
|
||||||
|
if self.debug:
|
||||||
|
print("Searching resource", resource_label)
|
||||||
|
|
||||||
|
loading_future = Future()
|
||||||
|
|
||||||
|
def show_prompt(*args, **kwargs):
|
||||||
|
loading_future.set_result(None)
|
||||||
|
return self.dialog_prompt(*args, **kwargs)
|
||||||
|
|
||||||
|
fetcher_thread = AsyncResourceFinder(
|
||||||
|
parent=self.graph,
|
||||||
|
resource_label=resource_label,
|
||||||
|
engine=self.engine,
|
||||||
|
prompt=show_prompt,
|
||||||
|
debug=self.debug,
|
||||||
|
)
|
||||||
|
self.loading_dialog.show()
|
||||||
|
loading_future.add_done_callback(lambda fut: self.loading_dialog.hide())
|
||||||
|
fetcher_thread.start()
|
||||||
|
return fetcher_thread.result
|
||||||
|
|
||||||
def dialog_prompt(self, options: dict[int, str], text: str, default: int) -> Future[int | None]:
|
def dialog_prompt(self, options: dict[int, str], text: str, default: int) -> Future[int | None]:
|
||||||
|
assert self.prompt_dialog.isHidden(), "Prompt dialog already visible"
|
||||||
if self.debug:
|
if self.debug:
|
||||||
print("Displaying QInputDialog with options:", ", ".join(options.values()))
|
print("Displaying QInputDialog with options:", ", ".join(options.values()))
|
||||||
|
|
||||||
dialog = QInputDialog(parent=self.graph.widget)
|
self.prompt_dialog.setLabelText(text)
|
||||||
dialog.setStyleSheet(generate_fgbg_stylesheet())
|
|
||||||
dialog.setModal(True)
|
|
||||||
dialog.setLabelText(text)
|
|
||||||
if default in options:
|
if default in options:
|
||||||
dialog.setTextValue(options[default])
|
self.prompt_dialog.setTextValue(options[default])
|
||||||
reversed_options: dict[str, int] = {value: key for key, value in options.items()}
|
reversed_options: dict[str, int] = {value: key for key, value in options.items()}
|
||||||
dialog.setComboBoxItems(reversed_options.keys())
|
self.prompt_dialog.setComboBoxItems(reversed_options.keys())
|
||||||
|
|
||||||
ret = Future()
|
ret = Future()
|
||||||
dialog.rejected.connect(lambda: ret.set_result(None))
|
self._dialog_future = Future()
|
||||||
dialog.accepted.connect(lambda: ret.set_result(reversed_options[dialog.textValue()]))
|
|
||||||
dialog.show()
|
def map_result(fut: Future):
|
||||||
|
result = fut.result()
|
||||||
|
if result is None:
|
||||||
|
ret.set_result(None)
|
||||||
|
else:
|
||||||
|
ret.set_result(reversed_options[result])
|
||||||
|
|
||||||
|
self._dialog_future.add_done_callback(map_result)
|
||||||
|
self.prompt_dialog.show()
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def dialog_loading(self, future: Future):
|
|
||||||
dialog = QDialog(parent=self.graph.widget, f=(Qt.Dialog | Qt.ToolTip))
|
|
||||||
dialog.setStyleSheet(generate_fgbg_stylesheet(0.5))
|
|
||||||
dialog.setModal(True)
|
|
||||||
dialog.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
|
||||||
label = QLabel("Loading…", dialog)
|
|
||||||
label.setAlignment(Qt.AlignCenter)
|
|
||||||
dialog.show()
|
|
||||||
|
|
||||||
def remove_dialog():
|
|
||||||
dialog.hide()
|
|
||||||
dialog.destroy()
|
|
||||||
|
|
||||||
future.add_done_callback(lambda fut: remove_dialog())
|
|
||||||
|
|
||||||
|
|
||||||
@click.command
|
@click.command
|
||||||
@click.option("--debug", is_flag=True)
|
@click.option("--debug", is_flag=True)
|
||||||
|
|
Loading…
Reference in a new issue