GEOG 489
Advanced Python Programming for GIS

2.7.3.5: Step 5

PrintPrint

Time to get back to the GUI and implement the required event handler functions for the different GUI elements, in particular the different buttons. This will be quite a bit of code, so we will go through the event handler functions individually. Please add each function in the order they are listed below to the section labeled “# GUI event handler and related functions”:

# query and direct input functions 

def runQuery(): 
    """run one of the different query services based on which tab is currently open""" 
    queryString = ui.queryTermLE.text() 
    activeTab = ui.queryServicesTW.currentWidget() 
    queryHandler[activeTab](queryString)   # call a function from the dictionary in queryHandler 

This is the event handler function for when the “Run query” button is clicked. We already mentioned the global variable queryHandler that maps tab widgets to functions. So we here first get the text the user entered into the queryTermLE widget, then get the currently selected tab from the queryServicesTW tab widget, and finally in the last line we call the corresponding function for querying Nominatim, GeoNames, or providing direct input. These functions still need to be defined.

def setListViewFromResult(r): 
    """populate list view with checkable entries created from result list in r""" 
    m = QStandardItemModel() 
    for item in r: 
        item = QStandardItem(item['name'] + ' ('+item['lat'] + ',' + item['lon'] + ')') 
        item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) 
        item.setData(QVariant(Qt.Checked), Qt.CheckStateRole) 
        m.appendRow(item) 
    ui.resultsLV.setModel(m) 

setListViewFromResult(…) is an auxiliary function for populating the resultsLV list view widget with the result from a query or a direct input. It will be called from the functions for querying the different web services or providing direct input that will be defined next. The given parameter needs to contain a list of dictionaries with name, lat, and lon properties that each represent one item from the result. The for-loop goes through these items and creates list items for the QStandardItemModel from them. Finally, the resulting model is used as the list model for the resultsLV widget.

def runNominatimQuery(query): 
    """query nominatim and update list view and web map with results""" 
    ui.statusbar.showMessage('Querying Nominatim... please wait!') 

    country = ui.nominatimCountryCodeLE.text() if ui.nominatimCountryCodeCB.isChecked() else '' 
    limit = ui.nominatimLimitLE.text()  

    try: 
        items = core_functions.queryNominatim(query, limit, country) # run query 
        # create result list from JSON response and store in global variable result 
        global result  
        result = [(lambda x: {'name': x['display_name'],'lat': x['lat'], 'lon': x['lon']})(i) for i in items] 
        # update list view and map with results 
        setListViewFromResult(result) 
        mapWV.setHtml(core_functions.webMapFromDictionaryList(result))       
        ui.statusbar.showMessage('Querying done, ' + str(len(result)) + ' results returned!') 
    except Exception as e: 
        QMessageBox.information(mainWindow, 'Operation failed', 'Querying Nominatim failed with '+ str(e.__class__) + ': ' + str(e), QMessageBox.Ok ) 
        ui.statusbar.clearMessage()

This is the function that will be called from function runQuery() defined above when the currently selected “Service” tab is the Nominatim tab. It gathers the required information from the line edit widgets on the Nominatim query tab, taking into account the status of the corresponding checkboxes for the optional elements (using the "... if ... else ..." ternary operator). Then it calls the queryNominatim(…) function from the core_functions module to perform the actual querying (line 9) and translates the returned JSON list into a result list of dictionaries with name, lat, and lon properties that will be stored in the global variable result. Note that we are using list comprehension here to realize this translation of one list into another. The resultLV list view and mapWV web map widget will then be updated accordingly. This happens inside a try-except block to catch exceptions when something goes wrong with querying the web service or interpreting the results. Statusbar messages are used to keep the user informed about the progress and a message box is shown if an exceptions occurs to inform the user.

def runGeonamesQuery(query): 
    """query geonames and update list view and web map with results""" 
    ui.statusbar.showMessage('Querying GeoNames... please wait!') 

    username = ui.geonamesUsernameLE.text()  
    country = ui.geonamesCountryCodeLE.text() if ui.geonamesCountryCodeCB.isChecked() else '' 
    fclass = ui.geonamesFeatureClassLE.text() if ui.geonamesFeatureClassCB.isChecked() else '' 
    limit = ui.geonamesLimitLE.text() 

    try: 
        items = core_functions.queryGeonames(query, limit, username, country, fclass ) # run query 
        # create result list from JSON response and store in global variable result 
        global result  
        result  = [(lambda x: {'name': x['toponymName'],'lat': x['lat'], 'lon': x['lng']})(i) for i in items] 
        # update list view and map with results 
        setListViewFromResult(result) 
        mapWV.setHtml(core_functions.webMapFromDictionaryList(result)) 
        ui.statusbar.showMessage('Querying done, ' + str(len(result)) + ' results returned!') 
    except Exception as e: 
        QMessageBox.information(mainWindow, 'Operation failed', 'Querying GeoNames failed with '+ str(e.__class__) + ': ' + str(e), QMessageBox.Ok) 
        ui.statusbar.clearMessage() 

