Exemple #1
0
class ORSIsochronesLayerAlgo(ORSBaseProcessingAlgorithm):
    def __init__(self):
        super().__init__()
        self.ALGO_NAME = 'isochrones_from_layer'
        self.GROUP = 'Isochrones'

        self.IN_POINTS = "INPUT_POINT_LAYER"
        self.IN_FIELD = "INPUT_FIELD"
        self.IN_METRIC = 'INPUT_METRIC'
        self.IN_RANGES = 'INPUT_RANGES'
        self.IN_KEY = 'INPUT_APIKEY'
        self.IN_DIFFERENCE = 'INPUT_DIFFERENCE'
        self.PARAMETERS = [
            QgsProcessingParameterFeatureSource(
                name=self.IN_POINTS,
                description="Input Point layer",
                types=[QgsProcessing.TypeVectorPoint]
            ),
            # QgsProcessingParameterBoolean(
            #     name=self.IN_DIFFERENCE,
            #     description="Dissolve and calculate isochrone difference",
            # )
            QgsProcessingParameterField(
                name=self.IN_FIELD,
                description="Input layer ID Field (mutually exclusive with Point option)",
                parentLayerParameterName=self.IN_POINTS,
                optional=True
            ),
            QgsProcessingParameterEnum(
                name=self.IN_METRIC,
                description="Dimension",
                options=DIMENSIONS,
                defaultValue=DIMENSIONS[0]
            ),
            QgsProcessingParameterString(
                name=self.IN_RANGES,
                description="Comma-separated ranges [min or m]",
                defaultValue="5, 10"
            )
        ]

    # Save some important references
    # TODO bad style, refactor
    isochrones = isochrones_core.Isochrones()
    dest_id = None
    crs_out = QgsCoordinateReferenceSystem.fromEpsgId(4326)
    # difference = None

    # TODO: preprocess parameters to options the range cleanup below:
    # https://www.qgis.org/pyqgis/master/core/Processing/QgsProcessingAlgorithm.html#qgis.core.QgsProcessingAlgorithm.prepareAlgorithm
    def processAlgorithm(self, parameters, context, feedback):
        ors_client = self._get_ors_client_from_provider(parameters[self.IN_PROVIDER], feedback)

        profile = dict(enumerate(PROFILES))[parameters[self.IN_PROFILE]]
        dimension = dict(enumerate(DIMENSIONS))[parameters[self.IN_METRIC]]

        factor = 60 if dimension == 'time' else 1
        ranges_raw = parameters[self.IN_RANGES]
        ranges_proc = [x * factor for x in map(int, ranges_raw.split(','))]

        # self.difference = self.parameterAsBool(parameters, self.IN_DIFFERENCE, context)
        source = self.parameterAsSource(parameters, self.IN_POINTS, context)

        options = self.parseOptions(parameters, context)

        # Make the actual requests
        requests = []
        if QgsWkbTypes.flatType(source.wkbType()) == QgsWkbTypes.MultiPoint:
            raise QgsProcessingException(
                "TypeError: Multipoint Layers are not accepted. Please convert to single geometry layer.")

        # Get ID field properties
        id_field_name = parameters[self.IN_FIELD]
        parameter_options = list()
        if id_field_name:
            id_field = source.fields().field(id_field_name)
            parameter_options = [id_field.type(), id_field_name]

        self.isochrones.set_parameters(profile, dimension, factor, *parameter_options)

        for locations, id_value in self.get_sorted_feature_parameters(source, id_field_name):
            # Stop the algorithm if cancel button has been clicked
            if feedback.isCanceled():
                break

            requests.append({
                "locations": locations,
                "range_type": dimension,
                "range": ranges_proc,
                "attributes": ['total_pop'],
                "id": id_value,
                "options": options
            })

        (sink, self.dest_id) = self.parameterAsSink(parameters, self.OUT, context,
                                                    self.isochrones.get_fields(),
                                                    QgsWkbTypes.Polygon,
                                                    # Needs Multipolygon if difference parameter will ever be
                                                    # reactivated
                                                    self.crs_out)

        for num, params in enumerate(requests):
            if feedback.isCanceled():
                break

            # If feature causes error, report and continue with next
            try:
                # Populate features from response
                response = ors_client.request('/v2/isochrones/' + profile, {}, post_json=params)

                for isochrone in self.isochrones.get_features(response, params['id']):
                    sink.addFeature(isochrone)

            except (exceptions.ApiError,
                    exceptions.InvalidKey,
                    exceptions.GenericServerError) as e:
                msg = f"Feature ID {params['id']} caused a {e.__class__.__name__}:\n{str(e)}"
                feedback.reportError(msg)
                logger.log(msg, 2)
                continue
            feedback.setProgress(int(100.0 / source.featureCount() * num))

        return {self.OUT: self.dest_id}

    # noinspection PyUnusedLocal
    def postProcessAlgorithm(self, context, feedback):
        """Style polygon layer in post-processing step."""
        # processed_layer = self.isochrones.calculate_difference(self.dest_id, context)
        processed_layer = QgsProcessingUtils.mapLayerFromString(self.dest_id, context)
        self.isochrones.stylePoly(processed_layer)

        return {self.OUT: self.dest_id}

    @staticmethod
    def get_sorted_feature_parameters(layer: QgsProcessingParameterFeatureSource, id_field_name: str):
        """
        Generator to yield geometry and id of features sorted by feature ID. Careful: feat.id() is not necessarily
        permanent

        :param layer: source input layer.
        :param id_field_name: layer field containing id values
        """
        # First get coordinate transformer
        x_former = transform.transformToWGS(layer.sourceCrs())

        for feat in sorted(layer.getFeatures(), key=lambda f: f.id()):
            x_point = x_former.transform(feat.geometry().asPoint())
            id_value = feat[id_field_name] if id_field_name else None

            yield [[round(x_point.x(), 6), round(x_point.y(), 6)]], id_value
