1243 lines
48 KiB
Python
1243 lines
48 KiB
Python
#-------------------------------------------------------------------------------
|
|
# Name: opal.py
|
|
# Purpose:
|
|
#
|
|
# Author: Anton Vakhrushev
|
|
#
|
|
# Created: 14.03.2012
|
|
# Copyright: (c) Anton Vakhrushev 2012
|
|
# Licence: LGPL
|
|
#-------------------------------------------------------------------------------
|
|
#!/usr/bin/env python#!/usr/bin/env python
|
|
#! coding: utf-8
|
|
|
|
import server
|
|
import task
|
|
|
|
import wx
|
|
import forms
|
|
wxpg = forms.wxpg
|
|
wxplot = forms.wxplot
|
|
from wx.lib.embeddedimage import PyEmbeddedImage
|
|
|
|
import time
|
|
import threading
|
|
import re
|
|
import json
|
|
import zlib
|
|
from pprint import pprint
|
|
|
|
import gettext
|
|
_ = gettext.gettext
|
|
|
|
# состояния модели, унаследованные от состояния задачи
|
|
MODEL_READY = server.JOB_READY
|
|
MODEL_RUNNING = server.JOB_RUNNING
|
|
MODEL_STOPPED = server.JOB_STOPPED
|
|
MODEL_COMPLETED = server.JOB_COMPLETED
|
|
# собственные состояния модели
|
|
MODEL_NO_EXEC = 101
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Данные о пользовательской модели
|
|
# --------------------------------------------------------------------------
|
|
|
|
class ModelData:
|
|
def __init__(self, server, model, parent_data = None):
|
|
# если мы создаем новый набор данных из описания модели
|
|
if isinstance(model, task.DataDescription):
|
|
self.mdef = task.DataDefinition(model, parent_data)
|
|
self.jid = server.CreateJob() if model.IsExecutable() else None
|
|
# если мы создаем набор данных из другого набора данных
|
|
elif isinstance(model, ModelData):
|
|
self.mdef = model.mdef.Copy()
|
|
self.jid = server.CreateJob() if model.jid else None
|
|
else:
|
|
self.mdef = None
|
|
self.jid = None
|
|
|
|
assert self.mdef
|
|
|
|
self.res = None # результаты выполнения работы
|
|
if self.jid:
|
|
self.state = MODEL_READY # состояние модели
|
|
else:
|
|
self.state = MODEL_NO_EXEC # состояние модели
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Данные о линии в графике
|
|
# --------------------------------------------------------------------------
|
|
|
|
LINE_CURVE = 1
|
|
LINE_MARKER = 2
|
|
LINE_HISTOGRAM = 3
|
|
|
|
class LineData:
|
|
"""
|
|
Данные одной линии для графика
|
|
|
|
Предназначен для использования совместно с графическим компонентом,
|
|
поэтому не имеет собственного значения названия. Вместо этого
|
|
название берется из графического компонента.
|
|
"""
|
|
def __init__(self, ums_ptr, plots_ptr,
|
|
type, columns, colour = None, style = None):
|
|
|
|
self.ums_ptr = ums_ptr # (ptr, item) указатель на компонент с моделями
|
|
# и индекс в этом компоненте
|
|
self.plots_ptr = plots_ptr # (ptr, item) указатель на компонент с графиками
|
|
# и индекс в этом компоненте
|
|
|
|
self.type = type # тип графика
|
|
self.columns = columns # пара (x, y)
|
|
self.colour = colour # цвет: если не задан выбирается из списка
|
|
self.style = style # стиль: если не задан, еспользуется по умолчанию
|
|
|
|
def GetModelTitle(self):
|
|
container, item = self.ums_ptr
|
|
return container.GetItemText(item)
|
|
|
|
def GetTitle(self):
|
|
# если есть указатель на компонент с графиками,
|
|
# извлекаем оттуда название
|
|
if self.plots_ptr:
|
|
container, item = self.plots_ptr
|
|
return container.GetItemText(item)
|
|
# иначе формируем название на основе имени модели и
|
|
# имен выбранных столбцов
|
|
else:
|
|
container, item = self.ums_ptr
|
|
title = container.GetItemText(item)
|
|
data = container.GetPyData(item)
|
|
assert data.res # если результата нет, то а-та-та
|
|
colx, coly = self.columns
|
|
title_x = data.res.columns[colx].GetTitle()
|
|
title_y = data.res.columns[coly].GetTitle()
|
|
return "{}: {}({})".format(title, title_y, title_x)
|
|
|
|
def GetPoints(self):
|
|
container, item = self.ums_ptr
|
|
data = container.GetPyData(item)
|
|
assert data.res # если результата нет, то а-та-та
|
|
return data.res.Zip(*self.columns)
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Ошибки доступа к элементам в контейнерах
|
|
# --------------------------------------------------------------------------
|
|
|
|
class ItemError(Exception):
|
|
pass
|
|
|
|
#-----------------------------------------------------------------------------
|
|
# Главная форма
|
|
#-----------------------------------------------------------------------------
|
|
|
|
class MainFrame(forms.MainFrame):
|
|
def __init__(self):
|
|
forms.MainFrame.__init__(self, None)
|
|
|
|
global _
|
|
_ = self.lang.ugettext
|
|
|
|
self.model = None
|
|
self.name_id = 1
|
|
|
|
conf = self.settings['conf']
|
|
workers = int(self.settings['workers'])
|
|
s = server.LocalServer(conf=conf, workers=workers)
|
|
s.LoadModels()
|
|
self.models = s.GetModels()
|
|
s.Start()
|
|
self.server = s
|
|
|
|
# События компонентов
|
|
|
|
self.m_user_models.Bind(wx.EVT_TREE_SEL_CHANGED,
|
|
self.OnModelSelected)
|
|
self.m_user_models.Bind(wx.EVT_TREE_DELETE_ITEM,
|
|
self.OnDeleteModelsItem)
|
|
self.m_params.Bind(wxpg.EVT_PG_CHANGING,
|
|
self.OnParamChanging)
|
|
self.m_params.Bind(wxpg.EVT_PG_CHANGED,
|
|
self.OnParamChanged)
|
|
self.m_specs.Bind(wx.EVT_TREE_ITEM_ACTIVATED,
|
|
self.OnAddModelToSelected)
|
|
self.m_user_models.Bind(wx.EVT_TREE_ITEM_ACTIVATED,
|
|
self.OnModelProcess)
|
|
self.m_plots.Bind(wx.EVT_TREE_ITEM_ACTIVATED,
|
|
self.OnPlotProcess)
|
|
self.m_plots.Bind(wx.EVT_CHAR,
|
|
self.OnPlotsKeyPressed)
|
|
|
|
# События меню
|
|
|
|
self.Bind(wx.EVT_MENU, self.OnNewProject,
|
|
id = forms.ID_NEW)
|
|
self.Bind(wx.EVT_MENU, self.OnOpenProject,
|
|
id = forms.ID_OPEN)
|
|
self.Bind(wx.EVT_MENU, self.OnSaveProject,
|
|
id = forms.ID_SAVE)
|
|
|
|
self.Bind(wx.EVT_MENU, self.OnTest,
|
|
id = forms.ID_TEST)
|
|
self.Bind(wx.EVT_MENU, self.OnAddModelToRoot,
|
|
id = forms.ID_ADD_MODEL_ROOT)
|
|
self.Bind(wx.EVT_MENU, self.OnAddModelToSelected,
|
|
id = forms.ID_ADD_MODEL_SELECTED)
|
|
self.Bind(wx.EVT_MENU, self.OnDuplicate,
|
|
id = forms.ID_DUPLICATE_MODEL)
|
|
self.Bind(wx.EVT_MENU, self.OnDuplicateTree,
|
|
id = forms.ID_DUPLICATE_TREE)
|
|
self.Bind(wx.EVT_MENU, self.OnDeleteModel,
|
|
id = forms.ID_DELETE_MODEL)
|
|
|
|
self.Bind(wx.EVT_MENU, self.OnModelProcess,
|
|
id = forms.ID_PROCESS_MODEL)
|
|
self.Bind(wx.EVT_MENU, self.OnModelStop,
|
|
id = forms.ID_STOP_MODEL)
|
|
|
|
self.Bind(wx.EVT_MENU, self.OnShowResult,
|
|
id = forms.ID_SHOW_RESULT)
|
|
|
|
self.Bind(wx.EVT_MENU, self.OnQuickShowPlot,
|
|
id = forms.ID_SHOW_PLOT)
|
|
self.Bind(wx.EVT_MENU, self.OnAddPlot,
|
|
id = forms.ID_ADD_PLOT)
|
|
self.Bind(wx.EVT_MENU, self.OnAddCurves,
|
|
id = forms.ID_ADD_CURVES)
|
|
self.Bind(wx.EVT_MENU, self.OnAddMarkers,
|
|
id = forms.ID_ADD_MARKERS)
|
|
|
|
self.Bind(wx.EVT_MENU, lambda x: self.ChangeLocale('en_EN'),
|
|
id = forms.ID_ENGLISH_LANG)
|
|
self.Bind(wx.EVT_MENU, lambda x: self.ChangeLocale('ru_RU'),
|
|
id = forms.ID_RUSSIAN_LANG)
|
|
|
|
self.Bind(wx.EVT_MENU, self.OnAbout,
|
|
id = forms.ID_ABOUT)
|
|
|
|
# События приложения
|
|
|
|
self.Bind(wx.EVT_CLOSE, self.OnClose)
|
|
self.Bind(wx.EVT_IDLE, self.OnIdle)
|
|
|
|
# если установлен в True, то обработчик состояний работ
|
|
# будет работать вхолостую, чтобы не создать deadlock
|
|
# проблема возникает в том, что при одновременной блокировке
|
|
# GUI и вызова модального диалога, последний весит все приложение напрочь
|
|
# в момент своего закрытия. а так как диалог все равно модальный,
|
|
# форме не обязательно обновляться в тот момент, когда он открыт
|
|
self.do_nothing = False
|
|
|
|
ov = threading.Thread(target = self.Overseer)
|
|
ov.daemon = True
|
|
ov.start()
|
|
|
|
# self.NewProject(self.models[0])
|
|
|
|
# Функции приложения и обработки сервера
|
|
|
|
def ChangeLocale(self, locale):
|
|
self.settings['locale'] = locale
|
|
wx.MessageBox(_('Locale changed. Restart application to apply settings'), _('Information'))
|
|
self.lang = gettext.translation('opal', './locale', languages=[locale], fallback=True)
|
|
self.lang.install(unicode=True)
|
|
|
|
|
|
def OnClose(self, event):
|
|
self.server.Stop()
|
|
self.SaveSettings()
|
|
self.Destroy()
|
|
|
|
def OnAbout(self, event):
|
|
self.do_nothing = True
|
|
forms.AboutDialog(self).ShowModal()
|
|
self.do_nothing = False
|
|
|
|
def OnIdle(self, event):
|
|
pass
|
|
|
|
def Overseer(self):
|
|
"""
|
|
Функция-надсмотрщик, которая периодически проверяет состояние
|
|
всех пользовательских моделей, в зависимости от этого изменяет
|
|
состояние окружения, выводит информацию, подгружает результаты
|
|
выполнения работ и др.
|
|
"""
|
|
try:
|
|
um = self.m_user_models
|
|
cycle_count = 0
|
|
while True:
|
|
time.sleep(0.1)
|
|
|
|
# если нужно подождать, то мы подождем
|
|
if self.do_nothing:
|
|
continue
|
|
|
|
wx.MutexGuiEnter()
|
|
try:
|
|
# print 'cycle{:-8}'.format(cycle_count)
|
|
cycle_count += 1
|
|
# просматриваем всю иерархию моделей
|
|
for item in um:
|
|
data = um.GetPyData(item)
|
|
if not data:
|
|
continue
|
|
jid = data.jid
|
|
if jid and self.server.IsJobChanged(jid):
|
|
# таким образом, тут мы обрабатываем новое состояние
|
|
# работы (модели)
|
|
|
|
state, percent, comment = self.server.GetJobState(jid)
|
|
|
|
data.state = state
|
|
self.SetModelState(item, data.state)
|
|
|
|
p = _('Unknown') if percent < 0 else '{:%}'.format(percent)
|
|
um.SetItemText(item, p, 2)
|
|
um.SetItemText(item, comment, 3)
|
|
|
|
print 'JID', jid, (state, percent, comment)
|
|
|
|
# завершающие действия по окончанию выполнения работы
|
|
if state == server.JOB_COMPLETED:
|
|
# получаем результаты выполнения
|
|
data.res = self.server.GetJobResult(jid)
|
|
# если завершившаяся задача в данный момент выделена
|
|
# то сразу же показываем этот результат
|
|
if um.IsSelected(item):
|
|
self.ShowQuickResult(data.res)
|
|
finally:
|
|
wx.MutexGuiLeave()
|
|
pass
|
|
except Exception, e:
|
|
print 'Error in overseer: ', e
|
|
|
|
def item_protector(func):
|
|
"""
|
|
Защитный механизм, который ловит исключения при неправильном
|
|
обращении к элементам деревьев (компоненты TreeCtrl, TreeListCtrl)
|
|
|
|
Возвращает None, если было поймано исключение.
|
|
Использование с функциями, которые не являются обработчиками событий
|
|
не желательно
|
|
"""
|
|
def Checker(*args, **kwargs):
|
|
try:
|
|
return func(*args, **kwargs)
|
|
except ItemError:
|
|
print 'Oops'
|
|
|
|
return Checker
|
|
|
|
# Функции создания модели, сохранения и загрузки
|
|
|
|
def BuildSpecs(self, model):
|
|
"""
|
|
Выстраивает иерархию спецификаций для выбранной модели
|
|
"""
|
|
def DoItem(item, model):
|
|
sp.SetPyData(item, model)
|
|
for label, spec in model.GetSpecs().iteritems():
|
|
child = sp.AppendItem(item, spec.GetTitle())
|
|
DoItem(child, spec)
|
|
|
|
sp = self.m_specs
|
|
sp.DeleteAllItems()
|
|
root = sp.AddRoot(model.GetTitle())
|
|
DoItem(root, model)
|
|
sp.ExpandAll()
|
|
sp.SortChildren(root)
|
|
|
|
def NewProject(self, model, create_default = True):
|
|
"""
|
|
Начать новый проект:
|
|
0. Очичтить все компоненты
|
|
1. Построить дерево спецификаций
|
|
2. Создать одну пользовательскую модель (по умолчанию)
|
|
3. Сделать заготовки для графиков/отчетов/прочего
|
|
"""
|
|
self.m_specs.DeleteAllItems()
|
|
self.m_user_models.DeleteAllItems()
|
|
self.m_params.Clear()
|
|
self.m_quick_result.Clear()
|
|
self.m_plots.DeleteAllItems()
|
|
# фиксируем выбранную модель
|
|
self.model = model
|
|
# Строим спецификации
|
|
self.BuildSpecs(model)
|
|
# Очищаем окно пользовательских моделей
|
|
# и создаем там одну
|
|
um = self.m_user_models
|
|
um.DeleteAllItems()
|
|
um.AddRoot('root')
|
|
if create_default:
|
|
self.AddModelToRoot(model)
|
|
# Создаем корневой элемент для окна с графиками
|
|
self.m_plots.AddRoot('root')
|
|
|
|
self.SetStatusText(_('Model "{}" selected').format(model.GetTitle()), 0)
|
|
|
|
return True # Project(model)
|
|
|
|
def OnNewProject(self, event):
|
|
self.do_nothing = True
|
|
f = SelectModelDialog(self, self.models)
|
|
if f.ShowModal() == wx.ID_OK:
|
|
model = f.GetSelectedModel()
|
|
if model:
|
|
self.NewProject(model)
|
|
else:
|
|
print 'Empty model'
|
|
|
|
self.do_nothing = False
|
|
|
|
def OnOpenProject(self, event):
|
|
|
|
def WalkModels(source, root, models, model_def = None):
|
|
# for имя-модели, параметры-модели
|
|
for mname, value in source.iteritems():
|
|
label = value['model']
|
|
|
|
if label not in models:
|
|
raise KeyError, 'no "{}"'.format(label)
|
|
|
|
data = ModelData(self.server, models[label], model_def)
|
|
data.mdef.params = value['data']
|
|
data.state = value['state']
|
|
if 'result' in value:
|
|
data.res = task.ResultData(value['result'])
|
|
# тут надо проверить все добавленные параметры
|
|
|
|
item = self.AddModel(root, mname, data)
|
|
model_items[mname] = item
|
|
|
|
WalkModels(value['um'], item, models[label].GetSpecs(), data)
|
|
|
|
def WalkPlots(source, root):
|
|
for plot in source:
|
|
item = self.m_plots.AppendItem(root, plot[0])
|
|
self.m_plots.SetPyData(item, 'plot')
|
|
self.m_plots.SetItemImage(item, self.icons.porg)
|
|
for line in plot[1]:
|
|
model_label = line['model']
|
|
data = LineData(
|
|
ums_ptr = (um, model_items[model_label]),
|
|
plots_ptr = None,
|
|
type = line['type'],
|
|
columns = (line['colx'], line['coly'])
|
|
)
|
|
self.AddLine(item, line['title'], data)
|
|
|
|
try:
|
|
wx.BeginBusyCursor()
|
|
self.do_nothing = True
|
|
|
|
selector = wx.FileDialog(
|
|
self,
|
|
_('Select file to load project'),
|
|
'',
|
|
'',
|
|
'Opal files (*.opl)|*.opl|Text files (*.txt)|*.txt',
|
|
wx.FD_OPEN | wx.FD_FILE_MUST_EXIST)
|
|
|
|
if selector.ShowModal() == wx.ID_OK:
|
|
|
|
filename = selector.GetPath()
|
|
data = {}
|
|
|
|
if selector.GetFilterIndex() == 0:
|
|
with open(filename, 'rb') as f:
|
|
data = json.loads(zlib.decompress(f.read()))
|
|
else:
|
|
with open(filename, 'r') as f:
|
|
data = json.loads(f.read())
|
|
|
|
|
|
tid = data['tid']
|
|
model_label = data['model']
|
|
model = self.server.CheckModel(tid, model_label)
|
|
if not model:
|
|
raise ValueError
|
|
|
|
self.NewProject(model, False)
|
|
|
|
um = self.m_user_models
|
|
model_items = {}
|
|
|
|
root = um.GetRootItem()
|
|
WalkModels(data['um'], root, {model.GetLabel(): model})
|
|
um.ExpandAll(root)
|
|
|
|
root = self.m_plots.GetRootItem()
|
|
WalkPlots(data['plots'], root)
|
|
self.m_plots.ExpandAll()
|
|
|
|
except Exception, e:
|
|
wx.MessageBox(_("Can't load saved file"), _('Error'), wx.ICON_ERROR | wx.OK)
|
|
print 'Oops', type(e), e
|
|
finally:
|
|
wx.EndBusyCursor()
|
|
self.do_nothing = False
|
|
|
|
def OnSaveProject(self, event):
|
|
|
|
def WalkModels(item, dest):
|
|
"""
|
|
Сохраняем информацию о каждой модели
|
|
"""
|
|
if item != um.GetRootItem():
|
|
data = um.GetPyData(item)
|
|
title = um.GetItemText(item)
|
|
mdef = data.mdef
|
|
dest[title] = {
|
|
'model': mdef.DD.GetLabel(),
|
|
'data': mdef.params,
|
|
'um': {},
|
|
'state': data.state,
|
|
}
|
|
if data.res:
|
|
dest[title]['result'] = data.res.DumpData()
|
|
dest = dest[title]['um']
|
|
|
|
child, _ = um.GetFirstChild(item)
|
|
while child.IsOk():
|
|
WalkModels(child, dest)
|
|
child = um.GetNextSibling(child)
|
|
|
|
def WalkPlots(root, dest):
|
|
"""
|
|
Сохраняем информацию о каждом графике
|
|
"""
|
|
# по всеи элементам первого уровня
|
|
item1, _ = self.m_plots.GetFirstChild(root)
|
|
while item1.IsOk():
|
|
# по всем элементам второго уровня
|
|
item2, _ = self.m_plots.GetFirstChild(item1)
|
|
lines = []
|
|
while item2.IsOk():
|
|
line = self.m_plots.GetPyData(item2)
|
|
data = {
|
|
'title': self.m_plots.GetItemText(item2),
|
|
'colx': line.columns[0],
|
|
'coly': line.columns[1],
|
|
'model': line.GetModelTitle(),
|
|
'type': line.type,
|
|
}
|
|
lines.append(data)
|
|
item2 = self.m_plots.GetNextSibling(item2)
|
|
title = self.m_plots.GetItemText(item1)
|
|
dest.append([title, lines])
|
|
item1 = self.m_plots.GetNextSibling(item1)
|
|
|
|
try:
|
|
wx.BeginBusyCursor()
|
|
self.do_nothing = True
|
|
|
|
selector = wx.FileDialog(
|
|
self,
|
|
_('Select file to save project'),
|
|
'',
|
|
self.model.GetTitle() + ' project',
|
|
'Opal files (*.opl)|*.opl|Text files (*.txt)|*.txt',
|
|
wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT)
|
|
|
|
if selector.ShowModal() == wx.ID_OK:
|
|
|
|
data = {}
|
|
|
|
data['tid'] = self.model.GetTaskId()
|
|
data['model'] = self.model.GetLabel()
|
|
|
|
um = self.m_user_models
|
|
data['um'] = {}
|
|
WalkModels(um.GetRootItem(), data['um'])
|
|
|
|
data['plots'] = []
|
|
WalkPlots(self.m_plots.GetRootItem(), data['plots'])
|
|
|
|
# pprint(data)
|
|
dump = json.dumps(data, indent = 2)
|
|
|
|
filename = selector.GetPath()
|
|
# сохраняем в упакованный бинарный формат
|
|
if selector.GetFilterIndex() == 0:
|
|
with open(filename, 'wb') as f:
|
|
f.write(zlib.compress(dump, 9))
|
|
|
|
# сохраняем в простой текстовый формат
|
|
else:
|
|
with open(filename, 'w') as f:
|
|
f.write(dump)
|
|
|
|
except Exception as e:
|
|
wx.MessageBox(_("Can't save the project"), _('Error'), wx.ICON_ERROR | wx.OK)
|
|
print e
|
|
finally:
|
|
wx.EndBusyCursor()
|
|
self.do_nothing = False
|
|
|
|
# Функции непосредственной работы с моделями:
|
|
# создание, изменение, дублирование и прочее
|
|
|
|
# Работа с именами моделей
|
|
|
|
def CheckName(self, name):
|
|
"""
|
|
Проверяет имя на уникальность в иерархии пользовательских моделей.
|
|
Возвращает True, если имя уникально, иначе False.
|
|
"""
|
|
um = self.m_user_models
|
|
for item in um:
|
|
item_name = um.GetItemText(item)
|
|
if item_name == name:
|
|
return False
|
|
return True
|
|
|
|
def GenerateName(self, name):
|
|
"""
|
|
На основе переданного имени генерирует новое имя модели таким образом,
|
|
чтобы оно осталось уникальным в рамках существующей иерархии моделей.
|
|
"""
|
|
m = re.match(r'^(.+)\s+\d*$', name, re.UNICODE)
|
|
basename = m.group(1) if m else name
|
|
while True:
|
|
name = basename + ' ' + str(self.name_id)
|
|
if self.CheckName(name):
|
|
return name
|
|
self.name_id += 1
|
|
|
|
# Добавление новых моделей
|
|
|
|
def SetModelState(self, item, state):
|
|
if state == MODEL_READY:
|
|
icon = self.icons.mready
|
|
text = _('Ready')
|
|
|
|
elif state == MODEL_RUNNING:
|
|
icon = self.icons.mrun
|
|
text = _('Running')
|
|
|
|
elif state == MODEL_COMPLETED:
|
|
icon = self.icons.mcomplete
|
|
text = _('Completed')
|
|
|
|
elif state == MODEL_STOPPED:
|
|
icon = self.icons.mstopped
|
|
text = _('Stopped')
|
|
|
|
else:
|
|
icon = self.icons.mnoexec
|
|
text = _('No executable')
|
|
|
|
self.m_user_models.SetItemImage(item, icon)
|
|
self.m_user_models.SetItemText(item, text, 1)
|
|
|
|
def AddModel(self, item, title, model_data):
|
|
"""
|
|
Добавляет модель к указанной,
|
|
устанавливает имя, данные, состояние, иконку
|
|
"""
|
|
um = self.m_user_models
|
|
item = um.AppendItem(item, title)
|
|
um.SetPyData(item, model_data)
|
|
self.SetModelState(item, model_data.state)
|
|
return item
|
|
|
|
def AddModelToRoot(self, model):
|
|
"""
|
|
Добавляет пользовательскую модель или спецификацию
|
|
в корень дерева моделей.
|
|
"""
|
|
# строим список моделей, которые будут добавлены
|
|
ms = []
|
|
while model:
|
|
ms.append(model)
|
|
model = model.GetParent()
|
|
ms.reverse()
|
|
# ms: [root-model, child, child-of-child1, ..., model]
|
|
|
|
um = self.m_user_models
|
|
item = um.GetRootItem()
|
|
defparent = None
|
|
root = None
|
|
for i, m in enumerate(ms):
|
|
name = self.GenerateName(m.GetTitle())
|
|
if not i:
|
|
root = item
|
|
data = ModelData(self.server, m, defparent)
|
|
item = self.AddModel(item, name, data)
|
|
defparent = data.mdef
|
|
if root:
|
|
um.Expand(root)
|
|
um.SelectItem(item)
|
|
um.SetFocus()
|
|
|
|
def OnAddModelToRoot(self, event):
|
|
model = self.GetSelectedData(self.m_specs)
|
|
self.AddModelToRoot(model)
|
|
|
|
def OnAddModelToSelected(self, event):
|
|
"""
|
|
Добавляет пользовательскую спецификацию к указанной модели
|
|
"""
|
|
# получаем модель, которая будет добавлена к пользовательским
|
|
model = self.GetSelectedData(self.m_specs)
|
|
# получаем пользовательскую модель, к которой хотим присоединить новую
|
|
item, data = self.GetSelectedItemData(self.m_user_models)
|
|
pmdef = data.mdef
|
|
um = self.m_user_models
|
|
# если новая модель может быть присоединена...
|
|
if pmdef.DD == model.parent:
|
|
name = self.GenerateName(model.GetTitle())
|
|
new_data = ModelData(self.server, model, pmdef)
|
|
child = self.AddModel(item, name, new_data)
|
|
um.SetFocus()
|
|
um.Expand(item)
|
|
um.SelectItem(child)
|
|
else:
|
|
wx.MessageBox(_("It's impossible to append model"), _('Error'))
|
|
|
|
# Реакция на выбор модели
|
|
|
|
def SelectUserModel(self, model_def):
|
|
|
|
def SelectProperty(param_type):
|
|
"""
|
|
По указанному имени типа возвращает "свойство" для списка "свойств"
|
|
|
|
Смотри руководство пользователя для того, чтобы получить полную
|
|
информацию о всех типах данных, используемых в Opal.
|
|
"""
|
|
if param_type == 'bool' or param_type == 'boolean':
|
|
return wxpg.BoolProperty
|
|
elif param_type == 'int':
|
|
return wxpg.IntProperty
|
|
elif param_type == 'float' or param_type == 'double':
|
|
return wxpg.FloatProperty
|
|
elif param_type == 'str' or param_type == 'string':
|
|
return wxpg.StringProperty
|
|
elif param_type == 'list':
|
|
return wxpg.ArrayStringProperty
|
|
else:
|
|
# очень плохо, если это произошло
|
|
raise KeyError()
|
|
|
|
pg = self.m_params
|
|
pg.Clear()
|
|
for label, value in model_def.params.iteritems():
|
|
param = model_def.DD[label]
|
|
title = param.GetTitle()
|
|
prop = SelectProperty(param.GetType())
|
|
pid = pg.Append(prop(title, value = value))
|
|
pg.SetPropertyClientData(pid, label)
|
|
pg.SetPropertyHelpString(pid, param.GetComment())
|
|
pg.Sort()
|
|
|
|
def ShowQuickResult(self, result):
|
|
if not result:
|
|
return
|
|
pg = self.m_quick_result
|
|
pg.Clear()
|
|
for label, param in result.data.iteritems():
|
|
pg.Append(wxpg.StringProperty(label, value = str(param.GetValue())))
|
|
pg.SetSplitterLeft()
|
|
|
|
def OnModelSelected(self, event):
|
|
item = event.GetItem()
|
|
data = self.m_user_models.GetPyData(item)
|
|
if data:
|
|
self.SelectUserModel(data.mdef)
|
|
self.ShowQuickResult(data.res)
|
|
|
|
# Изменение параметров модели
|
|
|
|
def OnParamChanging(self, event):
|
|
#value = event.GetValue()
|
|
#print repr(value)
|
|
#wx.MessageBox(value, 'changing')
|
|
#event.Veto()
|
|
pass
|
|
|
|
def OnParamChanged(self, event):
|
|
|
|
def Walk(item):
|
|
data = um.GetPyData(item)
|
|
if data.state != MODEL_NO_EXEC:
|
|
data.state = MODEL_READY
|
|
self.SetModelState(item, data.state)
|
|
|
|
child, _null = um.GetFirstChild(item)
|
|
while child.IsOk():
|
|
Walk(child)
|
|
child = um.GetNextSibling(child)
|
|
|
|
um = self.m_user_models
|
|
prop = event.GetProperty()
|
|
if not prop:
|
|
return
|
|
value = prop.GetValue()
|
|
param = prop.GetClientData()
|
|
item, data = self.GetSelectedItemData(um)
|
|
data.mdef[param] = value
|
|
# так как значение параметра изменилось,
|
|
# то все субмодели должны быть пересчитаны
|
|
Walk(item)
|
|
|
|
def OnTest(self, event):
|
|
pass
|
|
|
|
# Получение данных выбранной модели
|
|
|
|
def GetSelectedItem(self, source):
|
|
item = source.GetSelection()
|
|
if not item.IsOk():
|
|
raise ItemError(_('Invalid item'))
|
|
return item
|
|
|
|
def GetSelectedData(self, source):
|
|
item = self.GetSelectedItem(source)
|
|
data = source.GetPyData(item)
|
|
if not data:
|
|
raise ItemError(_('Empty data'))
|
|
return data
|
|
|
|
def GetSelectedItemData(self, source):
|
|
item = self.GetSelectedItem(source)
|
|
data = source.GetPyData(item)
|
|
if not data:
|
|
raise ItemError(_('Empty data'))
|
|
return (item, data)
|
|
|
|
# Дублирование модели
|
|
|
|
def Duplicate(self, item_src, item_dst):
|
|
um = self.m_user_models
|
|
data = um.GetPyData(item_src)
|
|
title = um.GetItemText(item_src)
|
|
new_data = ModelData(self.server, data)
|
|
um.SetItemText(item_dst, self.GenerateName(title))
|
|
um.SetPyData(item_dst, new_data)
|
|
self.SetModelState(item_dst, new_data.state)
|
|
|
|
def OnDuplicate(self, event):
|
|
"""
|
|
Обработчик события "дублирование модели"
|
|
|
|
Когда модель дублируется, ее параметры копируются в новую модель,
|
|
при неоходимости выделяется слот для работ на сервере.
|
|
Результаты модели-оригинала не копируются.
|
|
"""
|
|
um = self.m_user_models
|
|
item_src = self.GetSelectedItem(um)
|
|
parent = um.GetItemParent(item_src)
|
|
item_dst = um.AppendItem(parent, 'new-item')
|
|
self.Duplicate(item_src, item_dst)
|
|
# self.SetStatusText('Copy for "{}" created'.format(title), 0)
|
|
|
|
def OnDuplicateTree(self, event):
|
|
|
|
def Walk(item_src, item_dst):
|
|
self.Duplicate(item_src, item_dst)
|
|
|
|
child_src, _ = um.GetFirstChild(item_src)
|
|
while child_src.IsOk():
|
|
child_dst = um.AppendItem(item_dst, 'new-item')
|
|
Walk(child_src, child_dst)
|
|
child_src = um.GetNextSibling(child_src)
|
|
|
|
um = self.m_user_models
|
|
item_src = self.GetSelectedItem(um)
|
|
parent = um.GetItemParent(item_src)
|
|
item_dst = um.AppendItem(parent, 'new-item')
|
|
Walk(item_src, item_dst)
|
|
um.Expand(item_dst)
|
|
|
|
# Удаление модели
|
|
|
|
def OnDeleteModelsItem(self, event):
|
|
item = event.GetItem()
|
|
data = self.m_user_models.GetPyData(item)
|
|
if data:
|
|
self.server.DeleteJob(data.jid)
|
|
|
|
def OnDeleteModel(self, event):
|
|
item = self.GetSelectedItem(self.m_user_models)
|
|
self.m_user_models.Delete(item)
|
|
|
|
# Функции запуска модели на выполнение и управления очередью
|
|
|
|
def OnModelProcess(self, event):
|
|
um = self.m_user_models
|
|
for i in um.GetSelections():
|
|
data = um.GetItemPyData(i)
|
|
if data.jid:
|
|
self.server.LaunchJob(data.jid, data.mdef)
|
|
|
|
def OnModelStop(self, event):
|
|
um = self.m_user_models
|
|
for i in um.GetSelections():
|
|
data = um.GetItemPyData(i)
|
|
if data.jid:
|
|
self.server.StopJob(data.jid)
|
|
|
|
# Функции управления таблицами и отчетами
|
|
|
|
def OnShowResult(self, event):
|
|
item, data = self.GetSelectedItemData(self.m_user_models)
|
|
title = self.m_user_models.GetItemText(item)
|
|
title = _('Result for model "{}"').format(title)
|
|
rframe = ResultFrame(self, title, data.res)
|
|
rframe.Show()
|
|
|
|
# Функции управления графиками
|
|
|
|
def OnAddPlot(self, event):
|
|
root = self.m_plots.GetRootItem()
|
|
child = self.m_plots.AppendItem(root, _('New plot'))
|
|
self.m_plots.SetPyData(child, 'plot')
|
|
self.m_plots.SetItemImage(child, self.icons.porg)
|
|
self.m_plots.SelectItem(child)
|
|
|
|
def GetLines(self, line_type):
|
|
"""
|
|
Возвращает набор линий, которые пользователь указал для
|
|
построения графика к выбранной модели.
|
|
|
|
Возвращает список экземпляров LineData
|
|
"""
|
|
|
|
def CreateLineSelectDialog(parent, title, model_data):
|
|
f = LineSelectDialog(parent, title)
|
|
for index, col in enumerate(model_data.res.columns):
|
|
row_title = col.GetTitle()
|
|
row_data = index
|
|
f.Add(row_title, row_data)
|
|
f.SetSelections()
|
|
return f
|
|
|
|
def GetLinesFromUser(select_dialog):
|
|
lines = []
|
|
self.do_nothing = True
|
|
try:
|
|
if select_dialog.ShowModal() == wx.ID_OK:
|
|
for xy in select_dialog.GetLineColumns():
|
|
line_data = LineData(
|
|
(um, item), # указатель на модель
|
|
None, # указатель на график
|
|
line_type, xy) # тип линии, колонки и прочее
|
|
lines.append(line_data)
|
|
f.Destroy()
|
|
finally:
|
|
self.do_nothing = False
|
|
return lines
|
|
|
|
um = self.m_user_models
|
|
lines = []
|
|
items = um.GetSelections()
|
|
count = len(items)
|
|
for index, item in enumerate(items, 1):
|
|
data = um.GetPyData(item)
|
|
title = um.GetItemText(item)
|
|
|
|
msg = _('Line(s) for "{}" ({}/{})').format(title, index, count)
|
|
|
|
if not data.res:
|
|
wx.MessageBox(
|
|
_('There is no any result data for model!'),
|
|
msg, wx.OK | wx.ICON_EXCLAMATION)
|
|
else:
|
|
f = CreateLineSelectDialog(self, msg, data)
|
|
lines += GetLinesFromUser(f)
|
|
|
|
return lines
|
|
|
|
@item_protector
|
|
def AddLines(self, line_type):
|
|
"""
|
|
Добавляет линии в выделенный график
|
|
(компонента с графиками m_plots)
|
|
"""
|
|
# получаем указатель на индекс и данные элемента, который выделен
|
|
item, data = self.GetSelectedItemData(self.m_plots)
|
|
# если это на контейнер с графиками, то выходим
|
|
if data != 'plot':
|
|
return
|
|
# получаем указанные пользователем линии
|
|
lines = self.GetLines(line_type)
|
|
for line in lines:
|
|
self.AddLine(item, line.GetTitle(), line)
|
|
|
|
def AddLine(self, root, title, line_data):
|
|
item = self.m_plots.AppendItem(root, title)
|
|
self.m_plots.SetPyData(item, line_data)
|
|
line_data.plots_ptr = (self.m_plots, item)
|
|
if line_data.type == LINE_MARKER:
|
|
self.m_plots.SetItemImage(item, self.icons.pmarker)
|
|
else:
|
|
self.m_plots.SetItemImage(item, self.icons.pline)
|
|
self.m_plots.Expand(root)
|
|
return item
|
|
|
|
def OnAddCurves(self, event):
|
|
self.AddLines(LINE_CURVE)
|
|
|
|
def OnAddMarkers(self, event):
|
|
self.AddLines(LINE_MARKER)
|
|
|
|
def ShowPlot(self, lines, plot_title = ''):
|
|
if lines:
|
|
p = PlotFrame(self, _('Plot'), lines)
|
|
wx.FutureCall(20, p.Show)
|
|
# p.Show()
|
|
|
|
def OnQuickShowPlot(self, event):
|
|
lines = self.GetLines(LINE_CURVE)
|
|
um = self.m_user_models
|
|
item = self.GetSelectedItem(um)
|
|
title = um.GetItemText(item)
|
|
self.ShowPlot(lines, title)
|
|
|
|
def OnPlotProcess(self, event):
|
|
item = self.m_plots.GetSelection()
|
|
data = self.m_plots.GetItemPyData(item)
|
|
lines = []
|
|
if data == 'plot':
|
|
child, cookie = self.m_plots.GetFirstChild(item)
|
|
while child.IsOk():
|
|
title = self.m_plots.GetItemText(child)
|
|
line_data = self.m_plots.GetItemPyData(child)
|
|
line_data.title = title
|
|
lines.append(line_data)
|
|
child, cookie = self.m_plots.GetNextChild(item, cookie)
|
|
else:
|
|
title = self.m_plots.GetItemText(item)
|
|
data.title = title
|
|
lines = [ data ]
|
|
|
|
self.ShowPlot(lines)
|
|
|
|
def OnPlotsKeyPressed(self, event):
|
|
keycode = event.GetKeyCode()
|
|
item = self.GetSelectedItem(self.m_plots)
|
|
if keycode == wx.WXK_DELETE:
|
|
self.m_plots.Delete(item)
|
|
event.Skip()
|
|
|
|
#-----------------------------------------------------------------------------
|
|
# Форма с выбором модели из представленного списка
|
|
#-----------------------------------------------------------------------------
|
|
|
|
class SelectModelDialog(forms.SelectModelDialog):
|
|
def __init__(self, parent, models):
|
|
forms.SelectModelDialog.__init__(self, parent)
|
|
|
|
self.ilist = wx.ImageList(32, 32)
|
|
self.mlist.SetImageList(self.ilist, wx.IMAGE_LIST_NORMAL)
|
|
self.data_list = {}
|
|
|
|
for index, model in enumerate(models):
|
|
item = wx.ListItem()
|
|
item.SetId(index)
|
|
item.SetText(model.GetTitle())
|
|
self.data_list[index] = model
|
|
img_data = model.GetImage()
|
|
if img_data:
|
|
img = PyEmbeddedImage(img_data)
|
|
index = self.ilist.Add(img.GetBitmap())
|
|
item.SetImage(index)
|
|
self.mlist.InsertItem(item)
|
|
|
|
def GetSelectedModel(self):
|
|
index = self.mlist.GetNextItem(-1, wx.LIST_NEXT_ALL, wx.LIST_STATE_SELECTED)
|
|
return self.data_list.get(index)
|
|
|
|
|
|
#-----------------------------------------------------------------------------
|
|
# Форма с результатами выполнения работы
|
|
#-----------------------------------------------------------------------------
|
|
|
|
class ResultFrame(forms.ResultFrame):
|
|
def __init__(self, parent, title, result):
|
|
forms.ResultFrame.__init__(self, parent, title)
|
|
self.result = result
|
|
self.UpdateResults()
|
|
|
|
self.Bind(wx.EVT_MENU, self.OnExportToCSV,
|
|
id = forms.ID_EXPORT_CSV)
|
|
|
|
def UpdateResults(self):
|
|
self.scalar.Clear()
|
|
self.table.ClearGrid()
|
|
if not self.result:
|
|
return
|
|
|
|
cols = len(self.result.columns)
|
|
rows = len(self.result.rows)
|
|
self.table.CreateGrid(rows, cols)
|
|
#
|
|
for i, col in enumerate(self.result.columns):
|
|
label = "{} ({} {})".format(col.GetTitle(), col.GetType(), col.GetLabel())
|
|
self.table.SetColLabelValue(i, label)
|
|
#
|
|
for i, row in enumerate(self.result.rows):
|
|
for j, value in enumerate(row):
|
|
self.table.SetCellValue(i, j, str(value))
|
|
|
|
self.table.AutoSize()
|
|
|
|
pg = self.scalar
|
|
data = self.result.data
|
|
if not data:
|
|
pg.Show(0)
|
|
else:
|
|
for label, param in data.iteritems():
|
|
pg.Append(wxpg.StringProperty(label,
|
|
value = str(param.GetValue())))
|
|
|
|
def OnExportToCSV(self, event):
|
|
|
|
if not self.result or not self.result.table:
|
|
return
|
|
|
|
text_file = wx.FileSelector(
|
|
_('Save table to CSV'),
|
|
default_filename = 'table.csv',
|
|
wildcard = 'PNG files (*.csv)|*.csv|Text files (*.txt)|*.txt',
|
|
flags = wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT)
|
|
|
|
if not text_file:
|
|
return
|
|
|
|
tl = self.table.GetSelectionBlockTopLeft() # [(t, l)]
|
|
br = self.table.GetSelectionBlockBottomRight() # [(b, r)]
|
|
|
|
if not tl:
|
|
tl = (0, 0)
|
|
else:
|
|
tl = tl[0]
|
|
|
|
if not br:
|
|
br = (len(self.result.rows), len(self.result.columns))
|
|
else:
|
|
x, y = br[0]
|
|
br = x + 1, y + 1
|
|
|
|
with open(text_file, 'w') as f:
|
|
for i in xrange(tl[0], br[0]):
|
|
s = []
|
|
for j in xrange(tl[1], br[1]):
|
|
s.append(repr(self.result.GetCell(i, j)))
|
|
f.write('; '.join(s) + '\n')
|
|
|
|
#-----------------------------------------------------------------------------
|
|
# Форма с выбором наборов значений для построения графика
|
|
#-----------------------------------------------------------------------------
|
|
|
|
class LineSelectDialog(forms.LineSelectDialog):
|
|
def __init__(self, parent, title):
|
|
forms.LineSelectDialog.__init__(self, parent, title)
|
|
|
|
def Add(self, title, data):
|
|
self.left.Append(title, data)
|
|
self.right.Append(title, data)
|
|
|
|
def SetSelections(self):
|
|
"""
|
|
Выделяет первую строку в левом столбце колонок
|
|
и все, кроме первой, во втором.
|
|
|
|
Таким образом по умолчанию предлагается построить зависимость
|
|
каждого значения от первого. Это логично, поскольку первым
|
|
обычно идет независимый параметр.
|
|
"""
|
|
# выделяем первую строку слева
|
|
# (первый столбец результата)
|
|
if self.left.GetCount():
|
|
self.left.Select(0)
|
|
# выделяем все, кроме первой, строки справа
|
|
# (второй столбец результата)
|
|
for i in xrange(1, self.right.GetCount()):
|
|
self.right.Select(i)
|
|
|
|
def GetLineColumns(self):
|
|
"""
|
|
Возвращает список пар колонок, которые были выбраны
|
|
"""
|
|
item = self.left.GetSelection()
|
|
x = self.left.GetClientData(item)
|
|
|
|
items = self.right.GetSelections()
|
|
ys = [ self.right.GetClientData(i) for i in items ]
|
|
|
|
return [ (x, y) for y in ys ]
|
|
|
|
#-----------------------------------------------------------------------------
|
|
# Форма с изображением графика
|
|
#-----------------------------------------------------------------------------
|
|
|
|
class PlotFrame(forms.PlotFrame):
|
|
def __init__(self, parent, title, lines):
|
|
forms.PlotFrame.__init__(self, parent, title)
|
|
self.lines = lines # список объектор LineData
|
|
|
|
self.Bind(wx.EVT_MENU, self.OnSaveImage,
|
|
id = forms.ID_SAVE_PLOT)
|
|
|
|
colours = ['red', 'blue', 'green', 'magenta', 'purple', 'brown', 'yellow']
|
|
plot_lines = []
|
|
for i, line in enumerate(lines):
|
|
attr = {}
|
|
if line.type == LINE_MARKER:
|
|
handle = wxplot.PolyMarker
|
|
attr['size'] = 1
|
|
else:
|
|
handle = wxplot.PolyLine
|
|
points = line.GetPoints()
|
|
attr['colour'] = line.colour or colours[i % len(colours)]
|
|
attr['legend'] = line.GetTitle() or 'Unknown line'
|
|
plot_lines.append(handle(points, **attr))
|
|
|
|
graph = wxplot.PlotGraphics(plot_lines)
|
|
self.plot.Draw(graph)
|
|
|
|
def OnSaveImage(self, event):
|
|
img_file = wx.FileSelector(
|
|
_('Save plot'),
|
|
default_filename = 'plot.png',
|
|
default_extension = 'png',
|
|
wildcard = 'PNG files (*.png)|*.png',
|
|
flags = wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT)
|
|
size_sel = forms.SizeSelector(self)
|
|
if img_file and size_sel.ShowModal() == wx.ID_OK:
|
|
self.plot.Freeze()
|
|
w, h = size_sel.GetValues()
|
|
old_size = self.plot.GetSize()
|
|
self.plot.SetSize((w, h))
|
|
self.plot.SaveFile(img_file)
|
|
self.plot.SetSize(old_size)
|
|
self.plot.Thaw()
|
|
|
|
#-----------------------------------------------------------------------------
|
|
# Приложение
|
|
#-----------------------------------------------------------------------------
|
|
|
|
class ThisApp(wx.App):
|
|
|
|
def OnInit(self):
|
|
# Создание главного окна
|
|
frame = MainFrame()
|
|
self.SetTopWindow(frame)
|
|
frame.Show(True)
|
|
return True
|
|
|
|
#-----------------------------------------------------------------------------
|
|
# Запуск приложения
|
|
#-----------------------------------------------------------------------------
|
|
|
|
def main():
|
|
app = ThisApp(redirect = False)
|
|
app.MainLoop()
|
|
|
|
if __name__ == "__main__":
|
|
main() |