From 28a04462b0ddfb0d332cf07740fa1f6260989f06 Mon Sep 17 00:00:00 2001
From: Yup Yup
Date: Sun, 22 Dec 2024 03:46:54 -0500
Subject: [PATCH 1/2] Test files for creepy fork
---
creepy1.js | 0
creepy2.htm | 0
project/prjtTest1.js | 0
project/prjtcreep.htm | 0
4 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 creepy1.js
create mode 100644 creepy2.htm
create mode 100644 project/prjtTest1.js
create mode 100644 project/prjtcreep.htm
diff --git a/creepy1.js b/creepy1.js
new file mode 100644
index 0000000..e69de29
diff --git a/creepy2.htm b/creepy2.htm
new file mode 100644
index 0000000..e69de29
diff --git a/project/prjtTest1.js b/project/prjtTest1.js
new file mode 100644
index 0000000..e69de29
diff --git a/project/prjtcreep.htm b/project/prjtcreep.htm
new file mode 100644
index 0000000..e69de29
From ac45268e2aca95b1493a7ac475fed4ac5b8db35b Mon Sep 17 00:00:00 2001
From: danngdawg
Date: Sun, 22 Dec 2024 05:21:05 -0500
Subject: [PATCH 2/2] Update creepy1.js
---
creepy1.js | 926 +++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 926 insertions(+)
diff --git a/creepy1.js b/creepy1.js
index e69de29..5ee7f1c 100644
--- a/creepy1.js
+++ b/creepy1.js
@@ -0,0 +1,926 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+import codecs
+import sys
+import datetime
+import os
+import logging
+import shelve
+import functools
+import urllib2
+import webbrowser
+import pytz
+from distutils.version import StrictVersion
+from PyQt4.QtCore import QString, QThread, SIGNAL, QUrl, QDateTime, QDate, QRect, Qt
+from PyQt4.QtGui import QMainWindow, QApplication, QMessageBox, QFileDialog, QWidget, QScrollArea, QVBoxLayout, QIcon, \
+ QTableWidgetItem, QAbstractItemView
+from PyQt4.QtGui import QHBoxLayout, QLabel, QLineEdit, QCheckBox, QPushButton, QStackedWidget, QGridLayout, QMenu, \
+ QPixmap
+from PyQt4.QtWebKit import QWebPage, QWebSettings
+from dominate import document
+from ui.CreepyUI import Ui_CreepyMainWindow
+from yapsy.PluginManager import PluginManagerSingleton
+from models.LocationsList import LocationsTableModel
+from models.Project import Project
+from models.Location import Location
+from models.PluginConfigurationListModel import PluginConfigurationListModel
+from models.ProjectWizardPluginListModel import ProjectWizardPluginListModel
+from models.ProjectWizardSelectedTargetsTable import ProjectWizardSelectedTargetsTable
+from models.InputPlugin import InputPlugin
+from models.ProjectTree import ProjectNode, LocationsNode, ProjectTreeModel, ProjectTreeNode, AnalysisNode
+from components.PersonProjectWizard import PersonProjectWizard
+from components.PlaceProjectWizard import PlaceProjectWizard
+from components.PluginsConfigurationDialog import PluginsConfigurationDialog
+from components.FilterLocationsDateDialog import FilterLocationsDateDialog
+from components.FilterLocationsPointDialog import FilterLocationsPointDialog
+from components.FilterLocationsCustomDialog import FilterLocationsCustomDialog
+from components.AboutDialog import AboutDialog
+from components.VerifyDeleteDialog import VerifyDeleteDialog
+from components.UpdateCheckDialog import UpdateCheckDialog
+from utilities import GeneralUtilities, QtHandler
+from dominate.tags import *
+from utilities import XStream
+
+
+# set up logging
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.DEBUG)
+fh = logging.FileHandler(os.path.join(GeneralUtilities.getLogDir(), 'creepy_main.log'))
+fh.setLevel(logging.DEBUG)
+formatter = logging.Formatter('%(levelname)s:%(asctime)s In %(filename)s:%(lineno)d: %(message)s')
+fh.setFormatter(formatter)
+guiLoggingHandler = QtHandler.QtHandler()
+guiLoggingHandler.setFormatter(formatter)
+logger.addHandler(fh)
+logger.addHandler(guiLoggingHandler)
+# Capture stderr and stdout to a file
+sys.stdout = open(os.path.join(GeneralUtilities.getLogDir(), 'creepy_stdout.log'), 'w')
+sys.stderr = open(os.path.join(GeneralUtilities.getLogDir(), 'creepy_stderr.log'), 'w')
+try:
+ _fromUtf8 = QString.fromUtf8
+except AttributeError:
+ _fromUtf8 = lambda s: s
+
+
+class MainWindow(QMainWindow):
+ class AnalyzeProjectThread(QThread):
+ def __init__(self, project):
+ QThread.__init__(self)
+ self.project = project
+
+ def buildAnalysisPage(self):
+ analysisDocument = document()
+ analysisDocument.add(link(rel='stylesheet', href='http://yui.yahooapis.com/pure/0.5.0/pure-min.css'))
+ return analysisDocument
+
+ def run(self):
+ pluginManager = PluginManagerSingleton.get()
+ pluginManager.setCategoriesFilter({'Input': InputPlugin})
+ pluginManager.setPluginPlaces(GeneralUtilities.getPluginDirs())
+ pluginManager.locatePlugins()
+ pluginManager.loadPlugins()
+ locationsList = []
+ analysisDocument = self.buildAnalysisPage()
+ menuDiv = div(id='menu', cls='pure-menu pure-menu-open pure-menu-horizontal')
+ menuDivList = ul()
+ for target in self.project.selectedTargets:
+ menuDivList += li(a(str(target['pluginName']), href='#' + str(target['pluginName'])), __inline=True)
+ menuDiv.add(menuDivList)
+ analysisDocument.add(menuDiv)
+ for target in self.project.selectedTargets:
+ pluginObject = pluginManager.getPluginByName(target['pluginName'], 'Input').plugin_object
+ for pl in self.project.enabledPlugins:
+ if pl['pluginName'] == target['pluginName']:
+ runtimeConfig = pl['searchOptions']
+ if self.project.projectType == 'place':
+ targetLocations = pluginObject.searchForResultsNearPlace(','.join(target['poi']))
+ # No analysis page yet for place based projects
+ targetAnalysisDiv = ''
+ else:
+ targetLocations, targetAnalysisDiv = pluginObject.returnAnalysis(target, runtimeConfig)
+ if targetAnalysisDiv:
+ analysisDocument += targetAnalysisDiv
+ if targetLocations:
+ for loc in targetLocations:
+ location = Location()
+ location.plugin = loc['plugin']
+ location.datetime = loc['date']
+ location.longitude = loc['lon']
+ location.latitude = loc['lat']
+ location.context = loc['context']
+ location.infowindow = loc['infowindow']
+ location.shortName = loc['shortName']
+ location.accuracy = loc['accuracy']
+ location.username = loc['username']
+ location.updateId()
+ location.visible = True
+ locationsList.append(location)
+ # remove duplicates locations if any
+ for l in locationsList:
+ if l.id not in [loc.id for loc in self.project.locations]:
+ self.project.locations.append(l)
+ # sort on date
+ self.project.locations.sort(key=lambda x: x.datetime, reverse=True)
+ # Add analysis document on project
+ self.project.analysisDocument = analysisDocument
+ logger.debug('Analysis thread finished for all targets.')
+ # Emit the signal that we are done
+ self.emit(SIGNAL('locations(PyQt_PyObject)'), self.project)
+
+ def __init__(self, parent=None):
+ self.version = '1.5'
+ QWidget.__init__(self, parent)
+ self.ui = Ui_CreepyMainWindow()
+ self.ui.setupUi(self)
+ # Create folders for projects and temp if they do not exist
+ GeneralUtilities.getProjectsDir()
+ GeneralUtilities.getTempDir()
+ self.projectsList = []
+ self.currentProject = None
+ self.ui.mapWebPage = QWebPage()
+ self.ui.mapWebPage.mainFrame().setUrl(QUrl(os.path.join(GeneralUtilities.getIncludeDir(), 'map.html')))
+ self.ui.mapWebPage.setLinkDelegationPolicy(QWebPage.DelegateAllLinks)
+ self.ui.mapWebPage.linkClicked.connect(self.linkClicked)
+ self.ui.mapWebView.setPage(self.ui.mapWebPage)
+ self.ui.analysisWebPage = QWebPage()
+ self.ui.analysisWebView.setPage(self.ui.analysisWebPage)
+ self.ui.menuView.addAction(self.ui.dockWProjects.toggleViewAction())
+ self.ui.menuView.addAction(self.ui.dockWLocationsList.toggleViewAction())
+ self.ui.menuView.addAction(self.ui.dockWCurrentLocationDetails.toggleViewAction())
+ self.ui.menuView.addAction(self.ui.dockWLogging.toggleViewAction())
+ # Connect all the signals
+ self.ui.actionPluginsConfiguration.triggered.connect(self.showPluginsConfigurationDialog)
+ self.ui.actionNewPersonProject.triggered.connect(self.showPersonProjectWizard)
+ self.ui.actionNewPlaceProject.triggered.connect(self.showPlaceProjectWizard)
+ self.ui.actionAnalyzeCurrentProject.triggered.connect(self.analyzeProject)
+ self.ui.actionReanalyzeCurrentProject.triggered.connect(self.analyzeProject)
+ self.ui.actionDrawCurrentProject.triggered.connect(self.presentLocations)
+ self.ui.actionDrawCurrentProject.triggered.connect(self.presentAnalysis)
+ self.ui.actionExportCSV.triggered.connect(self.exportProjectCSV)
+ self.ui.actionExportKML.triggered.connect(self.exportProjectKML)
+ self.ui.actionExportFilteredCSV.triggered.connect(functools.partial(self.exportProjectCSV, filtering=True))
+ self.ui.actionExportFilteredKML.triggered.connect(functools.partial(self.exportProjectKML, filtering=True))
+ self.ui.actionDeleteCurrentProject.triggered.connect(self.deleteCurrentProject)
+ self.ui.actionFilterLocationsDate.triggered.connect(self.showFilterLocationsDateDialog)
+ self.ui.actionFilterLocationsPosition.triggered.connect(self.showFilterLocationsPointDialog)
+ self.ui.actionFilterLocationsCustom.triggered.connect(self.showFilterLocationsCustomDialog)
+ self.ui.actionFilterInaccurateLocations.triggered.connect(self.filterOnlyAccurateLocations)
+ self.ui.actionRemoveFilters.triggered.connect(self.removeAllFilters)
+ self.ui.actionShowHeatMap.toggled.connect(self.toggleHeatMap)
+ self.ui.actionReportProblem.triggered.connect(GeneralUtilities.reportProblem)
+ self.ui.actionAbout.triggered.connect(self.showAboutDialog)
+ self.ui.actionCheckUpdates.triggered.connect(self.checkForUpdatedVersion)
+ self.ui.actionExit.triggered.connect(self.close)
+ self.ui.treeViewProjects.doubleClicked.connect(self.doubleClickProjectItem)
+ self.ui.treeViewProjects.setContextMenuPolicy(Qt.CustomContextMenu)
+ self.ui.treeViewProjects.customContextMenuRequested.connect(self.showRightClickMenu)
+ self.ui.treeViewProjects.clicked.connect(self.currentProjectChanged)
+ self.ui.locationsTableView.clicked.connect(self.updateCurrentLocationDetails)
+ self.ui.locationsTableView.activated.connect(self.updateCurrentLocationDetails)
+ self.ui.locationsTableView.doubleClicked.connect(self.doubleClickLocationItem)
+ XStream.XStream.stdout().messageWritten.connect(self.ui.loggingContents.insertPlainText)
+ XStream.XStream.stderr().messageWritten.connect(self.ui.loggingContents.insertPlainText)
+ # default to showing the map
+ self.changeMainWidgetPage('map')
+ self.loadProjectsFromStorage()
+
+ def checkForUpdatedVersion(self):
+ """
+ Checks www.geocreepy.com for an updated version and returns a tuple with the
+ result and the latest version number
+ """
+ try:
+ latestVersion = urllib2.urlopen('http://www.geocreepy.com/version.html').read().rstrip()
+
+ updateCheckDialog = UpdateCheckDialog()
+ updateCheckDialog.ui.versionsTableWidget.setHorizontalHeaderLabels(
+ ('', 'Component', 'Status', 'Installed', 'Available'))
+ updateCheckDialog.ui.versionsTableWidget.setItem(0, 1, QTableWidgetItem('Creepy'))
+ if StrictVersion(latestVersion) > StrictVersion(self.version):
+ updateCheckDialog.ui.versionsTableWidget.setItem(0, 0, QTableWidgetItem(
+ QIcon(QPixmap(':/creepy/exclamation')), ''))
+ updateCheckDialog.ui.versionsTableWidget.setItem(0, 2, QTableWidgetItem('Outdated'))
+ updateCheckDialog.ui.dlNewVersionLabel.setText(
+ 'Download the latest version from geocreepy.com
')
+ else:
+ updateCheckDialog.ui.versionsTableWidget.setItem(0, 0,
+ QTableWidgetItem(QIcon(QPixmap(':/creepy/tick')), ''))
+ updateCheckDialog.ui.versionsTableWidget.setItem(0, 2, QTableWidgetItem('Up To Date'))
+ updateCheckDialog.ui.dlNewVersionLabel.setText(
+ '
You are already using the latest version of creepy.
')
+ updateCheckDialog.ui.versionsTableWidget.setItem(0, 3, QTableWidgetItem(self.version))
+ updateCheckDialog.ui.versionsTableWidget.setItem(0, 4, QTableWidgetItem(latestVersion))
+ updateCheckDialog.show()
+ updateCheckDialog.exec_()
+ except Exception, err:
+ if type(err) == 'string':
+ mes = err
+ else:
+ mes = err.message
+ self.showWarning(self.trUtf8('Error checking for updates'), mes)
+
+ def showFilterLocationsPointDialog(self):
+ filterLocationsPointDialog = FilterLocationsPointDialog()
+ filterLocationsPointDialog.ui.mapPage = QWebPage()
+ myPyObj = filterLocationsPointDialog.PyObj()
+ filterLocationsPointDialog.ui.mapPage.mainFrame().addToJavaScriptWindowObject('myPyObj', myPyObj)
+ filterLocationsPointDialog.ui.mapPage.mainFrame().setUrl(
+ QUrl(os.path.join(GeneralUtilities.getIncludeDir(), 'mapSetPoint.html')))
+ filterLocationsPointDialog.ui.radiusUnitComboBox.insertItem(0, QString('km'))
+ filterLocationsPointDialog.ui.radiusUnitComboBox.insertItem(1, QString('m'))
+ filterLocationsPointDialog.ui.radiusUnitComboBox.activated[str].connect(
+ filterLocationsPointDialog.onUnitChanged)
+ filterLocationsPointDialog.ui.webView.setPage(filterLocationsPointDialog.ui.mapPage)
+ filterLocationsPointDialog.show()
+ if filterLocationsPointDialog.exec_():
+ r = filterLocationsPointDialog.ui.radiusSpinBox.value()
+ if filterLocationsPointDialog.unit == 'km':
+ radius = r * 1000
+ else:
+ radius = r
+ if hasattr(myPyObj, 'lat') and hasattr(myPyObj, 'lng') and radius:
+ self.filterLocationsByPoint(myPyObj.lat, myPyObj.lng, radius)
+
+ def showFilterLocationsDateDialog(self):
+ filterLocationsDateDialog = FilterLocationsDateDialog()
+ filterLocationsDateDialog.ui.endDateCalendarWidget.setMaximumDate(QDate.currentDate())
+ filterLocationsDateDialog.show()
+ if filterLocationsDateDialog.exec_():
+ startDateTime = pytz.utc.localize(
+ QDateTime(filterLocationsDateDialog.ui.stardateCalendarWidget.selectedDate(),
+ filterLocationsDateDialog.ui.startDateTimeEdit.time()).toPyDateTime())
+ endDateTime = pytz.utc.localize(QDateTime(filterLocationsDateDialog.ui.endDateCalendarWidget.selectedDate(),
+ filterLocationsDateDialog.ui.endDateTimeEdit.time()).toPyDateTime())
+ logger.debug('Filtering based on dates between : ' + startDateTime.strftime(
+ '%Y-%m-%d %H:%M:%S %z') + ' ' + endDateTime.strftime('%Y-%m-%d %H:%M:%S %z'))
+ if startDateTime > endDateTime:
+ self.showWarning(self.trUtf8('Invalid Dates'), self.trUtf8(
+ 'The start date needs to be before the end date. Please try again !
'))
+ else:
+ self.filterLocationsByDate(startDateTime, endDateTime)
+
+ def showFilterLocationsCustomDialog(self):
+ filterLocationsCustomDialog = FilterLocationsCustomDialog()
+ filterLocationsCustomDialog.ui.daysOfWeekListWidget.setSelectionMode(QAbstractItemView.MultiSelection)
+ filterLocationsCustomDialog.ui.monthsOfYearListWidget.setSelectionMode(QAbstractItemView.MultiSelection)
+ filterLocationsCustomDialog.ui.hoursOfDayListWidget.setSelectionMode(QAbstractItemView.MultiSelection)
+ daysOfWeek = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
+ for dayOfWeek in daysOfWeek:
+ filterLocationsCustomDialog.ui.daysOfWeekListWidget.addItem(dayOfWeek)
+ monthsOfYear = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September',
+ 'October', 'November', 'December']
+ for monthOfYear in monthsOfYear:
+ filterLocationsCustomDialog.ui.monthsOfYearListWidget.addItem(monthOfYear)
+ for hourOfDay in range(24):
+ filterLocationsCustomDialog.ui.hoursOfDayListWidget.addItem(str(hourOfDay))
+ if filterLocationsCustomDialog.exec_():
+ days = [daysOfWeek.index(str(selItem.text())) for selItem in
+ filterLocationsCustomDialog.ui.daysOfWeekListWidget.selectedItems()]
+ hours = [int(selItem.text()) for selItem in
+ filterLocationsCustomDialog.ui.hoursOfDayListWidget.selectedItems()]
+ months = [monthsOfYear.index(str(selItem.text())) + 1 for selItem in
+ filterLocationsCustomDialog.ui.monthsOfYearListWidget.selectedItems()]
+ if len(days) == 0 and len(hours) == 0 and len(months) == 0:
+ self.showWarning(self.trUtf8('Invalid Selection'), self.trUtf8(
+ 'Please select at least one of the criteria ! '))
+ else:
+ self.filterLocationsCustom(days, months, hours)
+
+ def filterLocationsByDate(self, startDate, endDate):
+ if not self.currentProject:
+ self.showWarning(self.trUtf8('No project selected'), self.trUtf8('Please select a project !'))
+ self.ui.statusbar.showMessage(self.trUtf8('Please select a project !'))
+ return
+ for l in self.currentProject.locations:
+ if startDate < l.datetime < endDate:
+ l.visible = True
+ else:
+ l.visible = False
+ self.presentLocations([])
+
+ def filterLocationsCustom(self, days, months, hours):
+ if not self.currentProject:
+ self.showWarning(self.trUtf8('No project selected'), self.trUtf8('Please select a project !'))
+ self.ui.statusbar.showMessage(self.trUtf8('Please select a project !'))
+ return
+ for l in self.currentProject.locations:
+ if l.datetime.weekday() in days and l.datetime.month in months and l.datetime.hour in hours:
+ l.visible = True
+ else:
+ l.visible = False
+ self.presentLocations([])
+
+ def filterLocationsByPoint(self, lat, lng, radius):
+ if not self.currentProject:
+ self.showWarning(self.trUtf8('No project selected'), self.trUtf8('Please select a project !'))
+ self.ui.statusbar.showMessage(self.trUtf8('Please select a project !'))
+ return
+ for l in self.currentProject.locations:
+ if GeneralUtilities.calcDistance(float(lat), float(lng), float(l.latitude), float(l.longitude)) > radius:
+ l.visible = False
+ self.presentLocations([])
+
+ def filterOnlyAccurateLocations(self):
+ if not self.currentProject:
+ self.showWarning(self.trUtf8('No project selected'), self.trUtf8('Please select a project !'))
+ self.ui.statusbar.showMessage(self.trUtf8('Please select a project !'))
+ return
+ for l in self.currentProject.locations:
+ if l.accuracy == 'low':
+ l.visible = False
+ self.presentLocations([])
+
+ def removeAllFilters(self):
+ if not self.currentProject:
+ self.showWarning(self.trUtf8('No project selected'), self.trUtf8('Please select a project !'))
+ self.ui.statusbar.showMessage(self.trUtf8('Please select a project !'))
+ return
+ for l in self.currentProject.locations:
+ l.visible = True
+ self.presentLocations([])
+
+ def showAboutDialog(self):
+ aboutDialog = AboutDialog()
+ aboutDialog.show()
+ if aboutDialog.exec_():
+ pass
+
+ def showWarning(self, title, text):
+ QMessageBox.warning(self, title, text, None)
+
+ def toggleHeatMap(self, checked):
+ mapFrame = self.ui.mapWebPage.mainFrame()
+ if checked:
+ mapFrame.evaluateJavaScript('showHeatmap()')
+ mapFrame.evaluateJavaScript('hideMarkers()')
+ else:
+ mapFrame.evaluateJavaScript('showMarkers()')
+ mapFrame.evaluateJavaScript('hideHeatmap()')
+
+ def hideMarkers(self):
+ mapFrame = self.ui.mapWebPage.mainFrame()
+ mapFrame.evaluateJavaScript('hideMarkers()')
+
+ def showMarkers(self):
+ mapFrame = self.ui.mapWebPage.mainFrame()
+ mapFrame.evaluateJavaScript('showMarkers()')
+
+ def addMarkerToMap(self, mapFrame, location):
+ mapFrame.evaluateJavaScript(QString('addMarker(' + str(location.latitude) + ',' + str(location.longitude) +
+ ',\"' + location.infowindow + '\",\"' + location.plugin + '\",\"' +
+ location.accuracy + '\")'))
+
+ def refreshMap(self, mapFrame):
+ mapFrame.evaluateJavaScript(QString('refreshMap()'))
+
+ def centerMap(self, mapFrame, location):
+ mapFrame.evaluateJavaScript(
+ QString('centerMap(' + str(location.latitude) + ',' + str(location.longitude) + ')'))
+
+ def setMapZoom(self, mapFrame, level):
+ mapFrame.evaluateJavaScript(QString('setZoom(' + str(level) + ')'))
+
+ def clearMarkers(self, mapFrame):
+ mapFrame.evaluateJavaScript(QString('clearMarkers()'))
+
+ def linkClicked(self, link):
+ webbrowser.open(link.toEncoded(), new=1)
+
+ def deleteCurrentProject(self, project):
+ if not project:
+ project = self.currentProject
+ if project.isAnalysisRunning:
+ self.showWarning(self.trUtf8('Cannot Edit Project'), self.trUtf8(
+ 'Please wait until analysis is finished before performing further actions on the project'))
+ return
+ projectName = project.projectName + '.db'
+ verifyDeleteDialog = VerifyDeleteDialog()
+ verifyDeleteDialog.ui.label.setText(
+ unicode(verifyDeleteDialog.ui.label.text()).replace('@project@', project.projectName))
+ verifyDeleteDialog.show()
+ if verifyDeleteDialog.exec_():
+ project.deleteProject(projectName)
+ self.loadProjectsFromStorage()
+
+ def exportProjectCSV(self, project, filtering=False):
+ # If the project was not provided explicitly, analyze the currently selected one
+ if not project:
+ project = self.currentProject
+ if not project:
+ self.showWarning(self.trUtf8('No project selected'), self.trUtf8('Please select a project first'))
+ self.ui.statusbar.showMessage(self.trUtf8('Please select a project first'))
+ return
+ if not project.locations:
+ self.showWarning(self.trUtf8('No locations found'),
+ self.trUtf8('The selected project has no locations to be exported'))
+ self.ui.statusbar.showMessage(self.trUtf8('The selected project has no locations to be exported'))
+ return
+ fileName = QFileDialog.getSaveFileName(None, self.trUtf8('Save CSV export as...'), os.getcwd(),
+ 'CSV files (*.csv)')
+ if fileName:
+ try:
+ fileobj = codecs.open(fileName, 'wb', encoding='utf8')
+ linesList = []
+ linesList.append(
+ '"Timestamp","Latitude","Longitude","Accuracy",'
+ '"Location Name","Retrieved from","Username","Context"')
+ for loc in project.locations:
+ if (filtering and loc.visible) or not filtering:
+ linesList.append(u'"{0:s}","{1:s}","{2:s}","{3:s}","{4:s}","{5:s}","{6:s}","{7:s}"'.format(
+ loc.datetime.strftime('%Y-%m-%d %H:%M:%S %z'), str(loc.latitude), str(loc.longitude),
+ loc.accuracy,
+ loc.shortName, loc.plugin, loc.username, loc.context))
+ csv_string = '\n'.join(linesList)
+ fileobj.write(csv_string)
+ fileobj.close()
+ self.ui.statusbar.showMessage(self.trUtf8('Project Locations have been exported successfully'))
+ except Exception, err:
+ logger.error(err)
+ self.ui.statusbar.showMessage(self.trUtf8('Error saving the export.'))
+
+ def exportProjectKML(self, project, filtering=False):
+ # If the project was not provided explicitly, analyze the currently selected one
+ if not project:
+ project = self.currentProject
+ if not project:
+ self.showWarning(self.trUtf8('No project selected'), self.trUtf8('Please select a project first'))
+ self.ui.statusbar.showMessage(self.trUtf8('Please select a project first'))
+ return
+ if not project.locations:
+ self.showWarning(self.trUtf8('No locations found'),
+ self.trUtf8('The selected project has no locations to be exported'))
+ self.ui.statusbar.showMessage(self.trUtf8('The selected project has no locations to be exported'))
+ return
+
+ fileName = QFileDialog.getSaveFileName(None, self.trUtf8('Save KML export as...'), os.getcwd(),
+ 'KML files (*.kml)')
+ if fileName:
+ try:
+ fileobj = codecs.open(fileName, 'wb', encoding='utf8')
+ # kml is the list to hold all xml attribs. it will be joined in a string later
+ kml = []
+ kml.append('')
+ kml.append('')
+ kml.append('')
+ kml.append(' {0:s}.kml'.format(project.projectName))
+ for plugin in project.enabledPlugins:
+ plugin_name = plugin['pluginName'].lower().replace('plugin', '').rstrip()
+ kml.append(' ')
+ kml.append(' ')
+ for loc in project.locations:
+ if (filtering and loc.visible) or not filtering:
+ kml.append(' ')
+ kml.append(u' {0:s}'.format(GeneralUtilities.html_escape(loc.shortName)))
+
+ kml.append(u' {0:s}'.format(GeneralUtilities.html_escape(loc.context)))
+ kml.append(' ')
+ kml.append(' #{0}_{1}'.format(loc.plugin, loc.accuracy))
+ kml.append(' ')
+ kml.append(
+ ' {0:s}, {1:s}, 0'.format(str(loc.longitude),
+ str(loc.latitude)))
+ kml.append(' ')
+ kml.append(' ')
+ kml.append('')
+ kml.append('')
+
+ kml_string = '\n'.join(kml)
+ fileobj.write(kml_string)
+ fileobj.close()
+ self.ui.statusbar.showMessage(self.trUtf8('Project Locations have been exported successfully'))
+ except Exception, err:
+ logger.error(err)
+ self.ui.statusbar.showMessage(self.trUtf8('Error saving the export.'))
+
+ def analyzeProject(self, project):
+ """
+ This is called when the user clicks on "Analyze Target". It starts the background thread that
+ analyzes targets and returns locations
+ """
+ # If the project was not provided explicitly, analyze the currently selected one
+ if not project:
+ project = self.currentProject
+ if project:
+ if project.isAnalysisRunning:
+ self.showWarning(self.trUtf8('Cannot Edit Project'), self.trUtf8(
+ 'Please wait until analysis is finished before performing further actions on the project'))
+ return
+ self.ui.statusbar.showMessage(self.trUtf8('Analyzing project for locations. Please wait...'))
+ project.isAnalysisRunning = True
+ self.analyzeProjectThreadInstance = self.AnalyzeProjectThread(project)
+ self.connect(self.analyzeProjectThreadInstance, SIGNAL('locations(PyQt_PyObject)'),
+ self.projectAnalysisFinished)
+ self.analyzeProjectThreadInstance.start()
+ else:
+ self.showWarning(self.trUtf8('No project selected'), self.trUtf8('Please select a project !'))
+
+ def projectAnalysisFinished(self, project):
+ """
+ Called when the analysis thread finishes. It saves the project with the locations and draws the map
+ """
+ self.ui.statusbar.showMessage(self.trUtf8('Project Analysis complete !'))
+ projectNode = ProjectNode(project.projectName, project)
+ locationsNode = LocationsNode(self.trUtf8('Locations'), projectNode)
+ analysisNode = AnalysisNode(self.trUtf8('Analysis'), projectNode)
+ project.isAnalysisRunning = False
+ project.storeProject(projectNode)
+ # If the analysis produced no results whatsoever, inform the user
+ if not project.locations:
+ self.showWarning(self.trUtf8('No Locations Found'),
+ self.trUtf8('We could not find any locations for the analyzed project'))
+ else:
+ self.presentLocations(project.locations)
+ self.presentAnalysis(project.analysisDocument)
+
+ def presentAnalysis(self, analysisDocument):
+ """
+
+ :param analysisDocument:
+ """
+ if not analysisDocument:
+ if not self.currentProject:
+ self.showWarning(self.trUtf8('No project selected'), self.trUtf8('Please select a project !'))
+ self.ui.statusbar.showMessage(self.trUtf8('Please select a project !'))
+ return
+ else:
+ analysisDocument = self.currentProject.analysisDocument
+ analysisFrame = self.ui.analysisWebPage.mainFrame()
+ analysisFrame.setHtml(QString(unicode(analysisDocument)),
+ QUrl('file://' + os.path.join(os.getcwd(), 'include/')))
+
+ def presentLocations(self, locations):
+ """
+ Called when the user clicks on "Analyze Target". It redraws the map and populates the location list
+ """
+ logger.debug('Attempting to draw locations for the current project')
+ if not locations:
+ if not self.currentProject:
+ self.showWarning(self.trUtf8('No project selected'), self.trUtf8('Please select a project !'))
+ self.ui.statusbar.showMessage(self.trUtf8('Please select a project !'))
+ return
+ else:
+ locations = self.currentProject.locations
+ mapFrame = self.ui.mapWebPage.mainFrame()
+ self.clearMarkers(mapFrame)
+ visibleLocations = []
+ if locations:
+ for location in locations:
+ if location.visible:
+ visibleLocations.append(location)
+ self.addMarkerToMap(mapFrame, location)
+ self.refreshMap(mapFrame)
+ if visibleLocations:
+ self.centerMap(mapFrame, visibleLocations[0])
+ self.setMapZoom(mapFrame, 15)
+ else:
+ self.showWarning(self.trUtf8('No locations found'),
+ self.trUtf8('No locations found for the selected project.'))
+ self.ui.statusbar.showMessage(self.trUtf8('No locations found for the selected project.'))
+
+ self.locationsTableModel = LocationsTableModel(visibleLocations)
+ self.ui.locationsTableView.setModel(self.locationsTableModel)
+ self.ui.locationsTableView.resizeColumnsToContents()
+
+ def doubleClickLocationItem(self, index):
+ location = self.locationsTableModel.locations[index.row()]
+ mapFrame = self.ui.mapWebPage.mainFrame()
+ self.centerMap(mapFrame, location)
+ self.setMapZoom(mapFrame, 18)
+
+ def updateCurrentLocationDetails(self, index):
+ """
+ Called when the user clicks on a location from the location list. It updates the information
+ displayed on the Current Target Details Window
+ """
+ location = self.locationsTableModel.locations[index.row()]
+ self.ui.currentTargetDetailsLocationValue.setText(location.shortName)
+ self.ui.currentTargetDetailsDateValue.setText(location.datetime.strftime('%Y-%m-%d %H:%M:%S %z'))
+ self.ui.currentTargetDetailsSourceValue.setText(location.plugin)
+ self.ui.currentTargetDetailsContextValue.setText(location.context)
+
+ def changeMainWidgetPage(self, pageType):
+ """
+ Changes what is shown in the main window between the map mode and the analysis mode
+ """
+ if 'map' == pageType:
+ self.ui.centralStackedWidget.setCurrentIndex(0)
+ elif pageType == 'analysis':
+ self.ui.centralStackedWidget.setCurrentIndex(1)
+
+ def wizardButtonPressed(self, plugin):
+ """
+ This method calls the wizard of the selected plugin and then reads again the configuration options from file
+ for that specific plugin. This happens in order to reflect any changes the wizard might have made to the configuration
+ options.
+ """
+
+ plugin.plugin_object.runConfigWizard()
+ self.pluginsConfigurationDialog.close()
+ self.showPluginsConfigurationDialog(selected=plugin.name)
+
+ def showPluginsConfigurationDialog(self, selected=None):
+ """
+ Reads the configuration options for all the plugins, builds the relevant UI items and adds them to the dialog
+ """
+ # Show the stackWidget
+ self.pluginsConfigurationDialog = PluginsConfigurationDialog()
+ self.pluginsConfigurationDialog.ui.ConfigurationDetails = QStackedWidget(self.pluginsConfigurationDialog)
+ self.pluginsConfigurationDialog.ui.ConfigurationDetails.setGeometry(QRect(260, 10, 511, 561))
+ self.pluginsConfigurationDialog.ui.ConfigurationDetails.setObjectName(_fromUtf8('ConfigurationDetails'))
+ pl = []
+ for plugin in sorted(self.pluginsConfigurationDialog.PluginManager.getAllPlugins(), key=lambda x: x.name):
+ pl.append(plugin)
+ '''
+ Build the configuration page from the available configuration options
+ and add the page to the stackwidget
+ '''
+ page = QWidget()
+ page.setObjectName(_fromUtf8('page_' + plugin.name))
+ scroll = QScrollArea()
+ scroll.setWidgetResizable(True)
+ layout = QVBoxLayout()
+ titleLabel = QLabel(plugin.name + self.trUtf8(' Configuration Options'))
+ layout.addWidget(titleLabel)
+ vboxWidget = QWidget()
+ vboxWidget.setObjectName(_fromUtf8('vboxwidget_container_' + plugin.name))
+ vbox = QGridLayout()
+ vbox.setObjectName(_fromUtf8('vbox_container_' + plugin.name))
+ gridLayoutRowIndex = 0
+ # Load the String options first
+ pluginStringOptions = plugin.plugin_object.readConfiguration('string_options')[1]
+ if pluginStringOptions != None:
+ for idx, item in enumerate(pluginStringOptions.keys()):
+ itemLabel = plugin.plugin_object.getLabelForKey(item)
+ label = QLabel()
+ label.setObjectName(_fromUtf8('string_label_' + item))
+ label.setText(itemLabel)
+ vbox.addWidget(label, idx, 0)
+ value = QLineEdit()
+ if item.startswith('hidden_'):
+ value.setEchoMode(QLineEdit.Password)
+ value.setObjectName(_fromUtf8('string_value_' + item))
+ value.setText(pluginStringOptions[item])
+ vbox.addWidget(value, idx, 1)
+ gridLayoutRowIndex = idx + 1
+ # Load the boolean options
+ pluginBooleanOptions = plugin.plugin_object.readConfiguration('boolean_options')[1]
+ if pluginBooleanOptions != None:
+ for idx, item in enumerate(pluginBooleanOptions.keys()):
+ itemLabel = plugin.plugin_object.getLabelForKey(item)
+ cb = QCheckBox(itemLabel)
+ cb.setObjectName(_fromUtf8('boolean_label_' + item))
+ if pluginBooleanOptions[item] == 'True':
+ cb.toggle()
+ vbox.addWidget(cb, gridLayoutRowIndex + idx, 0)
+ gridLayoutRowIndex += 1
+ # Add the wizard button if the plugin has a configuration wizard
+ if plugin.plugin_object.hasWizard:
+ wizardButton = QPushButton(self.trUtf8('Run Configuration Wizard'))
+ wizardButton.setObjectName(_fromUtf8('wizardButton_' + plugin.name))
+ wizardButton.setToolTip(self.trUtf8('Click here to run the configuration wizard for the plugin'))
+ wizardButton.resize(wizardButton.sizeHint())
+ wizardButton.clicked.connect(functools.partial(self.wizardButtonPressed, plugin))
+ vbox.addWidget(wizardButton, gridLayoutRowIndex + 1, 0)
+ vboxWidget.setLayout(vbox)
+ scroll.setWidget(vboxWidget)
+ layout.addWidget(scroll)
+ layout.addStretch(1)
+ pluginsConfigButtonContainer = QHBoxLayout()
+ rateStatusButton = QPushButton(self.trUtf8('Check Rate Limits'))
+ rateStatusButton.setObjectName(_fromUtf8('checkRateButton_' + plugin.name))
+ if not plugin.plugin_object.hasRateLimitInfo:
+ rateStatusButton.setEnabled(False)
+ rateStatusButton.setToolTip(
+ self.trUtf8('Plugin do not support getting API rate limits'))
+ else:
+ rateStatusButton.setToolTip(
+ self.trUtf8('Click here to get information about the plugin\'s API rate limits'))
+ rateStatusButton.resize(rateStatusButton.sizeHint())
+ rateStatusButton.clicked.connect(
+ functools.partial(self.pluginsConfigurationDialog.getRateLimitStatus, plugin)
+ )
+ checkConfigButton = QPushButton(self.trUtf8('Test Plugin Configuration'))
+ checkConfigButton.setObjectName(_fromUtf8('checkConfigButton_' + plugin.name))
+ checkConfigButton.setToolTip(self.trUtf8('Click here to test the plugin\'s configuration'))
+ checkConfigButton.resize(checkConfigButton.sizeHint())
+ checkConfigButton.clicked.connect(
+ functools.partial(self.pluginsConfigurationDialog.checkPluginConfiguration, plugin))
+ applyConfigButton = QPushButton(self.trUtf8('Apply Configuration'))
+ applyConfigButton.setObjectName(_fromUtf8('applyConfigButton_' + plugin.name))
+ applyConfigButton.setToolTip(self.trUtf8('Click here to save the plugin\'s configuration options'))
+ applyConfigButton.resize(applyConfigButton.sizeHint())
+ applyConfigButton.clicked.connect(self.pluginsConfigurationDialog.saveConfiguration)
+ pluginsConfigButtonContainer.addStretch(1)
+ pluginsConfigButtonContainer.addWidget(applyConfigButton)
+ pluginsConfigButtonContainer.addWidget(checkConfigButton)
+ pluginsConfigButtonContainer.addWidget(rateStatusButton)
+ layout.addLayout(pluginsConfigButtonContainer)
+ page.setLayout(layout)
+ self.pluginsConfigurationDialog.ui.ConfigurationDetails.addWidget(page)
+ if selected:
+ for index, plugin in enumerate(pl):
+ if selected == plugin.name:
+ self.pluginsConfigurationDialog.ui.ConfigurationDetails.setCurrentIndex(index)
+ else:
+ self.pluginsConfigurationDialog.ui.ConfigurationDetails.setCurrentIndex(0)
+ self.PluginConfigurationListModel = PluginConfigurationListModel(pl, self)
+ self.PluginConfigurationListModel.checkPluginConfiguration()
+ self.pluginsConfigurationDialog.ui.PluginsList.setModel(self.PluginConfigurationListModel)
+ self.pluginsConfigurationDialog.ui.PluginsList.clicked.connect(self.changePluginConfigurationPage)
+ if self.pluginsConfigurationDialog.exec_():
+ self.pluginsConfigurationDialog.saveConfiguration()
+
+ def changePluginConfigurationPage(self, modelIndex):
+ """
+ Changes the page in the PluginConfiguration Dialog depending on which plugin is currently
+ selected in the plugin list
+ """
+ self.pluginsConfigurationDialog.ui.ConfigurationDetails.setCurrentIndex(modelIndex.row())
+
+ def showPersonProjectWizard(self):
+ """
+ Shows the PersonProjectWizard and stores the project information once the wizard is completed
+ """
+ personProjectWizard = PersonProjectWizard()
+ personProjectWizard.ProjectWizardPluginListModel = ProjectWizardPluginListModel(
+ personProjectWizard.loadConfiguredPlugins(), self)
+ personProjectWizard.ui.personProjectAvailablePluginsListView.setModel(
+ personProjectWizard.ProjectWizardPluginListModel)
+ personProjectWizard.ui.personProjectSearchButton.clicked.connect(personProjectWizard.searchForTargets)
+ # Creating it here so it becomes available globally in all functions
+ personProjectWizard.ProjectWizardSelectedTargetsTable = ProjectWizardSelectedTargetsTable([], self)
+ if personProjectWizard.exec_():
+ project = Project()
+ project.projectType = 'person'
+ project.projectName = unicode(personProjectWizard.ui.personProjectNameValue.text().toUtf8())
+ project.projectKeywords = [keyword.strip() for keyword in
+ unicode(personProjectWizard.ui.personProjectKeywordsValue.text().toUtf8()).split(
+ ',')]
+ project.projectDescription = personProjectWizard.ui.personProjectDescriptionValue.toPlainText()
+ project.enabledPlugins = personProjectWizard.readSearchConfiguration()
+ project.dateCreated = datetime.datetime.now()
+ project.dateEdited = datetime.datetime.now()
+ project.locations = []
+ project.analysisDocument = None
+ project.isAnalysisRunning = False
+ project.viewSettigns = {}
+ project.selectedTargets = personProjectWizard.selectedTargets
+ projectNode = ProjectNode(project.projectName, project)
+ locationsNode = LocationsNode('Locations', projectNode)
+ analysisNode = AnalysisNode('Analysis', projectNode)
+ project.storeProject(projectNode)
+ # Now that we have saved the project, reload all projects to be shown in the UI
+ self.loadProjectsFromStorage()
+
+ def showPlaceProjectWizard(self):
+ """
+ Shows the PlaceProjectWizard and stores the project information once the wizard is completed
+ """
+
+ def searchForPlace():
+ placeProjectWizard.ui.mapPage.mainFrame().evaluateJavaScript(
+ QString('searchForAddress(\"' + unicode(placeProjectWizard.ui.searchAddressInput.text().toUtf8()) +
+ '\");'))
+
+ placeProjectWizard = PlaceProjectWizard()
+ placeProjectWizard.ProjectWizardPluginListModel = ProjectWizardPluginListModel(
+ placeProjectWizard.loadConfiguredPlugins(), self)
+ placeProjectWizard.ui.placeProjectAvailablePluginsListView.setModel(
+ placeProjectWizard.ProjectWizardPluginListModel)
+
+ placeProjectWizard.ui.mapPage = QWebPage()
+ placeProjectWizard.ui.mapPage.mainFrame().addToJavaScriptWindowObject('myPyObj', placeProjectWizard.myPyObj)
+ placeProjectWizard.ui.mapPage.mainFrame().setUrl(
+ QUrl(os.path.join(GeneralUtilities.getIncludeDir(), 'mapSetPoint.html')))
+ placeProjectWizard.ui.radiusUnitComboBox.insertItem(0, QString('km'))
+ placeProjectWizard.ui.radiusUnitComboBox.insertItem(1, QString('m'))
+ placeProjectWizard.ui.radiusUnitComboBox.activated[str].connect(
+ placeProjectWizard.onUnitChanged)
+ placeProjectWizard.ui.searchAddressButton.clicked.connect(searchForPlace)
+ placeProjectWizard.ui.webView.setPage(placeProjectWizard.ui.mapPage)
+ if placeProjectWizard.exec_():
+ project = Project()
+ project.projectType = 'place'
+ project.projectName = unicode(placeProjectWizard.ui.placeProjectNameValue.text().toUtf8())
+ project.projectKeywords = [keyword.strip() for keyword in
+ unicode(placeProjectWizard.ui.placeProjectKeywordsValue.text().toUtf8()).split(
+ ',')]
+ project.projectDescription = placeProjectWizard.ui.placeProjectDescriptionValue.toPlainText()
+ project.selectedTargets = []
+ project.enabledPlugins = placeProjectWizard.readSearchConfiguration()
+
+ project.dateCreated = datetime.datetime.now()
+ project.dateEdited = datetime.datetime.now()
+ project.locations = []
+ if hasattr(placeProjectWizard.myPyObj, 'lat') and hasattr(placeProjectWizard.myPyObj, 'lng'):
+ project.poi = (str(placeProjectWizard.myPyObj.lat), str(placeProjectWizard.myPyObj.lng),
+ '{0}{1}'.format(placeProjectWizard.ui.radiusSpinBox.value(), placeProjectWizard.unit))
+ for enabledPlugin in project.enabledPlugins:
+ project.selectedTargets.append({'pluginName': enabledPlugin['pluginName'],
+ 'poi': project.poi})
+ projectNode = ProjectNode(project.projectName, project)
+ locationsNode = LocationsNode('Locations', projectNode)
+ project.storeProject(projectNode)
+ # Now that we have saved the project, reload all projects to be shown in the UI
+ self.loadProjectsFromStorage()
+
+ def loadProjectsFromStorage(self):
+ """
+ Loads all the existing projects from the storage to be shown in the UI
+ """
+ # Show the existing Projects
+ projectsDir = GeneralUtilities.getProjectsDir()
+ projectFileNames = [os.path.join(projectsDir, f) for f in os.listdir(projectsDir) if
+ (os.path.isfile(os.path.join(projectsDir, f)) and f.endswith('.db'))]
+ self.projectNames = [n.replace('.db', '').replace(str(projectsDir) + '/', '') for n in projectFileNames]
+ rootNode = ProjectTreeNode(self.trUtf8('Projects'))
+ for projectFile in projectFileNames:
+ projectObject = shelve.open(projectFile)
+ try:
+ rootNode.addChild(projectObject['project'])
+ except Exception, err:
+ logger.error('Could not read stored project from file')
+ logger.error(err)
+ self.projectTreeModel = ProjectTreeModel(rootNode)
+ self.ui.treeViewProjects.setModel(self.projectTreeModel)
+
+ def currentProjectChanged(self):
+ """
+ Called whenever a project node or one of its children is clicked
+ and makes this the currently selected project
+ """
+ nodeObject = self.ui.treeViewProjects.selectionModel().selection().indexes()[0].internalPointer()
+ if nodeObject.nodeType() == 'PROJECT':
+ self.currentProject = nodeObject.project
+ elif nodeObject.nodeType() == 'LOCATIONS':
+ self.currentProject = nodeObject.parent().project
+ elif nodeObject.nodeType() == 'ANALYSIS':
+ self.currentProject = nodeObject.parent().project
+
+ def doubleClickProjectItem(self):
+ """
+ Called when the user double-clicks on an item in the tree of the existing projects
+ """
+ nodeObject = self.ui.treeViewProjects.selectionModel().selection().indexes()[0].internalPointer()
+ if nodeObject.nodeType() == 'PROJECT':
+ self.currentProject = nodeObject.project
+ self.changeMainWidgetPage('map')
+ self.presentLocations([])
+ elif nodeObject.nodeType() == 'LOCATIONS':
+ self.currentProject = nodeObject.parent().project
+ self.changeMainWidgetPage('map')
+ self.presentLocations([])
+ elif nodeObject.nodeType() == 'ANALYSIS':
+ self.currentProject = nodeObject.parent().project
+ self.changeMainWidgetPage('analysis')
+ self.presentAnalysis(None)
+
+ def showRightClickMenu(self, pos):
+ """
+ Called when the user right-clicks somewhere in the area of the existing projects
+ """
+
+ # We will not allow multi select so the selectionModel().selection().indexes() will contain only one
+ if self.ui.treeViewProjects.selectionModel().selection().count() == 1:
+ nodeObject = self.ui.treeViewProjects.selectionModel().selection().indexes()[0].internalPointer()
+ if nodeObject.nodeType() == 'PROJECT':
+ # First make this the current project
+ self.currentProject = nodeObject.project
+ # now depending on if the project is analyzed or not add actions to the menu
+ rightClickMenu = QMenu()
+ if nodeObject.project.locations:
+ rightClickMenu.addAction(self.ui.actionReanalyzeCurrentProject)
+ rightClickMenu.addAction(self.ui.actionDrawCurrentProject)
+ rightClickMenu.addAction(self.ui.actionDeleteCurrentProject)
+ rightClickMenu.addAction(self.ui.actionExportCSV)
+ rightClickMenu.addAction(self.ui.actionExportKML)
+ rightClickMenu.addAction(self.ui.actionExportFilteredCSV)
+ rightClickMenu.addAction(self.ui.actionExportFilteredKML)
+ else:
+ rightClickMenu.addAction(self.ui.actionAnalyzeCurrentProject)
+ rightClickMenu.addAction(self.ui.actionDeleteCurrentProject)
+ if rightClickMenu.exec_(self.ui.treeViewProjects.viewport().mapToGlobal(pos)):
+ pass
+
+
+if __name__ == '__main__':
+ app = QApplication(sys.argv)
+ myapp = MainWindow()
+ myapp.show()
+ sys.exit(app.exec_())