class ORSIsochronesPointAlgo(ORSBaseProcessingAlgorithm):
    def __init__(self):
        super().__init__()
        self.ALGO_NAME = 'isochrones_from_point'
        self.GROUP = "Isochrones"
        self.IN_POINT = "INPUT_POINT"
        self.IN_METRIC = 'INPUT_METRIC'
        self.IN_RANGES = 'INPUT_RANGES'
        self.IN_KEY = 'INPUT_APIKEY'
        self.IN_DIFFERENCE = 'INPUT_DIFFERENCE'
        self.PARAMETERS = [
            QgsProcessingParameterPoint(
                name=self.IN_POINT,
                description=
                "Input Point from map canvas (mutually exclusive with layer option)",
                optional=True),
            QgsProcessingParameterEnum(name=self.IN_METRIC,
                                       description="Dimension",
                                       options=DIMENSIONS,
                                       defaultValue=DIMENSIONS[0]),
            QgsProcessingParameterString(
                name=self.IN_RANGES,
                description="Comma-separated ranges [min or m]",
                defaultValue="5, 10")
        ]

    # Save some important references
    # TODO bad style, refactor
    isochrones = isochrones_core.Isochrones()
    dest_id = None
    crs_out = QgsCoordinateReferenceSystem.fromEpsgId(4326)

    # difference = None

    # TODO: preprocess parameters to options the range cleanup below:
    # https://www.qgis.org/pyqgis/master/core/Processing/QgsProcessingAlgorithm.html#qgis.core.QgsProcessingAlgorithm.preprocessParameters
    def processAlgorithm(self, parameters, context, feedback):
        ors_client = self._get_ors_client_from_provider(
            parameters[self.IN_PROVIDER], feedback)

        profile = dict(enumerate(PROFILES))[parameters[self.IN_PROFILE]]
        dimension = dict(enumerate(DIMENSIONS))[parameters[self.IN_METRIC]]

        factor = 60 if dimension == 'time' else 1
        ranges_raw = parameters[self.IN_RANGES]
        ranges_proc = [x * factor for x in map(int, ranges_raw.split(','))]

        options = self.parseOptions(parameters, context)

        point = self.parameterAsPoint(parameters, self.IN_POINT, context,
                                      self.crs_out)

        # Make the actual requests
        # If layer source is set
        self.isochrones.set_parameters(profile, dimension, factor)
        params = {
            "locations": [[round(point.x(), 6),
                           round(point.y(), 6)]],
            "range_type": dimension,
            "range": ranges_proc,
            "attributes": ['total_pop'],
            "id": None,
            "options": options
        }

        (sink, self.dest_id) = self.parameterAsSink(
            parameters,
            self.OUT,
            context,
            self.isochrones.get_fields(),
            QgsWkbTypes.Polygon,
            # Needs Multipolygon if difference parameter will ever be
            # reactivated
            self.crs_out)

        try:
            response = ors_client.request('/v2/isochrones/' + profile, {},
                                          post_json=params)

            # Populate features from response
            for isochrone in self.isochrones.get_features(
                    response, params['id']):
                sink.addFeature(isochrone)

        except (exceptions.ApiError, exceptions.InvalidKey,
                exceptions.GenericServerError) as e:
            msg = f"Feature ID {params['id']} caused a {e.__class__.__name__}:\n{str(e)}"
            feedback.reportError(msg)
            logger.log(msg, 2)

        return {self.OUT: self.dest_id}

    # noinspection PyUnusedLocal
    def postProcessAlgorithm(self, context, feedback):
        """Style polygon layer in post-processing step."""
        processed_layer = QgsProcessingUtils.mapLayerFromString(
            self.dest_id, context)
        self.isochrones.stylePoly(processed_layer)

        return {self.OUT: self.dest_id}
