File: mcdisplay.py

package info (click to toggle)
mccode 3.5.19%2Bds5-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,113,256 kB
  • sloc: ansic: 40,697; python: 25,137; yacc: 8,438; sh: 5,405; javascript: 4,596; lex: 1,632; cpp: 742; perl: 296; lisp: 273; makefile: 226; fortran: 132
file content (301 lines) | stat: -rwxr-xr-x 11,853 bytes parent folder | download
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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
'''
mcdisplay webgl script.
'''
import os
import sys
import signal
import time
import logging
import json
import subprocess
import webbrowser
import time
from pathlib import Path
from http.server import HTTPServer, SimpleHTTPRequestHandler
import threading
import socket
if os.name=='nt':
    import _winapi

sys.path.append(str(Path(__file__).resolve().parent.parent.parent))

from mccodelib import mccode_config
from mccodelib.mcdisplayutils import McDisplayReader
from mccodelib.instrgeom import Vector3d
from mccodelib.utils import get_file_text_direct
from threading import Thread
from shutil import copy as shutil_copy, copytree, ignore_patterns

class SimpleWriter(object):
    ''' a minimal, django-omiting "glue file" writer tightly coupled to some comments in the file template.html '''
    def __init__(self, templatefile, html_filename, invcanvas):
        self.template = templatefile
        self.html_filename = html_filename
        self.invcanvas = invcanvas
    
    def write(self):
        # load and modify
        template = get_file_text_direct(self.template)
        lines = template.splitlines()
        for i in range(len(lines)):
            if 'INSERT_CAMPOS_HERE' in lines[i]:
                lines[i+2] = '        invert_canvas = %s; // line written by SimpleWriter' % 'true' if self.invcanvas else 'false'
        self.text = '\n'.join(lines)
        
        # write to disk
        try:
            f = open(self.html_filename, 'w')
            f.write(self.text)
        finally:
            f.close()

class DjangoWriter(object):
    ''' writes a django template from the instrument representation '''
    instrument = None
    text = ''
    templatefile = ''
    campos = None
    
    def __init__(self, instrument, templatefile, campos):
        self.instrument = instrument
        self.templatefile = templatefile
        self.campos = campos
        
        # django stuff
        from django.template import Context
        from django.template import Template
        self.Context = Context
        self.Template = Template
        from django.conf import settings
        settings.configure()
    
    def build(self):
        templ = get_file_text_direct(self.templatefile)
        t = self.Template(templ)
        c = self.Context({'instrument': self.instrument, 
            'campos_x': self.campos.x, 'campos_y': self.campos.y, 'campos_z': self.campos.z,})
        self.text = t.render(c)
    
    def save(self, filename):
        ''' save template to disk '''
        try:
            f = open(filename, 'w')
            f.write(self.text)
        finally:
            f.close()

def _write_html(instrument, html_filepath, first=None, last=None, invcanvas=False):
    ''' writes instrument definition to html/js '''
    
    # create camera view coordinates given the bounding box

    # render html
    templatefile = Path(__file__).absolute().parent.joinpath("template.html")
    writer = SimpleWriter(templatefile, html_filepath, invcanvas)
    writer.write()

