Thursday 29 November 2018

Challenge: QGIS plugin to create circle (based on aviation-like data)


Unsurprisingly, circle polygon in aviation world is also defined by center and radius, for example:
Wouldn't be convenient to create circle by passing center latitude and longitude and radius as it they are given in source - in terms of coordinate format and unit of measure?
A real-world example:
Lateral limits of airspace:
A CIRCLE OF 10NM RADIUS CENTERED AT 142230N0763430E

Let's start with defining variables that keeps circle center coordinates, raidus of circle, name of temporary layer and polygon name:


self.circ_center = None  # Circle center coordinate pair
self.radius_m = None  # Circle radius in meters
self.mlyr_name = ''  # Output layer name
self.polygon_name = ''

Now it's time to code behaviour of QWidgets that we used in plugin:
When user click button Add, function add_circle will be executed. This function will cover the all work: adding circle to layer.


self.dlg.pushButtonAddCircle.clicked.connect(self.add_circle)

To get circle radius unit of measure we need check which option is chosesn from comboBoxRadiusUOM and assign appropriate constant:


def get_radius_uom(self):
    """ Returns radius unit of measure """
    if self.dlg.comboBoxRadiusUOM.currentIndex() == 0:  # m
        return ct.UOM_M
    elif self.dlg.comboBoxRadiusUOM.currentIndex() == 1:  # km
        return ct.UOM_KM
    elif self.dlg.comboBoxRadiusUOM.currentIndex() == 2:  # NM
        return ct.UOM_NM
    elif self.dlg.comboBoxRadiusUOM.currentIndex() == 3:  # feet
        return ct.UOM_FEET
    elif self.dlg.comboBoxRadiusUOM.currentIndex() == 4:  # SM
        return ct.UOM_SM

We also need to check if input data is correct - method check_input():

def check_input(self):
    """ Checks if input data is correct """
    err_msg = ''
    check_result = True

Variables:
  • err_msg: keeps message with errors that in case some of required input data (e. g. distance) is incorrect of missing
  • check_result: keeps if input is correct (True) or incorrect (False)

Input data validation will be carried out as follows:
  • get value of QLineEdit widget that is appropriate for required input (e. g. distance)
  • check if the value is valid 
  • if it is not valid:
    • set check_result vale to False
    • assign to err_msg variable error message

lat_src = self.dlg.lineEditCenterLat.text()
lon_src = self.dlg.lineEditCenterLon.text()
radius_src = self.dlg.lineEditRadius.text()

self.circ_center = ct.CoordinatesPair(lat_src, lon_src)

if self.circ_center.is_valid is False:
    check_result = False
    err_msg = self.circ_center.err_msg

if radius_src == '':
    check_result = False
    err_msg += 'Enter radius!'
else:
    radius_check = ct.check_distance2(radius_src)
    if radius_check == ct.NOT_VALID:
        check_result = False
        err_msg += 'Radius error!'
    else:
        self.radius_m = ct.convert_distance(radius_check, self.get_radius_uom(), ct.UOM_M)

if check_result is False:
    QMessageBox.critical(w, "Message", err_msg)

return check_result

Now, if we check input data, we are almost ready to create polygons. But before we are going to fill in method add_circle() with code, let's write method that creates memory layer:

@staticmethod
def create_mem_lyr(lyr_name):
    """ Create temporary 'memory' layer to store results.
    :param lyr_name: string, layer name
    """
    mlyr = QgsVectorLayer('Polygon?crs=epsg:4326', lyr_name, 'memory')
    prov = mlyr.dataProvider()
    mlyr.startEditing()
    prov.addAttributes([QgsField("PNAME", QVariant.String)])
    mlyr.commitChanges()
    QgsProject.instance().addMapLayer(mlyr)

and adds polygon to memory layer:

def add_feature_to_layer(self, boundary):
    """ Add feature to layer
    :param: boundary: list, list of QgsPoint objects
    """
    out_lyr = QgsVectorLayer('Polygon?crs=epsg:4326', self.mlyr_name, 'memory')
    out_lyr = self.iface.activeLayer()
    out_lyr.startEditing()
    out_prov = out_lyr.dataProvider()
    feat = QgsFeature()
    feat.setGeometry(QgsGeometry.fromPolygonXY([boundary]))
    feat.setAttributes([self.polygon_name])
    out_prov.addFeatures([feat])
    out_lyr.commitChanges()
    out_lyr.updateExtents()
    self.iface.mapCanvas().setExtent(out_lyr.extent())
    self.iface.mapCanvas().refresh()

Finally, we can write method that creates circle and adds it into memory layer:
  • call method check_input()
  • if method check_input() returns True (input is correct) calculate vertices on circle
  • create list of QgsPointXY objects
  • add polygon to layer

def add_circle(self):
    if self.check_input():

        # Create circle polygon
        poly_vertices = []
        for i in range(0, 360):
            vertex_lat, vertex_lon = ct.vincenty_direct_solution(self.circ_center.dd_lat,
                                                                 self.circ_center.dd_lon,
                                                                 i, self.radius_m,
                                                                 ct.WGS84_A,
                                                                 ct.WGS84_B,
                                                                 ct.WGS84_F)

            poly_vertex = QgsPointXY(vertex_lon, vertex_lat)
            poly_vertices.append(poly_vertex)

        layers = QgsProject.instance().layerTreeRoot().children()
        layers_list = []  # List of layers in current (opened) QGIS project
        for layer in layers:
            layers_list.append(layer.name())

        if self.mlyr_name == '':
            self.mlyr_name = ct.get_tmp_name()
        self.polygon_name = self.dlg.lineEditPolyName.text()

        if self.mlyr_name not in layers_list:
            self.create_mem_lyr(self.mlyr_name)
            self.add_feature_to_layer(poly_vertices)
        else:
            self.add_feature_to_layer(poly_vertices)

Source code and plugin can be download from: https://github.com/strpaw/QGIS3-CreateCircle-plugin

No comments:

Post a Comment