Exemple #3
0
class ORSisochronesPointAlgo(QgsProcessingAlgorithm):
    # TODO: create base algorithm class common to all modules

    ALGO_NAME = 'isochrones_from_point'
    ALGO_NAME_LIST = ALGO_NAME.split('_')

    IN_PROVIDER = "INPUT_PROVIDER"
    IN_POINT = "INPUT_POINT"
    IN_PROFILE = "INPUT_PROFILE"
    IN_METRIC = 'INPUT_METRIC'
    IN_RANGES = 'INPUT_RANGES'
    IN_KEY = 'INPUT_APIKEY'
    IN_DIFFERENCE = 'INPUT_DIFFERENCE'
    OUT = 'OUTPUT'

    # Save some important references
    isochrones = isochrones_core.Isochrones()
    dest_id = None
    crs_out = QgsCoordinateReferenceSystem(4326)

    # difference = None

    def initAlgorithm(self,
                      configuration,
                      p_str=None,
                      Any=None,
                      *args,
                      **kwargs):

        providers = [
            provider['name']
            for provider in configmanager.read_config()['providers']
        ]
        self.addParameter(
            QgsProcessingParameterEnum(self.IN_PROVIDER,
                                       "Provider",
                                       providers,
                                       defaultValue=providers[0]))

        self.addParameter(
            QgsProcessingParameterPoint(
                name=self.IN_POINT,
                description=
                "Input Point from map canvas (mutually exclusive with layer option)",
                optional=True))

        self.addParameter(
            QgsProcessingParameterEnum(self.IN_PROFILE,
                                       "Travel mode",
                                       PROFILES,
                                       defaultValue=PROFILES[0]))

        self.addParameter(
            QgsProcessingParameterEnum(name=self.IN_METRIC,
                                       description="Dimension",
                                       options=DIMENSIONS,
                                       defaultValue=DIMENSIONS[0]))

        self.addParameter(
            QgsProcessingParameterString(
                name=self.IN_RANGES,
                description="Comma-separated ranges [mins or m]",
                defaultValue="5, 10"))

        self.addParameter(
            QgsProcessingParameterFeatureSink(name=self.OUT,
                                              description="Isochrones",
                                              createByDefault=False))

    def group(self):
        return "Isochrones"

    def groupId(self):
        return 'isochrones'

    def name(self):
        return self.ALGO_NAME

    def shortHelpString(self):
        """Displays the sidebar help in the algorithm window"""

        file = os.path.join(HELP_DIR, 'algorithm_isochrone_point.help')
        with open(file) as helpf:
            msg = helpf.read()

        return msg

    def helpUrl(self):
        """will be connected to the Help button in the Algorithm window"""
        return __help__

    def displayName(self):
        return " ".join(map(lambda x: x.capitalize(), self.ALGO_NAME_LIST))

    def icon(self):
        return QIcon(RESOURCE_PREFIX + 'icon_isochrones.png')

    def createInstance(self):
        return ORSisochronesPointAlgo()

    # TODO: preprocess parameters to options the range clenaup below:
    # https://www.qgis.org/pyqgis/master/core/Processing/QgsProcessingAlgorithm.html#qgis.core.QgsProcessingAlgorithm.preprocessParameters

    def processAlgorithm(self, parameters, context, feedback):
        # Init ORS client
        providers = configmanager.read_config()['providers']
        provider = providers[self.parameterAsEnum(parameters, self.IN_PROVIDER,
                                                  context)]
        clnt = client.Client(provider)
        clnt.overQueryLimit.connect(
            lambda: feedback.reportError("OverQueryLimit: Retrying..."))

        params = dict()
        params['attributes'] = ['total_pop']

        profile = PROFILES[self.parameterAsEnum(parameters, self.IN_PROFILE,
                                                context)]
        params['range_type'] = dimension = DIMENSIONS[self.parameterAsEnum(
            parameters, self.IN_METRIC, context)]

        factor = 60 if params['range_type'] == 'time' else 1
        ranges_raw = self.parameterAsString(parameters, self.IN_RANGES,
                                            context)
        ranges_proc = [x * factor for x in map(int, ranges_raw.split(','))]
        params['range'] = ranges_proc

        point = self.parameterAsPoint(parameters, self.IN_POINT, context,
                                      self.crs_out)

        # Make the actual requests
        # If layer source is set
        requests = []
        self.isochrones.set_parameters(profile, dimension, factor)
        params['locations'] = [[round(point.x(), 6), round(point.y(), 6)]]
        params['id'] = None
        requests.append(params)

        (sink, self.dest_id) = self.parameterAsSink(
            parameters,
            self.OUT,
            context,
            self.isochrones.get_fields(),
            QgsWkbTypes.
            Polygon,  # Needs Multipolygon if difference parameter will ever be reactivated
            self.crs_out)

        # If feature causes error, report and continue with next
        try:
            # Populate features from response
            response = clnt.request('/v2/isochrones/' + profile, {},
                                    post_json=params)

            for isochrone in self.isochrones.get_features(
                    response, params['id']):
                sink.addFeature(isochrone)

        except (exceptions.ApiError, exceptions.InvalidKey,
                exceptions.GenericServerError) as e:
            msg = "Feature ID {} caused a {}:\n{}".format(
                params['id'], e.__class__.__name__, str(e))
            feedback.reportError(msg)
            logger.log(msg, 2)

        return {self.OUT: self.dest_id}

    def postProcessAlgorithm(self, context, feedback):
        """Style polygon layer in post-processing step."""
        processed_layer = QgsProcessingUtils.mapLayerFromString(
            self.dest_id, context)
        self.isochrones.stylePoly(processed_layer)

        return {self.OUT: self.dest_id}
