5

I am attempting to write a QGIS 3.10 Field Calculator Expression that will use the mz2geohash library to encode the centroid of the current feature as LAT/LON, and then pass this tuple to the mz2geohash.encode() function to return a value.

I've in-lined my current code below, as well as linked to where it lives on my GitHub:

from qgis.core import *
from qgis.gui import *
import mz2geohash

@qgsfunction(args='auto', group='Custom')
def geohash32(value,feature,parent):
    ft_centroid = centroid($geometry)
    ft_lon = x(transform(ft_centroid),layer_property(@layer_id,'crs'),'EPSG:4326'))
    ft_lat = y(transform(ft_centroid),layer_property(@layer_id,'crs'),'EPSG:4326'))
    ft_array = []
    ft_array.append(ft_lon)
    ft_array.append(ft_lat)
    geohash32 = mz2geohash.encode
    return geohash32(ft_array)

Steps I have taken:

  • Installed mz2geohash library into QGIS 3.10 python environment using pip
  • Verified functionality of mz2geohash library in QGIS 3.10 python environment enter image description here
  • Integrated suggested changes in Comments 1 & 2 from Gabriel De Luca and jbalk (removal of extraneous parentheses)
  • Recast ft_tup as an array using the QGIS array() function
  • Removed empty initialization of tuple since it is immutable and makes no sense, as per Gabriel De Luca and ThingumaBob.
  • Added ft_array to create an empty array element
  • Added ft_array.append(ft_lon) and ft_array.append(ft_lat) to add values to ft_array
  • aliased mz2geohash.encode as geohash32
  • Verified that these steps work stepwise in the Python terminal enter image description here

What I'm doing with my code:
ft_tup is establishing an empty tuple
ft_centroid is getting the centroid as a geometry object for the current feature
ft_lon is returning the X value of the centroid projected into WGS84
ft_lat is returning the Y value of the centroid projected into WGS84
ft_array is establishing an empty array to hold the results of ft_lon,ft_lat
ft_array.append(ft_lon) is inserting the x value into position 0 of the array
ft_array.append(ft_lat) is inserting the y value into position 1 of the array
geohash32 is created as an alias for mz2geohash.encode
return geohash32(ft_array) is supposed to give the encoded geohash

What QGIS has to say about my code:

An error occurred during execution of following code:
from qgis.core import *
from qgis.gui import *
import mz2geohash 

@qgsfunction(args='auto', group='Custom')
def geohash32(value,feature,parent):
    ft_centroid = centroid($geometry) 
    ft_lon = x(transform(ft_centroid),layer_property(@layer_id,'crs'),'EPSG:4326')) 
    ft_lat = y(transform(ft_centroid),layer_property(@layer_id,'crs'),'EPSG:4326')) 
    ft_array = [] 
    ft_array.append(ft_lon) 
    ft_array.append(ft_lat) 
    geohash32 = mz2geohash.encode 
    return geohash32(ft_array)


traceback.print_exception() failed

Python version:


QGIS version:
3.10.0-A Coruña 'A Coruña', 6ffa89eb3e

Python path:

Individually, each line from the above works when tested in the python console outside of QGIS, or when each line is tested in the Field Calculator (with variables substituted for actual values).
Examples:

ft_centroid  
centroid($geometry) = <geometry: Point>  
ft_lon  
x(transform(centroid($geometry),layer_property(@layer_id,'crs'),'EPSG:4326')) = -82.83264665692754  
ft_lat  
y(transform(centroid($geometry),layer_property(@layer_id,'crs'),'EPSG:4326')) = 42.61281133650196  

Why do I want this?
I strongly desire the ability for QGIS to create human-readable spatially-derived interoperable/standardized GUIDs.

See my issue here:
https://github.com/qgis/QGIS/issues/31327

What will I do with the fixed code?
Open a PR to have this moved to core so everyone can use it.

What am I missing here?

12
  • 2
    Seen only from the point of view of QGIS expressions, I would say that the correct way is:x( transform( ft_centroid, layer_property( @layer_id, 'crs'), 'EPSG:4326')) and y( transform( ft_centroid, layer_property( @layer_id, 'crs'), 'EPSG:4326')). Individually, each line from the above works surprises me because it seems that they have a parenthesis left over. Nov 21 '19 at 23:57
  • 1
    remove the parenthesis after both instances of ft_centroid so it's like x(transform(ft_centroid,layer_prop...
    – jbalk
    Nov 22 '19 at 1:07
  • 1
    for EPSG:4326 shouldn't x be lat (the first axis)?
    – nmtoken
    Nov 22 '19 at 9:11
  • 1
    I don't know if ft_tup = () is the way to initialize an empty array. stackoverflow.com/questions/1514553/… Nov 22 '19 at 9:44
  • 1
    @GabrielDeLuca it's the way to create an empty tuple; a tuple, however, is immutable, so it doesn't make sense to initialize it empty. It seems the encode function accepts either a tuple (!= array), or two separate values. Maybe try to fulfill that explicitly.
    – geozelot
    Nov 22 '19 at 9:49
