Using python QGIS network analysis library to calculate short path based on speed and distance?

i found how to do it. i converted RgSpeedProperter class from RoadGraphPlugin cpp Source to python. the python class definition is below:

from PyQt4.QtCore import *
from PyQt4.QtGui import *
from qgis.core import *
from qgis.gui import *
from qgis.networkanalysis import *
from qgis.utils import *
import qgis 
class SpeedProperter(QgsArcProperter):
    def __init__(self, attributeId, defaultValue, toMetricFactor = 1000 ):
        QgsArcProperter.__init__(self)
        self.AttributeId = attributeId
        self.DefaultValue = defaultValue
        self.ToMetricFactor = toMetricFactor
    def property(self, distance, Feature):
        map = Feature.attributeMap()
        it = map[ self.AttributeId ]
        #if ( it == map.end() ):
        #    return QVariant( distance / ( self.DefaultValue * self.ToMetricFactor ) )      
        val = distance / ( it.toDouble()[0] * self.ToMetricFactor )
        if ( val <= 0.0 ):
            return QVariant( distance / ( self.DefaultValue * self.ToMetricFactor ) )
        return QVariant( val )
    def requiredAttributes(self):
        l = []
        l.append( self.AttributeId );
        return l  

i set toMetricFactor default value as 1000 because i'm using the speed in km/h and the default speed is 25.


I'm using Ubuntu 14.04, QGIS 2.8.1-Wien and python2.7.

Feature.attributeMap() does not work.

After much struggles with the undocumented way of implementing new ArcProperter's, here is my working solution. Also serves as a rogue documentation.

class SpeedFieldProperter(QgsArcProperter):
    """
    (attributeIndex, defaultSpeed=2.71828, speedToDistanceFactor = 1000)
    SpeedProperter to factor in speed and distance to edge tavel time cost
    @attributeIndex - (int) the index of the speed attribute in your shapefile attributes
        the speed attribute is an int/real/double/string number (convertible to number using .toDouble() )
    @defaultSpeed - speed value to be used if it can't be parsed from the edge or equals 0
    @speedToDistanceFactor - factor to adjust speed units (e.g. km/h) to distance units (e.g. meters)
        if the speed attribute is in km/h and distance in meters, this should equal 1000
    """
    def __init__(self, attributeIndex, defaultSpeed=2.71828, speedToDistanceFactor = 1000):
        QgsArcProperter.__init__(self)
        self.AttributeIndex = attributeIndex
        self.DefaultSpeed = defaultSpeed
        self.SpeedToDistanceFactor = speedToDistanceFactor

    def property(self, distance, Feature):
        """
        returns the cost of the edge. In this case travel time.
        """
        attrs = Feature.attributes() # list of QVariant objects
        speed,success = attrs[self.AttributeIndex].toDouble()
        if not success or speed<=0:
            speed = self.DefaultSpeed
        travel_time = distance / (speed * self.SpeedToDistanceFactor)
        return QVariant( travel_time )

    def requiredAttributes(self):
        """
        returns list of indices of feature attributes
        needed for cost calculation in property()
        """
        return [self.AttributeIndex]

Here is example code that uses this and successfully runs on my system. For more info on most of these functions, see the PyQGIS Developer Cookbook on network analysis.

from PyQt4.QtCore import *
from PyQt4.QtGui import *
import qgis
from qgis.core import *
from qgis.gui import *
from qgis.networkanalysis import *
from qgis.utils import *

# Only necessary outside the QGIS console. You might need your own version of these lines, there's a different thread that discusses this.
QgsApplication.setPrefixPath('/usr', True)
QgsApplication.initQgis()


roads_path = '/path/to/your/road/shapefile/roads.shp'
import ogr
driver = ogr.GetDriverByName('ESRI Shapefile')
dataSource = driver.Open(roads_path, 1)
layer = dataSource.GetLayer()
layer_defn = layer.GetLayerDefn()
# this is your way to find the index of your speed field
field_names = [layer_defn.GetFieldDefn(i).GetName() for i in range(layer_defn.GetFieldCount())]
startPoint,endPoint = [QgsPoint(619046,10119913), QgsPoint(615414,10115660)]
points_to_tie = [startPoint, endPoint]

# load layer
roads_layer = QgsVectorLayer(roads_path, 'roads', "ogr")
if not roads_layer.isValid():
    raise IOError, "Failed to open the road layer"

# this is where we use our Properter.
properter = SpeedFieldProperter(attributeIndex=3, defaultSpeed=17.09, speedToDistanceFactor=1000)
# properter = QgsDistanceArcProperter() # for the default distance properter instead

director = QgsLineVectorLayerDirector( roads_layer, -1, '', '', '', 3 )
director.addProperter( properter )
builder = QgsGraphBuilder( roads_layer.crs(), topologyTolerance=0 )
tiedPoints = director.makeGraph( builder, points_to_tie )
graph = builder.graph()
startId = graph.findVertex( tiedPoints[ 0 ] )
endId   = graph.findVertex( tiedPoints[ -1 ] )
(dtree, dcost) = QgsGraphAnalyzer.dijkstra( graph, startId, 0 )
print 'Travel time to endPoint: {} hours'.format(dcost[endId])

I think you don't need QVariant. I'm trying to create my own Properter for bike trips which (as a start) excludes freeways and roads that haven't been built yet. In my road layer, these are CLASS_CODE 0 and 9.

Here's some code that seems to be working so far for me (still testing).

In the shortest path function:

    directionField = self.roadLayer.fieldNameIndex('DIR_CODE')
    classCodeField = self.roadLayer.fieldNameIndex('CLASS_CODE') 

    properter = MyProperter(directionField, classCodeField)

The properter class:

class MyProperter(QgsArcProperter):
    def __init__(self, directionIndex, classCodeIndex):
        QgsArcProperter.__init__(self)
        self.directionIndex = directionIndex
        self.classCodeIndex = classCodeIndex

    # returns cost of edge, set at infinity for freeways and proposed roads, otherwise distance
    def property(self, distance, feature):
        expression = feature.attribute("CLASS_CODE") in (0, 9) # 0 is freeway, 9 is proposed road
        if expression:
            return float('inf')
        return distance

    def requiredAttributes(self):
        # return the indices for (1) the direction attribute, and (2) any other attribute used in the property() method
        return [self.directionIndex, self.classCodeIndex]