Ejemplo n.º 1
0
class QScoutValueGrabberAlgorithm(QScoutFeatureIOAlgorithm,
                                  QScoutRasterInterface):
    POINTS_INPUT = 'POINTS_INPUT'
    RASTER_INPUT = 'RASTER_INPUT'
    GRAB_RADIUS_INPUT = 'GRAB_RADIUS_INPUT'
    GRAB_AREA_DISTANCE_WEIGHT_INPUT = 'GRAB_AREA_DISTANCE_WEIGHT_INPUT'
    GRAB_FUNCTION_INPUT = 'GRAB_FUNCTION_INPUT'
    POINTS_WITH_VALUES_OUTPUT = 'POINTS_OUTPUT'

    def initAlgorithm(self, config):
        self.addParameter(
            QgsProcessingParameterFeatureSource(
                self.POINTS_INPUT, self.tr("Points Input"),
                [QgsProcessing.TypeVectorPoint]))

        # have to use QgsProcessingParameterFile to account for rasters too large to load in qgis
        self.addParameter(
            QgsProcessingParameterFile(self.RASTER_INPUT,
                                       self.tr("Raster File Input")))

        param = QgsProcessingParameterDistance(
            self.GRAB_RADIUS_INPUT,
            self.tr("Grab Radius"),
            parentParameterName=self.POINTS_INPUT,
            optional=True)
        param.setFlags(param.flags()
                       | QgsProcessingParameterDefinition.FlagAdvanced)
        self.addParameter(param)

        param = QgsProcessingParameterNumber(
            self.GRAB_AREA_DISTANCE_WEIGHT_INPUT,
            self.tr("Grab Area Distance Weight"),
            type=QgsProcessingParameterNumber.Double,
            optional=True)
        param.setFlags(param.flags()
                       | QgsProcessingParameterDefinition.FlagAdvanced)
        self.addParameter(param)

        param = QgsProcessingParameterFile(self.GRAB_FUNCTION_INPUT,
                                           self.tr("Grab Function"),
                                           optional=True)
        param.setFlags(param.flags()
                       | QgsProcessingParameterDefinition.FlagAdvanced)
        self.addParameter(param)

        self.addParameter(
            QgsProcessingParameterFeatureSink(
                self.POINTS_WITH_VALUES_OUTPUT,
                self.tr("Points with Grabbed Values")))

    def processAlgorithm(self, parameters, context, feedback):
        self.points_input_layer = self.parameterAsVectorLayer(
            parameters, self.POINTS_INPUT, context)
        raster_file = self.parameterAsFile(parameters, self.RASTER_INPUT,
                                           context)
        self._grab_radius = self.parameterAsDouble(parameters,
                                                   self.GRAB_RADIUS_INPUT,
                                                   context)
        self._grab_distance_weight = self.parameterAsDouble(
            parameters, self.GRAB_AREA_DISTANCE_WEIGHT_INPUT, context)

        grab_func_file = self.parameterAsFile(parameters,
                                              self.GRAB_FUNCTION_INPUT,
                                              context)
        if grab_func_file.strip():
            spec = spec_from_file_location(
                grab_func_file[grab_func_file.find(sep):grab_func_file.
                               find(".")], grab_func_file)
            module = module_from_spec(spec)
            spec.loader.exec_module(module)
            self.grab_analysis_function = module.grab
        else:
            self.grab_analysis_function = None
        self.load_raster_data(raster_file)
        return self.grab_values(parameters, context, feedback)

    def grab_values(self, parameters, context, feedback):

        assert round(abs(self._raster_transform[1]), 4) == round(abs(self._raster_transform[5]), 4), \
            "Raster should have square pixels"

        self.raster_crs_transform = QgsCoordinateTransform(
            self.feature_input_crs(), self.raster_crs(),
            QgsProject.instance().transformContext())

        self.output_fields = QgsFields()
        self.output_fields.extend(self.points_input_layer.fields())

        for i in range(self.num_raster_bands()):
            self.output_fields.append(QgsField(band_field(i), QVariant.Double))

        dest_id = self.create_sink(parameters, self.POINTS_WITH_VALUES_OUTPUT,
                                   context, QgsWkbTypes.Point)

        count = 0
        # loop features
        for in_feat in self.feature_input():
            count = self.process_pin(in_feat, count, feedback)

        return {self.POINTS_WITH_VALUES_OUTPUT: dest_id}

    def process_pin(self, in_feat, count, feedback):
        # skip features with no geometry
        if in_feat.hasGeometry():
            band_vals = self.query_raster(in_feat)
            if band_vals is not None:
                feature = QgsFeature(self.feature_output_fields(), count)
                for field in in_feat.fields().names():
                    feature.setAttribute(field, in_feat[field])

                for band in range(self.num_raster_bands()):
                    feature.setAttribute(band_field(band),
                                         float(band_vals[band]))
                feature.setGeometry(in_feat.geometry())
                count = self.append_to_feature_output(feature, count)
            else:
                feedback.pushInfo("Could not grab raster data for x:%s, y:%s" %
                                  ((in_feat.geometry().asPoint().x(),
                                    in_feat.geometry().asPoint().y())))
        else:
            feedback.pushInfo("Feature %s has no geometry" % in_feat.id())
        return count

    def feature_input_crs(self):
        return self.points_input_layer.crs()

    def name(self):
        """
        Returns the algorithm name, used for identifying the algorithm. This
        string should be fixed for the algorithm, and must not be localised.
        The name should be unique within each provider. Names should contain
        lowercase alphanumeric characters only and no spaces or other
        formatting characters.
        """
        return 'valuegrab'

    def displayName(self):
        """
        Returns the translated algorithm name, which should be used for any
        user-visible display of the algorithm name.
        """
        return self.tr("Value Grabber")

    def group(self):
        """
        Returns the name of the group this algorithm belongs to. This string
        should be localised.
        """
        return self.tr("QScout")

    def groupId(self):
        """
        Returns the unique ID of the group this algorithm belongs to. This
        string should be fixed for the algorithm, and must not be localised.
        The group id should be unique within each provider. Group id should
        contain lowercase alphanumeric characters only and no spaces or other
        formatting characters.
        """
        return 'qscout'

    def tr(self, string):
        return QCoreApplication.translate('Processing', string)

    def createInstance(self):
        return QScoutValueGrabberAlgorithm()

    def grab_radius(self):
        return self._grab_radius

    def grab_distance_weight(self):
        return self._grab_distance_weight

    def get_pixel_radius_around(self, point):
        if isinstance(point, QgsPointXY):
            x = point.x()
            y = point.y()
        else:
            x = point[0]
            y = point[1]
        ref_ext = QgsRectangle(
            QgsPointXY(x - self.grab_radius(), y - self.grab_radius()),
            QgsPointXY(x + self.grab_radius(), y + self.grab_radius()))
        scale_factor = self.raster_crs_transform.scaleFactor(ref_ext)
        return int(
            abs(self._grab_radius * scale_factor * 2 /
                self._raster_transform[1]))

    def mesh_with_distances(self, r, cx=0, cy=0, filter_circle=True):
        xs, ys = np.meshgrid(np.arange(-r, r), np.arange(-r, r))
        xs, ys = np.ravel(xs), np.ravel(ys)
        distances = np.sqrt(np.power(xs, 2) + np.power(ys, 2))
        xs, ys = xs + cx, ys + cy
        if filter_circle:
            allowed = np.all([
                distances < r, -1 < xs, xs < self.raster_width(), -1 < ys,
                ys < self.raster_height()
            ],
                             axis=0)
            xs = xs[allowed]
            ys = ys[allowed]
            distances = distances[allowed]
        return xs, ys, distances

    def query_raster(self, point_feature, bands=np.s_[:]):
        try:
            point = point_feature.geometry().asPoint()
            raster_x, raster_y = self.as_raster_coords(
                point.x(), point.y(), self.raster_crs_transform)
            if self._grab_radius == 0:
                return self.data(raster_x, raster_y, bands)

            rpixel = self.get_pixel_radius_around(point)
            xs, ys, distances = self.mesh_with_distances(
                rpixel, raster_x, raster_y)
            pixels = self.data(xs, ys, bands)

            if self.grab_analysis_function is None:
                if self.grab_distance_weight() != 0:
                    nanvals = np.any(np.isnan(pixels), axis=1)
                    weights = 1 / (
                        (distances**2) * self.grab_distance_weight())
                    weights[distances ==
                            0] = 1  # will appear as np.inf on the above line
                    return_data = np.average(pixels[~nanvals, :].astype(
                        np.float_),
                                             axis=0,
                                             weights=weights[~nanvals])
                else:
                    return_data = np.nanmean(pixels, axis=0)
            else:
                center_geo_rasterunits = self.raster_crs_transform.transform(
                    point)
                return_data = self.grab_analysis_function(
                    coords=(xs, ys),
                    distances=distances,
                    bands=bands,
                    pixels=pixels,
                    center_geo=(center_geo_rasterunits.x(),
                                center_geo_rasterunits.y()),
                    center_raster=(raster_x, raster_y),
                    feature=point_feature,
                    context=self)
            return return_data
        except IndexError as e:
            print(e)
            return None

    def feature_input(self):
        return self.points_input_layer.getFeatures()

    def feature_output_fields(self):
        return self.output_fields