This function that will be called from runQuery() if the currently selected “Service” tab is the GeoNames tab works exactly like the previous function for Nominatim, just the query parameters extracted in lines 5 to 8 are different and the translation into a result list looks a little bit different because GeoNames uses other property names ("toponymName" instead of "display_name" and "lng" instead of "lon").

def runDirectInput(query): 
    """create single feature and update list view and web map with results""" 
    name = ui.directInputNameLE.text() 
    lon = ui.directInputLonLE.text() 
    lat = ui.directInputLatLE.text()  

    # create result list with single feature and store in global variable result 
    global result 
    result = [{ 'name': name, 'lat': lat, 'lon': lon }] 
    # update list view and map with results 
    setListViewFromResult(result) 
    mapWV.setHtml(core_functions.webMapFromDictionaryList(result))     
    ui.statusbar.showMessage('Direct input has been added to results list!') 

This function will be called from runQuery() if the currently selected “Service” tab is the Direct Input tab. Again, we are collecting the relevant information from the input widgets (line 3 to 5) but here we directly produce the result consisting of just a single item (line 9). The rest works in the same way as in the previous two functions.

These were the functions required for the query section of our tool. So we can now move on to the Results section where we just need the three event handler functions for the three buttons located below the list view widget.

# list view selection functions 

def selectAll(): 
    """select all items of the list view widget""" 
    for i in range(ui.resultsLV.model().rowCount()): 
        ui.resultsLV.model().item(i).setCheckState(Qt.Checked)  

def clearSelection(): 
    """deselect all items of the list view widget""" 
    for i in range(ui.resultsLV.model().rowCount()): 
        ui.resultsLV.model().item(i).setCheckState(Qt.Unchecked)  

def invertSelection(): 
    """invert current selection of the list view widget""" 
    for i in range(ui.resultsLV.model().rowCount()): 
        currentValue = ui.resultsLV.model().item(i).checkState() 
        ui.resultsLV.model().item(i).setCheckState(Qt.Checked if currentValue == Qt.Unchecked else Qt.Unchecked) 

These three functions all work very similarly: We go through all items in the list model underlying the resultsLV list view widget. In selectAll(), the check state of each item is set to “Checked”, while in clearSelection() it is set to “Unchecked” for each item. In invertSelection(), we take the item’s current state and either change it from “Checked” to “Unchecked” or vice versa (using the ternary "... if ... else ..." operator once more).

# adding features functions 

def addFeatures(): 
    """run one of the different functions for adding features based on which tab is currently open""" 
    activeTab = ui.addFeaturesTW.currentWidget() 
    addFeaturesHandler[activeTab]() # call a function from the dictionary in addFeatureHandler 

We have now arrived at the last row of our main window graphical interface for adding the selected result features to a layer, shapefile, or csv file. The addFeatures() function corresponds to the runQuery() function from the beginning in that it invokes the right function depending on which tab of the addFeaturesTW tab widget is currently selected. This is based on the global variable addFeaturesHandler that map tabs to functions.

def updateShapefileFieldCB(): 
    """update shapefileFieldCB combo box with field names based on shapefile name""" 
    ui.shapefileFieldCB.clear() 
    fileName = ui.shapefileAddLE.text() 
    ui.shapefileFieldCB.addItems(core_functions.getValidFieldsForShapefile(fileName)) 

updateShapefileFieldCB() is an auxiliary function for updating the content of the shapefileFieldCB combo box whenever the name of the shapefile in the shapefileAddLE line edit widget changes so that the combo always displays the editable string fields of that shapefile.

def selectShapefile():     
    """open file dialog to select exising shapefile and if accepted, update GUI accordingly""" 
    fileName, _ = QFileDialog.getOpenFileName(mainWindow,"Select shapefile", "","Shapefile (*.shp)") 
    if fileName: 
        ui.shapefileAddLE.setText(fileName) 
        updateShapefileFieldCB()

