1

Main question

Suppose I have a few OGR geometries, all of them containing M-values.

Is there a generic solution that allows me to translate them to Shapely geometries without M-values?

Shapely and M-values

Sadly, shapely and M-values don't mix (see this GitHub issue). So in order to generate the Shapely geometries, I'm willing to drop the M-values. However, I'm not sure what the best approach is.

Small example

from osgeo import ogr

pt_2D = ogr.Geometry(ogr.wkbPointM)
pt_2D.AddPointM(0,0,100)

line1_2D = ogr.Geometry(ogr.wkbLineStringM)
line1_2D.AddPointM(0, 0, 50)
line1_2D.AddPointM(1, 1, 250)

line2_2D = ogr.Geometry(ogr.wkbLineStringM)
line2_2D.AddPointM(3, 1, 150)
line2_2D.AddPointM(4, 1, 25)

mline_2D = ogr.Geometry(ogr.wkbMultiLineStringM)
mline_2D.AddGeometry(line1_2D)
mline_2D.AddGeometry(line2_2D)

pt_3D = ogr.Geometry(ogr.wkbPointM)
pt_3D.AddPointZM(0,0,0,100)

line1_3D = ogr.Geometry(ogr.wkbLineStringM)
line1_3D.AddPointZM(0, 0, 0, 50)
line1_3D.AddPointZM(1, 1, 1, 250)

line2_3D = ogr.Geometry(ogr.wkbLineStringM)
line2_3D.AddPointZM(3, 1, 3, 150)
line2_3D.AddPointZM(4, 1, 6, 25)

mline_3D = ogr.Geometry(ogr.wkbMultiLineStringM)
mline_3D.AddGeometry(line1_3D)
mline_3D.AddGeometry(line2_3D)

In the code chunk above, I've created a few 2D and 3D M-value-enabled OGR geometries. How can I translate them into Shapely geometries without the M-values?

First attempt: Exporting to WKT

My first attempt was to convert the geometries using the WKT strings and to use the shapely.wkt.loads function. However, when I use this approach on 2D geometries, the M-value gets incorrectly interpreted as a Z-value. Interestingly, though, this approach does work for 3D geometries!

import shapely.wkt

def ogr_to_shapely(input_ogr_geom):
    return shapely.wkt.loads(input_ogr_geom.ExportToIsoWkt())

print(ogr_to_shapely(pt_2D).wkt)
# POINT Z (0 0 100) # wrong

print(ogr_to_shapely(line1_2D).wkt)
# LINESTRING Z (0 0 50, 1 1 250) # wrong

print(ogr_to_shapely(mline_2D).wkt)
# MULTILINESTRING Z ((0 0 50, 1 1 250), (3 1 150, 4 1 25)) # wrong

print(ogr_to_shapely(pt_3D).wkt)
# POINT Z (0 0 0) # right!

print(ogr_to_shapely(line1_3D).wkt)
# LINESTRING Z (0 0 0, 1 1 1) # right!

print(ogr_to_shapely(mline_3D).wkt)
# MULTILINESTRING Z ((0 0 0, 1 1 1), (3 1 3, 4 1 6)) # right!

Second attempt: Exporting coordinates

My second attempt was to just get the list of raw coordinates (without the M-values!) and pass them to Shapely. The problem, though, is finding which type of geometry to create after I've extracted the coordinate values.

def get_ogr_coords(input_ogr_geom):
    geom_type = input_ogr_geom.GetGeometryName()
    
    if geom_type[:len('multi')].lower() == 'multi':
        nested  = True
        coords = []
        geoms = [input_ogr_geom.GetGeometryRef(j) for 
                 j in range(input_ogr_geom.GetGeometryCount())]
        for geom in geoms:
            coords.append(geom.GetPoints())
    else:
        nested = False
        coords = list(input_ogr_geom.GetPoints())

    # Got stuck here. I can't find a generic solution for all geometry types.
    # I feel like I have to write one solution for every geometry type.

Third attempt: Exporting to WKT and using shapely.ops.transform

Using this post from miekwatt as inspiration, I tried to use the shapely.ops.transform function in tandem with the WKT-based transformation:

