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:
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.
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
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.
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)
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).
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
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.
# 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
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)
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
18.218.239.182