Converting the spatial reference system and units

Fortunately, we already did this kind of operation before and now we are going to adapt it to our data model.

We will transform the coordinates of the geometries only when they are needed. To perform the transformation, we will create a new utility function, as follows:

  1. Open geo_functions.py in our utils folder and create a new function:
    def transform_geometry(geom, src_epsg=4326, dst_epsg=3395):
        """Transforms a single wkb geometry.
    
        :param geom: wkb geom.
        :param src_epsg: EPSG code for the source geometry.
        :param dst_epsg: EPSG code for the destination geometry.
        """
        ogr_geom = ogr.CreateGeometryFromWkb(geom)
        ogr_transformation = create_transform(src_epsg, dst_epsg)
        ogr_geom.Transform(ogr_transformation)
        return ogr_geom.ExportToWkb()

    It takes as arguments geometries in the WKB format, its EPSG code, and the EPSG code for the desired coordinate system for the output. It performs the transformation and returns a WKB geometry again.

    Now back to the models; let's import this function and use it.

  2. Edit the import at the beginning of the models.py file:
    # coding=utf-8
    
    from __future__ import print_function
    import gdal
    from shapely.geometry import Point
    from shapely import wkb, wkt
    from utils.geo_functions import open_vector_file
    from utils.geo_functions import transform_geometry
    
  3. Now, edit BaseGeoObject, so our classes can inherit this new functionality:
    class BaseGeoObject(object):
        """Base class for a single geo object."""
        def __init__(self, geometry, attributes=None):
            self.geom = geometry
            self.attributes = attributes
            self.wm_geom = None
    
            # Makes a lookup table of case insensitive attributes.
            self._attributes_lowercase = {}
            for key in self.attributes.keys():
                self._attributes_lowercase[key.lower()] = key
    
        def transformed_geom(self):
            """Returns the geometry transformed into WorldMercator
            coordinate system.
            """
            if not self.wm_geom:
                geom = transform_geometry(self.geom.wkb)
                self.wm_geom = wkb.loads(geom)
            return self.wm_geom
    
        def get_attribute(self, attr_name, case_sensitive=False):
            """Gets an attribute by its name.
    
            :param attr_name: The name of the attribute.
            :param case_sensitive: True or False.
            """
            if not case_sensitive:
                attr_name = attr_name.lower()
                attr_name = self._attributes_lowercase[attr_name]
            return self.attributes[attr_name]
    
        def __repr__(self):
            raise NotImplementedError

    Note that we opted to keep the geometries in both the coordinate systems. The geometry in WorldMercator is stored in the wm_geom property the first time the transformation occurs. The next time transformed_geom is called, it will only get the property value. This is called memorization and we will see more of this technique later in the book.

    Depending on your application, this may be a good practice because you may want to use different coordinate systems for specific purposes. For example, to draw a map, you may want to use lat/lon and, to perform calculation, you would need the coordinates in meters. The downside is that the memory consumption is higher, because you will be storing two sets of geometry.

  4. Finally, we go back to the LineString class and change its __repr__ method to use transformed_geom to calculate the length:
    class LineString(BaseGeoObject):
        """Represents a single linestring."""
        def __repr__(self):
            return "{}-{}".format(self.get_attribute('name'),
                                  self.transformed_geom().length)
    
  5. Run the code and see the new output:
    File imported: ../data/roads.shp
    State Route 3-100928.690515
    State Route 411-3262.29448315
    State Route 3-331878.76971
    State Route 3-56013.8246795.73
    ...
    Process finished with exit code 0

It's much better now as we can see the road lengths in meters. But it is still not perfect because, normally, we would want the lengths in kilometres or miles. So, we need to convert the unit.

In Chapter 1, Preparing the Work Environment, we made a beautiful function capable of performing these transformations; we used it to convert area units. Using it as a template, we are going to implement it to convert length units.

Since it's a function that can be used in other parts of any application, we are going to put it into the geo_functions.py module in the utils package (that is, directory).

  1. Edit the geo_functions.py files and copy and paste the function that we used in Chapter 1, Preparing the Work Environment, to calculate and transform area units. We will keep it there for later use:
    def calculate_areas(geometries, unity='km2'):
        """Calculate the area for a list of ogr geometries."""    
        conversion_factor = {
            'sqmi': 2589988.11,
            'km2': 1000000,
            'm': 1}
        if unity not in conversion_factor:
            raise ValueError(
                "This unity is not defined: {}".format(unity))
        areas = []
        for geom in geometries:
            area = geom.Area()
            areas.append(area / conversion_factor[unity])
        return areas
  2. Duplicate this function (copy and paste) and edit it to make it like the following code:
    def convert_length_unit(value, unit='km', decimal_places=2):
        """Convert the leng unit of a given value.
         The input is in meters and the output is set by the unity
          argument.
    
        :param value: Input value in meters.
        :param unit: The desired output unit.
        :param decimal_places: Number of decimal places of the output.
        """
        conversion_factor = {
            'mi': 0.000621371192,
            'km': 0.001,
            'm': 1.0}
    
        if unit not in conversion_factor:
            raise ValueError(
                "This unit is not defined: {}".format(unit))
        return round(value * conversion_factor[unit], decimal_places)

    Again, it's a very versatile function because you can easily change its code to add more conversion factors to it. Here, we also introduced the round() function, so we can see a more readable result. By default, it will round the result to two decimal places, which in most cases, is enough for a good representation of length.

  3. Go back to the models and import this new function after the other imports:
    # coding=utf-8
    
    from __future__ import print_function
    import gdal
    from shapely.geometry import Point
    from shapely import wkb, wkt
    from utils.geo_functions import open_vector_file
    from utils.geo_functions import transform_geometry
    from utils.geo_functions import convert_length_unit
    
  4. Now edit the LineString class. We will add a convenience method (we will see more about this later in the chapter) that will return the length in a converted unit, change the __repr__ value to use it, and also improve the string formatting to display the unit and get a better output:
    class LineString(BaseGeoObject):
        """Represents a single linestring."""
        def __repr__(self):
            unit = 'km'       
            return "{}  ({}{})".format(self.get_attribute('name'),
                                       self.length(unit), unit)         
        
        def length(self, unit='km'):
            """Convenience method that returns the length of the
            linestring in a given unit.
    
            :param unit: The desired output unit.
            """
            return convert_length_unit(self.transformed_geom().length,
                                       unit)
    
  5. Run the code again and see what we have accomplished:
    File imported: ../data/roads.shp
    
    State Route 146  (10.77km)
    US Route 7, US Route 20  (5.81km)
    State Route 295  (13.67km)
    Interstate Route 90  (3.55km)
    State Route 152  (18.22km)
    State Route 73  (65.19km)
    State Route 20  (53.89km)
    State Route 95  (10.38km)
    ...
    Process finished with exit code 0
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.2.157