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