Exemple #4
0
class ORSisochronesLayerAlgo(QgsProcessingAlgorithm):
    # TODO: create base algorithm class common to all modules

    ALGO_NAME = 'isochrones_from_layer'
    ALGO_NAME_LIST = ALGO_NAME.split('_')

    IN_PROVIDER = "INPUT_PROVIDER"
    IN_POINTS = "INPUT_POINT_LAYER"
    IN_FIELD = "INPUT_FIELD"
    IN_PROFILE = "INPUT_PROFILE"
    IN_METRIC = 'INPUT_METRIC'
    IN_RANGES = 'INPUT_RANGES'
    IN_KEY = 'INPUT_APIKEY'
    IN_DIFFERENCE = 'INPUT_DIFFERENCE'
    OUT = 'OUTPUT'

    # Save some important references
    isochrones = isochrones_core.Isochrones()
    dest_id = None
    crs_out = QgsCoordinateReferenceSystem(4326)
    # difference = None

    def initAlgorithm(self, configuration, p_str=None, Any=None, *args, **kwargs):

        providers = [provider['name'] for provider in configmanager.read_config()['providers']]
        self.addParameter(
            QgsProcessingParameterEnum(
                self.IN_PROVIDER,
                "Provider",
                providers,
                defaultValue=providers[0]
            )
        )

        self.addParameter(
            QgsProcessingParameterFeatureSource(
                name=self.IN_POINTS,
                description="Input Point layer",
                types=[QgsProcessing.TypeVectorPoint]
            )
        )

        # self.addParameter(
        #     QgsProcessingParameterBoolean(
        #         name=self.IN_DIFFERENCE,
        #         description="Dissolve and calulate isochrone difference",
        #     )
        # )

        self.addParameter(
            QgsProcessingParameterField(
                name=self.IN_FIELD,
                description="Input layer ID Field (mutually exclusive with Point option)",
                parentLayerParameterName=self.IN_POINTS
            )
        )

        self.addParameter(
            QgsProcessingParameterEnum(
                self.IN_PROFILE,
                "Travel mode",
                PROFILES,
                defaultValue=PROFILES[0]
            )
        )

        self.addParameter(
            QgsProcessingParameterEnum(
                name=self.IN_METRIC,
                description="Dimension",
                options=DIMENSIONS,
                defaultValue=DIMENSIONS[0]
            )
        )

        self.addParameter(
            QgsProcessingParameterString(
                name=self.IN_RANGES,
                description="Comma-separated ranges [mins or m]",
                defaultValue="5, 10"
            )
        )

        self.addParameter(
            QgsProcessingParameterFeatureSink(
                name=self.OUT,
                description="Isochrones",
                createByDefault=False
            )
        )

    def group(self):
        return "Isochrones"

    def groupId(self):
        return 'isochrones'

    def name(self):
        return self.ALGO_NAME

    def shortHelpString(self):
        """Displays the sidebar help in the algorithm window"""

        file = os.path.join(
            HELP_DIR,
            'algorithm_isochrone_layer.help'
        )
        with open(file) as helpf:
            msg = helpf.read()

        return msg

    def helpUrl(self):
        """will be connected to the Help button in the Algorithm window"""
        return __help__

    def displayName(self):
        return " ".join(map(lambda x: x.capitalize(), self.ALGO_NAME_LIST))

    def icon(self):
        return QIcon(RESOURCE_PREFIX + 'icon_isochrones.png')

    def createInstance(self):
        return ORSisochronesLayerAlgo()

    # TODO: preprocess parameters to options the range clenaup below:
    # https://www.qgis.org/pyqgis/master/core/Processing/QgsProcessingAlgorithm.html#qgis.core.QgsProcessingAlgorithm.preprocessParameters

    def processAlgorithm(self, parameters, context, feedback):
        # Init ORS client
        providers = configmanager.read_config()['providers']
        provider = providers[self.parameterAsEnum(parameters, self.IN_PROVIDER, context)]
        clnt = client.Client(provider)
        clnt.overQueryLimit.connect(lambda : feedback.reportError("OverQueryLimit: Retrying..."))

        params = dict()
        params['attributes'] = ['total_pop']

        profile = PROFILES[self.parameterAsEnum(parameters, self.IN_PROFILE, context)]
        params['range_type'] = dimension = DIMENSIONS[self.parameterAsEnum(parameters, self.IN_METRIC, context)]

        factor = 60 if params['range_type'] == 'time' else 1
        ranges_raw = self.parameterAsString(parameters, self.IN_RANGES, context)
        ranges_proc = [x * factor for x in map(int, ranges_raw.split(','))]
        params['range'] = ranges_proc

        # self.difference = self.parameterAsBool(parameters, self.IN_DIFFERENCE, context)
        source = self.parameterAsSource(parameters, self.IN_POINTS, context)

        # Make the actual requests
        requests = []
        if source.wkbType() == 4:
            raise QgsProcessingException("TypeError: Multipoint Layers are not accepted. Please convert to single geometry layer.")

        # Get ID field properties
        id_field_name = self.parameterAsString(parameters, self.IN_FIELD, context)
        id_field_id = source.fields().lookupField(id_field_name)
        if id_field_name == '':
            id_field_id = 0
            id_field_name = source.fields().field(id_field_id).name()
        id_field = source.fields().field(id_field_id)

        # Populate iso_layer instance with parameters
        self.isochrones.set_parameters(profile, dimension, factor, id_field.type(), id_field_name)

        for properties in self.get_sorted_feature_parameters(source):
            # Stop the algorithm if cancel button has been clicked
            if feedback.isCanceled():
                break

            # Get transformed coordinates and feature
            params['locations'], feat = properties
            params['id'] = feat[id_field_name] or None
            requests.append(deepcopy(params))

        (sink, self.dest_id) = self.parameterAsSink(parameters, self.OUT, context,
                                                    self.isochrones.get_fields(),
                                                    QgsWkbTypes.Polygon,  # Needs Multipolygon if difference parameter will ever be reactivated
                                                    self.crs_out)

        for num, params in enumerate(requests):
            if feedback.isCanceled():
                break

            # If feature causes error, report and continue with next
            try:
                # Populate features from response
                response = clnt.request('/v2/isochrones/' + profile, {}, post_json=params)

                for isochrone in self.isochrones.get_features(response, params['id']):
                    sink.addFeature(isochrone)

            except (exceptions.ApiError,
                    exceptions.InvalidKey,
                    exceptions.GenericServerError) as e:
                msg = "Feature ID {} caused a {}:\n{}".format(
                    params['id'],
                    e.__class__.__name__,
                    str(e))
                feedback.reportError(msg)
                logger.log(msg, 2)
                continue
            feedback.setProgress(int(100.0 / source.featureCount() * num))

        return {self.OUT: self.dest_id}

    def postProcessAlgorithm(self, context, feedback):
        """Style polygon layer in post-processing step."""
        # processed_layer = self.isochrones.calculate_difference(self.dest_id, context)
        processed_layer= QgsProcessingUtils.mapLayerFromString(self.dest_id, context)
        self.isochrones.stylePoly(processed_layer)

        return {self.OUT: self.dest_id}

    def get_sorted_feature_parameters(self, layer):
        """
        Generator to yield geometry and id of features sorted by feature ID. Careful: feat.id() is not necessarily
        permanent

        :param layer: source input layer.
        :type layer: QgsProcessingParameterFeatureSource
        """
        # First get coordinate transformer
        xformer = transform.transformToWGS(layer.sourceCrs())

        for feat in sorted(layer.getFeatures(), key=lambda f: f.id()):
            x_point = xformer.transform(feat.geometry().asPoint())

            yield ([[round(x_point.x(), 6), round(x_point.y(), 6)]], feat)