source: modrana.py

Last change on this file was 337d6cb, checked in by Martin Kolman <martin.kolman@…>, 3 months ago

Some PEP8 for modrana.py

  • Property mode set to 100755
File size: 35.4 KB
Line 
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3#----------------------------------------------------------------------------
4# modRana main GUI.  Displays maps & much more, for use on a mobile device.
5#
6# Controls:
7# Start by clicking on the main menu icon.
8#----------------------------------------------------------------------------
9# Copyright 2007-2008, Oliver White
10#
11# This program is free software: you can redistribute it and/or modify
12# it under the terms of the GNU General Public License as published by
13# the Free Software Foundation, either version 3 of the License, or
14# (at your option) any later version.
15#
16# This program is distributed in the hope that it will be useful,
17# but WITHOUT ANY WARRANTY; without even the implied warranty of
18# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19# GNU General Public License for more details.
20#
21# You should have received a copy of the GNU General Public License
22# along with this program.  If not, see <http://www.gnu.org/licenses/>.
23#----------------------------------------------------------------------------
24import subprocess
25import sys
26import time
27
28startTimestamp = time.time()
29import os
30import marshal
31import imp
32import platform
33
34# initialize logging
35from core import modrana_log
36from core.signal import Signal
37modrana_log.init_logging()
38import logging
39log = logging.getLogger("")
40
41# qrc handling
42from core import qrc
43USING_QRC = qrc.is_qrc
44qrc.handle_qrc()
45
46
47def setCorrectCWD():
48    # change to folder where the main modRana file is located
49    # * this enables to run modRana with absolute path without adverse
50    # effect such as modRana not finding modules or
51    currentAbsolutePath = os.path.dirname(os.path.abspath(__file__))
52    if USING_QRC:
53        # we are running from qrc, so just use the qrc:/ prefix as CWD,
54        # no need to chdir
55        currentAbsolutePath = "qrc:/"
56    else:
57        os.chdir(currentAbsolutePath)
58    # append the path to the bundle directory so that modRana can fall-back
59    # to the it's bundled modules if a given module is not available on the system
60    # - at the same just a simple "import foo" import is enough and
61    #   "from core.bundle import foo" is not needed
62    sys.path.append(os.path.join(currentAbsolutePath, 'core', 'bundle'))
63    # do the same thing for the backports folder, which serves a similar role
64    # as the bundle folder (TODO: merge content of backports to bundle ?)
65    sys.path.append(os.path.join(currentAbsolutePath, 'core', 'backports'))
66    #sys.path.append(os.path.join('core', 'bundle'))
67    #sys.path.append(os.path.join("qrc:/", 'core', 'bundle'))
68    # add the modules folder to path, so that third-party modules (such as Upoints),
69    # that expect to be placed to path work correctly
70    # NOTE: most of those modules were moved to the core/bundle
71    # directory so it might be possible to get rid of this in the
72    # future
73    sys.path.append(os.path.join(currentAbsolutePath, 'modules'))
74    sys.path.append(os.path.join(currentAbsolutePath, 'modules/device_modules'))
75    sys.path.append(os.path.join(currentAbsolutePath, 'modules/gui_modules'))
76
77# before we start importing our stuff we need to correctly setup CWD
78# and Python import paths
79setCorrectCWD()
80
81# import core modules/classes
82from core import startup
83from core import utils
84from core import paths
85from core import configs
86from core import threads
87from core import gs
88from core import singleton
89from core.backports import six
90# record that imports-done timestamp
91importsDoneTimestamp = time.time()
92
93MAIN_MODULES_FOLDER = 'modules'
94DEVICE_MODULES_FOLDER = os.path.join(MAIN_MODULES_FOLDER, "device_modules")
95GUI_MODULES_FOLDER = os.path.join(MAIN_MODULES_FOLDER, "gui_modules")
96ALL_MODULE_FOLDERS = [
97    MAIN_MODULES_FOLDER,
98    DEVICE_MODULES_FOLDER,
99    GUI_MODULES_FOLDER
100]
101
102
103class ModRana(object):
104    """
105    This is THE main modRana class.
106    """
107
108    def __init__(self):
109        singleton.modrana = self
110
111        self.timing = []
112        self.addCustomTime("modRana start", startTimestamp)
113        self.addCustomTime("imports done", importsDoneTimestamp)
114
115        # constants & variable initialization
116        self.dmod = None  # device specific module
117        self.gui = None
118        self.GUIString = ""
119        self.optLoadingOK = None
120
121        self.d = {}  # persistent dictionary of data
122        self.m = {}  # dictionary of loaded modules
123        self.watches = {}  # List of data change watches
124        self.maxWatchId = 0
125
126        self.initInfo = {
127            'modrana': self,
128            'device': None, # TODO: do this directly
129            'name': ""
130        }
131
132        # signals
133        self.notificationTriggered = Signal()
134        self.shutdown_signal = Signal()
135
136        self.mapRotationAngle = 0  # in radians
137        self.notMovingSpeed = 1  # in m/s
138
139        # per mode options
140        # NOTE: this variable is automatically saved by the
141        # options module
142        self.keyModifiers = {}
143
144        # initialize threading
145        threads.initThreading()
146
147        # start timing modRana launch
148        self.addTime("GUI creation")
149
150        # add the startup handling core module
151        self.startup = startup.Startup(self)
152        self.args = self.startup.getArgs()
153
154        # handle any early tasks specified from CLI
155        self.startup.handleEarlyTasks()
156
157        # early CLI tasks might need a "silent" modRana
158        # so the startup announcement is here
159        log.info(" == modRana Starting == ")
160        # load the version string (needs to be done here
161        # as PWD might change after the paths module is
162        # imported, for example when running
163        # with the Qt 5 GUI)
164        paths.loadVersionString()
165        version = paths.VERSION_STRING
166        if version is None:
167            version = "unknown version"
168        log.info(%s" % version)
169        log.info("  Python %s" % platform.python_version())
170
171        # load the device module now as it might override
172        # the default profile directory, so it needs to be
173        # before ve init the core paths module
174        self._loadDeviceModule()
175
176        # initialize the paths handling core module
177        self.paths = paths.Paths(self)
178
179        # add the configs handling core module
180        self.configs = configs.Configs(configs_dir=self.paths.getProfilePath())
181
182        # load persistent options
183        self.optLoadingOK = self._loadOptions()
184        self._optionsLoaded()
185
186        # check if upgrade took place
187
188        if self.optLoadingOK:
189            savedVersionString = self.get('modRanaVersionString', "")
190            versionStringFromFile = paths.VERSION_STRING
191            if savedVersionString != versionStringFromFile:
192                log.info("possible upgrade detected")
193                self._postUpgradeCheck()
194
195        # save current version string
196        self.set('modRanaVersionString', paths.VERSION_STRING)
197
198        # load all configuration files
199        self.configs.load_all()
200
201        # start loading other modules
202
203        # handle tasks that require the device
204        # module but not GUI
205        self.startup.handleNonGUITasks()
206
207        # then the GUI module
208        self._loadGUIModule()
209
210        # and all other modules
211        self._loadModules()
212
213        # startup done, log some statistics
214        self._startupDone()
215
216    def _postUpgradeCheck(self):
217        """
218        perform post upgrade checks
219        """
220        self.configs.upgrade_config_files()
221
222    ##  MODULE HANDLING ##
223
224    def _loadDeviceModule(self):
225        """Load the device module"""
226        if self.dmod:  # don't reload the module
227            return
228
229        # get the device module string
230        # (a unique device module string identificator)
231        if self.args.d:
232            device = self.args.d
233        else:  # no device specified from CLI
234            # try to auto-detect the current device
235            from core import platform_detection
236
237            device = platform_detection.getBestDeviceModuleId()
238
239        device = device.lower()  # convert to lowercase
240
241        self.initInfo["device"] = device
242
243        # get GUI ID from the CLI argument
244        if self.args.u:
245            try:
246                self.GUIString = self.args.u.split(":")[0].upper()
247            except Exception:
248                log.exception('splitting the GUI string failed')
249        else:  # no ID specified
250            # the N900 device module needs the GUIString
251            # at startup
252            if device == "n900":
253                self.GUIString = "GTK"
254
255        # set the pre-import-visible GUIString
256        # for the device module
257        gs.GUIString = self.GUIString
258
259        ## load the device specific module
260
261        # NOTE: other modules need the device and GUI modules
262        # during init
263        deviceModulesPath = os.path.join(MAIN_MODULES_FOLDER, "device_modules")
264        sys.path.append(deviceModulesPath)
265        dmod_instance = self._loadModule("device_%s" % device, "device")
266        if dmod_instance is None:
267            log.critical("!! device module failed to load !!\n"
268                         "loading the Neo device module as fail-safe")
269            device = "neo"
270            dmod_instance = self._loadModule("device_%s" % device, "device")
271        self.dmod = dmod_instance
272
273        # if no GUIString was specified from CLI,
274        # get preferred GUI module strings from the device module
275
276        if self.GUIString == "":
277            ids = self.dmod.getSupportedGUIModuleIds()
278            if ids:
279                self.GUIString = ids[0]
280            else:
281                self.GUIString = "GTK"  # fallback
282                # export the GUI string
283                # set the pre-import visible GUI string and subtype
284        splitGUIString = self.GUIString.split(":")
285        gs.GUIString = splitGUIString[0]
286        if len(splitGUIString) >= 2:
287            gs.GUISubtypeString = splitGUIString[1]
288
289            # TODO: if loading GUI module fails, retry other modules in
290            # order of preference as provided by the  device module
291
292    def _loadGUIModule(self):
293        """load the GUI module"""
294
295        # add the GUI module folder to path
296        GUIModulesPath = os.path.join(MAIN_MODULES_FOLDER, "gui_modules")
297        sys.path.append(GUIModulesPath)
298        gui = None
299        splitGUIString = self.GUIString.split(":")
300
301        GUIModuleId = splitGUIString[0]
302        if len(splitGUIString) > 1:
303            subtypeId = splitGUIString[1]
304        else:
305            subtypeId = None
306
307        if GUIModuleId == "GTK":
308            gui = self._loadModule("gui_gtk", "gui")
309        elif GUIModuleId == "QML":
310            gui = self._loadModule("gui_qml", "gui")
311        elif GUIModuleId == "QT5":
312            gui = self._loadModule("gui_qt5", "gui")
313
314        # make device module available to the GUI module
315        if gui:
316            gui.setSubtypeId(subtypeId)
317            gui.dmod = self.dmod
318        self.gui = gui
319
320    def _loadModules(self):
321        """load all "normal" (other than device & GUI) modules"""
322
323        log.info("importing modules:")
324        start = time.clock()
325
326        # make shortcut for the loadModule function
327        loadModule = self._loadModule
328
329        # get possible module names
330        moduleNames = self._getModuleNamesFromFolder(MAIN_MODULES_FOLDER)
331        # load if possible
332        for moduleName in moduleNames:
333            # filter out .py
334            moduleName = moduleName.split('.')[0]
335            loadModule(moduleName, moduleName[4:])
336
337        log.info("Loaded all modules in %1.2f ms, initialising" % (1000 * (time.clock() - start)))
338        self.addTime("all modules loaded")
339
340        # make sure all modules have the device module and other variables before first time
341        for m in self.m.values():
342            m.modrana = self  # make this class accessible from modules
343            m.dmod = self.dmod
344
345            # run what needs to be done before firstTime is called
346        self._modulesLoadedPreFirstTime()
347
348        start = time.clock()
349        for m in self.m.values():
350            m.firstTime()
351
352        # run what needs to be done after firstTime is called
353        self._modulesLoadedPostFirstTime()
354
355        log.info( "Initialization complete in %1.2f ms" % (1000 * (time.clock() - start)) )
356
357        # add last timing checkpoint
358        self.addTime("all modules initialized")
359
360    def _getModuleNamesFromFolder(self, folder, prefix='mod_'):
361        """list a given folder and find all possible module names
362        Module names:
363        Module names start with the "mod_" and don't end with .pyc or .pyo.
364        Consequences:
365        Valid modules need to have an existing .py file or be folder-modules
366        (don't name a folder module mod_foo.pyc :) ), even if they are
367        actually loaded from the .pyc or .pyo in the end.
368        This is done so that dangling .pyc/.pyo file from a module
369        that was removed are not loaded by mistake.
370        This situation shouldn't really happen if modRana is installed from a package,
371        as all .pyc files are purged during package upgrade and regenerated."""
372        if USING_QRC:
373            # if we are running from qrc, we need to use the pyotherside function for enumerating
374            # the modules stored in the qrc "bundle"
375            import pyotherside
376            moduleNames = filter(
377                lambda x: x[0:len(prefix)] == prefix, pyotherside.qrc_list_dir(os.path.join("/", folder))
378            )
379        else:
380            moduleNames = filter(
381                lambda x: x[0:len(prefix)] == prefix, os.listdir(folder)
382            )
383
384        # remove the extension
385        moduleNames = map(lambda x: os.path.splitext(x)[0], moduleNames)
386        # return a set of unique module names
387        # * like this, two module names will not be returned if there are
388        # both py and pyc files
389        return set(moduleNames)
390
391    def _listAvailableDeviceModulesByID(self):
392        moduleNames = self._getModuleNamesFromFolder(DEVICE_MODULES_FOLDER, prefix='device_')
393        # remove the device_ prefix and return the results
394        # NOTE: .py, .pyc & .pyo should be removed already in _getModuleNamesFromFolder()
395        # also sort the module names alphabetically
396        return sorted(map(lambda x: x[7:], moduleNames))
397
398    def _listAvailableGUIModulesByID(self):
399        return self._getModuleNamesFromFolder(GUI_MODULES_FOLDER)
400
401    def _loadModule(self, importName, moduleName):
402        """load a single module by name from path"""
403        startM = time.clock()
404        fp = None
405        try:
406            if USING_QRC:
407                # we need to use importlib for importing modules from qrc,
408                # the "old" imp modules seems to be unable to do that
409                import importlib
410                a = importlib.import_module(importName)
411            else:
412                fp, pathName, description = imp.find_module(importName, ALL_MODULE_FOLDERS)
413                a = imp.load_module(importName, fp, pathName, description)
414
415            module = a.getModule(self, moduleName, importName)
416            self.m[moduleName] = module
417            log.info(" * %s: %s (%1.2f ms)",
418                     moduleName,
419                     self.m[moduleName].__doc__,
420                     (1000 * (time.clock() - startM))
421                     )
422            return module
423        except Exception:
424            log.exception("module: %s/%s failed to load", importName, moduleName)
425            return None
426        finally:
427            if fp:
428                fp.close()
429
430    def _optionsLoaded(self):
431        """This is run after the persistent options dictionary is
432        loaded from storage
433        """
434        # tell the log manager what where it should store log files
435        modrana_log.log_manager.log_folder_path = self.paths.getLogFolderPath()
436
437        # check if logging to file should be enabled
438        if self.get('loggingStatus', False):
439            logCompression = self.get('compressLogFile', False)
440            modrana_log.log_manager.enable_log_file(compression=logCompression)
441        else:
442            modrana_log.log_manager.clear_early_log()
443            # tell log manager log file is not needed and that it should
444            # purge early log messages it is storing in case file log is enabled
445
446        # add a watch on the loggingStatus key, so that log file can be enabled
447        # and disabled at runtime with immediate effect
448        self.watch("loggingStatus", self._logFileCB)
449
450    def _logFileCB(self, _key, _oldValue, newValue):
451        """Convenience function turning the log file on or off"""
452        if newValue:
453            logCompression = self.get('compressLogFile', False)
454            modrana_log.log_manager.enable_log_file(compression=logCompression)
455        else:
456            modrana_log.log_manager.disable_log_file()
457
458    def _modulesLoadedPreFirstTime(self):
459        """this is run after all the modules have been loaded,
460        but before their first time is called"""
461
462        # and mode change
463        self.watch('mode', self._modeChangedCB)
464        # cache key modifiers
465        self.keyModifiers = self.d.get('keyModifiers', {})
466        # check if own Quit button is needed
467        if self.gui.showQuitButton():
468            menus = self.m.get('menu', None)
469            if menus:
470                menus.addItem('main', 'Quit', 'quit', 'menu:askQuit')
471
472    def _modulesLoadedPostFirstTime(self):
473        """this is run after all the modules have been loaded,
474        after before their first time is called"""
475
476        # check if redrawing time should be logged
477        if 'showRedrawTime' in self.d and self.d['showRedrawTime'] == True:
478            self.showRedrawTime = True
479
480        # run any tasks specified by CLI arguments
481        self.startup.handlePostFirstTimeTasks()
482
483    def getModule(self, name, default=None):
484        """
485        return a given module instance, return default if no instance
486        with given name is found
487        """
488        return self.m.get(name, default)
489
490    def getModules(self):
491        """
492        return the dictionary of all loaded modules
493        """
494        return self.m
495
496    ## STARTUP AND SHUTDOWN ##
497
498    def _startupDone(self):
499        """called when startup has been finished"""
500
501        # report startup time
502        self.reportStartupTime()
503
504        # check if loading options failed
505        if self.optLoadingOK:
506            self.gui.notify("Loading saved options failed", 7000)
507
508        # start the mainloop or equivalent
509        self.gui.startMainLoop()
510
511    def shutdown(self):
512        """
513        start shutdown cleanup and stop GUI main loop
514        when finished
515        """
516        start_timestamp = time.clock()
517        log.info("Shutting-down modules")
518        for m in self.m.values():
519            m.shutdown()
520        # trigger the shudown signal
521        self.shutdown_signal()
522        self._saveOptions()
523        modrana_log.log_manager.disable_log_file()
524        log.info("Shutdown complete (%s)" % utils.get_elapsed_time_string(start_timestamp))
525
526    ## OPTIONS SETTING AND WATCHING ##
527
528    def get(self, name, default=None, mode=None):
529        """Get an item of data"""
530
531        # check if the value depends on current mode
532        if name in self.keyModifiers.keys():
533            # get the current mode
534            if mode is None:
535                mode = self.d.get('mode', 'car')
536            if mode in self.keyModifiers[name]['modes'].keys():
537                # get the dictionary with per mode values
538                multiDict = self.d.get('%s#multi' % name, {})
539                # return the value for current mode
540                return multiDict.get(mode, default)
541            else:
542                return self.d.get(name, default)
543
544        else:  # just return the normal value
545            return self.d.get(name, default)
546
547    def set(self, name, value, save=False, mode=None):
548        """Set an item of data,
549        if there is a watch set for this key,
550        notify the watcher that its value has changed"""
551
552        oldValue = self.get(name, value)
553
554        if name in self.keyModifiers.keys():
555            # get the current mode
556            if mode is None:
557                mode = self.d.get('mode', 'car')
558                # check if there is a modifier for the current mode
559            if mode in self.keyModifiers[name]['modes'].keys():
560                # save it to the name + #multi key under the mode key
561                try:
562                    self.d['%s#multi' % name][mode] = value
563                except KeyError:  # key not yet created
564                    self.d['%s#multi' % name] = {mode: value}
565            else:  # just save to the key as usual
566                self.d[name] = value
567        else:  # just save to the key as usual
568            self.d[name] = value
569
570        self._notifyWatcher(name, oldValue)
571        # options are normally saved on shutdown,
572        # but for some data we want to make sure they are stored and not
573        # lost for example because of power outage/empty battery, etc.
574        if save:
575            options = self.m.get('options')
576            if options:
577                options.save()
578
579    def optionsKeyExists(self, key):
580        """Report if a given key exists"""
581        return key in self.d.keys()
582
583    def purgeKey(self, key):
584        """remove a key from the persistent dictionary,
585        including possible key modifiers and alternate values"""
586        if key in self.d:
587            oldValue = self.get(key, None)
588            del self.d[key]
589            # purge any key modifiers
590            if key in self.keyModifiers.keys():
591                del self.keyModifiers[key]
592                # also remove the possibly present
593                # alternative states for different modes"""
594                multiKey = "%s#multi" % key
595                if multiKey in self.d:
596                    del self.d[multiKey]
597            self._notifyWatcher(key, oldValue)
598            return True
599        else:
600            log.error("can't purge a not-present key: %s", key)
601
602    def watch(self, key, callback, args=None, runNow=False):
603        """add a callback on an options key
604        callback will get:
605        key, newValue, oldValue, *args
606
607        NOTE: watch ids should be >0, so that they evaluate as True
608        """
609        if not args: args = []
610        nrId = self.maxWatchId + 1
611        id = "%d_%s" % (nrId, key)
612        self.maxWatchId = nrId  # TODO: recycle ids ? (alla PID)
613        if key not in self.watches:
614            self.watches[key] = []  # create the initial list
615        self.watches[key].append((id, callback, args))
616        # should we now run the callback one ?
617        # -> this is useful for modules that configure
618        # themselves according to an options value at startup
619        if runNow:
620            currentValue = self.get(key, None)
621            callback(key, currentValue, currentValue, *args)
622        return id
623
624    def removeWatch(self, id):
625        """remove watch specified by the given watch id"""
626        (nrId, key) = id.split('_')
627        if key in self.watches:
628            remove = lambda x: x[0] == id
629            self.watches[key][:] = [x for x in self.watches[key] if not remove(x)]
630        log.error("can't remove watch - key does not exist, watchId: %s", id)
631
632    def _notifyWatcher(self, key, oldValue):
633        """run callbacks registered on an options key
634        HOW IT WORKS
635        * the watcher is notified before the key is written to the persistent
636        dictionary, so that it can react before the change is visible
637        * the watcher gets the key and both the new and old values
638        """
639        callbacks = self.watches.get(key, None)
640        if callbacks:
641            for item in callbacks:
642                (id, callback, args) = item
643                # rather supply the old value than None
644                newValue = self.get(key, oldValue)
645                if callback:
646                    if callback(key, oldValue, newValue, *args) == False:
647                        # remove watches that return False
648                        self.removeWatch(id)
649                else:
650                    log.error("invalid watcher callback :", callback)
651
652    def addKeyModifier(self, key, modifier=None, mode=None, copyInitialValue=True):
653        """add a key modifier
654        NOTE: currently only used to make value of some keys
655        dependent on the current mode"""
656        options = self.m.get('options', None)
657        # remember the old value, if not se use default from options
658        # if available
659        if options:
660            defaultValue = options.getKeyDefault(key, None)
661        else:
662            defaultValue = None
663        oldValue = self.get(key, defaultValue)
664        if mode is None:
665            mode = self.d.get('mode', 'car')
666        if key not in self.keyModifiers.keys():  # initialize
667            self.keyModifiers[key] = {'modes': {mode: modifier}}
668        else:
669            self.keyModifiers[key]['modes'][mode] = modifier
670
671        # make sure the multi mode dictionary exists
672        multiKey = '%s#multi' % key
673        multiDict = self.d.get(multiKey, {})
674        self.d[multiKey] = multiDict
675
676        # if the modifier is set for the first time,
677        # do we copy the value from the normal key or not ?
678        if copyInitialValue:
679            # check if the key is unset for this mode
680            if mode not in multiDict:
681                # set for first time, copy value
682                self.set(key, self.d.get(key, defaultValue), mode=mode)
683                # notify watchers
684        self._notifyWatcher(key, oldValue)
685
686    def removeKeyModifier(self, key, mode=None):
687        """remove key modifier
688        NOTE: currently this just makes the key independent
689        on the current mode"""
690        # if no mode is provided, use the current one
691        if mode is None:
692            mode = self.d.get('mode', 'car')
693        if key in self.keyModifiers.keys():
694            # just remove the key modifier preserving the alternative values
695            if mode in self.keyModifiers[key]['modes'].keys():
696                # get the previous value
697                options = self.m.get('options', None)
698                # remember the old value, if not se use default from options
699                # if available
700                if options:
701                    defaultValue = options.getKeyDefault(key, None)
702                else:
703                    defaultValue = None
704                oldValue = self.get(key, defaultValue)
705                del self.keyModifiers[key]['modes'][mode]
706                # was this the last key ?
707                if len(self.keyModifiers[key]['modes']) == 0:
708                    # no modes registered - unregister from modifiers
709                    # TODO: handle non-mode modifiers in the future
710                    del self.keyModifiers[key]
711                    # notify watchers
712                self._notifyWatcher(key, oldValue)
713                # done
714                return True
715            else:
716                log.error("can't remove modifier that is not present")
717                log.error("key: %s, mode: %s", key, mode)
718                return False
719        else:
720            log.error("key %s has no modifier and thus cannot be removed", key)
721            return False
722
723    def hasKeyModifier(self, key):
724        """return if a key has a key modifier"""
725        return key in self.keyModifiers.keys()
726
727    def hasKeyModifierInMode(self, key, mode=None):
728        """return if a key has a key modifier"""
729        if mode is None:
730            mode = self.d.get('mode', 'car')
731        if key in self.keyModifiers.keys():
732            return mode in self.keyModifiers[key]['modes'].keys()
733        else:
734            return False
735
736    def notify(self, message, msTimeout=0, icon=""):
737        log.info("modRana notify: %s", message)
738        # trigger the notification signal - this will
739        # trigger an actual notification by one of the
740        # notification systems connected to the notification
741        # signal (if any)
742        self.notificationTriggered(message, msTimeout, icon)
743
744        # notify = self.m.get('notification')
745        # if notify:
746        #     # the notification module counts timeout in seconds
747        #     sTimeout = msTimeout / 1000.0
748        #     notify.handleNotification(message, sTimeout, icon)
749
750    def sendMessage(self, message):
751        m = self.m.get("messages", None)
752        if m is not None:
753            log.info("Sending message: " + message)
754            m.routeMessage(message)
755        else:
756            log.error("No message handler, can't send message.")
757
758    def getModes(self):
759        """return supported modes"""
760        modes = {
761            'cycle': 'Cycle',
762            'walk': 'Foot',
763            'car': 'Car',
764            'train': 'Train',
765            'bus': 'Bus',
766        }
767        return modes
768
769    def getModeLabel(self, modeName):
770        """get a label for a given mode"""
771        try:
772            return self.getModes()[modeName]
773        except KeyError:
774            log.error('mode %s does not exist and thus has no label' % modeName)
775            return None
776
777    def _modeChangedCB(self, key=None, oldMode=None, newMode=None):
778        """handle mode change in regards to key modifiers and option key watchers"""
779        # get keys that have both a key modifier and a watcher
780        keys = filter(lambda x: x in self.keyModifiers.keys(), self.watches.keys())
781        # filter out only those keys that have a modifier for the new mode
782        # or had a modifier in the previous mode
783        # otherwise their value would not change and thus
784        # triggering a watch is not necessary
785        keys = filter(
786            lambda x: newMode in self.keyModifiers[x]['modes'].keys() or oldMode in self.keyModifiers[x][
787                'modes'].keys(),
788            keys)
789        for key in keys:
790            # try to get some value if the old value is not available
791            options = self.m.get('options', None)
792            # remember the old value, if not se use default from options
793            # if available
794            if options:
795                defaultValue = options.getKeyDefault(key, None)
796            else:
797                defaultValue = None
798            oldValue = self.get(key, defaultValue)
799            # notify watchers
800            self._notifyWatcher(key, oldValue)
801
802    def _removeNonPersistentOptions(self, inputDict):
803        """keys that begin with # are not saved
804        (as they mostly contain data that is either time sensitive or is
805        reloaded on startup)
806        ASSUMPTION: keys are strings of length>=1"""
807        try:
808            return dict((k, v) for k, v in six.iteritems(inputDict) if k[0] != '#')
809        except Exception:
810            log.exception('options: error while filtering options\nsome nonpersistent keys might have been left in\nNOTE: keys should be strings of length>=1')
811            return self.d
812
813    def _saveOptions(self):
814        """save the persistent dictionary to file"""
815        log.info("saving options")
816        try:
817            f = open(self.paths.getOptionsFilePath(), "wb")
818            # remove keys marked as nonpersistent
819            self.d['keyModifiers'] = self.keyModifiers
820            d = self._removeNonPersistentOptions(self.d)
821            marshal.dump(d, f, 2)
822            f.close()
823            log.info("options successfully saved")
824        except IOError:
825            log.exception("can't save options")
826        except Exception:
827            log.exception("saving options failed")
828
829    def _loadOptions(self):
830        """load the persistent dictionary from file"""
831        log.info("loading options")
832        try:
833            f = open(self.paths.getOptionsFilePath(), "rb")
834            newData = marshal.load(f)
835            f.close()
836            purgeKeys = ["fix"]
837            for key in purgeKeys:
838                if key in newData:
839                    del newData[key]
840            for k, v in newData.items():
841                self.set(k, v)
842            success = True
843            #print("Options content")
844            #for key, value in newData.iteritems():
845            #    print(key, value)
846
847
848        except Exception:
849            e = sys.exc_info()[1]
850            log.exception("exception while loading saved options")
851            # TODO: a yes/no dialog for clearing (renaming with timestamp :) the corrupted options file (options.bin)
852            success = False
853
854        self.overrideOptions()
855        return success
856
857    def overrideOptions(self):
858        """
859        without this, there would not be any projection values at start,
860        because modRana does not know, what part of the map to show
861        """
862        self.set('centred', True)  # set centering to True at start to get setView to run
863        self.set('editBatchMenuActive', False)
864
865    ## PROFILE PATH ##
866
867    def getProfilePath(self):
868        """return the profile folder (create it if it does not exist)
869        NOTE: this function is provided here in the main class as some
870        ordinary modRana modules need to know the profile folder path before the
871        option module that normally handles it is fully initialized
872        (for example the config module might need to copy default
873        configuration files to the profile folder in its init)
874        """
875        # get the path
876        modRanaProfileFolderName = '.modrana'
877        userHomePath = os.getenv("HOME", "")
878        profileFolderPath = os.path.join(userHomePath, modRanaProfileFolderName)
879        # make sure it exists
880        utils.createFolderPath(profileFolderPath)
881        # return it
882        return profileFolderPath
883
884    ## STARTUP TIMING ##
885
886    def addTime(self, message):
887        timestamp = time.time()
888        self.timing.append((message, timestamp))
889        return timestamp
890
891    def addCustomTime(self, message, timestamp):
892        self.timing.append((message, timestamp))
893        return timestamp
894
895    def reportStartupTime(self):
896        if self.timing:
897            log.info("** modRana startup timing **")
898
899            # log device identificator and name
900            if self.dmod:
901                deviceName = self.dmod.getDeviceName()
902                deviceString = self.dmod.getDeviceIDString()
903                log.info("# device: %s (%s)" % (deviceName, deviceString))
904
905            tl = self.timing
906            startupTime = tl[0][1] * 1000
907            lastTime = startupTime
908            totalTime = (tl[-1][1] * 1000) - startupTime
909            for i in tl:
910                (message, t) = i
911                t *= 1000  # convert to ms
912                timeSpent = t - lastTime
913                timeSinceStart = t - startupTime
914                log.info("* %s (%1.0f ms), %1.0f/%1.0f ms", message, timeSpent, timeSinceStart, totalTime)
915                lastTime = t
916            log.info("** whole startup: %1.0f ms **" % totalTime)
917        else:
918            log.info("* timing list empty *")
919
920modrana = None
921dmod = None
922gui = None
923
924
925def start(argv=None):
926    """This function is used when starting modRana with PyOtherSide.
927    When modRana is started from PyOtherSide there is no sys.argv,
928    so QML needs to pass it from its side.
929
930    :param list argv: arguments the program got on cli or arguments
931                      injected by QML
932    """
933    if not argv: argv = []
934    # only assign fake values to argv if argv is empty or missing,
935    # so that real command line arguments are not overwritten
936    if not hasattr(sys, "argv") or not isinstance(sys.argv, list) or not sys.argv:
937        log.debug("argv from QML:\n%s", argv)
938        sys.argv = ["modrana.py"]
939    # only log full argv if it was extended
940    if argv:
941        sys.argv.extend(argv)
942        log.debug("full argv:\n%s", sys.argv)
943
944    global modrana
945    global dmod
946    global gui
947    modrana = ModRana()
948    dmod = modrana.dmod
949    gui = modrana.gui
950
951if __name__ == "__main__":
952    # check if reload has been requested
953    reloadArg = "--reload"
954    if len(sys.argv) >= 3 and sys.argv[1] == reloadArg:
955        # following argument is path to the modRana main class we want to reload to,
956        # optionally followed by any argument for the main class
957        log.info(" == modRana Reloading == ")
958        reloadPath = sys.argv[2]
959        callArgs = [reloadPath]
960        callArgs.extend(sys.argv[3:])
961        subprocess.call(callArgs)
962    else:
963        start()
Note: See TracBrowser for help on using the repository browser.