deparkes/OOMMFTools

View on GitHub
oommftools/user_interfaces/gui/oommfconvert.py

Summary

Maintainability
F
4 days
Test Coverage

"""
OOMMFConvert
Copyright (C) 2010  Mark Mascaro

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

"""
from __future__ import division
from __future__ import print_function
from __future__ import absolute_import

from builtins import str
from builtins import range
from past.utils import old_div
from wx import adv
import wx, os, sys, subprocess, shutil, tempfile, math, re, time, imp
from fnameutil import filterOnExtensions
from core.oommfdecode import slowlyPainfullyMaximize
import _about as about
from core import oommfconvert as oommfconvert

#########
# About #
#########

VERSION = about.__version__
NAME = "OOMMFConvert"
LICENSE = about.__license__
COPYRIGHT = about.__copyright__
WEBSITE = about.__uri__
DESCRIPTION = """OOMMFConvert is an OOMMF postprocessing tool for
generating bitmap and movie files from OMF files
using a simple drag-and-drop interface.
It uses OOMMF's own avf2ppm utility
and employs FFmpeg to link images.
\nOOMMFConvert is part of OOMMFTools."""

########
# DECS #
########

MODE = 'advanced'
#Determine if workarounds are necessary
PY2EXE_COMPENSATION = hasattr(sys, "frozen") or hasattr(sys, "importers") or imp.is_frozen("__main__")
#Codecs: ffmpeg signature, file extension, short name
CODECS = {"HuffYUV": (r" -vcodec huffyuv ",".avi", "HuffYUV"),
          "MPEG4": (r" -sameq ",".mp4","MPEG4"),
          "DivX 3": (r" -sameq -vcodec msmpeg4 ",".avi","DivX3"),
          "H263+": (r" -vcodec h263p ",".avi","H263+")}

if __name__ == "__main__":
    app = wx.App(None)
    #app = wx.App(redirect=True)
    #app = wx.App(redirect=True, filename="oommfconvert.log")

    #app.SetOutputWindowAttributes("OOMMF Console Log", (-1,-1), (550, 400))

SETUP_LOAD = 5
RENDER_LOAD = 10
CLEANUP_LOAD = 5
FRAMEDUPE_LOAD = 1
MOVIE_LOAD = 50

if PY2EXE_COMPENSATION:
    print("Py2EXE detected. Will perform defensive stdin redirection.")

############
# GUI BODY #
############

