Add search_resource_async for visualisation

This commit is contained in:
Ben 2024-02-03 23:05:51 +01:00
parent 459bef9c45
commit 6c0f1dcbce
Signed by: ben
GPG key ID: 0F54A7ED232D3319
2 changed files with 109 additions and 50 deletions

View file

@ -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'
``` ```

View file

@ -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)