-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathmplace.py
More file actions
239 lines (195 loc) · 8.12 KB
/
mplace.py
File metadata and controls
239 lines (195 loc) · 8.12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# Copyright 2026 Ramiz Gindullin.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Description: MPLACE - MicroPlate Layout Arrangement with Constraint Engines
# Main application entry point with MVC architecture.
#
# Authors: Ramiz GINDULLIN (ramiz.gindullin@it.uu.se)
# Version: 1.3.5
# Last Revision: April 2026
import tkinter as tk
from tkinter import messagebox
import logging
import sys
from pathlib import Path
# Controllers
from controllers.main_controller import MainController
from controllers.dzn_controller import DznController
from controllers.csv_controller import CsvController
from controllers.viz_controller import VisualizationController
# Views
from views.main_view import MainView
from views.dzn_view import DznView
from views.viz_view import VizView
# Models
from models.application_state import ApplicationState
# Constants
from models.constants import PathsIni, Messages
logger = logging.getLogger(__name__)
def setup_logging() -> None:
"""Configure application-wide logging."""
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('mplace.log'),
logging.StreamHandler(sys.stdout)
]
)
logger.info("MPLACE application starting")
class MPlaceApplication:
"""
Main MPLACE application coordinator.
This class wires together the MVC components and manages the application lifecycle.
Attributes:
root: Tkinter root window
state: Application state
controllers: Dictionary of controller instances
views: Dictionary of view instances
"""
def __init__(self):
logger.info("Initializing MPlaceApplication")
self.root = tk.Tk()
self.state = ApplicationState()
logger.info("Application state initialized")
self.controllers = self._create_controllers()
self.views = self._create_views()
logger.info("MPlaceApplication initialized successfully")
def _create_controllers(self) -> dict:
"""
Create all controller instances.
Returns:
Dictionary of controller instances
"""
# MainController creates and manages MiniZincController internally
controllers = {
'main': MainController(self.state),
'dzn': DznController(),
'csv': CsvController(),
'viz': VisualizationController()
}
logger.debug(f"Created {len(controllers)} controllers")
return controllers
def _create_views(self) -> dict:
"""
Create all view instances and wire them to controllers.
Returns:
Dictionary of view instances
"""
# Create main view - access minizinc_controller through main controller
main_view = MainView(
root=self.root,
controller=self.controllers['main'],
minizinc_controller=self.controllers['main'].minizinc_controller, # ← Access through main
csv_controller=self.controllers['csv'],
on_generate_dzn_clicked=self._open_dzn_window,
on_visualize_clicked=self._open_viz_window
)
# Create DZN view with callback for completion
dzn_view = DznView(
parent=self.root,
controller=self.controllers['dzn'],
on_generation_complete=self._on_dzn_generated,
on_window_closed=lambda: main_view.unlock()
)
# Create visualization view
viz_view = VizView(
parent=self.root,
viz_controller=self.controllers['viz'],
csv_controller=self.controllers['csv'],
on_window_closed=lambda: self.views['main'].unlock()
)
views = {
'main': main_view,
'dzn': dzn_view,
'viz': viz_view
}
logger.debug(f"Created {len(views)} views")
return views
def _show_startup_warnings(self) -> None:
"""Display warnings for missing or misconfigured files."""
startup_warnings = []
config = self.controllers['main'].app_config
if config.minizinc_path == PathsIni.FILE_ERROR_PLACEHOLDER:
startup_warnings.append("MiniZinc is not found - can not use PLAID or COMPD.")
if config.plaid_path == PathsIni.FILE_ERROR_PLACEHOLDER:
startup_warnings.append("PLAID model file is not loaded: can not use PLAID model.")
if config.compd_path == PathsIni.FILE_ERROR_PLACEHOLDER:
startup_warnings.append("COMPD model file is not loaded: can not use COMPD model.")
if config.plaid_mpc_path == PathsIni.FILE_ERROR_PLACEHOLDER:
startup_warnings.append("Solver configuration for PLAID is not loaded. Default configuration (Gecode, 1 thread) will be used.")
if config.compd_mpc_path == PathsIni.FILE_ERROR_PLACEHOLDER:
startup_warnings.append("Solver configuration for COMPD is not loaded. Default configuration (Gecode, 1 thread) will be used.")
if len(startup_warnings) == 1:
messagebox.showwarning("Warning", startup_warnings[0])
elif len(startup_warnings) > 1:
messagebox.showwarning("Warnings", "\n\n".join(startup_warnings))
def _open_dzn_window(self) -> None:
"""Open the DZN generation window."""
logger.info("Opening DZN generation window")
self.views['main'].lock() # ← lock before opening
self.views['dzn'].show()
def _open_viz_window(self) -> None:
"""Open the visualization window."""
logger.info("Opening visualization window")
state = self.state
if not state.csv_file_path:
logger.warning("Attempted to open visualization without CSV loaded")
messagebox.showerror("Error", "Please load a CSV file first")
return
self.views['main'].lock()
csv_path = Path(state.csv_file_path)
model_name = Messages.MODEL_COMPD if state.use_compd else Messages.MODEL_PLAID
figure_name_template = str(csv_path.parent / csv_path.stem) + '_' + model_name + '_'
self.views['viz'].show(
file_path=state.csv_file_path,
figure_name_template=figure_name_template,
rows=state.num_rows,
cols=state.num_cols,
control_names=state.control_names
)
def _on_dzn_generated(self, file_path: str, rows: str, cols: str, controls: str) -> None:
"""
Handle DZN file generation completion.
Args:
file_path: Path to generated DZN file
rows: Number of rows
cols: Number of columns
controls: Control names string
"""
logger.info(f"DZN generated: {file_path}")
# Update main view
self.views['main'].update_after_dzn_generation(
file_path=file_path,
rows=rows,
cols=cols,
controls=controls
)
def run(self) -> None:
"""Start the application main loop."""
logger.info("Starting MPLACE application main loop")
# Display startup warnings for missing configurations
self.root.after(1, self._show_startup_warnings) # the window renders before the dialog
self.views['main'].show()
def main():
"""Application entry point."""
try:
setup_logging()
app = MPlaceApplication()
app.run()
except Exception as e:
logger.critical(f"Fatal error: {e}", exc_info=True)
sys.exit(1)
if __name__ == "__main__":
main()