def _on_prov_refresh_click(self): """Populates provider dropdown with fresh list from config.yml""" providers = configmanager.read_config()['providers'] self.provider_combo.clear() for provider in providers: self.provider_combo.addItem(provider['name'], provider)
def _init_gui_control(self): """Slot for main plugin button. Initializes the GUI and shows it.""" # Only populate GUI if it's the first start of the plugin within the QGIS session # If not checked, GUI would be rebuilt every time! if self.first_start: self.first_start = False self.dlg = ORStoolsDialog( self.iface, self.iface.mainWindow()) # setting parent enables modal view self.dlg.routing_advanced_button.clicked.connect( self._on_advanced_click) # Make sure plugin window stays open when OK is clicked by reconnecting the accepted() signal self.dlg.global_buttons.accepted.disconnect(self.dlg.accept) self.dlg.global_buttons.accepted.connect(self.run_gui_control) # Populate provider box on window startup, since can be changed from multiple menus/buttons providers = configmanager.read_config()['providers'] self.dlg.provider_combo.clear() for provider in providers: self.dlg.provider_combo.addItem(provider['name'], provider) # Populate Advanced dialog; makes sure that dialog is re-populated every time plugin starts, # but stays alive during one session self.advanced = ORStoolsDialogAdvancedMain(parent=self.dlg) self.dlg.show()
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 interval_raw = self.parameterAsString(parameters, self.IN_INTERVAL, context) if interval_raw: params['interval'] = interval_raw smoothing_raw = self.parameterAsString(parameters, self.IN_SMOOTH, context) if smoothing_raw: params['smoothing'] = smoothing_raw 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 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_START, description="Input Start Point layer", types=[QgsProcessing.TypeVectorPoint], )) self.addParameter( QgsProcessingParameterField( name=self.IN_START_FIELD, description="Start ID Field (can be used for joining)", parentLayerParameterName=self.IN_START, )) self.addParameter( QgsProcessingParameterFeatureSource( name=self.IN_END, description="Input End Point layer", types=[QgsProcessing.TypeVectorPoint], )) self.addParameter( QgsProcessingParameterField( name=self.IN_END_FIELD, description="End ID Field (can be used for joining)", parentLayerParameterName=self.IN_END, )) self.addParameter( QgsProcessingParameterEnum(self.IN_PROFILE, "Travel mode", PROFILES, defaultValue=PROFILES[0])) self.addParameter( QgsProcessingParameterFeatureSink( name=self.OUT, description="Matrix", ))
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 (Multi)Point layer", types=[QgsProcessing.TypeVectorPoint], )) self.addParameter( QgsProcessingParameterField( name=self.IN_FIELD, description="Layer ID Field", parentLayerParameterName=self.IN_POINTS, )) self.addParameter( QgsProcessingParameterEnum(self.IN_PROFILE, "Travel mode", PROFILES, defaultValue=PROFILES[0])) self.addParameter( QgsProcessingParameterEnum(self.IN_PREFERENCE, "Travel preference", PREFERENCES, defaultValue=PREFERENCES[0])) self.addParameter( QgsProcessingParameterBoolean( name=self.IN_OPTIMIZE, description="Optimize waypoint order (except first and last)", defaultValue=False)) self.addParameter( QgsProcessingParameterFeatureSink( name=self.OUT, description="Output Layer", ))
def _get_ors_client_from_provider( provider: str, feedback: QgsProcessingFeedback) -> client.Client: """ Connects client to provider and returns a client instance for requests to the ors API """ providers = configmanager.read_config()['providers'] ors_provider = providers[provider] ors_client = client.Client(ors_provider) ors_client.overQueryLimit.connect( lambda: feedback.reportError("OverQueryLimit: Retrying...")) return ors_client
def provider_parameter(self) -> QgsProcessingParameterEnum: """ Parameter definition for provider, used in all child classes """ providers = [ provider['name'] for provider in configmanager.read_config()['providers'] ] return QgsProcessingParameterEnum(self.IN_PROVIDER, "Provider", providers, defaultValue=providers[0])
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 __init__(self, parent=None): """ :param parent: Parent window for modality. :type parent: QDialog """ QDialog.__init__(self, parent) self.setupUi(self) # Temp storage for config dict self.temp_config = configmanager.read_config() self._build_ui() self._collapse_boxes() self.provider_add.clicked.connect(self._add_provider) self.provider_remove.clicked.connect(self._remove_provider)
def _init_gui_control(self): """Slot for main plugin button. Initializes the GUI and shows it.""" # Only populate GUI if it's the first start of the plugin within the QGIS session # If not checked, GUI would be rebuilt every time! if self.first_start: self.first_start = False self.dlg = ORStoolsDialog( self.iface, self.iface.mainWindow()) # setting parent enables modal view # Make sure plugin window stays open when OK is clicked by reconnecting the accepted() signal self.dlg.global_buttons.accepted.disconnect(self.dlg.accept) self.dlg.global_buttons.accepted.connect(self.run_gui_control) self.dlg.avoidpolygon_dropdown.setFilters( QgsMapLayerProxyModel.PolygonLayer) # Populate provider box on window startup, since can be changed from multiple menus/buttons providers = configmanager.read_config()['providers'] self.dlg.provider_combo.clear() for provider in providers: self.dlg.provider_combo.addItem(provider['name'], provider) self.dlg.show()
class ORSdirectionsLinesAlgo(QgsProcessingAlgorithm): """Algorithm class for Directions Lines.""" ALGO_NAME = 'directions_lines' ALGO_NAME_LIST = ALGO_NAME.split('_') IN_PROVIDER = "INPUT_PROVIDER" IN_LINES = "INPUT_LINE_LAYER" IN_FIELD = "INPUT_LAYER_FIELD" IN_PROFILE = "INPUT_PROFILE" IN_PREFERENCE = "INPUT_PREFERENCE" IN_MODE = "INPUT_MODE" OUT = 'OUTPUT' providers = configmanager.read_config()['providers'] def initAlgorithm(self, configuration, p_str=None, Any=None, *args, **kwargs): providers = [provider['name'] for provider in self.providers] self.addParameter( QgsProcessingParameterEnum(self.IN_PROVIDER, "Provider", providers)) self.addParameter( QgsProcessingParameterFeatureSource( name=self.IN_LINES, description="Input Line layer", types=[QgsProcessing.TypeVectorLine], )) self.addParameter( QgsProcessingParameterField( name=self.IN_FIELD, description="Layer ID Field", parentLayerParameterName=self.IN_LINES, )) self.addParameter( QgsProcessingParameterEnum(self.IN_PROFILE, "Travel mode", PROFILES)) self.addParameter( QgsProcessingParameterEnum(self.IN_PREFERENCE, "Travel preference", PREFERENCES)) self.addParameter( QgsProcessingParameterFeatureSink( name=self.OUT, description="Directions", )) 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_directions_line.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 'Generate ' + " ".join( map(lambda x: x.capitalize(), self.ALGO_NAME_LIST)) def icon(self): return QIcon(RESOURCE_PREFIX + 'icon_directions.png') def createInstance(self): return ORSdirectionsLinesAlgo() 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...")) profile = PROFILES[self.parameterAsEnum(parameters, self.IN_PROFILE, context)] preference = PREFERENCES[self.parameterAsEnum(parameters, self.IN_PREFERENCE, context)] # Get parameter values source = self.parameterAsSource(parameters, self.IN_LINES, context) source_field_idx = self.parameterAsEnum(parameters, self.IN_FIELD, context) source_field_name = self.parameterAsString(parameters, self.IN_FIELD, context) params = { 'profile': profile, 'preference': preference, 'geometry': 'true', 'format': 'geojson', 'geometry_format': 'geojson', 'instructions': 'false', 'id': None } (sink, dest_id) = self.parameterAsSink( parameters, self.OUT, context, directions_core.get_fields( from_type=source.fields().field(source_field_name).type(), from_name=source_field_name, line=True), source.wkbType(), QgsCoordinateReferenceSystem(4326)) count = source.featureCount() for num, (line, field_value) in enumerate( self.get_sorted_lines(source, source_field_name)): # Stop the algorithm if cancel button has been clicked if feedback.isCanceled(): break params['coordinates'] = convert.build_coords( [[point.x(), point.y()] for point in line]) try: response = clnt.request( provider['endpoints'][self.ALGO_NAME_LIST[0]], params) except (exceptions.ApiError, exceptions.InvalidKey, exceptions.GenericServerError) as e: msg = "Feature ID {} caused a {}:\n{}".format( line[source_field_name], e.__class__.__name__, str(e)) feedback.reportError(msg) logger.log(msg) continue sink.addFeature( directions_core.get_output_feature(response, profile, preference, from_value=field_value, line=True)) feedback.setProgress(int(100.0 / count * num)) return {self.OUT: dest_id} def get_sorted_lines(self, layer, field_name): """ Generator to yield geometry and ID value sorted by feature ID. Careful: feat.id() is not necessarily permanent :param layer: source input layer :type layer: QgsProcessingParameterFeatureSource :param field_name: name of ID field :type field_name: str """ # First get coordinate transformer xformer = transform.transformToWGS(layer.sourceCrs()) for feat in sorted(layer.getFeatures(), key=lambda f: f.id()): line = None field_value = feat[field_name] # for if layer.wkbType() == QgsWkbTypes.MultiLineString: line = [ xformer.transform(QgsPointXY(point)) for point in feat.geometry().asMultiPolyline()[0] ] elif layer.wkbType() == QgsWkbTypes.LineString: line = [ xformer.transform(QgsPointXY(point)) for point in feat.geometry().asPolyline() ] yield line, field_value
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...")) profile = PROFILES[self.parameterAsEnum(parameters, self.IN_PROFILE, context)] preference = PREFERENCES[self.parameterAsEnum(parameters, self.IN_PREFERENCE, context)] # Get parameter values source = self.parameterAsSource(parameters, self.IN_LINES, context) source_field_idx = self.parameterAsEnum(parameters, self.IN_FIELD, context) source_field_name = self.parameterAsString(parameters, self.IN_FIELD, context) params = { 'profile': profile, 'preference': preference, 'geometry': 'true', 'format': 'geojson', 'geometry_format': 'geojson', 'instructions': 'false', 'id': None } (sink, dest_id) = self.parameterAsSink( parameters, self.OUT, context, directions_core.get_fields( from_type=source.fields().field(source_field_name).type(), from_name=source_field_name, line=True), source.wkbType(), QgsCoordinateReferenceSystem(4326)) count = source.featureCount() for num, (line, field_value) in enumerate( self.get_sorted_lines(source, source_field_name)): # Stop the algorithm if cancel button has been clicked if feedback.isCanceled(): break params['coordinates'] = convert.build_coords( [[point.x(), point.y()] for point in line]) try: response = clnt.request( provider['endpoints'][self.ALGO_NAME_LIST[0]], params) except (exceptions.ApiError, exceptions.InvalidKey, exceptions.GenericServerError) as e: msg = "Feature ID {} caused a {}:\n{}".format( line[source_field_name], e.__class__.__name__, str(e)) feedback.reportError(msg) logger.log(msg) continue sink.addFeature( directions_core.get_output_feature(response, profile, preference, from_value=field_value, line=True)) feedback.setProgress(int(100.0 / count * num)) return {self.OUT: dest_id}
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...")) profile = PROFILES[self.parameterAsEnum(parameters, self.IN_PROFILE, context)] preference = PREFERENCES[self.parameterAsEnum(parameters, self.IN_PREFERENCE, context)] mode = self.MODE_SELECTION[self.parameterAsEnum( parameters, self.IN_MODE, context)] # Get parameter values source = self.parameterAsSource(parameters, self.IN_START, context) source_field_name = self.parameterAsString(parameters, self.IN_START_FIELD, context) destination = self.parameterAsSource(parameters, self.IN_END, context) destination_field_name = self.parameterAsString( parameters, self.IN_END_FIELD, context) # Get fields from field name source_field_id = source.fields().lookupField(source_field_name) source_field = source.fields().field(source_field_id) destination_field_id = destination.fields().lookupField( destination_field_name) destination_field = destination.fields().field(destination_field_id) params = { 'preference': preference, 'geometry': 'true', 'instructions': 'false', 'elevation': True, 'id': None } route_dict = self._get_route_dict(source, source_field, destination, destination_field) if mode == 'Row-by-Row': route_count = min( [source.featureCount(), destination.featureCount()]) else: route_count = source.featureCount() * destination.featureCount() (sink, dest_id) = self.parameterAsSink( parameters, self.OUT, context, directions_core.get_fields(source_field.type(), destination_field.type()), QgsWkbTypes.LineString, QgsCoordinateReferenceSystem(4326)) counter = 0 for coordinates, values in directions_core.get_request_point_features( route_dict, mode): # Stop the algorithm if cancel button has been clicked if feedback.isCanceled(): break params['coordinates'] = coordinates try: response = clnt.request('/v2/directions/' + profile + '/geojson', {}, post_json=params) except (exceptions.ApiError, exceptions.InvalidKey, exceptions.GenericServerError) as e: msg = "Route from {} to {} caused a {}:\n{}".format( values[0], values[1], e.__class__.__name__, str(e)) feedback.reportError(msg) logger.log(msg) continue sink.addFeature( directions_core.get_output_feature_directions( response, profile, preference, from_value=values[0], to_value=values[1])) counter += 1 feedback.setProgress(int(100.0 / route_count * counter)) return {self.OUT: dest_id}
class ORSisochronesAlgo(QgsProcessingAlgorithm): # TODO: create base algorithm class common to all modules ALGO_NAME = 'isochrones' IN_PROVIDER = "INPUT_PROVIDER" IN_POINT = "INPUT_POINT" 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) providers = configmanager.read_config()['providers'] # difference = None def initAlgorithm(self, configuration, p_str=None, Any=None, *args, **kwargs): providers = [provider['name'] for provider in self.providers] self.addParameter( QgsProcessingParameterEnum(self.IN_PROVIDER, "Provider", providers)) self.addParameter( QgsProcessingParameterPoint( name=self.IN_POINT, description= "Input Point from map canvas (mutually exclusive with layer option)", optional=True, )) self.addParameter( QgsProcessingParameterFeatureSource( name=self.IN_POINTS, description= "Input Point layer (mutually exclusive with Point option)", types=[QgsProcessing.TypeVectorPoint], defaultValue=None, optional=True)) # 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, optional=True)) self.addParameter( QgsProcessingParameterEnum(self.IN_PROFILE, "Travel mode", PROFILES)) self.addParameter( QgsProcessingParameterEnum( name=self.IN_METRIC, description="Dimension", options=DIMENSIONS, )) self.addParameter( QgsProcessingParameterString( name=self.IN_RANGES, description="Comma-separated ranges [mins or m]", )) self.addParameter( QgsProcessingParameterFeatureSink(name=self.OUT, description="Isochrones", createByDefault=False)) 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.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 'Generate ' + self.ALGO_NAME.capitalize() def icon(self): return QIcon(RESOURCE_PREFIX + 'icon_isochrones.png') def createInstance(self): return ORSisochronesAlgo() # TODO: preprocess parameters to avoid 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' params['profile'] = 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'] = convert.comma_list(ranges_proc) # self.difference = self.parameterAsBool(parameters, self.IN_DIFFERENCE, context) point = self.parameterAsPoint(parameters, self.IN_POINT, context, self.crs_out) source = self.parameterAsSource(parameters, self.IN_POINTS, context) # Make the actual requests # If layer source is set requests = [] if source: if source.wkbType() == 4: raise QgsProcessingException( "TypeError: Multipoint Layers are not accepted. Please convert to single geometry layer." ) # Get ID field properties # TODO: id_field should have a default (#90) 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] requests.append(deepcopy(params)) # elif point source is set else: self.isochrones.set_parameters(profile, dimension, factor) params['locations'] = convert.build_coords([point.x(), point.y()]) 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) for num, params in enumerate(requests): # If feature causes error, report and continue with next try: # Populate features from response response = clnt.request(provider['endpoints'][self.ALGO_NAME], 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 if source: 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 (convert.build_coords([x_point.x(), x_point.y()]), feat)
def run_gui_control(self): """Slot function for OK button of main dialog.""" layer_out = QgsVectorLayer("LineString?crs=EPSG:4326", "Route_ORS", "memory") layer_out.dataProvider().addAttributes(directions_core.get_fields()) layer_out.updateFields() provider_id = self.dlg.provider_combo.currentIndex() provider = configmanager.read_config()['providers'][provider_id] # Check if API key was set when using ORS if provider['key'] is None: QMessageBox.critical( self.dlg, "Missing API key", """ Did you forget to set an <b>API key</b> for openrouteservice?<br><br> If you don't have an API key, please visit https://openrouteservice.org/sign-up to get one. <br><br> Then enter the API key for openrouteservice provider in Web â–º ORS Tools â–º Provider Settings or the settings symbol in the main ORS Tools GUI, next to the provider dropdown. """) return clnt = client.Client(provider) clnt_msg = '' directions = directions_gui.Directions(self.dlg, self.advanced) params = directions.get_basic_paramters() from_id = None to_id = None try: if self.dlg.routing_tab.currentIndex() == 0: x_start = self.dlg.routing_frompoint_start_x.value() y_start = self.dlg.routing_frompoint_start_y.value() x_end = self.dlg.routing_frompoint_end_x.value() y_end = self.dlg.routing_frompoint_end_y.value() params['coordinates'] = convert.build_coords( [[x_start, y_start], [x_end, y_end]]) from_id = convert.comma_list([x_start, y_start]) to_id = convert.comma_list([x_end, y_end]) elif self.dlg.routing_tab.currentIndex() == 1: params['coordinates'] = convert.build_coords( directions.get_request_line_feature()) response = clnt.request(provider['endpoints']['directions'], params) layer_out.dataProvider().addFeature( directions_core.get_output_feature(response, params['profile'], params['preference'], directions.avoid, from_id, to_id)) layer_out.updateExtents() self.project.addMapLayer(layer_out) # Update quota; handled in client module after successful request if provider.get('ENV_VARS'): self.dlg.quota_text.setText( self.get_quota(provider) + ' calls') except exceptions.Timeout: msg = "The connection has timed out!" logger.log(msg, 2) self.dlg.debug_text.setText(msg) return except (exceptions.ApiError, exceptions.InvalidKey, exceptions.GenericServerError) as e: msg = (e.__class__.__name__, str(e)) logger.log("{}: {}".format(*msg), 2) clnt_msg += "<b>{}</b>: ({})<br>".format(*msg) return except Exception as e: msg = [e.__class__.__name__, str(e)] logger.log("{}: {}".format(*msg), 2) clnt_msg += "<b>{}</b>: {}<br>".format(*msg) raise finally: # Set URL in debug window clnt_msg += '<a href="{0}">{0}</a><br>'.format(clnt.url) self.dlg.debug_text.setHtml(clnt_msg) return
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() # Get profile value profile = PROFILES[self.parameterAsEnum( parameters, self.IN_PROFILE, context )] # Get parameter values source = self.parameterAsSource( parameters, self.IN_START, context ) source_field_name = self.parameterAsString( parameters, self.IN_START_FIELD, context ) destination = self.parameterAsSource( parameters, self.IN_END, context ) destination_field_name = self.parameterAsString( parameters, self.IN_END_FIELD, context ) # Get fields from field name source_field_id = source.fields().lookupField(source_field_name) source_field = source.fields().field(source_field_id) destination_field_id = destination.fields().lookupField(destination_field_name) destination_field = destination.fields().field(destination_field_id) # Abort when MultiPoint type if (source.wkbType() or destination.wkbType()) == 4: raise QgsProcessingException("TypeError: Multipoint Layers are not accepted. Please convert to single geometry layer.") # Get source and destination features sources_features = list(source.getFeatures()) destination_features = list(destination.getFeatures()) # Get feature amounts/counts sources_amount = source.featureCount() destinations_amount = destination.featureCount() # Allow for 50 features in source if source == destination source_equals_destination = parameters['INPUT_START_LAYER'] == parameters['INPUT_END_LAYER'] if source_equals_destination: features = sources_features xformer = transform.transformToWGS(source.sourceCrs()) features_points = [xformer.transform(feat.geometry().asPoint()) for feat in features] else: xformer = transform.transformToWGS(source.sourceCrs()) sources_features_xformed = [xformer.transform(feat.geometry().asPoint()) for feat in sources_features] xformer = transform.transformToWGS(destination.sourceCrs()) destination_features_xformed = [xformer.transform(feat.geometry().asPoint()) for feat in destination_features] features_points = sources_features_xformed + destination_features_xformed # Get IDs sources_ids = list(range(sources_amount)) if source_equals_destination else list(range(sources_amount)) destination_ids = list(range(sources_amount)) if source_equals_destination else list(range(sources_amount, sources_amount + destinations_amount)) # Populate parameters further params.update({ 'locations': [[point.x(), point.y()] for point in features_points], 'sources': sources_ids, 'destinations': destination_ids, 'metrics': ["duration", "distance"], 'id': 'Matrix' }) # Make request and catch ApiError try: response = clnt.request('/v2/matrix/' + profile, {}, post_json=params) except (exceptions.ApiError, exceptions.InvalidKey, exceptions.GenericServerError) as e: msg = "{}: {}".format( e.__class__.__name__, str(e)) feedback.reportError(msg) logger.log(msg) (sink, dest_id) = self.parameterAsSink( parameters, self.OUT, context, self.get_fields( source_field.type(), destination_field.type() ), QgsWkbTypes.NoGeometry ) sources_attributes = [feat.attribute(source_field_name) for feat in sources_features] destinations_attributes = [feat.attribute(destination_field_name) for feat in destination_features] for s, source in enumerate(sources_attributes): for d, destination in enumerate(destinations_attributes): duration = response['durations'][s][d] distance = response['distances'][s][d] feat = QgsFeature() feat.setAttributes([ source, destination, duration / 3600 if duration is not None else None, distance / 1000 if distance is not None else None ]) sink.addFeature(feat) return {self.OUT: dest_id}
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( QgsProcessingParameterString( name=self.IN_INTERVAL, description="Interval range in seconds or meters", optional=True ) ) self.addParameter( QgsProcessingParameterString( name=self.IN_SMOOTH, description="Applies a level of generalisation to the isochrone polygons generated as a smoothing_factor between 0 and 100.0", optional=True ) ) self.addParameter( QgsProcessingParameterFeatureSink( name=self.OUT, description="Isochrones", createByDefault=False ) )
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 run_gui_control(self): """Slot function for OK button of main dialog.""" layer_out = QgsVectorLayer("LineString?crs=EPSG:4326", "Route_ORS", "memory") layer_out.dataProvider().addAttributes(directions_core.get_fields()) layer_out.updateFields() # Associate annotations with map layer, so they get deleted when layer is deleted for annotation in self.dlg.annotations: # Has the potential to be pretty cool: instead of deleting, associate with mapLayer, you can change order after optimization # Then in theory, when the layer is remove, the annotation is removed as well # Doesng't work though, the annotations are still there when project is re-opened # annotation.setMapLayer(layer_out) self.project.annotationManager().removeAnnotation(annotation) self.dlg.annotations = [] provider_id = self.dlg.provider_combo.currentIndex() provider = configmanager.read_config()['providers'][provider_id] # if there are no coordinates, throw an error message if not self.dlg.routing_fromline_list.count(): QMessageBox.critical( self.dlg, "Missing API key", """ Did you forget to set routing waypoints?<br><br> Use the 'Add Waypoint' button to add up to 50 waypoints. """) return # if no API key is present, when ORS is selected, throw an error message if not provider['key'] and provider['base_url'].startswith( 'https://api.openrouteservice.org'): QMessageBox.critical( self.dlg, "Missing API key", """ Did you forget to set an <b>API key</b> for openrouteservice?<br><br> If you don't have an API key, please visit https://openrouteservice.org/sign-up to get one. <br><br> Then enter the API key for openrouteservice provider in Web â–º ORS Tools â–º Provider Settings or the settings symbol in the main ORS Tools GUI, next to the provider dropdown. """) return clnt = client.Client(provider) clnt_msg = '' directions = directions_gui.Directions(self.dlg) params = directions.get_parameters() try: if self.dlg.optimization_group.isChecked(): if len(params['jobs'] ) <= 1: # Start/end locations don't count as job QMessageBox.critical( self.dlg, "Wrong number of waypoints", """At least 3 or 4 waypoints are needed to perform routing optimization. Remember, the first and last location are not part of the optimization. """) return response = clnt.request('/optimization', {}, post_json=params) feat = directions_core.get_output_features_optimization( response, params['vehicles'][0]['profile']) else: params['coordinates'] = directions.get_request_line_feature() profile = self.dlg.routing_travel_combo.currentText() response = clnt.request('/v2/directions/' + profile + '/geojson', {}, post_json=params) feat = directions_core.get_output_feature_directions( response, profile, params['preference'], directions.options) layer_out.dataProvider().addFeature(feat) layer_out.updateExtents() self.project.addMapLayer(layer_out) # Update quota; handled in client module after successful request # if provider.get('ENV_VARS'): # self.dlg.quota_text.setText(self.get_quota(provider) + ' calls') except exceptions.Timeout: msg = "The connection has timed out!" logger.log(msg, 2) self.dlg.debug_text.setText(msg) return except (exceptions.ApiError, exceptions.InvalidKey, exceptions.GenericServerError) as e: msg = (e.__class__.__name__, str(e)) logger.log("{}: {}".format(*msg), 2) clnt_msg += "<b>{}</b>: ({})<br>".format(*msg) raise except Exception as e: msg = [e.__class__.__name__, str(e)] logger.log("{}: {}".format(*msg), 2) clnt_msg += "<b>{}</b>: {}<br>".format(*msg) raise finally: # Set URL in debug window clnt_msg += '<a href="{0}">{0}</a><br>Parameters:<br>{1}'.format( clnt.url, json.dumps(params, indent=2)) self.dlg.debug_text.setHtml(clnt_msg)
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...")) profile = PROFILES[self.parameterAsEnum(parameters, self.IN_PROFILE, context)] preference = PREFERENCES[self.parameterAsEnum(parameters, self.IN_PREFERENCE, context)] optimize = self.parameterAsBool(parameters, self.IN_OPTIMIZE, context) # Get parameter values source = self.parameterAsSource(parameters, self.IN_POINTS, context) source_field_idx = self.parameterAsEnum(parameters, self.IN_FIELD, context) source_field_name = self.parameterAsString(parameters, self.IN_FIELD, context) (sink, dest_id) = self.parameterAsSink( parameters, self.OUT, context, directions_core.get_fields( from_type=source.fields().field(source_field_name).type(), from_name=source_field_name, line=True), QgsWkbTypes.LineString, QgsCoordinateReferenceSystem(4326)) count = source.featureCount() input_points = list() from_values = list() xformer = transform.transformToWGS(source.sourceCrs()) if source.wkbType() == QgsWkbTypes.Point: points = list() for feat in sorted(source.getFeatures(), key=lambda f: f.id()): points.append( xformer.transform(QgsPointXY(feat.geometry().asPoint()))) input_points.append(points) from_values.append('') elif source.wkbType() == QgsWkbTypes.MultiPoint: # loop through multipoint features for feat in sorted(source.getFeatures(), key=lambda f: f.id()): points = list() for point in feat.geometry().asMultiPoint(): points.append(xformer.transform(QgsPointXY(point))) input_points.append(points) from_values.append(feat[source_field_name]) for num, (points, from_value) in enumerate(zip(input_points, from_values)): # Stop the algorithm if cancel button has been clicked if feedback.isCanceled(): break try: if optimize: params = self._get_params_optimize(points, profile) response = clnt.request('/optimization', {}, post_json=params) sink.addFeature( directions_core.get_output_features_optimization( response, profile, from_value=from_value)) else: params = self._get_params_directions( points, profile, preference) response = clnt.request('/v2/directions/' + profile + '/geojson', {}, post_json=params) sink.addFeature( directions_core.get_output_feature_directions( response, profile, preference, from_value=from_value)) except (exceptions.ApiError, exceptions.InvalidKey, exceptions.GenericServerError) as e: msg = "Feature ID {} caused a {}:\n{}".format( from_value, e.__class__.__name__, str(e)) feedback.reportError(msg) logger.log(msg) continue feedback.setProgress(int(100.0 / count * num)) return {self.OUT: dest_id}
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) providers = configmanager.read_config()['providers'] # difference = None def initAlgorithm(self, configuration, p_str=None, Any=None, *args, **kwargs): providers = [provider['name'] for provider in self.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}
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...")) profile = PROFILES[self.parameterAsEnum(parameters, self.IN_PROFILE, context)] preference = PREFERENCES[self.parameterAsEnum(parameters, self.IN_PREFERENCE, context)] optimize = self.parameterAsBool(parameters, self.IN_OPTIMIZE, context) # Get parameter values source = self.parameterAsSource(parameters, self.IN_LINES, context) source_field_idx = self.parameterAsEnum(parameters, self.IN_FIELD, context) source_field_name = self.parameterAsString(parameters, self.IN_FIELD, context) (sink, dest_id) = self.parameterAsSink( parameters, self.OUT, context, directions_core.get_fields( from_type=source.fields().field(source_field_name).type(), from_name=source_field_name, line=True), source.wkbType(), QgsCoordinateReferenceSystem(4326)) count = source.featureCount() for num, (line, field_value) in enumerate( self._get_sorted_lines(source, source_field_name)): # Stop the algorithm if cancel button has been clicked if feedback.isCanceled(): break try: if optimize: params = self._get_params_optimize(line, profile) response = clnt.request('/optimization', {}, post_json=params) sink.addFeature( directions_core.get_output_features_optimization( response, profile, from_value=field_value)) else: params = self._get_params_directions( line, profile, preference) response = clnt.request( '/v2/directions/' + profile + '/geojson', params) sink.addFeature( directions_core.get_output_feature_directions( response, profile, preference, from_value=field_value)) except (exceptions.ApiError, exceptions.InvalidKey, exceptions.GenericServerError) as e: msg = "Feature ID {} caused a {}:\n{}".format( line[source_field_name], e.__class__.__name__, str(e)) feedback.reportError(msg) logger.log(msg) continue feedback.setProgress(int(100.0 / count * num)) return {self.OUT: dest_id}
class ORSdirectionsLinesAlgo(QgsProcessingAlgorithm): """Algorithm class for Directions Lines.""" ALGO_NAME = 'directions_from_polylines_layer' ALGO_NAME_LIST = ALGO_NAME.split('_') IN_PROVIDER = "INPUT_PROVIDER" IN_LINES = "INPUT_LINE_LAYER" IN_FIELD = "INPUT_LAYER_FIELD" IN_PROFILE = "INPUT_PROFILE" IN_PREFERENCE = "INPUT_PREFERENCE" IN_OPTIMIZE = "INPUT_OPTIMIZE" IN_MODE = "INPUT_MODE" OUT = 'OUTPUT' providers = configmanager.read_config()['providers'] def initAlgorithm(self, configuration, p_str=None, Any=None, *args, **kwargs): providers = [provider['name'] for provider in self.providers] self.addParameter( QgsProcessingParameterEnum(self.IN_PROVIDER, "Provider", providers, defaultValue=providers[0])) self.addParameter( QgsProcessingParameterFeatureSource( name=self.IN_LINES, description="Input Line layer", types=[QgsProcessing.TypeVectorLine], )) self.addParameter( QgsProcessingParameterField( name=self.IN_FIELD, description="Layer ID Field", parentLayerParameterName=self.IN_LINES, )) self.addParameter( QgsProcessingParameterEnum(self.IN_PROFILE, "Travel mode", PROFILES, defaultValue=PROFILES[0])) self.addParameter( QgsProcessingParameterEnum(self.IN_PREFERENCE, "Travel preference", PREFERENCES, defaultValue=PREFERENCES[0])) self.addParameter( QgsProcessingParameterBoolean( name=self.IN_OPTIMIZE, description="Optimize waypoint order (except first and last)", defaultValue=False)) self.addParameter( QgsProcessingParameterFeatureSink( name=self.OUT, description="Output Layer", )) def group(self): return "Directions" def groupId(self): return 'directions' 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_directions_line.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_directions.png') def createInstance(self): return ORSdirectionsLinesAlgo() 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...")) profile = PROFILES[self.parameterAsEnum(parameters, self.IN_PROFILE, context)] preference = PREFERENCES[self.parameterAsEnum(parameters, self.IN_PREFERENCE, context)] optimize = self.parameterAsBool(parameters, self.IN_OPTIMIZE, context) # Get parameter values source = self.parameterAsSource(parameters, self.IN_LINES, context) source_field_idx = self.parameterAsEnum(parameters, self.IN_FIELD, context) source_field_name = self.parameterAsString(parameters, self.IN_FIELD, context) (sink, dest_id) = self.parameterAsSink( parameters, self.OUT, context, directions_core.get_fields( from_type=source.fields().field(source_field_name).type(), from_name=source_field_name, line=True), source.wkbType(), QgsCoordinateReferenceSystem(4326)) count = source.featureCount() for num, (line, field_value) in enumerate( self._get_sorted_lines(source, source_field_name)): # Stop the algorithm if cancel button has been clicked if feedback.isCanceled(): break try: if optimize: params = self._get_params_optimize(line, profile) response = clnt.request('/optimization', {}, post_json=params) sink.addFeature( directions_core.get_output_features_optimization( response, profile, from_value=field_value)) else: params = self._get_params_directions( line, profile, preference) response = clnt.request( '/v2/directions/' + profile + '/geojson', params) sink.addFeature( directions_core.get_output_feature_directions( response, profile, preference, from_value=field_value)) except (exceptions.ApiError, exceptions.InvalidKey, exceptions.GenericServerError) as e: msg = "Feature ID {} caused a {}:\n{}".format( line[source_field_name], e.__class__.__name__, str(e)) feedback.reportError(msg) logger.log(msg) continue feedback.setProgress(int(100.0 / count * num)) return {self.OUT: dest_id} @staticmethod def _get_sorted_lines(layer, field_name): """ Generator to yield geometry and ID value sorted by feature ID. Careful: feat.id() is not necessarily permanent :param layer: source input layer :type layer: QgsProcessingParameterFeatureSource :param field_name: name of ID field :type field_name: str """ # First get coordinate transformer xformer = transform.transformToWGS(layer.sourceCrs()) for feat in sorted(layer.getFeatures(), key=lambda f: f.id()): line = None field_value = feat[field_name] # for if layer.wkbType() == QgsWkbTypes.MultiLineString: # TODO: only takes the first polyline geometry from the multiline geometry currently # Loop over all polyline geometries line = [ xformer.transform(QgsPointXY(point)) for point in feat.geometry().asMultiPolyline()[0] ] elif layer.wkbType() == QgsWkbTypes.LineString: line = [ xformer.transform(QgsPointXY(point)) for point in feat.geometry().asPolyline() ] yield line, field_value @staticmethod def _get_params_directions(line, profile, preference): """ Build parameters for optimization endpoint :param line: individual polyline points :type line: list of QgsPointXY :param profile: transport profile to be used :type profile: str :param preference: routing preference, shortest/fastest :type preference: str :returns: parameters for optimization endpoint :rtype: dict """ params = { 'coordinates': convert.build_coords([[point.x(), point.y()] for point in line]), 'profile': profile, 'preference': preference, 'geometry': 'true', 'format': 'geojson', 'geometry_format': 'geojson', 'instructions': 'false', 'elevation': True, 'id': None } return params @staticmethod def _get_params_optimize(line, profile): """ Build parameters for optimization endpoint :param line: individual polyline points :type line: list of QgsPointXY :param profile: transport profile to be used :type profile: str :returns: parameters for optimization endpoint :rtype: dict """ start = line.pop(0) end = line.pop(-1) params = { 'jobs': list(), 'vehicles': [{ "id": 0, "profile": profile, "start": [start.x(), start.y()], "end": [end.x(), end.y()] }], 'options': { 'g': True } } for point in line: params['jobs'].append({ "location": [point.x(), point.y()], "id": line.index(point) }) return params
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' params['profile'] = 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'] = convert.comma_list(ranges_proc) # self.difference = self.parameterAsBool(parameters, self.IN_DIFFERENCE, context) point = self.parameterAsPoint(parameters, self.IN_POINT, context, self.crs_out) source = self.parameterAsSource(parameters, self.IN_POINTS, context) # Make the actual requests # If layer source is set requests = [] if source: if source.wkbType() == 4: raise QgsProcessingException( "TypeError: Multipoint Layers are not accepted. Please convert to single geometry layer." ) # Get ID field properties # TODO: id_field should have a default (#90) 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] requests.append(deepcopy(params)) # elif point source is set else: self.isochrones.set_parameters(profile, dimension, factor) params['locations'] = convert.build_coords([point.x(), point.y()]) 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) for num, params in enumerate(requests): # If feature causes error, report and continue with next try: # Populate features from response response = clnt.request(provider['endpoints'][self.ALGO_NAME], 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 if source: feedback.setProgress(int(100.0 / source.featureCount() * num)) return {self.OUT: self.dest_id}
class ORSmatrixAlgo(QgsProcessingAlgorithm): # TODO: create base algorithm class common to all modules ALGO_NAME = 'matrix_from_layers' ALGO_NAME_LIST = ALGO_NAME.split('_') IN_PROVIDER = "INPUT_PROVIDER" IN_START = "INPUT_START_LAYER" IN_START_FIELD = "INPUT_START_FIELD" IN_END = "INPUT_END_LAYER" IN_END_FIELD = "INPUT_END_FIELD" IN_PROFILE = "INPUT_PROFILE" OUT = 'OUTPUT' providers = configmanager.read_config()['providers'] def initAlgorithm(self, configuration, p_str=None, Any=None, *args, **kwargs): providers = [provider['name'] for provider in self.providers] self.addParameter( QgsProcessingParameterEnum( self.IN_PROVIDER, "Provider", providers, defaultValue=providers[0] ) ) self.addParameter( QgsProcessingParameterFeatureSource( name=self.IN_START, description="Input Start Point layer", types=[QgsProcessing.TypeVectorPoint], ) ) self.addParameter( QgsProcessingParameterField( name=self.IN_START_FIELD, description="Start ID Field (can be used for joining)", parentLayerParameterName=self.IN_START, ) ) self.addParameter( QgsProcessingParameterFeatureSource( name=self.IN_END, description="Input End Point layer", types=[QgsProcessing.TypeVectorPoint], ) ) self.addParameter( QgsProcessingParameterField( name=self.IN_END_FIELD, description="End ID Field (can be used for joining)", parentLayerParameterName=self.IN_END, ) ) self.addParameter( QgsProcessingParameterEnum( self.IN_PROFILE, "Travel mode", PROFILES, defaultValue=PROFILES[0] ) ) self.addParameter( QgsProcessingParameterFeatureSink( name=self.OUT, description="Matrix", ) ) def group(self): return "Matrix" def groupId(self): return 'matrix' 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_matrix.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_matrix.png') def createInstance(self): return ORSmatrixAlgo() 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() # Get profile value profile = PROFILES[self.parameterAsEnum( parameters, self.IN_PROFILE, context )] # Get parameter values source = self.parameterAsSource( parameters, self.IN_START, context ) source_field_name = self.parameterAsString( parameters, self.IN_START_FIELD, context ) destination = self.parameterAsSource( parameters, self.IN_END, context ) destination_field_name = self.parameterAsString( parameters, self.IN_END_FIELD, context ) # Get fields from field name source_field_id = source.fields().lookupField(source_field_name) source_field = source.fields().field(source_field_id) destination_field_id = destination.fields().lookupField(destination_field_name) destination_field = destination.fields().field(destination_field_id) # Abort when MultiPoint type if (source.wkbType() or destination.wkbType()) == 4: raise QgsProcessingException("TypeError: Multipoint Layers are not accepted. Please convert to single geometry layer.") # Get source and destination features sources_features = list(source.getFeatures()) destination_features = list(destination.getFeatures()) # Get feature amounts/counts sources_amount = source.featureCount() destinations_amount = destination.featureCount() # Allow for 50 features in source if source == destination source_equals_destination = parameters['INPUT_START_LAYER'] == parameters['INPUT_END_LAYER'] if source_equals_destination: features = sources_features xformer = transform.transformToWGS(source.sourceCrs()) features_points = [xformer.transform(feat.geometry().asPoint()) for feat in features] else: xformer = transform.transformToWGS(source.sourceCrs()) sources_features_xformed = [xformer.transform(feat.geometry().asPoint()) for feat in sources_features] xformer = transform.transformToWGS(destination.sourceCrs()) destination_features_xformed = [xformer.transform(feat.geometry().asPoint()) for feat in destination_features] features_points = sources_features_xformed + destination_features_xformed # Get IDs sources_ids = list(range(sources_amount)) if source_equals_destination else list(range(sources_amount)) destination_ids = list(range(sources_amount)) if source_equals_destination else list(range(sources_amount, sources_amount + destinations_amount)) # Populate parameters further params.update({ 'locations': [[point.x(), point.y()] for point in features_points], 'sources': sources_ids, 'destinations': destination_ids, 'metrics': ["duration", "distance"], 'id': 'Matrix' }) # Make request and catch ApiError try: response = clnt.request('/v2/matrix/' + profile, {}, post_json=params) except (exceptions.ApiError, exceptions.InvalidKey, exceptions.GenericServerError) as e: msg = "{}: {}".format( e.__class__.__name__, str(e)) feedback.reportError(msg) logger.log(msg) (sink, dest_id) = self.parameterAsSink( parameters, self.OUT, context, self.get_fields( source_field.type(), destination_field.type() ), QgsWkbTypes.NoGeometry ) sources_attributes = [feat.attribute(source_field_name) for feat in sources_features] destinations_attributes = [feat.attribute(destination_field_name) for feat in destination_features] for s, source in enumerate(sources_attributes): for d, destination in enumerate(destinations_attributes): duration = response['durations'][s][d] distance = response['distances'][s][d] feat = QgsFeature() feat.setAttributes([ source, destination, duration / 3600 if duration is not None else None, distance / 1000 if distance is not None else None ]) sink.addFeature(feat) return {self.OUT: dest_id} @staticmethod def get_fields(source_type, destination_type): fields = QgsFields() fields.append(QgsField("FROM_ID", source_type)) fields.append(QgsField("TO_ID", destination_type)) fields.append(QgsField("DURATION_H", QVariant.Double)) fields.append(QgsField("DIST_KM", QVariant.Double)) return fields @staticmethod def chunks(l, n): """Yield successive n-sized chunks from l.""" for i in range(0, len(l), n): yield l[i:i + n]
class ORSdirectionsPointsLayersAlgo(QgsProcessingAlgorithm): # TODO: create base algorithm class common to all modules ALGO_NAME = 'directions_from_points_2_layers' ALGO_NAME_LIST = ALGO_NAME.split('_') MODE_SELECTION = ['Row-by-Row', 'All-by-All'] IN_PROVIDER = "INPUT_PROVIDER" IN_START = "INPUT_START_LAYER" IN_START_FIELD = "INPUT_START_FIELD" IN_END = "INPUT_END_LAYER" IN_END_FIELD = "INPUT_END_FIELD" IN_PROFILE = "INPUT_PROFILE" IN_PREFERENCE = "INPUT_PREFERENCE" IN_MODE = "INPUT_MODE" OUT = 'OUTPUT' providers = configmanager.read_config()['providers'] def initAlgorithm(self, configuration, p_str=None, Any=None, *args, **kwargs): providers = [provider['name'] for provider in self.providers] self.addParameter( QgsProcessingParameterEnum(self.IN_PROVIDER, "Provider", providers, defaultValue=providers[0])) self.addParameter( QgsProcessingParameterFeatureSource( name=self.IN_START, description="Input Start Point layer", types=[QgsProcessing.TypeVectorPoint], )) self.addParameter( QgsProcessingParameterField( name=self.IN_START_FIELD, description="Start ID Field (can be used for joining)", parentLayerParameterName=self.IN_START, )) self.addParameter( QgsProcessingParameterFeatureSource( name=self.IN_END, description="Input End Point layer", types=[QgsProcessing.TypeVectorPoint], )) self.addParameter( QgsProcessingParameterField( name=self.IN_END_FIELD, description="End ID Field (can be used for joining)", parentLayerParameterName=self.IN_END, )) self.addParameter( QgsProcessingParameterEnum(self.IN_PROFILE, "Travel mode", PROFILES, defaultValue=PROFILES[0])) self.addParameter( QgsProcessingParameterEnum(self.IN_PREFERENCE, "Travel preference", PREFERENCES, defaultValue=PREFERENCES[0])) self.addParameter( QgsProcessingParameterEnum(self.IN_MODE, "Layer mode", self.MODE_SELECTION, defaultValue=self.MODE_SELECTION[0])) self.addParameter( QgsProcessingParameterFeatureSink( name=self.OUT, description="Directions", )) def group(self): return "Directions" def groupId(self): return 'directions' 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_directions_points.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_directions.png') def createInstance(self): return ORSdirectionsPointsLayersAlgo() # 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...")) profile = PROFILES[self.parameterAsEnum(parameters, self.IN_PROFILE, context)] preference = PREFERENCES[self.parameterAsEnum(parameters, self.IN_PREFERENCE, context)] mode = self.MODE_SELECTION[self.parameterAsEnum( parameters, self.IN_MODE, context)] # Get parameter values source = self.parameterAsSource(parameters, self.IN_START, context) source_field_name = self.parameterAsString(parameters, self.IN_START_FIELD, context) destination = self.parameterAsSource(parameters, self.IN_END, context) destination_field_name = self.parameterAsString( parameters, self.IN_END_FIELD, context) # Get fields from field name source_field_id = source.fields().lookupField(source_field_name) source_field = source.fields().field(source_field_id) destination_field_id = destination.fields().lookupField( destination_field_name) destination_field = destination.fields().field(destination_field_id) params = { 'preference': preference, 'geometry': 'true', 'instructions': 'false', 'elevation': True, 'id': None } route_dict = self._get_route_dict(source, source_field, destination, destination_field) if mode == 'Row-by-Row': route_count = min( [source.featureCount(), destination.featureCount()]) else: route_count = source.featureCount() * destination.featureCount() (sink, dest_id) = self.parameterAsSink( parameters, self.OUT, context, directions_core.get_fields(source_field.type(), destination_field.type()), QgsWkbTypes.LineString, QgsCoordinateReferenceSystem(4326)) counter = 0 for coordinates, values in directions_core.get_request_point_features( route_dict, mode): # Stop the algorithm if cancel button has been clicked if feedback.isCanceled(): break params['coordinates'] = coordinates try: response = clnt.request('/v2/directions/' + profile + '/geojson', {}, post_json=params) except (exceptions.ApiError, exceptions.InvalidKey, exceptions.GenericServerError) as e: msg = "Route from {} to {} caused a {}:\n{}".format( values[0], values[1], e.__class__.__name__, str(e)) feedback.reportError(msg) logger.log(msg) continue sink.addFeature( directions_core.get_output_feature_directions( response, profile, preference, from_value=values[0], to_value=values[1])) counter += 1 feedback.setProgress(int(100.0 / route_count * counter)) return {self.OUT: dest_id} def _get_route_dict(self, source, source_field, destination, destination_field): """ Compute route_dict from input layer. :param source: Input from layer :type source: QgsProcessingParameterFeatureSource :param source_field: ID field from layer. :type source_field: QgsField :param destination: Input to layer. :type destination: QgsProcessingParameterFeatureSource :param destination_field: ID field to layer. :type destination_field: QgsField :returns: route_dict with coordinates and ID values :rtype: dict """ route_dict = dict() source_feats = list(source.getFeatures()) xformer_source = transform.transformToWGS(source.sourceCrs()) route_dict['start'] = dict( geometries=[ xformer_source.transform(feat.geometry().asPoint()) for feat in source_feats ], values=[ feat.attribute(source_field.name()) for feat in source_feats ], ) destination_feats = list(destination.getFeatures()) xformer_destination = transform.transformToWGS(destination.sourceCrs()) route_dict['end'] = dict( geometries=[ xformer_destination.transform(feat.geometry().asPoint()) for feat in destination_feats ], values=[ feat.attribute(destination_field.name()) for feat in destination_feats ], ) return route_dict