def write_browse(instrument, raybundle, dirname, instrname, timeout, nobrowse=None, first=None, last=None, invcanvas=None, **kwds):
    ''' writes instrument definitions to html/ js '''
    print("Launching WebGL... Once launched, server will run for " + str(timeout) + " s")
    def copy(a, b):
        shutil_copy(str(a), str(b))

    if os.name == 'nt':
            source =  Path(os.path.join(os.path.expandvars("$USERPROFILE"),"AppData",mccode_config.configuration['MCCODE'],mccode_config.configuration['MCCODE_VERSION'],'webgl'))
    else:
            source =  Path(os.path.join(os.path.expandvars("$HOME"),"." + mccode_config.configuration['MCCODE'],mccode_config.configuration['MCCODE_VERSION'],'webgl'))

    dest = Path(dirname)
    if dest.exists():
        raise RuntimeError(f"The specified destination {dirname} already exists!")

    # Copy the app files - i.e. creating dest
    copytree(source.joinpath('dist'), dest)

    # Copy package.json from source to dest
    package_json_source = source.joinpath('package.json')
    package_json_dest = dest.joinpath('package.json')
    shutil_copy(package_json_source, package_json_dest)

    # Copy node_modules from source to destination
    node_modules_source = source.joinpath('node_modules')
    node_modules_dest = dest.joinpath('node_modules')
    if not os.name=='nt':
        os.symlink(node_modules_source, node_modules_dest)
    else:
        _winapi.CreateJunction(str(node_modules_source), str(node_modules_dest))

    # Ensure execute permissions for vite binary
    vite_bin = node_modules_dest.joinpath('.bin/vite')
    os.chmod(vite_bin, 0o755)

    # Copy start-vite.js script to destination
    start_vite_source = source.joinpath('start-vite.js')
    start_vite_dest = dest.joinpath('start-vite.js')
    shutil_copy(start_vite_source, start_vite_dest)

    # Write instrument
    json_instr = '%s' % json.dumps(instrument.jsonize(), indent=2)
    file_save(json_instr, dest.joinpath('instrument.json'))

    # Write particles
    json_particles = '%s' % json.dumps(raybundle.jsonize(), indent=2)
    file_save(json_particles, dest.joinpath('particles.json'))

    # Exit if nobrowse flag has been set
    if nobrowse is not None and nobrowse:
        return

    destdist = dest

    # Function to run npm commands and capture port
    def run_npm_and_capture_port(port_container):
        if not os.name == 'nt':
            npmexe = "npm"
        else:
            npmexe = "npm.cmd"

        try:
            proc = subprocess.Popen([npmexe,"run","dev"], cwd=str(destdist), stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
            for line in proc.stdout:
                print(line)
                if 'Local:' in line:
                    parts = line.strip().split(' ')
                    for part in parts:
                        if part.startswith('http://localhost:'):
                            port = part.split(':')[-1].rstrip('/')
                            port_container['port'] = port
                            port_container['process'] = proc
                            return
        except subprocess.CalledProcessError as e:
            print(f"npm run dev failed: {e}")
            return None

    def signal_handler(sig, frame):
        global port_container
        print('Received signal ' + str(sig))
        port_container['process'].send_signal(signal.SIGTERM)
        sys.exit(0)

    # Container to hold the port information
    global port_container
    port_container = {'port': None, 'process': None}

    # Start npm and capture the port in a separate thread
    npm_thread = Thread(target=lambda: run_npm_and_capture_port(port_container))
    npm_thread.start()
    npm_thread.join()

    # If a port was found, open the browser
    if port_container['port']:
        print("Opening browser...")
        webbrowser.open(f"http://localhost:{port_container['port']}/")
    else:
        print("Failed to determine the localhost port")
    if port_container['process']:
        signal.signal(signal.SIGTERM, signal_handler)
        signal.signal(signal.SIGINT, signal_handler)
        signal.signal(signal.SIGUSR1, signal_handler)
        signal.signal(signal.SIGUSR2, signal_handler)
        if not os.name == 'nt':
            print('Press Ctrl+C to exit\n(visualisation server willterminate server after ' + str(timeout) + ' s)')
            time.sleep(timeout)
            print("Sending SIGTERM to npm/vite server")
            port_container['process'].send_signal(signal.SIGTERM)
            sys.exit(0)
        else:
            print('Press Ctrl+C to exit visualisation server')
            port_container['process'].wait()
            sys.exit(0)

def file_save(data, filename):
    ''' saves data for debug purposes '''
    with open(filename, 'w') as f:
        f.write(data)

def main(instr=None, dirname=None, debug=None, n=None, timeout=None, **kwds):
    logging.basicConfig(level=logging.INFO)

    # Function to run npm commands and capture port
    def run_npminstall():
        toolpath=str(Path(__file__).absolute().parent)
        print(toolpath)
        if not os.name == 'nt':
            npminst = Path(toolpath + "/npminstall")
        else:
            npminst = Path(toolpath + "/npminstall.bat")

        print("Executing " + str(npminst))
        try:
            proc = subprocess.Popen(npminst, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
            print("Installing npm / vite modules")
            for line in proc.stdout:
                 print(line.rstrip())
            print("Installing npm / vite modules - stderr:")  
            for line in proc.stderr:
                 print(line.rstrip())            
            print("Done installing npm / vite modules")
        except subprocess.CalledProcessError as e:
            print(f"npminstall failed: {e}")
            return None
    
    # 1st run setup: Check if the user has a "webgl" folder or not
    if os.name == 'nt':
            userdir =  os.path.join(os.path.expandvars("$USERPROFILE"),"AppData",mccode_config.configuration['MCCODE'],mccode_config.configuration['MCCODE_VERSION'],'webgl')
    else:
            userdir =  os.path.join(os.path.expandvars("$HOME"),"." + mccode_config.configuration['MCCODE'],mccode_config.configuration['MCCODE_VERSION'],'webgl')

    if not os.path.isdir(userdir):
        try:
            run_npminstall()
        except Exception as e:
            print("Local WebGL Directory %s could not be created: %s " % (userdir, e.__str__()))


    # output directory
    if dirname is None:
        from datetime import datetime as dt
        p = Path(instr).absolute()
        dirname = str(p.parent.joinpath(f"{p.stem}_{dt.strftime(dt.now(), '%Y%m%d_%H%M%S')}"))
    
    # set up a pipe, read and parse the particle trace
    reader = McDisplayReader(instr=instr, n=n, dir=dirname, debug=debug, **kwds)
    instrument = reader.read_instrument()
    raybundle = reader.read_particles()
    
    # write output files
    write_browse(instrument, raybundle, dirname, instr, timeout, **kwds)

    if debug:
        # this should enable template.html to load directly
        jsonized = json.dumps(instrument.jsonize(), indent=0)
        file_save(jsonized, 'jsonized.json')

if __name__ == '__main__':
    from mccodelib.mcdisplayutils import make_common_parser
    # Only pre-sets instr, --default, options
    parser, prefix = make_common_parser(__file__, __doc__)
    parser.add_argument('--dirname', help='output directory name override')
    parser.add_argument('--inspect', help='display only particle rays reaching this component')
    parser.add_argument('--nobrowse', action='store_true', help='do not open a webbrowser viewer')
    parser.add_argument('--invcanvas', action='store_true', help='invert canvas background from black to white')
    parser.add_argument('--first', help='zoom range first component')
    parser.add_argument('--last', help='zoom range last component')
    parser.add_argument('-n', '--ncount', dest='n', type=float, default=300, help='Number of particles to simulate')
    parser.add_argument('-t', '--trace', dest='trace', type=int, default=2, help='Select visualization mode')
    parser.add_argument('--timeout', dest='timeout', type=int, default=300, help='Shutdown time of npm/vite server')
    args, unknown = parser.parse_known_args()
    # Convert the defined arguments in the args Namespace structure to a dict
    args = {k: args.__getattribute__(k) for k in dir(args) if k[0] != '_'}
    # if --inspect --first or --last are given after instr, the remaining args become "unknown",
    # but we assume that they are instr_options
    if len(unknown):
        args['options'] = unknown

    try:
        main(**args)
    except KeyboardInterrupt:
        print('')