class MainFrame(wx.Frame):
    def __init__(self, manager=None):
        wx.Frame.__init__(self, manager, -1, " ".join([NAME, VERSION]), size=(700,700))

        BigFont = wx.Font(16, wx.FONTFAMILY_DEFAULT, style=wx.NORMAL, weight=wx.FONTWEIGHT_BOLD)
        TinyFont = wx.Font(8, wx.FONTFAMILY_DEFAULT, style=wx.NORMAL, weight=wx.FONTWEIGHT_NORMAL)

        self.dt = OOMMFSelectiveTarget(self)
        self.SetDropTarget(self.dt)
        self.config = None
        self.manager = manager
        self.Bind(wx.EVT_CLOSE, self.onClose)
        self.OOMMFPath = oommfconvert.getOOMMFPath("." + os.path.sep + "oommf.path")

        #A very simple menubar
        menubar = wx.MenuBar()
        about = wx.Menu()
        about.Append(999, 'About', 'Program information and license')
        menubar.Append(about, "About")
        self.SetMenuBar(menubar)

        self.Bind(wx.EVT_MENU, self.showAbout, id=999)

        #NOW we can deal with actual GUI stuff
        panel = wx.Panel(self, -1)

        #Oops, safety for resize events
        self.panel = panel

        #TODO : Deal with DropTarget

        #Main sizer
        sizer = wx.BoxSizer(wx.VERTICAL)

        #OOMMF configuration part

        #Need to block this out so we can have the script call overloaded. I blame ActiveTcl. A lot.

        titleText = wx.StaticText(panel, -1, "Path to OOMMF")
        titleText.SetFont(BigFont)
        sizer.Add(titleText, 0, wx.ALIGN_CENTER | wx.TOP, 10)


        hsizer = wx.BoxSizer(wx.HORIZONTAL)
        self.TclCall = wx.ComboBox(panel, 101, choices=["tclsh","tclsh85"])
        self.TclCall.SetStringSelection("tclsh")
        hsizer.Add(self.TclCall, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 20)

        self.OOMMFPathLabel = wx.StaticText(panel, -1, "OOMMF not located.", style = wx.ALIGN_CENTER | wx.ST_NO_AUTORESIZE | wx.ALIGN_CENTER_VERTICAL)
        if self.OOMMFPath: self.OOMMFPathLabel.SetLabel(self.OOMMFPath)
        hsizer.Add(self.OOMMFPathLabel, 0, wx.ALIGN_CENTER | wx.ALIGN_CENTER_VERTICAL)

        sizer.Add(hsizer, 0, wx.ALIGN_CENTER | wx.TOP | wx.BOTTOM, 10)

        hsizer = wx.BoxSizer(wx.HORIZONTAL)
        hsizer.Add(wx.StaticText(panel, -1, "Drag and drop or", style=wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL), 0, wx.ALIGN_RIGHT | wx.RIGHT | wx.ALIGN_CENTER_VERTICAL, 6)
        self.LoadOOMMFButton = wx.Button(panel, 10, "Load OOMMF")
        self.Bind(wx.EVT_BUTTON, self.GUILocateOOMMF, id=10)
        hsizer.Add(self.LoadOOMMFButton, 0)
        sizer.Add(hsizer, 0, wx.ALIGN_CENTER | wx.BOTTOM, 10)



        sizer.Add(wx.StaticLine(panel, -1), 0, wx.EXPAND | wx.BOTTOM, 10)

        #Conf-file configuration part


        titleText = wx.StaticText(panel, -1, "Configuration File")
        titleText.SetFont(BigFont)
        sizer.Add(titleText, 0, wx.ALIGN_CENTER | wx.TOP, 10)

        #Now we need a pile of intermediate sizers, sadly
        self.ConfPathLabel = wx.StaticText(panel, -1, "No config loaded.", style = wx.EXPAND | wx.ALIGN_CENTER | wx.ST_NO_AUTORESIZE)
        sizer.Add(self.ConfPathLabel, 0, wx.ALIGN_CENTER | wx.TOP | wx.BOTTOM, 10)

        self.autoMaxVectors = wx.CheckBox(panel, -1, "Generate Vector Field Maxima")
        self.autoMaxVectors.SetValue(False)
        sizer.Add(self.autoMaxVectors, 0, wx.ALIGN_CENTER | wx.BOTTOM, 10)

        psizer = wx.BoxSizer(wx.HORIZONTAL)
        psizer.Add(wx.StaticText(panel, -1, "Drag and drop or", style=wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL), 0, wx.ALIGN_RIGHT | wx.RIGHT | wx.ALIGN_CENTER_VERTICAL, 6)
        self.LoadConfButton = wx.Button(panel, 20, "Load Config")
        self.Bind(wx.EVT_BUTTON, self.GUILocateConf, id=20)
        psizer.Add(self.LoadConfButton, 0)

        sizer.Add(psizer, 0, wx.ALIGN_CENTER | wx.BOTTOM | wx.BOTTOM, 10)
        sizer.Add(wx.StaticLine(panel, -1), 0, wx.EXPAND | wx.BOTTOM, 10)

        # Fancy Stuff
        titleText = wx.StaticText(panel, -1, "Images")
        titleText.SetFont(BigFont)
        sizer.Add(titleText, 0, wx.ALIGN_CENTER | wx.TOP, 10)

        imsizer = wx.BoxSizer(wx.HORIZONTAL)

        self.doImages = wx.CheckBox(panel, -1, "Make Bitmaps")
        self.doImages.SetValue(True)
        imsizer.Add(self.doImages, 0, wx.ALIGN_CENTER | wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 30)

        hsizer = wx.BoxSizer(wx.HORIZONTAL)
        hsizer.Add(wx.StaticText(panel, -1, "Image Magnify%", style=wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL), 0, wx.ALIGN_RIGHT | wx.RIGHT | wx.ALIGN_CENTER_VERTICAL, 6)
        self.magnifierSpin = wx.SpinCtrl(panel, 100, "100", min=100, max=1000, size=(50,-1))
        hsizer.Add(self.magnifierSpin, 0)
        imsizer.Add(hsizer, 0)


        sizer.Add(imsizer, 0, wx.ALIGN_CENTER | wx.TOP | wx.BOTTOM, 10)
        sizer.Add(wx.StaticLine(panel, -1), 0, wx.EXPAND | wx.BOTTOM, 10)

        # Fancy Stuff
        titleText = wx.StaticText(panel, -1, "Movies")
        titleText.SetFont(BigFont)
        sizer.Add(titleText, 0, wx.ALIGN_CENTER | wx.TOP, 10)

        hsizer = wx.GridSizer(2,3,7,7)

        self.doMovie = wx.CheckBox(panel, -1, "Make Movies")
        hsizer.Add(self.doMovie, 0, wx.ALIGN_CENTER | wx.ALIGN_CENTER_VERTICAL)

        joiner = wx.BoxSizer(wx.VERTICAL)
        a = wx.StaticText(panel, -1, "HuffYUV is superior.")
        a.SetFont(TinyFont)
        joiner.Add(a, 0, wx.ALIGN_CENTER)
        a = wx.StaticText(panel, -1, "Consider installing it.")
        a.SetFont(TinyFont)
        joiner.Add(a, 0, wx.ALIGN_CENTER)
        hsizer.Add(joiner, 0, wx.ALIGN_CENTER)

        joiner = wx.BoxSizer(wx.VERTICAL)
        a = wx.StaticText(panel, -1, "Magnifying video may fail")
        a.SetFont(TinyFont)
        joiner.Add(a, 0, wx.ALIGN_CENTER)
        a = wx.StaticText(panel, -1, "violently. User's risk.")
        a.SetFont(TinyFont)
        joiner.Add(a, 0, wx.ALIGN_CENTER)
        hsizer.Add(joiner)


        joiner = wx.BoxSizer()
        self.movieFPS = wx.SpinCtrl(panel, 201, "25", min=1, max=25, size=(50,-1))
        joiner.Add(self.movieFPS, 0, wx.RIGHT, 10)
        joiner.Add(wx.StaticText(panel, -1, "FPS"), 0, wx.ALIGN_CENTER_VERTICAL)
        hsizer.Add(joiner, 0, wx.ALIGN_CENTER)

        self.movieCodec = wx.ComboBox(panel, 200, choices = list(CODECS.keys()), style=wx.CB_READONLY)
        self.movieCodec.SetStringSelection("HuffYUV")
        hsizer.Add(self.movieCodec, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_CENTER)

        bsizer = wx.BoxSizer(wx.HORIZONTAL)
        bsizer.Add(wx.StaticText(panel, -1, "Movie Magnify%", style=wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL), 0, wx.ALIGN_RIGHT | wx.RIGHT | wx.ALIGN_CENTER_VERTICAL, 6)
        self.movieMagnifierSpin = wx.SpinCtrl(panel, 300, "100", min=100, max=300, size=(50,-1))
        bsizer.Add(self.movieMagnifierSpin, 0, wx.ALIGN_CENTER_VERTICAL)
        hsizer.Add(bsizer, 0, wx.ALIGN_CENTER_VERTICAL)


        sizer.Add(hsizer, 0, wx.ALIGN_CENTER | wx.TOP | wx.BOTTOM, 10)
        sizer.Add(wx.StaticLine(panel, -1), 0, wx.EXPAND | wx.BOTTOM, 10)


        #Clarification

        ins = wx.StaticText(panel, -1, "Drop OOMMF Files Here!")
        ins.SetFont(BigFont)
        sizer.Add(ins, 0, wx.ALIGN_CENTER | wx.ALIGN_CENTER_VERTICAL | wx.BOTTOM | wx.TOP, 60)

        #Cleanup initialization
        panel.SetSizer(sizer)

        if self.manager:
            self.CenterOnParent()
        panel.Fit()
        self.Show()

    def onClose(self, evt):
        if self.manager:
            self.manager.droppedWindow(self)
        self.Destroy()

    def showAbout(self, evt):
        info = wx.adv.AboutDialogInfo()
        mydesc = DESCRIPTION
        mylicense = LICENSE
        info.SetName(NAME)
        info.SetVersion(VERSION)
        info.SetDescription(''.join(mydesc))
        info.SetLicense(''.join(mylicense))
        info.SetCopyright(COPYRIGHT)
        info.SetWebSite(WEBSITE)
        wx.adv.AboutBox(info)

    def GUILocateOOMMF(self, evt):
        dlg = wx.FileDialog(self, "Find OOMMF Location", os.getcwd(), "", "OOMMF TCL File (*.tcl)|*.tcl",wx.FD_OPEN)
        if dlg.ShowModal() == wx.ID_OK and dlg.GetFilename():
            filename = dlg.GetPath()
            #self.fileLabel.SetLabel("Open: " + os.getcwd() + os.path.sep + dlg.GetFilename())
            #self.digest = chomp(dlg.GetPath())
            #self.leftbox.Set(self.digest.getNames())
            self.locateOOMMF(filename)

    def locateOOMMF(self, path):
        self.OOMMFPath = path
        self.OOMMFPathLabel.SetLabel(path)
        f = open("oommf.path", "w")
        f.write(path)
        f.close()
        try:
            self.panel.SendSizeEvent()
        except:
            print("wx.Panel.SendSizeEvent() missed - probably using old wxPython. Cosmetic bug will result.")

    def GUILocateConf(self, evt):
        dlg = wx.FileDialog(self, "Select Configuration File", os.getcwd(), "", "mmDisp Config File (*.config)|*.config",wx.FD_OPEN)
        if dlg.ShowModal() == wx.ID_OK and dlg.GetFilename():
            filename = dlg.GetPath()
            #self.fileLabel.SetLabel("Open: " + os.getcwd() + os.path.sep + dlg.GetFilename())
            #self.digest = chomp(dlg.GetPath())
            #self.leftbox.Set(self.digest.getNames())
            self.locateConf(filename)

    def locateConf(self, path):
        self.config = path
        self.ConfPathLabel.SetLabel(path)
        try:
            self.panel.SendSizeEvent()
        except:
            print("wx.Panel.SendSizeEvent() missed - probably using old wxPython. Cosmetic bug will result.")