When the shapefileOpenFileTB tool button is clicked, we want to display a file dialog for picking the shapefile. Opening the dialog and processing the result happens in the function selectShapefile(). When a file name is returned (meaning the dialog wasn’t cancelled by the user), the name is put into the shapefileAddLE line edit field and updateShapefieldCB() is called to update the combo box with the field names of that file.

def addFeaturesToShapefile(): 
    """add selected features from list view to shapefile""" 
    fieldName = ui.shapefileFieldCB.currentText() 
    fileName = ui.shapefileAddLE.text() 
    ui.statusbar.showMessage('Adding entities has started... please wait!') 

    try: 
        with arcpy.da.InsertCursor(fileName, ("SHAPE@",fieldName)) as cursor:  
           for i in range(ui.resultsLV.model().rowCount()): # go through all items in list view 
               if ui.resultsLV.model().item(i).checkState() == Qt.Checked: 
                   point = arcpy.Point( result[i]['lon'], result[i]['lat']) 
                   cursor.insertRow( (point, result[i]['name'][:30]) ) # name shortened to 30 chars       
        ui.statusbar.showMessage('Adding entities has finished.') 
    except Exception as e: 
        QMessageBox.information(mainWindow, 'Operation failed', 'Writing to shapefile failed with '+ str(e.__class__) + ': ' + str(e), QMessageBox.Ok ) 
        ui.statusbar.clearMessage()

This function contains the code for writing the selected features from the results list in global variable result to the shapefile with the help of an arcpy insert cursor. We first read the relevant information from the shapefileAddLE and shapefileFieldCB widgets and then in the for-loop go through the items in the resultsLV list view to see whether they are checked or not. If an item is checked, an arcpy.Point object is created from the corresponding dictionary in variable result and then written to the shapefile together with the name of the location. Statusbar messages are used to inform on the progress or a message box will be shown if an exception occurs while trying to write to the shapefile.

def updateLayerFieldCB(): 
    """update layerFieldCB combo box with field names based on selected layer""" 
    ui.layerFieldCB.clear() 
    layer = ui.layerPickLayerCB.currentText() 

    try: 
        ui.layerFieldCB.addItems(core_functions.getStringFieldsForDescribeObject(arcpy.Describe(arcValidLayers[layer]))) 
    except Exception as e: 
        QMessageBox.information(mainWindow, 'Operation failed', 'Obtaining field list failed with '+ str(e.__class__) + ': ' + str(e), QMessageBox.Ok ) 
        ui.statusbar.clearMessage() 

This is the corresponding function to updateShapefileFieldCB() but for the layerFieldCB combo box widget part of the Layer tab.

def updateLayers(): 
    """refresh layers in global variable arcValidLayers and layerPickLayerCB combo box""" 
    layers = [] 
    global  arcValidLayers 
    arcValidLayers = {} 
    ui.layerPickLayerCB.clear() 
    ui.layerFieldCB.clear() 

    try:  
        layers = core_functions.getPointLayersFromArcGIS() # get all point layers 
        for l in layers:                                   # add layers to arcValidLayers and GUI 
            arcValidLayers[l.name] = l 
            ui.layerPickLayerCB.addItem(l.name) 
            updateLayerFieldCB() 
    except Exception as e: 
        QMessageBox.information(mainWindow, 'Operation failed', 'Obtaining layer list from  ArcGIS failed with '+ str(e.__class__) + ': ' + str(e), QMessageBox.Ok ) 
        ui.statusbar.clearMessage() 
        ui.shapefileFieldCB.clear() 

This function is for populating the layerPickLayerCB and arcValidLayer global variable with the Point vector layers currently open in ArcGIS. It uses the getPointLayersFrom ArcGIS() function from core_functions.py to get the list of layers and then in the for-loop stores the layer objects under their layer name in the arcValidLayers dictionary and just the names as items in the combo box. If something goes wrong with getting the layers from ArcGIS, the corresponding exception will be caught and a message box will warn the user about the failure of the operation.