# Inspiration taken from https://gis.stackexchange.com/a/415879/123828
def drop_m(input_ogr_geom):
    shapely_geom = shapely.wkt.loads(input_ogr_geom.ExportToIsoWkt())
    def _drop_m(x, y, z):
        return x, y
    if input_ogr_geom.Is3D():
        return shapely_geom
    else:
        return shapely.ops.transform(_drop_m, shapely_geom)

print(drop_m(pt_2D).wkt)
# POINT (0 0) # right!

print(drop_m(line1_2D).wkt)
# LINESTRING (0 0, 1 1) # right!

print(drop_m(mline_2D).wkt)
# MULTILINESTRING ((0 0, 1 1), (3 1, 4 1)) # right!

print(drop_m(pt_3D).wkt)
# POINT Z (0 0 0) # right!

print(drop_m(line1_3D).wkt)
# LINESTRING Z (0 0 0, 1 1 1) # right!

print(drop_m(mline_3D).wkt)
# MULTILINESTRING Z ((0 0 0, 1 1 1), (3 1 3, 4 1 6)) # right!

This is the best approach so far! It works for all types of geometries, it's flexible enough to handle both 2D and 3D geometries, and it's pretty concise. However, it still relies on a WKT-based approach, which can be quite slow.

Back to the main question

Given all of the above, I'm brought back to the main question: is there a simple, quick and generic solution to transform an OGR geometry with M-values into a Shapely geometry without M-values other than the ones I tried in the code chunks above?

6
  • 1
    GDAL has SetMeasured, which adds or removes the M dimension gdal.org/python/osgeo.ogr.Geometry-class.html#SetMeasured
    – jbalk
    Jan 12 at 3:48
  • Oh, nice! That sounds promising, thank you!!! I can use that to strip the M-values.
    – Felipe D.
    Jan 12 at 4:37
  • After I use that function and generate new OGR geometry (now without M-values), do you know if there's also a way to convert it into a shapely geometry without going through WKT?
    – Felipe D.
    Jan 12 at 4:42
  • 1
    WKT tends to change last decimals so I recommend WKB if you do not find any direct method. There is a reverse example in gis.stackexchange.com/questions/52705/….
    – user30184
    Jan 12 at 7:30
  • Great! I can use geom.SetMeasured(False) and then shapely_geom = shapely.wkb.loads(geom.ExportToIsoWkb()). I can understand that the WKB conversion is more precise than the WKT conversion. But is it quicker, or mostly the same?
    – Felipe D.
    Jan 12 at 16:12

1 Answer 1

2

Here's a potential solution following suggestions from jbalk's and user30184's comments. It uses OGR's SetMeasured function to drop the M-values and relies on WKB (instead of WKT) to do the actual conversion between OGR and Shapely.

def from_ogr_to_shapely(ogr_geom):
    # Creating a copy of the input OGR geometry. This is done in order to 
    # ensure that when we drop the M-values, we are only doing so in a 
    # local copy of the geometry, not in the actual original geometry. 
    ogr_geom_copy = ogr.CreateGeometryFromWkb(ogr_geom.ExportToIsoWkb())
    
    # Dropping the M-values
    ogr_geom_copy.SetMeasured(False)

    # Generating a new shapely geometry
    shapely_geom = shapely.wkb.loads(ogr_geom_copy.ExportToIsoWkb())

    return shapely_geom

As mentioned in the commented section in the code above, we need to create a local copy of the input OGR geometry. This is because the function used to drop the M-values (the SetMeasured function) actually modifies the original input geometry. So in order to ensure that our original geometry remains intact, we need to create a local copy. Then we can modify the local copy to our heart's content without fear of changes creeping upstream to the original OGR geometry.

Here's the function being applied to the inputs in the question:

print(from_ogr_to_shapely(pt_2D).wkt)
# POINT (0 0) # right!

print(from_ogr_to_shapely(line1_2D).wkt)
# LINESTRING (0 0, 1 1) # right!

print(from_ogr_to_shapely(mline_2D).wkt)
# MULTILINESTRING ((0 0, 1 1), (3 1, 4 1)) # right!

print(from_ogr_to_shapely(pt_3D).wkt)
# POINT Z (0 0 0) # right!

print(from_ogr_to_shapely(line1_3D).wkt)
# LINESTRING Z (0 0 0, 1 1 1) # right!

print(from_ogr_to_shapely(mline_3D).wkt)
# MULTILINESTRING Z ((0 0 0, 1 1 1), (3 1 3, 4 1 6)) # right!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.