###########
# BACKEND #
###########

class OOMMFSelectiveTarget(wx.FileDropTarget):
    def __init__(self, parent):
        wx.FileDropTarget.__init__(self)
        self.parent = parent

    def OnDropFiles(self, x, y, filenames):
        #Find OOMMF, then a config, then files
        oommf = filterOnExtensions(["tcl"], filenames)
        if oommf:
            #What did you DO? Only the last one counts!
            self.parent.locateOOMMF(oommf[-1])
        config = filterOnExtensions(["conf", "config", "cnf"], filenames)
        if config:
            #Again, only the last conf file dropped counts.
            self.parent.locateConf(config[-1])

        if not (self.parent.config and self.parent.OOMMFPath):
            return 0

        #Try to save a lot of work - only do magic if OMF-type files were dropped.
        targets = filterOnExtensions(["omf","ovf","oef","ohf"], filenames)
        if not targets: return 0

        #Save more work by verifying that the user actually wants to make some sort of thing.
        if not self.parent.doImages.GetValue() and not self.parent.doMovie.GetValue():
            #Er... you don't want to do anything?
            return 0

        #Convince user that everything is OK, and provide me with entertainment.
        #Set up a dialog box that will track progress.

        dial = self.initializeProgressBar(targets)

        #To avoid some Py2EXE funkiness, we may need to redirect stdin.
        #Figure out where it needs to go, and catch that.

        childstd = self.findStandardIn()

        #We are now in the danger zone where the progress bar can get locked, and we
        #are going to shield that with some top-level exception handling

        try:

            dial.workDone(SETUP_LOAD, "Checking for Movies")


            #The first thing to do is try to make movies - in some cases, this process will leave behind
            #the images we need, and we can skip the image step entirely.

            print("Entering movie mode.")
            if self.parent.doMovie.GetValue():
                #Make some movies. This also hands off the progressDialog, so it can be updated.
                self.doMovies(targets, childstd, dial)
                #Finish making some movies.
                dial.workDone(CLEANUP_LOAD, "Doing Images")

            #Awkward short-circuit - If you made movies and they have the same magnification as the images, you get to keep the images!
            if self.parent.doMovie.GetValue() and self.parent.movieMagnifierSpin.GetValue() == self.parent.magnifierSpin.GetValue():
                dial.finish()
                return 1

            if self.parent.doImages.GetValue():
                #Oh well, I guess you're stuck making images. Make them!
                self.doImages(targets, childstd, dial)


            dial.workDone(CLEANUP_LOAD, "All Done!")
        except Exception as e:
            wx.MessageBox('Unpacking error: ' + repr(e), "Error")
            print(e)
        finally:
            dial.finish()
            return 1


    def findStandardIn(self):
        #Py2EXE is bad. Failing to set this correctly can cause the packaged version to explode on failure to
        #correctly grab a handle to a shell-like thing, so we deflect that by redirecting stdin to a file, which will always be empty. "Oops."
        if PY2EXE_COMPENSATION:
            childstd = file('null_data', 'a') #Detection methods unreliable
            print("Enforced running windows - py2exe mode. Using dump file stdin.")
        elif hasattr(sys.stdin, 'fileno'):
            childstd = sys.stdin
            print("Using standard stdin")
        elif hasattr(sys.stdin, '_file') and hasattr(sys.stdin._file, 'fileno'):
            childstd = sys.stdin._file
            print("Using hacked stdin adjustment - probably running Windows.")
        else:
            childstderr = file('null_data', 'a')
            print("Definitely using Windows. Using fake stdin.")
        return childstd

    def initializeProgressBar(self, targetList):
        #Cost of setup
        workload = SETUP_LOAD
        #Cost of a single avf2ppm render
        workload += RENDER_LOAD * len(targetList)
        workload += CLEANUP_LOAD
        if self.parent.doMovie.GetValue():
            #Cost of making a movie with ffmpeg; may add cleanup
            workload += MOVIE_LOAD
            if self.parent.doImages.GetValue() and not self.parent.movieMagnifierSpin.GetValue() == self.parent.magnifierSpin.GetValue():
                #Surprise! You get to rerender at a different magnification!
                workload += RENDER_LOAD * len(targetList) + CLEANUP_LOAD #And cleanup
            #Cost of frameduping
            workload += FRAMEDUPE_LOAD * (int(old_div(25, self.parent.movieFPS.GetValue())) * len(targetList))

        dial = SupportDialog("Render In Progress", "", maximum=workload,parent=self.parent)
        dial.Update(0, "This is a suitably long message to ensure the window title renders correctly") #Because this class lacks a size control!
        dial.Fit()
        dial.Update(0, "Initializing Render")
        dial.CenterOnParent()
        return dial

   
    def doImages(self, targetList, stdinRedirect, dial):
        dial.workDone(0, "Rendering")
        imageConfig = {'magnifierSpin': self.parent.magnifierSpin.GetValue(),
                       'autoMaxVectors': self.parent.autoMaxVectors.GetValue(),
                       'config': self.parent.config}
        oommfconvert.doImages(targetList, stdinRedirect, imageConfig, self.parent.TclCall.GetValue(), self.parent.OOMMFPath)
        dial.workDone(RENDER_LOAD, "Rendering")
        dial.workDone(0, "Cleaning Up")

    def doMovies(self, targetList, stdinRedirect, dial):
        print('in the movie function (not core)')
        dial.workDone(0, "Rendering")
        imageConfig = {'magnifierSpin': self.parent.magnifierSpin.GetValue(),
                       'autoMaxVectors': self.parent.autoMaxVectors.GetValue(),
                       'config': self.parent.config}
        oommfconvert.doMovies(targetList, stdinRedirect, imageConfig,self.parent.movieCodec.GetValue(), self.parent.movieFPS.GetValue(), self.parent.TclCall.GetValue(), self.parent.OOMMFPath, self.parent.doImages.GetValue(), CODECS)
        dial.workDone(0, "Rendering Movie")
        dial.workDone(MOVIE_LOAD, "Cleaning")
        return None


class SupportDialog(wx.ProgressDialog):
    def __init__(self, title, message, **kwargs):
        wx.ProgressDialog.__init__(self, title, message, **kwargs)
        self._workDone = 0
        self.workmax = kwargs["maximum"]

    def workDone(self, delta, newmsg):
        self._workDone += delta
        self.Update(self._workDone, newmsg)

    def finish(self):
        self.Update(self.workmax, "Done!")
        self.Destroy()
########
# MAIN #
########
if __name__ == "__main__":
    BigBoss = MainFrame()
    app.MainLoop()