def addFeaturesToLayer(): 
    """add selected features from list view to layer""" 
    layer = ui.layerPickLayerCB.currentText(); 
    fieldName = ui.layerFieldCB.currentText() 
    ui.statusbar.showMessage('Adding entities has started... please wait!') 

    try: 
        with arcpy.da.InsertCursor(arcValidLayers[layer], ("SHAPE@",fieldName)) as cursor: 
            for i in range(ui.resultsLV.model().rowCount()): # go through all items in list view 
                if ui.resultsLV.model().item(i).checkState() == Qt.Checked: 
                    point = arcpy.Point( float(result[i]['lon']), float(result[i]['lat'])) 
                    cursor.insertRow( (point, result[i]['name'][:30]) ) # name shortened to 30 chars    
        ui.statusbar.showMessage('Adding entities has finished.') 
    except Exception as e: 
        QMessageBox.information(mainWindow, 'Operation failed', 'Writing to layer failed with '+ str(e.__class__) + ': ' + str(e), QMessageBox.Ok ) 
        ui.statusbar.clearMessage() 

This is the analogous function to the previously defined function addFeaturesToShapefile() but for a currently open layer and based on the information in the widgets of the Layer tab.

def selectCSV():     
    """open file dialog to select exising csv/text file and if accepted, update GUI accordingly""" 
    fileName, _ = QFileDialog.getOpenFileName(mainWindow,"Select CSV file", "","(*.*)") 
    if fileName: 
        ui.csvAddToFileLE.setText(fileName) 

Similarly to selectShapefile(), this function opens a file dialog to select a csv file to append the features to.

def addFeaturesToCSV(): 
    """add selected features from list view to csv/text file""" 
    fileName = ui.csvAddToFileLE.text() 
    ui.statusbar.showMessage('Adding entities has started... please wait!') 

    try: 
        with open(fileName, 'a', newline='') as csvfile: 
             csvWriter = csv.writer(csvfile) 
             for i in range(ui.resultsLV.model().rowCount()): # go through all items in list view 
                if ui.resultsLV.model().item(i).checkState() == Qt.Checked: 
                     csvWriter.writerow( [ result[i]['name'], result[i]['lon'], result[i]['lat'] ])    
        ui.statusbar.showMessage('Adding entities has finished.') 
    except Exception as e: 
        QMessageBox.information(mainWindow, 'Operation failed', 'Writing to csv file failed with '+ str(e.__class__) + ': ' + str(e), QMessageBox.Ok ) 
        ui.statusbar.clearMessage()

Working similarly to addFeaturesToShapefile() and addFeaturesToLayer(), this function writes the selected features as rows to a text file using the csv.writer class from the csv module of the Python standard library.

def selectNewShapefile():     
    """open file dialog to creaete new shapefile and if accepted, update GUI accordingly""" 
    fileName, _ = QFileDialog.getSaveFileName(mainWindow,"Save new shapefile as", "","Shapefile (*.shp)") 
    if fileName: 
        createShapefileDialog_ui.newShapefileLE.setText(fileName) 

The final two functions are for creating a new shapefile. selectNewShapefile() is called when the newShapefileBrowseTB button that is part of the dialog box for creating a new shapefile is clicked and displays a file dialog for saving a file under a new name. The chosen name is used to set the text of the newShapefileBrowseTB line edit widget.

def createNewShapefile(): 
    """create new shapefile and adds field based on info in dialog GUI""" 
    if createShapefileDialog.exec_() == QDialog.Accepted: 
        file = createShapefileDialog_ui.newShapefileLE.text() 
        field = createShapefileDialog_ui.fieldForNameLE.text() 

        try: 
            core_functions.createPointWGS1984Shapefile(file,field) 
            ui.shapefileAddLE.setText(file) 
            updateShapefileFieldCB() 
            ui.shapefileFieldCB.setCurrentIndex(ui.shapefileFieldCB.findText(field)) 
            ui.statusbar.showMessage('New shapefile has been created.') 
        except Exception as e: 
            QMessageBox.information(mainWindow, 'Operation failed', 'Creating new shapefile failed with '+ str(e.__class__) + ': ' + str(e), QMessageBox.Ok ) 
            ui.statusbar.clearMessage() 
            ui.shapefileFieldCB.clear() 

This function is called when the “Create new shapefile” button on the Shapefile tab is clicked. It first displays the createShapefileDialog dialog box modally by calling its exec_() method. If the dialog is accepted (= closed by clicking Ok), the function creates the new shapefile with the help of the createPointWGS1984Shapefile() function from core_functions.py and based on the input fields in the dialog box for creating a new shapefile (newShapefileLE and fieldForNameLE). If no exception is raised, the file name and field name from the dialog box will be used to change the text of the shapefileAddLE line edit widget and the shapefileFieldCB combo box.