Thursday 15 November 2018

Challenge: QGIS Plugin to create polygon from text

This is the first post from series Challenge - where you can find how to automate repetitive task to save time and money.

Let's assume that you have to create vector data based on textual description of polygons, e. g.:

585200N0312800E-584700N0312700E-583500N0311700E-583200N0312700E-
583200N0313300E-582900N0313800E-582800N0314400E-
582500N0314500E-585200N0312800E.

It is not a big problem, when you have to enter into system 1, 2, 5 or even 10 such polygons that are have not too many vertices.
But what if you have many of them and deadline is coming?

We can write plugin for QGIS which will create polygons.



Note: The solution presented below is working but have some limitation of usability. I have made following assumption:
  • coordinates are given in DMSH, degrees, minutes, seconds without fraction of seconds, e.g. 585200N
  • textual description of polygon is given as pairs of latitude-longitude or longitude-latitude, that create segments of polygon ring
  • the number of latitude and longitude is the same (I mean, that scripts does not check if the pairs of coordinates are given correctly)
  • tool does not check if polygon is valid, e. g. if has no overlapping rings, self-intersections, dangling segments
  • only polygon with one ring is possible (no outer and inner rings)
  • due to the way of extracting coordinates from text the tool is designed for relatively small polygons
  • the lines between vertices are 'strait' lines, not geodesic ones - this mean that for large polygons, or cases where distance between vertices is large it can lead to wrong results.
Some of that constraints can be fixed easy (various coordinates formats) or with little more work effort (not only strait segments but arcs) - that will be solved in the future.
    Let's create new python file poly_from_text_tools.py

    Regular expressions for coordinates in DMSH format - decimal, degree and seconds followed by hemisphere letter:

    LAT_DMSH_COMP_REGEX = re.compile(r'\d{6}[NS]')
    LON_DMSH_COMP_REGEX = re.compile(r'\d{7}[EW]')
    

    Function that converts coordinate from DMS format to DD (decimal degrees) format, which will be used later in QGIS functions to create polygon:

    def dmsh_comp2dd(dms):
        """ Converts coordinate in DMSH format into DD format.
        :param dms: str, coordinate in DMSH format
        :return: float, decimal degrees
        """
        h = dms[-1]  # Get hemisphere letter
        dms = dms[:-1]  # Trim hemisphere letter
        if h in ['N', 'S']:  # for latitude
            d = float(dms[:2])
            m = float(dms[2:4])
            s = float(dms[4:])
        elif h in ['E', 'W']:  # for longitude
            d = float(dms[:3])
            m = float(dms[3:5])
            s = float(dms[5:])
    
        if h in ['S', 'W']:  # for southern  and western hemisphere coordinates are negative
            return -(d + m / 60 + s / 3600)
        else:
            return d + m / 60 + s / 3600
    

    And function that will return two lists: for latitude and longitude from text with description of polygon:

    def get_latlon_list(raw_text, lat_regex, lon_regex):
        """ Extracts latitude and longitude from given text.
        :param raw_text: str, text for search latitude and longitude
        :param lat_regex: regex object, regular expression for latitude
        :param lon_regex: regex object, regular expression for latitude
        :return: 2 element tuple of latitude and longitude list
        """
        lat_list = re.findall(lat_regex, raw_text)
        lon_list = re.findall(lon_regex, raw_text)
        return lat_list, lon_list
    

    Now, we have to create QGIS plugin - the easy way to do it is to use plugin PluginBuilder.
    A good tutorial how to do this can be found here:
    https://www.qgistutorials.com/en/docs/building_a_python_plugin.html

    Next, we have to create GUI, using for example QT Deisgner. Some of the basic of Qt will be discussed in separate posts.

    First we need to extract latitude and longitude from text:


    def extract_latlon(self):
        raw_text = self.dlg.textEditRawText.toPlainText()
    
        if self.dlg.comboBoxCoordFormat.currentIndex() == 0:  # DMSH - no degree, minute, second symbol
            lat_list, lon_list = poly_tools.get_latlon_list(raw_text,
                                                            poly_tools.LAT_DMSH_COMP_REGEX,
                                                            poly_tools.LON_DMSH_COMP_REGEX)
    
        for i in range(len(lat_list)):
            for j in range(len(lon_list)):
                if i == j:
                    row_pos = self.dlg.tableWidgetCoordinates.rowCount()
                    self.dlg.tableWidgetCoordinates.insertRow(row_pos)
    
                    self.dlg.tableWidgetCoordinates.setItem(row_pos, 0, QTableWidgetItem(lat_list[i]))
                    self.dlg.tableWidgetCoordinates.setItem(row_pos, 1, QTableWidgetItem(lon_list[j]))
        return
    

    NOTE: Writing extracting coordinates into QTableWidget is not necessary. I did it only to check myself is coordinates are extracted correctly and to learn how to use some basic of QTableWidget.

    Now, we are ready to create polygon. We are going to do this in the following steps:
    • read coordinates values from QTableWidget
    • create QgsPointXY (QGIS 3) objects from coordinates pairs 
    • append QgsPointXY object to list of points
    • create temporary memory layer (if it doesn't exist)
    • pass list of points to QgsGeometry.fromPolygonXY to create polygon
    Reading content of QTableWidget and creating list of points:

    vertices = []  # List of vertices for polygon feature
    
    point_count = self.dlg.tableWidgetCoordinates.rowCount()
    
    if self.dlg.comboBoxCoordFormat.currentIndex() == 0:  # DMSH - no degree, minute, second symbol
        for i in range(0, point_count):
            vertex_lat = float(poly_tools.dmsh_comp2dd(self.dlg.tableWidgetCoordinates.item(i, 0).text()))
            vertex_lon = float(poly_tools.dmsh_comp2dd(self.dlg.tableWidgetCoordinates.item(i, 1).text()))
            vertex = QgsPointXY(vertex_lon, vertex_lat)
            vertices.append(vertex)
    

    Creating polygon from extracted coordinates and adding it to layer that name is keep in self.mlyr_name variable.

    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([vertices]))
    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()
    

    I am ware that is not comprehensive, so you can find full source code here and download plugin from: https://github.com/strpaw/QGIS3-CreatePolygonFromText

    1 comment:

    1. Good afternoon! Is your plugin still available in github? the link provided above is a dead link and do you still share it? Thanks in advance!

      ReplyDelete