2

The expression can be introduced, as a string, as an argument of the QgsExpression() function.
Then, it must be evaluated in the context in which the geohash() function is called.

The $geometry expression seems to be evaluating well within the function, but no. The output is shown correctly in the preview, but when executed it returns a null value, to which the encode function returns an error.

I was able to access the geometry through the row_number variable. I disarmed the whole expression in parts so that it is a little less confusing.

from qgis.core import *
from qgis.gui import *
import mz2geohash 

@qgsfunction(args='auto', group='Custom')
def geohash32_noargs(feature, parent, context):

    geometry_expression = r"geometry( get_feature_by_id( '" +  context.variable( 'layer_id' ) + r"' , " + str(context.variable( 'row_number' )) + r"))"
    centroid_expression = r"centroid( " + geometry_expression + r")"
    layerCRS_expression = r"layer_property( '" + context.variable( 'layer_id' ) + r"' , 'crs')"
    transform_expression = r"transform( " + centroid_expression + r", " + layerCRS_expression + r", 'EPSG:4326')"

    x_expression = QgsExpression('x( ' + transform_expression + ')')
    x_value = x_expression.evaluate(context)

    y_expression = QgsExpression('y( ' + transform_expression + ')')
    y_value = y_expression.evaluate(context)

    value = (x_value, y_value)
    return mz2geohash.encode(value)

Finally, create a new string data type field and populate it with the geohash32_noargs() expression.


I am absolutely sure that the transformation, obtaining the centroid, and obtaining its coordinates, can be performed from functions of the imported libraries, without evaluating them as an expression (but I still wouldn't know how to do it). However, learning to evaluate expressions within the function is valid.


Another option (seems better to me) would be to define the geohash32() function with two parameters:

from qgis.core import *
from qgis.gui import *
import mz2geohash 

@qgsfunction(args='auto', group='Custom')
def geohash32(x_value, y_value, feature, parent):
    """
    Encodes a ( lon , lat ) pair to a GeoHash,
    requires the <a href="https://github.com/jason-h-simplifi/mapzen-geohash">mz2geohash package</a> installed...
    <br>
    <h2>Example usage:</h2>
    <ul>
        <li>geohash32( -58.4 , -34.6 ) -> '69y7pj62mkeb'</li> 
        <li>geohash32( "lon" , "lat" ) ->  a  base 32 GeoHash string </li>
    </ul>
    """
    value = (x_value, y_value)
    return mz2geohash.encode(value)

And call it from the expression builder with the proper arguments:

geohash32( 
  x( transform( 
    centroid( $geometry),
    layer_property( @layer_id, 'crs'),
    'EPSG:4326')), 
  y( transform( 
    centroid( $geometry),
    layer_property( @layer_id, 'crs'),
    'EPSG:4326')))

I'm sorry if I'm confusing functions with objects, methods or classes. Take this answer as a beginner's effort.

7
  • This looks amazing! I can't thank you enough for your effort. Please allow me a few days to read this over, test it out, and try implementing it. Once I do, I'll follow back up here with you. Nov 28 '19 at 6:28
  • 1
    @Saijin_Naib You are welcome. I have just added a description for the geohash32() function. Nov 28 '19 at 12:04
  • The top function doesn't seem to work when I'm computing the geohash32 using the geohash32_noargs() expression. It throws this error: An error occurred while evaluating the calculation string: '>=' not supported between instances of 'NoneType' and 'float' Dec 2 '19 at 17:23
  • 1
    I have that function working. Have you tried to debug it returning the expressions strings? About the geohash32 funtion, it just run the mz2geohash function with the arguments given. Maybe are you giving it different arguments or I don't know what is the function used in Lat Lon Tools. Dec 2 '19 at 19:28
  • 1
    If you call the geohash32 function with numerical arguments and the mz32geohash function with the same arguments, they must return the same result. About the geohash_noargs function, if the expression is well formed all you need to do is check the context: you need to run it from an active layer, with a defined CRS, with geometries in their features. Dec 2 '19 at 19:34

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.