From 7d14f526935bfa5617dacbc7e356c97e41dd0601 Mon Sep 17 00:00:00 2001 From: Abdul Date: Sat, 30 Jan 2021 11:46:53 -0500 Subject: [PATCH 01/15] initialize branch --- .gitignore | 1 + weighted_average.py | 160 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+) create mode 100644 .gitignore create mode 100644 weighted_average.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7d19373 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__/weighted_average.cpython-37.pyc diff --git a/weighted_average.py b/weighted_average.py new file mode 100644 index 0000000..a450fcb --- /dev/null +++ b/weighted_average.py @@ -0,0 +1,160 @@ +""" +Model exported as python. +Name : Weighted Average +Group : Tests +With QGIS : 31601 +""" + +from qgis.core import QgsProcessing +from qgis.core import QgsProcessingAlgorithm +from qgis.core import QgsProcessingMultiStepFeedback +from qgis.core import QgsProcessingParameterField +from qgis.core import QgsProcessingParameterVectorLayer +from qgis.core import QgsProcessingParameterBoolean +from qgis.core import QgsProcessingParameterFeatureSink +import processing + + +class WeightedAverage(QgsProcessingAlgorithm): + + def initAlgorithm(self, config=None): + self.addParameter(QgsProcessingParameterVectorLayer('inputlayer', 'Input Layer', types=[QgsProcessing.TypeVectorPolygon], defaultValue=None)) + self.addParameter(QgsProcessingParameterVectorLayer('overlaylayer', 'Overlay Layer', types=[QgsProcessing.TypeVectorPolygon], defaultValue=None)) + self.addParameter(QgsProcessingParameterField('fieldtoaverage', 'Field to Average', type=QgsProcessingParameterField.Numeric, parentLayerParameterName='overlaylayer', allowMultiple=False, defaultValue=None)) + self.addParameter(QgsProcessingParameterField('AdditionalFields', 'Additional Fields to keep for HTML Table', optional=True, type=QgsProcessingParameterField.Any, parentLayerParameterName='overlaylayer', allowMultiple=True,)) + self.addParameter(QgsProcessingParameterBoolean('VERBOSE_LOG', 'Verbose logging', optional=True, defaultValue=False)) + self.addParameter(QgsProcessingParameterFeatureSink('result', 'Result', type=QgsProcessing.TypeVectorAnyGeometry, createByDefault=True, supportsAppend=True, defaultValue=None)) + self.addParameter(QgsProcessingParameterFeatureSink('Intersected', 'Intersected', type=QgsProcessing.TypeVectorAnyGeometry, createByDefault=True, supportsAppend=True, defaultValue=None)) + + def processAlgorithm(self, parameters, context, model_feedback): + # Use a multi-step feedback, so that individual child algorithm progress reports are adjusted for the + # overall progress through the model + feedback = QgsProcessingMultiStepFeedback(7, model_feedback) + results = {} + outputs = {} + + # add_ID_field + alg_params = { + 'FIELD_NAME': '__Unique_ID__', + 'GROUP_FIELDS': [''], + 'INPUT': parameters['inputlayer'], + 'SORT_ASCENDING': True, + 'SORT_EXPRESSION': '', + 'SORT_NULLS_FIRST': False, + 'START': 0, + 'OUTPUT': QgsProcessing.TEMPORARY_OUTPUT + } + outputs['Add_id_field'] = processing.run('native:addautoincrementalfield', alg_params, context=context, feedback=feedback, is_child_algorithm=True) + + feedback.setCurrentStep(1) + if feedback.isCanceled(): + return {} + + # add_area_field + alg_params = { + 'FIELD_LENGTH': 0, + 'FIELD_NAME': '__Area_SPW__', + 'FIELD_PRECISION': 0, + 'FIELD_TYPE': 0, + 'FORMULA': 'area($geometry)', + 'INPUT': outputs['Add_id_field']['OUTPUT'], + 'OUTPUT': QgsProcessing.TEMPORARY_OUTPUT + } + outputs['Add_area_field'] = processing.run('native:fieldcalculator', alg_params, context=context, feedback=feedback, is_child_algorithm=True) + + feedback.setCurrentStep(2) + if feedback.isCanceled(): + return {} + + # Intersection + alg_params = { + 'INPUT': outputs['Add_area_field']['OUTPUT'], + 'INPUT_FIELDS': [''], + 'OVERLAY': parameters['overlaylayer'], + 'OVERLAY_FIELDS': [str(parameters['fieldtoaverage'])]+parameters['AdditionalFields'], + 'OVERLAY_FIELDS_PREFIX': '', + 'OUTPUT': QgsProcessing.TEMPORARY_OUTPUT + } + outputs['Intersection'] = processing.run('native:intersection', alg_params, context=context, feedback=feedback, is_child_algorithm=True) + + feedback.setCurrentStep(3) + if feedback.isCanceled(): + return {} + + # add_Weight + alg_params = { + 'FIELD_LENGTH': 0, + 'FIELD_NAME': parameters['fieldtoaverage'] + '_Area', + 'FIELD_PRECISION': 0, + 'FIELD_TYPE': 0, + 'FORMULA': ' "' + parameters['fieldtoaverage'] + '" * area($geometry)', + 'INPUT': outputs['Intersection']['OUTPUT'], + 'OUTPUT': parameters['Intersected'] + #'OUTPUT': QgsProcessing.TEMPORARY_OUTPUT + } + outputs['Add_Weight'] = processing.run('native:fieldcalculator', alg_params, context=context, feedback=feedback, is_child_algorithm=True) + results['HTML'] = outputs['Add_Weight']['OUTPUT'] # to be commented + + feedback.setCurrentStep(4) + if feedback.isCanceled(): + return {} + + # area_average + alg_params = { + 'FIELD_LENGTH': 0, + 'FIELD_NAME': 'Weighted_' + parameters['fieldtoaverage'], + 'FIELD_PRECISION': 0, + 'FIELD_TYPE': 0, + 'FORMULA': ' sum("' + parameters['fieldtoaverage'] + '_Area''",\"__Unique_ID__\")/\"__Area_SPW__\"', + 'INPUT': outputs['Add_Weight']['OUTPUT'], + 'OUTPUT': QgsProcessing.TEMPORARY_OUTPUT + } + outputs['area_average'] = processing.run('native:fieldcalculator', alg_params, context=context, feedback=feedback, is_child_algorithm=True) + + feedback.setCurrentStep(5) + if feedback.isCanceled(): + return {} + + # Dissolve + alg_params = { + 'FIELD': ['__Unique_ID__'], + 'INPUT': outputs['area_average']['OUTPUT'], + 'OUTPUT': QgsProcessing.TEMPORARY_OUTPUT + } + outputs['Dissolve'] = processing.run('native:dissolve', alg_params, context=context, feedback=feedback, is_child_algorithm=True) + + feedback.setCurrentStep(6) + if feedback.isCanceled(): + return {} + + input_layer = self.parameterAsVectorLayer(parameters, 'inputlayer', context) + result_name = input_layer.name()+'_'+parameters['fieldtoaverage'] + + parameters['result'].destinationName = result_name + + # Drop field(s) + alg_params = { + 'COLUMN': ['__Unique_ID__','__Area_SPW__']+[parameters['fieldtoaverage']]+parameters['AdditionalFields']+[parameters['fieldtoaverage'] + '_Area'], + 'INPUT': outputs['Dissolve']['OUTPUT'], + 'OUTPUT': parameters['result'] + } + outputs[result_name] = processing.run('qgis:deletecolumn', alg_params, context=context, feedback=feedback, is_child_algorithm=True) + outputs[result_name]['OUTPUT']=result_name + results['result'] = outputs[result_name]['OUTPUT'] + + return results + + def name(self): + return 'Weighted Average' + + def displayName(self): + return 'Weighted Average' + + def group(self): + return 'Tests' + + def groupId(self): + return 'Tests' + + def createInstance(self): + return WeightedAverage() From c29ce5610304c307206859fb2b3580861752c0ed Mon Sep 17 00:00:00 2001 From: Abdul Raheem Siddiqui <53625184+ar-siddiqui@users.noreply.github.com> Date: Sat, 30 Jan 2021 16:45:52 -0500 Subject: [PATCH 02/15] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a54dd64..e7e0ffa 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ -# weighted_average_analysis +# Weighted Average Analysis Area Weighted Average Analysis. Given an input polygon layer and overlay polygon layer, calculate weighted average of overlay parameters on input layer. From 37d3d7120bb47d4a7a5897767f9399b363a86348 Mon Sep 17 00:00:00 2001 From: Abdul Date: Sun, 21 Feb 2021 19:46:56 -0500 Subject: [PATCH 03/15] intermediate code --- .gitignore | 6 +- pandas_part.py | 31 +++++ weighted_average.py | 287 +++++++++++++++++++++++++++++++++----------- 3 files changed, 251 insertions(+), 73 deletions(-) create mode 100644 pandas_part.py diff --git a/.gitignore b/.gitignore index 7d19373..1e3db99 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ -__pycache__/weighted_average.cpython-37.pyc +# pyc files +*.pyc + + +Table.jpeg diff --git a/pandas_part.py b/pandas_part.py new file mode 100644 index 0000000..f0a24e7 --- /dev/null +++ b/pandas_part.py @@ -0,0 +1,31 @@ +# import tempfile +# import os + +# try: +# import pandas as pd +# except ImportError: +# import pip + +# pip.main(["install", "pandas"]) +# import pandas as pd + + +# with tempfile.TemporaryDirectory() as td: +# f_name = os.path.join(td, "test.csv") + +# layer = iface.activeLayer() + +# QgsVectorFileWriter.writeAsVectorFormat( +# layer, +# f_name, +# fileEncoding="utf-8", +# driverName="CSV", +# ) +# print(f_name) +# data = pd.read_csv(f_name) +# print(data.head()) +# print(f_name) + +# import pip + +# pip.main() \ No newline at end of file diff --git a/weighted_average.py b/weighted_average.py index a450fcb..527ab4d 100644 --- a/weighted_average.py +++ b/weighted_average.py @@ -16,15 +16,90 @@ class WeightedAverage(QgsProcessingAlgorithm): - def initAlgorithm(self, config=None): - self.addParameter(QgsProcessingParameterVectorLayer('inputlayer', 'Input Layer', types=[QgsProcessing.TypeVectorPolygon], defaultValue=None)) - self.addParameter(QgsProcessingParameterVectorLayer('overlaylayer', 'Overlay Layer', types=[QgsProcessing.TypeVectorPolygon], defaultValue=None)) - self.addParameter(QgsProcessingParameterField('fieldtoaverage', 'Field to Average', type=QgsProcessingParameterField.Numeric, parentLayerParameterName='overlaylayer', allowMultiple=False, defaultValue=None)) - self.addParameter(QgsProcessingParameterField('AdditionalFields', 'Additional Fields to keep for HTML Table', optional=True, type=QgsProcessingParameterField.Any, parentLayerParameterName='overlaylayer', allowMultiple=True,)) - self.addParameter(QgsProcessingParameterBoolean('VERBOSE_LOG', 'Verbose logging', optional=True, defaultValue=False)) - self.addParameter(QgsProcessingParameterFeatureSink('result', 'Result', type=QgsProcessing.TypeVectorAnyGeometry, createByDefault=True, supportsAppend=True, defaultValue=None)) - self.addParameter(QgsProcessingParameterFeatureSink('Intersected', 'Intersected', type=QgsProcessing.TypeVectorAnyGeometry, createByDefault=True, supportsAppend=True, defaultValue=None)) + self.addParameter( + QgsProcessingParameterVectorLayer( + "inputlayer", + "Input Layer", + types=[QgsProcessing.TypeVectorPolygon], + defaultValue=None, + ) + ) + self.addParameter( + QgsProcessingParameterVectorLayer( + "overlaylayer", + "Overlay Layer", + types=[QgsProcessing.TypeVectorPolygon], + defaultValue=None, + ) + ) + self.addParameter( + QgsProcessingParameterField( + "IDFieldMustnothaveduplicates", + "ID Field [Must not have duplicates]", + optional=True, + type=QgsProcessingParameterField.Any, + parentLayerParameterName="inputlayer", + allowMultiple=False, + defaultValue="", + ) + ) + self.addParameter( + QgsProcessingParameterField( + "fieldtoaverage", + "Field to Average", + type=QgsProcessingParameterField.Numeric, + parentLayerParameterName="overlaylayer", + allowMultiple=False, + defaultValue=None, + ) + ) + self.addParameter( + QgsProcessingParameterField( + "AdditionalFields", + "Additional Fields to keep for HTML Table", + optional=True, + type=QgsProcessingParameterField.Any, + parentLayerParameterName="overlaylayer", + allowMultiple=True, + ) + ) + self.addParameter( + QgsProcessingParameterBoolean( + "VERBOSE_LOG", "Verbose logging", optional=True, defaultValue=False + ) + ) + self.addParameter( + QgsProcessingParameterFeatureSink( + "result", + "Result", + type=QgsProcessing.TypeVectorAnyGeometry, + createByDefault=True, + supportsAppend=True, + defaultValue=None, + ) + ) + self.addParameter( + QgsProcessingParameterFeatureSink( + "Intersected", + "Intersected", + type=QgsProcessing.TypeVectorAnyGeometry, + createByDefault=True, + supportsAppend=True, + defaultValue=None, + ) + ) + self.addParameter( + QgsProcessingParameterField( + "NameorID", # to do: reduce name length + "Name or ID Field [Must not have duplicates]", + optional=True, + type=QgsProcessingParameterField.Any, + parentLayerParameterName="inputlayer", + allowMultiple=False, + defaultValue="", + ) + ) def processAlgorithm(self, parameters, context, model_feedback): # Use a multi-step feedback, so that individual child algorithm progress reports are adjusted for the @@ -33,49 +108,68 @@ def processAlgorithm(self, parameters, context, model_feedback): results = {} outputs = {} - # add_ID_field + # add_ID_field to input layer alg_params = { - 'FIELD_NAME': '__Unique_ID__', - 'GROUP_FIELDS': [''], - 'INPUT': parameters['inputlayer'], - 'SORT_ASCENDING': True, - 'SORT_EXPRESSION': '', - 'SORT_NULLS_FIRST': False, - 'START': 0, - 'OUTPUT': QgsProcessing.TEMPORARY_OUTPUT + "FIELD_NAME": "__Unique_ID__", + "GROUP_FIELDS": [""], + "INPUT": parameters["inputlayer"], + "SORT_ASCENDING": True, + "SORT_EXPRESSION": "", + "SORT_NULLS_FIRST": False, + "START": 0, + "OUTPUT": QgsProcessing.TEMPORARY_OUTPUT, } - outputs['Add_id_field'] = processing.run('native:addautoincrementalfield', alg_params, context=context, feedback=feedback, is_child_algorithm=True) + outputs["Add_id_field"] = processing.run( + "native:addautoincrementalfield", + alg_params, + context=context, + feedback=feedback, + is_child_algorithm=True, + ) feedback.setCurrentStep(1) if feedback.isCanceled(): return {} - # add_area_field + # add_area_field to input layer alg_params = { - 'FIELD_LENGTH': 0, - 'FIELD_NAME': '__Area_SPW__', - 'FIELD_PRECISION': 0, - 'FIELD_TYPE': 0, - 'FORMULA': 'area($geometry)', - 'INPUT': outputs['Add_id_field']['OUTPUT'], - 'OUTPUT': QgsProcessing.TEMPORARY_OUTPUT + "FIELD_LENGTH": 0, + "FIELD_NAME": "__Area_SPW__", + "FIELD_PRECISION": 0, + "FIELD_TYPE": 0, + "FORMULA": "area($geometry)", + "INPUT": outputs["Add_id_field"]["OUTPUT"], + "OUTPUT": QgsProcessing.TEMPORARY_OUTPUT, } - outputs['Add_area_field'] = processing.run('native:fieldcalculator', alg_params, context=context, feedback=feedback, is_child_algorithm=True) + outputs["Add_area_field"] = processing.run( + "native:fieldcalculator", + alg_params, + context=context, + feedback=feedback, + is_child_algorithm=True, + ) feedback.setCurrentStep(2) if feedback.isCanceled(): return {} - # Intersection + # intersection between input and overlay layer alg_params = { - 'INPUT': outputs['Add_area_field']['OUTPUT'], - 'INPUT_FIELDS': [''], - 'OVERLAY': parameters['overlaylayer'], - 'OVERLAY_FIELDS': [str(parameters['fieldtoaverage'])]+parameters['AdditionalFields'], - 'OVERLAY_FIELDS_PREFIX': '', - 'OUTPUT': QgsProcessing.TEMPORARY_OUTPUT + "INPUT": outputs["Add_area_field"]["OUTPUT"], + "INPUT_FIELDS": [""], + "OVERLAY": parameters["overlaylayer"], + "OVERLAY_FIELDS": [str(parameters["fieldtoaverage"])] + + parameters["AdditionalFields"], + "OVERLAY_FIELDS_PREFIX": "", + "OUTPUT": QgsProcessing.TEMPORARY_OUTPUT, } - outputs['Intersection'] = processing.run('native:intersection', alg_params, context=context, feedback=feedback, is_child_algorithm=True) + outputs["Intersection"] = processing.run( + "native:intersection", + alg_params, + context=context, + feedback=feedback, + is_child_algorithm=True, + ) feedback.setCurrentStep(3) if feedback.isCanceled(): @@ -83,17 +177,22 @@ def processAlgorithm(self, parameters, context, model_feedback): # add_Weight alg_params = { - 'FIELD_LENGTH': 0, - 'FIELD_NAME': parameters['fieldtoaverage'] + '_Area', - 'FIELD_PRECISION': 0, - 'FIELD_TYPE': 0, - 'FORMULA': ' "' + parameters['fieldtoaverage'] + '" * area($geometry)', - 'INPUT': outputs['Intersection']['OUTPUT'], - 'OUTPUT': parameters['Intersected'] + "FIELD_LENGTH": 0, + "FIELD_NAME": parameters["fieldtoaverage"] + "_Area", + "FIELD_PRECISION": 0, + "FIELD_TYPE": 0, + "FORMULA": ' "' + parameters["fieldtoaverage"] + '" * area($geometry)', + "INPUT": outputs["Intersection"]["OUTPUT"], + "OUTPUT": parameters["Intersected"] #'OUTPUT': QgsProcessing.TEMPORARY_OUTPUT } - outputs['Add_Weight'] = processing.run('native:fieldcalculator', alg_params, context=context, feedback=feedback, is_child_algorithm=True) - results['HTML'] = outputs['Add_Weight']['OUTPUT'] # to be commented + outputs["Add_Weight"] = processing.run( + "native:fieldcalculator", + alg_params, + context=context, + feedback=feedback, + is_child_algorithm=True, + ) feedback.setCurrentStep(4) if feedback.isCanceled(): @@ -101,15 +200,22 @@ def processAlgorithm(self, parameters, context, model_feedback): # area_average alg_params = { - 'FIELD_LENGTH': 0, - 'FIELD_NAME': 'Weighted_' + parameters['fieldtoaverage'], - 'FIELD_PRECISION': 0, - 'FIELD_TYPE': 0, - 'FORMULA': ' sum("' + parameters['fieldtoaverage'] + '_Area''",\"__Unique_ID__\")/\"__Area_SPW__\"', - 'INPUT': outputs['Add_Weight']['OUTPUT'], - 'OUTPUT': QgsProcessing.TEMPORARY_OUTPUT + "FIELD_LENGTH": 0, + "FIELD_NAME": "Weighted_" + parameters["fieldtoaverage"], + "FIELD_PRECISION": 0, + "FIELD_TYPE": 0, + "FORMULA": ' sum("' + parameters["fieldtoaverage"] + "_Area" + '","__Unique_ID__")/"__Area_SPW__"', + "INPUT": outputs["Add_Weight"]["OUTPUT"], + "OUTPUT": QgsProcessing.TEMPORARY_OUTPUT, } - outputs['area_average'] = processing.run('native:fieldcalculator', alg_params, context=context, feedback=feedback, is_child_algorithm=True) + outputs["area_average"] = processing.run( + "native:fieldcalculator", + alg_params, + context=context, + feedback=feedback, + is_child_algorithm=True, + ) feedback.setCurrentStep(5) if feedback.isCanceled(): @@ -117,44 +223,81 @@ def processAlgorithm(self, parameters, context, model_feedback): # Dissolve alg_params = { - 'FIELD': ['__Unique_ID__'], - 'INPUT': outputs['area_average']['OUTPUT'], - 'OUTPUT': QgsProcessing.TEMPORARY_OUTPUT + "FIELD": ["__Unique_ID__"], + "INPUT": outputs["area_average"]["OUTPUT"], + "OUTPUT": QgsProcessing.TEMPORARY_OUTPUT, } - outputs['Dissolve'] = processing.run('native:dissolve', alg_params, context=context, feedback=feedback, is_child_algorithm=True) + outputs["Dissolve"] = processing.run( + "native:dissolve", + alg_params, + context=context, + feedback=feedback, + is_child_algorithm=True, + ) feedback.setCurrentStep(6) if feedback.isCanceled(): return {} - input_layer = self.parameterAsVectorLayer(parameters, 'inputlayer', context) - result_name = input_layer.name()+'_'+parameters['fieldtoaverage'] - - parameters['result'].destinationName = result_name + input_layer = self.parameterAsVectorLayer(parameters, "inputlayer", context) + result_name = input_layer.name() + "_" + parameters["fieldtoaverage"] + + parameters["result"].destinationName = result_name - # Drop field(s) + # in input layer for 'Drop field(s) for Report' add Area and area as % + + # Drop field(s) for Report alg_params = { - 'COLUMN': ['__Unique_ID__','__Area_SPW__']+[parameters['fieldtoaverage']]+parameters['AdditionalFields']+[parameters['fieldtoaverage'] + '_Area'], - 'INPUT': outputs['Dissolve']['OUTPUT'], - 'OUTPUT': parameters['result'] + "COLUMN": ["__Unique_ID__", "__Area_SPW__"] + + [ + parameters["fieldtoaverage"] + ] # to do: drop all fields in input layer except id field + + [parameters["fieldtoaverage"] + "_Area"], + "INPUT": outputs["Add_Weight"]["OUTPUT"], + "OUTPUT": parameters["result"], } - outputs[result_name] = processing.run('qgis:deletecolumn', alg_params, context=context, feedback=feedback, is_child_algorithm=True) - outputs[result_name]['OUTPUT']=result_name - results['result'] = outputs[result_name]['OUTPUT'] - + outputs[result_name] = processing.run( + "qgis:deletecolumn", + alg_params, + context=context, + feedback=feedback, + is_child_algorithm=True, + ) + outputs[result_name]["OUTPUT"] = result_name + results["result"] = outputs[result_name]["OUTPUT"] + + # Drop field(s) for Result + alg_params = { + "COLUMN": ["__Unique_ID__", "__Area_SPW__"] + + [parameters["fieldtoaverage"]] + + parameters["AdditionalFields"] + + [parameters["fieldtoaverage"] + "_Area"], + "INPUT": outputs["Dissolve"]["OUTPUT"], + "OUTPUT": parameters["result"], + } + outputs[result_name] = processing.run( + "qgis:deletecolumn", + alg_params, + context=context, + feedback=feedback, + is_child_algorithm=True, + ) + outputs[result_name]["OUTPUT"] = result_name + results["result"] = outputs[result_name]["OUTPUT"] + return results def name(self): - return 'Weighted Average' + return "Weighted Average" def displayName(self): - return 'Weighted Average' + return "Weighted Average" def group(self): - return 'Tests' + return "Tests" def groupId(self): - return 'Tests' + return "Tests" def createInstance(self): return WeightedAverage() From feeb543224094eacb667413b1fe7601e29e8fec5 Mon Sep 17 00:00:00 2001 From: Abdul Date: Sun, 21 Feb 2021 19:56:53 -0500 Subject: [PATCH 04/15] plugin builder --- Makefile | 244 ++++++++++++++++ README.html | 39 +++ README.txt | 26 ++ __init__.py | 39 +++ help/Makefile | 130 +++++++++ help/make.bat | 155 ++++++++++ help/source/conf.py | 216 ++++++++++++++ help/source/index.rst | 20 ++ i18n/af.ts | 11 + metadata.txt | 47 ++++ pb_tool.cfg | 80 ++++++ plugin_upload.py | 111 ++++++++ pylintrc | 281 +++++++++++++++++++ scripts/compile-strings.sh | 12 + scripts/run-env-linux.sh | 28 ++ scripts/update-strings.sh | 56 ++++ test/__init__.py | 2 + test/qgis_interface.py | 205 ++++++++++++++ test/tenbytenraster.asc | 19 ++ test/tenbytenraster.asc.aux.xml | 13 + test/tenbytenraster.keywords | 1 + test/tenbytenraster.lic | 18 ++ test/tenbytenraster.prj | 1 + test/tenbytenraster.qml | 26 ++ test/test_init.py | 64 +++++ test/test_qgis_environment.py | 60 ++++ test/test_translations.py | 55 ++++ test/utilities.py | 61 ++++ weighted_average_analysis.py | 59 ++++ weighted_average_analysis_algorithm.py | 373 +++++++++++++++++++++++++ weighted_average_analysis_provider.py | 91 ++++++ 31 files changed, 2543 insertions(+) create mode 100644 Makefile create mode 100644 README.html create mode 100644 README.txt create mode 100644 __init__.py create mode 100644 help/Makefile create mode 100644 help/make.bat create mode 100644 help/source/conf.py create mode 100644 help/source/index.rst create mode 100644 i18n/af.ts create mode 100644 metadata.txt create mode 100644 pb_tool.cfg create mode 100644 plugin_upload.py create mode 100644 pylintrc create mode 100644 scripts/compile-strings.sh create mode 100644 scripts/run-env-linux.sh create mode 100644 scripts/update-strings.sh create mode 100644 test/__init__.py create mode 100644 test/qgis_interface.py create mode 100644 test/tenbytenraster.asc create mode 100644 test/tenbytenraster.asc.aux.xml create mode 100644 test/tenbytenraster.keywords create mode 100644 test/tenbytenraster.lic create mode 100644 test/tenbytenraster.prj create mode 100644 test/tenbytenraster.qml create mode 100644 test/test_init.py create mode 100644 test/test_qgis_environment.py create mode 100644 test/test_translations.py create mode 100644 test/utilities.py create mode 100644 weighted_average_analysis.py create mode 100644 weighted_average_analysis_algorithm.py create mode 100644 weighted_average_analysis_provider.py diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..23975d3 --- /dev/null +++ b/Makefile @@ -0,0 +1,244 @@ +#/*************************************************************************** +# WeightedAverageAnalysis +# +# Area Weighted Average Analysis. +# ------------------- +# begin : 2021-02-21 +# git sha : $Format:%H$ +# copyright : (C) 2021 by Abdul Raheem Siddiqui +# email : ars.work.ce@gmail.com +# ***************************************************************************/ +# +#/*************************************************************************** +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU General Public License as published by * +# * the Free Software Foundation; either version 2 of the License, or * +# * (at your option) any later version. * +# * * +# ***************************************************************************/ + +################################################# +# Edit the following to match your sources lists +################################################# + + +#Add iso code for any locales you want to support here (space separated) +# default is no locales +# LOCALES = af +LOCALES = + +# If locales are enabled, set the name of the lrelease binary on your system. If +# you have trouble compiling the translations, you may have to specify the full path to +# lrelease +#LRELEASE = lrelease +#LRELEASE = lrelease-qt4 + + +# translation +SOURCES = \ + __init__.py \ + weighted_average_analysis.py + +PLUGINNAME = weighted_average_analysis + +PY_FILES = \ + __init__.py \ + weighted_average_analysis.py + +UI_FILES = + +EXTRAS = metadata.txt + +EXTRA_DIRS = + +COMPILED_RESOURCE_FILES = + +PEP8EXCLUDE=pydev,resources.py,conf.py,third_party,ui + +# QGISDIR points to the location where your plugin should be installed. +# This varies by platform, relative to your HOME directory: +# * Linux: +# .local/share/QGIS/QGIS3/profiles/default/python/plugins/ +# * Mac OS X: +# Library/Application Support/QGIS/QGIS3/profiles/default/python/plugins +# * Windows: +# AppData\Roaming\QGIS\QGIS3\profiles\default\python\plugins' + +QGISDIR=C:\Users\abdul\AppData/Roaming/QGIS/QGIS3/profiles/default/python/plugins + +################################################# +# Normally you would not need to edit below here +################################################# + +HELP = help/build/html + +PLUGIN_UPLOAD = $(c)/plugin_upload.py + +RESOURCE_SRC=$(shell grep '^ *@@g;s/.*>//g' | tr '\n' ' ') + +.PHONY: default +default: + @echo While you can use make to build and deploy your plugin, pb_tool + @echo is a much better solution. + @echo A Python script, pb_tool provides platform independent management of + @echo your plugins and runs anywhere. + @echo You can install pb_tool using: pip install pb_tool + @echo See https://g-sherman.github.io/plugin_build_tool/ for info. + +compile: $(COMPILED_RESOURCE_FILES) + +%.py : %.qrc $(RESOURCES_SRC) + pyrcc5 -o $*.py $< + +%.qm : %.ts + $(LRELEASE) $< + +test: compile transcompile + @echo + @echo "----------------------" + @echo "Regression Test Suite" + @echo "----------------------" + + @# Preceding dash means that make will continue in case of errors + @-export PYTHONPATH=`pwd`:$(PYTHONPATH); \ + export QGIS_DEBUG=0; \ + export QGIS_LOG_FILE=/dev/null; \ + nosetests -v --with-id --with-coverage --cover-package=. \ + 3>&1 1>&2 2>&3 3>&- || true + @echo "----------------------" + @echo "If you get a 'no module named qgis.core error, try sourcing" + @echo "the helper script we have provided first then run make test." + @echo "e.g. source run-env-linux.sh ; make test" + @echo "----------------------" + +deploy: compile doc transcompile + @echo + @echo "------------------------------------------" + @echo "Deploying plugin to your .qgis2 directory." + @echo "------------------------------------------" + # The deploy target only works on unix like operating system where + # the Python plugin directory is located at: + # $HOME/$(QGISDIR)/python/plugins + mkdir -p $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) + cp -vf $(PY_FILES) $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) + cp -vf $(UI_FILES) $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) + cp -vf $(COMPILED_RESOURCE_FILES) $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) + cp -vf $(EXTRAS) $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) + cp -vfr i18n $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) + cp -vfr $(HELP) $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME)/help + # Copy extra directories if any + (foreach EXTRA_DIR,(EXTRA_DIRS), cp -R (EXTRA_DIR) (HOME)/(QGISDIR)/python/plugins/(PLUGINNAME)/;) + + +# The dclean target removes compiled python files from plugin directory +# also deletes any .git entry +dclean: + @echo + @echo "-----------------------------------" + @echo "Removing any compiled python files." + @echo "-----------------------------------" + find $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) -iname "*.pyc" -delete + find $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) -iname ".git" -prune -exec rm -Rf {} \; + + +derase: + @echo + @echo "-------------------------" + @echo "Removing deployed plugin." + @echo "-------------------------" + rm -Rf $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) + +zip: deploy dclean + @echo + @echo "---------------------------" + @echo "Creating plugin zip bundle." + @echo "---------------------------" + # The zip target deploys the plugin and creates a zip file with the deployed + # content. You can then upload the zip file on http://plugins.qgis.org + rm -f $(PLUGINNAME).zip + cd $(HOME)/$(QGISDIR)/python/plugins; zip -9r $(CURDIR)/$(PLUGINNAME).zip $(PLUGINNAME) + +package: compile + # Create a zip package of the plugin named $(PLUGINNAME).zip. + # This requires use of git (your plugin development directory must be a + # git repository). + # To use, pass a valid commit or tag as follows: + # make package VERSION=Version_0.3.2 + @echo + @echo "------------------------------------" + @echo "Exporting plugin to zip package. " + @echo "------------------------------------" + rm -f $(PLUGINNAME).zip + git archive --prefix=$(PLUGINNAME)/ -o $(PLUGINNAME).zip $(VERSION) + echo "Created package: $(PLUGINNAME).zip" + +upload: zip + @echo + @echo "-------------------------------------" + @echo "Uploading plugin to QGIS Plugin repo." + @echo "-------------------------------------" + $(PLUGIN_UPLOAD) $(PLUGINNAME).zip + +transup: + @echo + @echo "------------------------------------------------" + @echo "Updating translation files with any new strings." + @echo "------------------------------------------------" + @chmod +x scripts/update-strings.sh + @scripts/update-strings.sh $(LOCALES) + +transcompile: + @echo + @echo "----------------------------------------" + @echo "Compiled translation files to .qm files." + @echo "----------------------------------------" + @chmod +x scripts/compile-strings.sh + @scripts/compile-strings.sh $(LRELEASE) $(LOCALES) + +transclean: + @echo + @echo "------------------------------------" + @echo "Removing compiled translation files." + @echo "------------------------------------" + rm -f i18n/*.qm + +clean: + @echo + @echo "------------------------------------" + @echo "Removing uic and rcc generated files" + @echo "------------------------------------" + rm $(COMPILED_UI_FILES) $(COMPILED_RESOURCE_FILES) + +doc: + @echo + @echo "------------------------------------" + @echo "Building documentation using sphinx." + @echo "------------------------------------" + cd help; make html + +pylint: + @echo + @echo "-----------------" + @echo "Pylint violations" + @echo "-----------------" + @pylint --reports=n --rcfile=pylintrc . || true + @echo + @echo "----------------------" + @echo "If you get a 'no module named qgis.core' error, try sourcing" + @echo "the helper script we have provided first then run make pylint." + @echo "e.g. source run-env-linux.sh ; make pylint" + @echo "----------------------" + + +# Run pep8 style checking +#http://pypi.python.org/pypi/pep8 +pep8: + @echo + @echo "-----------" + @echo "PEP8 issues" + @echo "-----------" + @pep8 --repeat --ignore=E203,E121,E122,E123,E124,E125,E126,E127,E128 --exclude $(PEP8EXCLUDE) . || true + @echo "-----------" + @echo "Ignored in PEP8 check:" + @echo $(PEP8EXCLUDE) diff --git a/README.html b/README.html new file mode 100644 index 0000000..01a259b --- /dev/null +++ b/README.html @@ -0,0 +1,39 @@ + + +

Plugin Builder Results

+ +Congratulations! You just built a plugin for QGIS!

+ +
+Your plugin WeightedAverageAnalysis was created in:
+  C:/Users/abdul/Documents\weighted_average_analysis +

+Your QGIS plugin directory is located at:
+  C:/Users/abdul/AppData/Roaming/QGIS/QGIS3/profiles/default/python/plugins +

+

What's Next

+
    +
  1. Test the generated sources using make test (or run tests from your IDE) +
  2. Copy the entire directory containing your new plugin to the QGIS plugin directory (see Notes below) +
  3. Test the plugin by enabling it in the QGIS plugin manager and enabling the provider in the Processing Options +
  4. Customize it by editing the implementation file weighted_average_analysis_algorithm.py +
+Notes: +
    +
  • You can use the Makefile to compile and deploy when you + make changes. This requires GNU make (gmake). The Makefile is ready to use, however you + will have to edit it to add addional Python source files, dialogs, and translations. +
  • You can also use pb_tool to compile and deploy your plugin. Tweak the pb_tool.cfg file included with your plugin as you add files. Install pb_tool using + pip or easy_install. See http://loc8.cc/pb_tool for more information. +
+
+
+

+For information on writing PyQGIS code, see http://loc8.cc/pyqgis_resources for a list of resources. +

+
+

+©2011-2018 GeoApt LLC - geoapt.com +

+ + diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..127cc49 --- /dev/null +++ b/README.txt @@ -0,0 +1,26 @@ +Plugin Builder Results + +Your plugin WeightedAverageAnalysis was created in: + C:/Users/abdul/Documents\weighted_average_analysis + +Your QGIS plugin directory is located at: + C:/Users/abdul/AppData/Roaming/QGIS/QGIS3/profiles/default/python/plugins + +What's Next: + + * Copy the entire directory containing your new plugin to the QGIS plugin + directory + + * Run the tests (``make test``) + + * Test the plugin by enabling it in the QGIS plugin manager + + * Customize it by editing the implementation file: ``weighted_average_analysis.py`` + + * You can use the Makefile to compile your Ui and resource files when + you make changes. This requires GNU make (gmake) + +For more information, see the PyQGIS Developer Cookbook at: +http://www.qgis.org/pyqgis-cookbook/index.html + +(C) 2011-2018 GeoApt LLC - geoapt.com diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..d9a0438 --- /dev/null +++ b/__init__.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +""" +/*************************************************************************** + WeightedAverageAnalysis + A QGIS plugin + Area Weighted Average Analysis. + Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ + ------------------- + begin : 2021-02-21 + copyright : (C) 2021 by Abdul Raheem Siddiqui + email : ars.work.ce@gmail.com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + This script initializes the plugin, making it known to QGIS. +""" + +__author__ = 'Abdul Raheem Siddiqui' +__date__ = '2021-02-21' +__copyright__ = '(C) 2021 by Abdul Raheem Siddiqui' + + +# noinspection PyPep8Naming +def classFactory(iface): # pylint: disable=invalid-name + """Load WeightedAverageAnalysis class from file WeightedAverageAnalysis. + + :param iface: A QGIS interface instance. + :type iface: QgsInterface + """ + # + from .weighted_average_analysis import WeightedAverageAnalysisPlugin + return WeightedAverageAnalysisPlugin() diff --git a/help/Makefile b/help/Makefile new file mode 100644 index 0000000..9def777 --- /dev/null +++ b/help/Makefile @@ -0,0 +1,130 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/template_class.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/template_class.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/template_class" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/template_class" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + make -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." diff --git a/help/make.bat b/help/make.bat new file mode 100644 index 0000000..3377610 --- /dev/null +++ b/help/make.bat @@ -0,0 +1,155 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. changes to make an overview over all changed/added/deprecated items + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\template_class.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\template_class.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +:end diff --git a/help/source/conf.py b/help/source/conf.py new file mode 100644 index 0000000..748d054 --- /dev/null +++ b/help/source/conf.py @@ -0,0 +1,216 @@ +# -*- coding: utf-8 -*- +# +# WeightedAverageAnalysis documentation build configuration file, created by +# sphinx-quickstart on Sun Feb 12 17:11:03 2012. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys, os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ----------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.todo', 'sphinx.ext.imgmath', 'sphinx.ext.viewcode'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'WeightedAverageAnalysis' +copyright = u'2013, Abdul Raheem Siddiqui' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '0.1' +# The full version, including alpha/beta/rc tags. +release = '0.1' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = [] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_TemplateModuleNames = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'TemplateClassdoc' + + +# -- Options for LaTeX output -------------------------------------------------- + +# The paper size ('letter' or 'a4'). +#latex_paper_size = 'letter' + +# The font size ('10pt', '11pt' or '12pt'). +#latex_font_size = '10pt' + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'WeightedAverageAnalysis.tex', u'WeightedAverageAnalysis Documentation', + u'Abdul Raheem Siddiqui', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Additional stuff for the LaTeX preamble. +#latex_preamble = '' + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output -------------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'TemplateClass', u'WeightedAverageAnalysis Documentation', + [u'Abdul Raheem Siddiqui'], 1) +] diff --git a/help/source/index.rst b/help/source/index.rst new file mode 100644 index 0000000..75d0134 --- /dev/null +++ b/help/source/index.rst @@ -0,0 +1,20 @@ +.. WeightedAverageAnalysis documentation master file, created by + sphinx-quickstart on Sun Feb 12 17:11:03 2012. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to WeightedAverageAnalysis's documentation! +============================================ + +Contents: + +.. toctree:: + :maxdepth: 2 + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/i18n/af.ts b/i18n/af.ts new file mode 100644 index 0000000..615a88c --- /dev/null +++ b/i18n/af.ts @@ -0,0 +1,11 @@ + + + + @default + + + Good morning + Goeie more + + + diff --git a/metadata.txt b/metadata.txt new file mode 100644 index 0000000..6631888 --- /dev/null +++ b/metadata.txt @@ -0,0 +1,47 @@ +# This file contains metadata for your plugin. + +# This file should be included when you package your plugin.# Mandatory items: + +[general] +name=Weighted Average Analysis +qgisMinimumVersion=3.6 +description=Area Weighted Average Analysis. +version=0.1 +author=Abdul Raheem Siddiqui +email=ars.work.ce@gmail.com + +about=Given an input polygon layer and overlay polygon layer, calculate weighted average of overlay parameters on input layer and generate report. + +tracker=https://github.com/ar-siddiqui/weighted_average_analysis/issues +repository=https://github.com/ar-siddiqui/weighted_average_analysis +# End of mandatory metadata + +# Recommended items: + +hasProcessingProvider=yes +# Uncomment the following line and add your changelog: +# changelog= + +# Tags are comma separated with spaces allowed +tags=weighted, spatial, average, mean, analysis, report, overlap, spatial join + +homepage=https://github.com/ar-siddiqui/weighted_average_analysis +category=Analysis +icon=icon.png +# experimental flag +experimental=True + +# deprecated flag (applies to the whole plugin, not just a single version) +deprecated=False + +# Since QGIS 3.8, a comma separated list of plugins to be installed +# (or upgraded) can be specified. +# Check the documentation for more information. +# plugin_dependencies= + +Category of the plugin: Raster, Vector, Database or Web +# category= + +# If the plugin can run on QGIS Server. +server=False + diff --git a/pb_tool.cfg b/pb_tool.cfg new file mode 100644 index 0000000..ee58447 --- /dev/null +++ b/pb_tool.cfg @@ -0,0 +1,80 @@ +#/*************************************************************************** +# WeightedAverageAnalysis +# +# Configuration file for plugin builder tool (pb_tool) +# Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ +# ------------------- +# begin : 2021-02-21 +# copyright : (C) 2021 by Abdul Raheem Siddiqui +# email : ars.work.ce@gmail.com +# ***************************************************************************/ +# +#/*************************************************************************** +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU General Public License as published by * +# * the Free Software Foundation; either version 2 of the License, or * +# * (at your option) any later version. * +# * * +# ***************************************************************************/ +# +# +# You can install pb_tool using: +# pip install http://geoapt.net/files/pb_tool.zip +# +# Consider doing your development (and install of pb_tool) in a virtualenv. +# +# For details on setting up and using pb_tool, see: +# http://g-sherman.github.io/plugin_build_tool/ +# +# Issues and pull requests here: +# https://github.com/g-sherman/plugin_build_tool: +# +# Sane defaults for your plugin generated by the Plugin Builder are +# already set below. +# +# As you add Python source files and UI files to your plugin, add +# them to the appropriate [files] section below. + +[plugin] +# Name of the plugin. This is the name of the directory that will +# be created in .qgis2/python/plugins +name: weighted_average_analysis + +# Full path to where you want your plugin directory copied. If empty, +# the QGIS default path will be used. Don't include the plugin name in +# the path. +plugin_path: + +[files] +# Python files that should be deployed with the plugin +python_files: __init__.py weighted_average_analysis.py + +# The main dialog file that is loaded (not compiled) +main_dialog: + +# Other ui files for dialogs you create (these will be compiled) +compiled_ui_files: + +# Resource file(s) that will be compiled +resource_files: + +# Other files required for the plugin +extras: metadata.txt + +# Other directories to be deployed with the plugin. +# These must be subdirectories under the plugin directory +extra_dirs: + +# ISO code(s) for any locales (translations), separated by spaces. +# Corresponding .ts files must exist in the i18n directory +locales: + +[help] +# the built help directory that should be deployed with the plugin +dir: help/build/html +# the name of the directory to target in the deployed plugin +target: help + + + diff --git a/plugin_upload.py b/plugin_upload.py new file mode 100644 index 0000000..a88ea2b --- /dev/null +++ b/plugin_upload.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python +# coding=utf-8 +"""This script uploads a plugin package to the plugin repository. + Authors: A. Pasotti, V. Picavet + git sha : $TemplateVCSFormat +""" + +import sys +import getpass +import xmlrpc.client +from optparse import OptionParser + +standard_library.install_aliases() + +# Configuration +PROTOCOL = 'https' +SERVER = 'plugins.qgis.org' +PORT = '443' +ENDPOINT = '/plugins/RPC2/' +VERBOSE = False + + +def main(parameters, arguments): + """Main entry point. + + :param parameters: Command line parameters. + :param arguments: Command line arguments. + """ + address = "{protocol}://{username}:{password}@{server}:{port}{endpoint}".format( + protocol=PROTOCOL, + username=parameters.username, + password=parameters.password, + server=parameters.server, + port=parameters.port, + endpoint=ENDPOINT) + print("Connecting to: %s" % hide_password(address)) + + server = xmlrpc.client.ServerProxy(address, verbose=VERBOSE) + + try: + with open(arguments[0], 'rb') as handle: + plugin_id, version_id = server.plugin.upload( + xmlrpc.client.Binary(handle.read())) + print("Plugin ID: %s" % plugin_id) + print("Version ID: %s" % version_id) + except xmlrpc.client.ProtocolError as err: + print("A protocol error occurred") + print("URL: %s" % hide_password(err.url, 0)) + print("HTTP/HTTPS headers: %s" % err.headers) + print("Error code: %d" % err.errcode) + print("Error message: %s" % err.errmsg) + except xmlrpc.client.Fault as err: + print("A fault occurred") + print("Fault code: %d" % err.faultCode) + print("Fault string: %s" % err.faultString) + + +def hide_password(url, start=6): + """Returns the http url with password part replaced with '*'. + + :param url: URL to upload the plugin to. + :type url: str + + :param start: Position of start of password. + :type start: int + """ + start_position = url.find(':', start) + 1 + end_position = url.find('@') + return "%s%s%s" % ( + url[:start_position], + '*' * (end_position - start_position), + url[end_position:]) + + +if __name__ == "__main__": + parser = OptionParser(usage="%prog [options] plugin.zip") + parser.add_option( + "-w", "--password", dest="password", + help="Password for plugin site", metavar="******") + parser.add_option( + "-u", "--username", dest="username", + help="Username of plugin site", metavar="user") + parser.add_option( + "-p", "--port", dest="port", + help="Server port to connect to", metavar="80") + parser.add_option( + "-s", "--server", dest="server", + help="Specify server name", metavar="plugins.qgis.org") + options, args = parser.parse_args() + if len(args) != 1: + print("Please specify zip file.\n") + parser.print_help() + sys.exit(1) + if not options.server: + options.server = SERVER + if not options.port: + options.port = PORT + if not options.username: + # interactive mode + username = getpass.getuser() + print("Please enter user name [%s] :" % username, end=' ') + + res = input() + if res != "": + options.username = res + else: + options.username = username + if not options.password: + # interactive mode + options.password = getpass.getpass() + main(options, args) diff --git a/pylintrc b/pylintrc new file mode 100644 index 0000000..7e168f6 --- /dev/null +++ b/pylintrc @@ -0,0 +1,281 @@ +[MASTER] + +# Specify a configuration file. +#rcfile= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Profiled execution. +profile=no + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Pickle collected data for later comparisons. +persistent=yes + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + + +[MESSAGES CONTROL] + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time. See also the "--disable" option for examples. +#enable= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +# see http://stackoverflow.com/questions/21487025/pylint-locally-defined-disables-still-give-warnings-how-to-suppress-them +disable=locally-disabled,C0103 + + +[REPORTS] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html. You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Put messages in a separate file for each module / package specified on the +# command line instead of printing them on stdout. Reports (if any) will be +# written in a file name "pylint_global.[txt|html]". +files-output=no + +# Tells whether to display a full report or only the messages +reports=yes + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Add a comment according to your evaluation note. This is used by the global +# evaluation report (RP0004). +comment=no + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + + +[BASIC] + +# Required attributes for module, separated by a comma +required-attributes= + +# List of builtins function names that should not be used, separated by a comma +bad-functions=map,filter,apply,input + +# Regular expression which should only match correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression which should only match correct module level names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression which should only match correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression which should only match correct function names +function-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct method names +method-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct instance attribute names +attr-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct argument names +argument-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct variable names +variable-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct attribute names in class +# bodies +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Regular expression which should only match correct list comprehension / +# generator expression variable names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Good variable names which should always be accepted, separated by a comma +good-names=i,j,k,ex,Run,_ + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=__.*__ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + + +[TYPECHECK] + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# List of classes names for which member attributes should not be checked +# (useful for classes with attributes dynamically set). +ignored-classes=SQLObject + +# When zope mode is activated, add a predefined set of Zope acquired attributes +# to generated-members. +zope=no + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E0201 when accessed. Python regular +# expressions are accepted. +generated-members=REQUEST,acl_users,aq_parent + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching the beginning of the name of dummy variables +# (i.e. not used). +dummy-variables-rgx=_$|dummy + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=80 + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + +# List of optional constructs for which whitespace checking is disabled +no-space-check=trailing-comma,dict-separator + +# Maximum number of lines in a module +max-module-lines=1000 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=4 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + + +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub,TERMIOS,Bastion,rexec + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=5 + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.* + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of branch for function / method body +max-branches=12 + +# Maximum number of statements in function / method body +max-statements=50 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + + +[CLASSES] + +# List of interface methods to ignore, separated by a comma. This is used for +# instance to not check methods defines in Zope's Interface base class. +ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception diff --git a/scripts/compile-strings.sh b/scripts/compile-strings.sh new file mode 100644 index 0000000..9d76083 --- /dev/null +++ b/scripts/compile-strings.sh @@ -0,0 +1,12 @@ +#!/bin/bash +LRELEASE=$1 +LOCALES=$2 + + +for LOCALE in ${LOCALES} +do + echo "Processing: ${LOCALE}.ts" + # Note we don't use pylupdate with qt .pro file approach as it is flakey + # about what is made available. + $LRELEASE i18n/${LOCALE}.ts +done diff --git a/scripts/run-env-linux.sh b/scripts/run-env-linux.sh new file mode 100644 index 0000000..668247c --- /dev/null +++ b/scripts/run-env-linux.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +QGIS_PREFIX_PATH=/usr/local/qgis-2.0 +if [ -n "$1" ]; then + QGIS_PREFIX_PATH=$1 +fi + +echo ${QGIS_PREFIX_PATH} + + +export QGIS_PREFIX_PATH=${QGIS_PREFIX_PATH} +export QGIS_PATH=${QGIS_PREFIX_PATH} +export LD_LIBRARY_PATH=${QGIS_PREFIX_PATH}/lib +export PYTHONPATH=${QGIS_PREFIX_PATH}/share/qgis/python:${QGIS_PREFIX_PATH}/share/qgis/python/plugins:${PYTHONPATH} + +echo "QGIS PATH: $QGIS_PREFIX_PATH" +export QGIS_DEBUG=0 +export QGIS_LOG_FILE=/tmp/inasafe/realtime/logs/qgis.log + +export PATH=${QGIS_PREFIX_PATH}/bin:$PATH + +echo "This script is intended to be sourced to set up your shell to" +echo "use a QGIS 2.0 built in $QGIS_PREFIX_PATH" +echo +echo "To use it do:" +echo "source $BASH_SOURCE /your/optional/install/path" +echo +echo "Then use the make file supplied here e.g. make guitest" diff --git a/scripts/update-strings.sh b/scripts/update-strings.sh new file mode 100644 index 0000000..a31f712 --- /dev/null +++ b/scripts/update-strings.sh @@ -0,0 +1,56 @@ +#!/bin/bash +LOCALES=$* + +# Get newest .py files so we don't update strings unnecessarily + +CHANGED_FILES=0 +PYTHON_FILES=`find . -regex ".*\(ui\|py\)$" -type f` +for PYTHON_FILE in $PYTHON_FILES +do + CHANGED=$(stat -c %Y $PYTHON_FILE) + if [ ${CHANGED} -gt ${CHANGED_FILES} ] + then + CHANGED_FILES=${CHANGED} + fi +done + +# Qt translation stuff +# for .ts file +UPDATE=false +for LOCALE in ${LOCALES} +do + TRANSLATION_FILE="i18n/$LOCALE.ts" + if [ ! -f ${TRANSLATION_FILE} ] + then + # Force translation string collection as we have a new language file + touch ${TRANSLATION_FILE} + UPDATE=true + break + fi + + MODIFICATION_TIME=$(stat -c %Y ${TRANSLATION_FILE}) + if [ ${CHANGED_FILES} -gt ${MODIFICATION_TIME} ] + then + # Force translation string collection as a .py file has been updated + UPDATE=true + break + fi +done + +if [ ${UPDATE} == true ] +# retrieve all python files +then + echo ${PYTHON_FILES} + # update .ts + echo "Please provide translations by editing the translation files below:" + for LOCALE in ${LOCALES} + do + echo "i18n/"${LOCALE}".ts" + # Note we don't use pylupdate with qt .pro file approach as it is flakey + # about what is made available. + pylupdate4 -noobsolete ${PYTHON_FILES} -ts i18n/${LOCALE}.ts + done +else + echo "No need to edit any translation files (.ts) because no python files" + echo "has been updated since the last update translation. " +fi diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..8feeb0b --- /dev/null +++ b/test/__init__.py @@ -0,0 +1,2 @@ +# import qgis libs so that ve set the correct sip api version +import qgis # pylint: disable=W0611 # NOQA \ No newline at end of file diff --git a/test/qgis_interface.py b/test/qgis_interface.py new file mode 100644 index 0000000..a407052 --- /dev/null +++ b/test/qgis_interface.py @@ -0,0 +1,205 @@ +# coding=utf-8 +"""QGIS plugin implementation. + +.. note:: This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + +.. note:: This source code was copied from the 'postgis viewer' application + with original authors: + Copyright (c) 2010 by Ivan Mincik, ivan.mincik@gista.sk + Copyright (c) 2011 German Carrillo, geotux_tuxman@linuxmail.org + Copyright (c) 2014 Tim Sutton, tim@linfiniti.com + +""" + +__author__ = 'tim@linfiniti.com' +__revision__ = '$Format:%H$' +__date__ = '10/01/2011' +__copyright__ = ( + 'Copyright (c) 2010 by Ivan Mincik, ivan.mincik@gista.sk and ' + 'Copyright (c) 2011 German Carrillo, geotux_tuxman@linuxmail.org' + 'Copyright (c) 2014 Tim Sutton, tim@linfiniti.com' +) + +import logging +from qgis.PyQt.QtCore import QObject, pyqtSlot, pyqtSignal +from qgis.core import QgsMapLayerRegistry +from qgis.gui import QgsMapCanvasLayer +LOGGER = logging.getLogger('QGIS') + + +#noinspection PyMethodMayBeStatic,PyPep8Naming +class QgisInterface(QObject): + """Class to expose QGIS objects and functions to plugins. + + This class is here for enabling us to run unit tests only, + so most methods are simply stubs. + """ + currentLayerChanged = pyqtSignal(QgsMapCanvasLayer) + + def __init__(self, canvas): + """Constructor + :param canvas: + """ + QObject.__init__(self) + self.canvas = canvas + # Set up slots so we can mimic the behaviour of QGIS when layers + # are added. + LOGGER.debug('Initialising canvas...') + # noinspection PyArgumentList + QgsMapLayerRegistry.instance().layersAdded.connect(self.addLayers) + # noinspection PyArgumentList + QgsMapLayerRegistry.instance().layerWasAdded.connect(self.addLayer) + # noinspection PyArgumentList + QgsMapLayerRegistry.instance().removeAll.connect(self.removeAllLayers) + + # For processing module + self.destCrs = None + + @pyqtSlot('QStringList') + def addLayers(self, layers): + """Handle layers being added to the registry so they show up in canvas. + + :param layers: list list of map layers that were added + + .. note:: The QgsInterface api does not include this method, + it is added here as a helper to facilitate testing. + """ + #LOGGER.debug('addLayers called on qgis_interface') + #LOGGER.debug('Number of layers being added: %s' % len(layers)) + #LOGGER.debug('Layer Count Before: %s' % len(self.canvas.layers())) + current_layers = self.canvas.layers() + final_layers = [] + for layer in current_layers: + final_layers.append(QgsMapCanvasLayer(layer)) + for layer in layers: + final_layers.append(QgsMapCanvasLayer(layer)) + + self.canvas.setLayerSet(final_layers) + #LOGGER.debug('Layer Count After: %s' % len(self.canvas.layers())) + + @pyqtSlot('QgsMapLayer') + def addLayer(self, layer): + """Handle a layer being added to the registry so it shows up in canvas. + + :param layer: list list of map layers that were added + + .. note: The QgsInterface api does not include this method, it is added + here as a helper to facilitate testing. + + .. note: The addLayer method was deprecated in QGIS 1.8 so you should + not need this method much. + """ + pass + + @pyqtSlot() + def removeAllLayers(self): + """Remove layers from the canvas before they get deleted.""" + self.canvas.setLayerSet([]) + + def newProject(self): + """Create new project.""" + # noinspection PyArgumentList + QgsMapLayerRegistry.instance().removeAllMapLayers() + + # ---------------- API Mock for QgsInterface follows ------------------- + + def zoomFull(self): + """Zoom to the map full extent.""" + pass + + def zoomToPrevious(self): + """Zoom to previous view extent.""" + pass + + def zoomToNext(self): + """Zoom to next view extent.""" + pass + + def zoomToActiveLayer(self): + """Zoom to extent of active layer.""" + pass + + def addVectorLayer(self, path, base_name, provider_key): + """Add a vector layer. + + :param path: Path to layer. + :type path: str + + :param base_name: Base name for layer. + :type base_name: str + + :param provider_key: Provider key e.g. 'ogr' + :type provider_key: str + """ + pass + + def addRasterLayer(self, path, base_name): + """Add a raster layer given a raster layer file name + + :param path: Path to layer. + :type path: str + + :param base_name: Base name for layer. + :type base_name: str + """ + pass + + def activeLayer(self): + """Get pointer to the active layer (layer selected in the legend).""" + # noinspection PyArgumentList + layers = QgsMapLayerRegistry.instance().mapLayers() + for item in layers: + return layers[item] + + def addToolBarIcon(self, action): + """Add an icon to the plugins toolbar. + + :param action: Action to add to the toolbar. + :type action: QAction + """ + pass + + def removeToolBarIcon(self, action): + """Remove an action (icon) from the plugin toolbar. + + :param action: Action to add to the toolbar. + :type action: QAction + """ + pass + + def addToolBar(self, name): + """Add toolbar with specified name. + + :param name: Name for the toolbar. + :type name: str + """ + pass + + def mapCanvas(self): + """Return a pointer to the map canvas.""" + return self.canvas + + def mainWindow(self): + """Return a pointer to the main window. + + In case of QGIS it returns an instance of QgisApp. + """ + pass + + def addDockWidget(self, area, dock_widget): + """Add a dock widget to the main window. + + :param area: Where in the ui the dock should be placed. + :type area: + + :param dock_widget: A dock widget to add to the UI. + :type dock_widget: QDockWidget + """ + pass + + def legendInterface(self): + """Get the legend.""" + return self.canvas diff --git a/test/tenbytenraster.asc b/test/tenbytenraster.asc new file mode 100644 index 0000000..96a0ee1 --- /dev/null +++ b/test/tenbytenraster.asc @@ -0,0 +1,19 @@ +NCOLS 10 +NROWS 10 +XLLCENTER 1535380.000000 +YLLCENTER 5083260.000000 +DX 10 +DY 10 +NODATA_VALUE -9999 +0 1 2 3 4 5 6 7 8 9 +0 1 2 3 4 5 6 7 8 9 +0 1 2 3 4 5 6 7 8 9 +0 1 2 3 4 5 6 7 8 9 +0 1 2 3 4 5 6 7 8 9 +0 1 2 3 4 5 6 7 8 9 +0 1 2 3 4 5 6 7 8 9 +0 1 2 3 4 5 6 7 8 9 +0 1 2 3 4 5 6 7 8 9 +0 1 2 3 4 5 6 7 8 9 +CRS +NOTES diff --git a/test/tenbytenraster.asc.aux.xml b/test/tenbytenraster.asc.aux.xml new file mode 100644 index 0000000..cfb1578 --- /dev/null +++ b/test/tenbytenraster.asc.aux.xml @@ -0,0 +1,13 @@ + + + Point + + + + 9 + 4.5 + 0 + 2.872281323269 + + + diff --git a/test/tenbytenraster.keywords b/test/tenbytenraster.keywords new file mode 100644 index 0000000..8be3f61 --- /dev/null +++ b/test/tenbytenraster.keywords @@ -0,0 +1 @@ +title: Tenbytenraster diff --git a/test/tenbytenraster.lic b/test/tenbytenraster.lic new file mode 100644 index 0000000..8345533 --- /dev/null +++ b/test/tenbytenraster.lic @@ -0,0 +1,18 @@ + + + + Tim Sutton, Linfiniti Consulting CC + + + + tenbytenraster.asc + 2700044251 + Yes + Tim Sutton + Tim Sutton (QGIS Source Tree) + Tim Sutton + This data is publicly available from QGIS Source Tree. The original + file was created and contributed to QGIS by Tim Sutton. + + + diff --git a/test/tenbytenraster.prj b/test/tenbytenraster.prj new file mode 100644 index 0000000..a30c00a --- /dev/null +++ b/test/tenbytenraster.prj @@ -0,0 +1 @@ +GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]] \ No newline at end of file diff --git a/test/tenbytenraster.qml b/test/tenbytenraster.qml new file mode 100644 index 0000000..85247d4 --- /dev/null +++ b/test/tenbytenraster.qml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + 0 + diff --git a/test/test_init.py b/test/test_init.py new file mode 100644 index 0000000..a11ca44 --- /dev/null +++ b/test/test_init.py @@ -0,0 +1,64 @@ +# coding=utf-8 +"""Tests QGIS plugin init.""" + +__author__ = 'Tim Sutton ' +__revision__ = '$Format:%H$' +__date__ = '17/10/2010' +__license__ = "GPL" +__copyright__ = 'Copyright 2012, Australia Indonesia Facility for ' +__copyright__ += 'Disaster Reduction' + +import os +import unittest +import logging +import configparser + +LOGGER = logging.getLogger('QGIS') + + +class TestInit(unittest.TestCase): + """Test that the plugin init is usable for QGIS. + + Based heavily on the validator class by Alessandro + Passoti available here: + + http://github.com/qgis/qgis-django/blob/master/qgis-app/ + plugins/validator.py + + """ + + def test_read_init(self): + """Test that the plugin __init__ will validate on plugins.qgis.org.""" + + # You should update this list according to the latest in + # https://github.com/qgis/qgis-django/blob/master/qgis-app/ + # plugins/validator.py + + required_metadata = [ + 'name', + 'description', + 'version', + 'qgisMinimumVersion', + 'email', + 'author'] + + file_path = os.path.abspath(os.path.join( + os.path.dirname(__file__), os.pardir, + 'metadata.txt')) + LOGGER.info(file_path) + metadata = [] + parser = configparser.ConfigParser() + parser.optionxform = str + parser.read(file_path) + message = 'Cannot find a section named "general" in %s' % file_path + assert parser.has_section('general'), message + metadata.extend(parser.items('general')) + + for expectation in required_metadata: + message = ('Cannot find metadata "%s" in metadata source (%s).' % ( + expectation, file_path)) + + self.assertIn(expectation, dict(metadata), message) + +if __name__ == '__main__': + unittest.main() diff --git a/test/test_qgis_environment.py b/test/test_qgis_environment.py new file mode 100644 index 0000000..1becb30 --- /dev/null +++ b/test/test_qgis_environment.py @@ -0,0 +1,60 @@ +# coding=utf-8 +"""Tests for QGIS functionality. + + +.. note:: This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + +""" +__author__ = 'tim@linfiniti.com' +__date__ = '20/01/2011' +__copyright__ = ('Copyright 2012, Australia Indonesia Facility for ' + 'Disaster Reduction') + +import os +import unittest +from qgis.core import ( + QgsProviderRegistry, + QgsCoordinateReferenceSystem, + QgsRasterLayer) + +from .utilities import get_qgis_app +QGIS_APP = get_qgis_app() + + +class QGISTest(unittest.TestCase): + """Test the QGIS Environment""" + + def test_qgis_environment(self): + """QGIS environment has the expected providers""" + + r = QgsProviderRegistry.instance() + self.assertIn('gdal', r.providerList()) + self.assertIn('ogr', r.providerList()) + self.assertIn('postgres', r.providerList()) + + def test_projection(self): + """Test that QGIS properly parses a wkt string. + """ + crs = QgsCoordinateReferenceSystem() + wkt = ( + 'GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",' + 'SPHEROID["WGS_1984",6378137.0,298.257223563]],' + 'PRIMEM["Greenwich",0.0],UNIT["Degree",' + '0.0174532925199433]]') + crs.createFromWkt(wkt) + auth_id = crs.authid() + expected_auth_id = 'EPSG:4326' + self.assertEqual(auth_id, expected_auth_id) + + # now test for a loaded layer + path = os.path.join(os.path.dirname(__file__), 'tenbytenraster.asc') + title = 'TestRaster' + layer = QgsRasterLayer(path, title) + auth_id = layer.crs().authid() + self.assertEqual(auth_id, expected_auth_id) + +if __name__ == '__main__': + unittest.main() diff --git a/test/test_translations.py b/test/test_translations.py new file mode 100644 index 0000000..035dc62 --- /dev/null +++ b/test/test_translations.py @@ -0,0 +1,55 @@ +# coding=utf-8 +"""Safe Translations Test. + +.. note:: This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + +""" +from .utilities import get_qgis_app + +__author__ = 'ismailsunni@yahoo.co.id' +__date__ = '12/10/2011' +__copyright__ = ('Copyright 2012, Australia Indonesia Facility for ' + 'Disaster Reduction') +import unittest +import os + +from qgis.PyQt.QtCore import QCoreApplication, QTranslator + +QGIS_APP = get_qgis_app() + + +class SafeTranslationsTest(unittest.TestCase): + """Test translations work.""" + + def setUp(self): + """Runs before each test.""" + if 'LANG' in iter(os.environ.keys()): + os.environ.__delitem__('LANG') + + def tearDown(self): + """Runs after each test.""" + if 'LANG' in iter(os.environ.keys()): + os.environ.__delitem__('LANG') + + def test_qgis_translations(self): + """Test that translations work.""" + parent_path = os.path.join(__file__, os.path.pardir, os.path.pardir) + dir_path = os.path.abspath(parent_path) + file_path = os.path.join( + dir_path, 'i18n', 'af.qm') + translator = QTranslator() + translator.load(file_path) + QCoreApplication.installTranslator(translator) + + expected_message = 'Goeie more' + real_message = QCoreApplication.translate("@default", 'Good morning') + self.assertEqual(real_message, expected_message) + + +if __name__ == "__main__": + suite = unittest.makeSuite(SafeTranslationsTest) + runner = unittest.TextTestRunner(verbosity=2) + runner.run(suite) diff --git a/test/utilities.py b/test/utilities.py new file mode 100644 index 0000000..be7ee3b --- /dev/null +++ b/test/utilities.py @@ -0,0 +1,61 @@ +# coding=utf-8 +"""Common functionality used by regression tests.""" + +import sys +import logging + + +LOGGER = logging.getLogger('QGIS') +QGIS_APP = None # Static variable used to hold hand to running QGIS app +CANVAS = None +PARENT = None +IFACE = None + + +def get_qgis_app(): + """ Start one QGIS application to test against. + + :returns: Handle to QGIS app, canvas, iface and parent. If there are any + errors the tuple members will be returned as None. + :rtype: (QgsApplication, CANVAS, IFACE, PARENT) + + If QGIS is already running the handle to that app will be returned. + """ + + try: + from qgis.PyQt import QtGui, QtCore + from qgis.core import QgsApplication + from qgis.gui import QgsMapCanvas + from .qgis_interface import QgisInterface + except ImportError: + return None, None, None, None + + global QGIS_APP # pylint: disable=W0603 + + if QGIS_APP is None: + gui_flag = True # All test will run qgis in gui mode + #noinspection PyPep8Naming + QGIS_APP = QgsApplication(sys.argv, gui_flag) + # Make sure QGIS_PREFIX_PATH is set in your env if needed! + QGIS_APP.initQgis() + s = QGIS_APP.showSettings() + LOGGER.debug(s) + + global PARENT # pylint: disable=W0603 + if PARENT is None: + #noinspection PyPep8Naming + PARENT = QtGui.QWidget() + + global CANVAS # pylint: disable=W0603 + if CANVAS is None: + #noinspection PyPep8Naming + CANVAS = QgsMapCanvas(PARENT) + CANVAS.resize(QtCore.QSize(400, 400)) + + global IFACE # pylint: disable=W0603 + if IFACE is None: + # QgisInterface is a stub implementation of the QGIS plugin interface + #noinspection PyPep8Naming + IFACE = QgisInterface(CANVAS) + + return QGIS_APP, CANVAS, IFACE, PARENT diff --git a/weighted_average_analysis.py b/weighted_average_analysis.py new file mode 100644 index 0000000..bf858a1 --- /dev/null +++ b/weighted_average_analysis.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- + +""" +/*************************************************************************** + WeightedAverageAnalysis + A QGIS plugin + Area Weighted Average Analysis. + Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ + ------------------- + begin : 2021-02-21 + copyright : (C) 2021 by Abdul Raheem Siddiqui + email : ars.work.ce@gmail.com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +""" + +__author__ = "Abdul Raheem Siddiqui" +__date__ = "2021-02-21" +__copyright__ = "(C) 2021 by Abdul Raheem Siddiqui" + +# This will get replaced with a git SHA1 when you do a git archive + +__revision__ = "$Format:%H$" + +import os +import sys +import inspect + +from qgis.core import QgsProcessingAlgorithm, QgsApplication +from .weighted_average_analysis_provider import WeightedAverageAnalysisProvider + +cmd_folder = os.path.split(inspect.getfile(inspect.currentframe()))[0] + +if cmd_folder not in sys.path: + sys.path.insert(0, cmd_folder) + + +class WeightedAverageAnalysisPlugin(object): + def __init__(self): + self.provider = None + + def initProcessing(self): + """Init Processing provider for QGIS >= 3.8.""" + self.provider = WeightedAverageAnalysisProvider() + QgsApplication.processingRegistry().addProvider(self.provider) + + def initGui(self): + self.initProcessing() + + def unload(self): + QgsApplication.processingRegistry().removeProvider(self.provider) diff --git a/weighted_average_analysis_algorithm.py b/weighted_average_analysis_algorithm.py new file mode 100644 index 0000000..6471c03 --- /dev/null +++ b/weighted_average_analysis_algorithm.py @@ -0,0 +1,373 @@ +# -*- coding: utf-8 -*- + +""" +/*************************************************************************** + WeightedAverageAnalysis + A QGIS plugin + Area Weighted Average Analysis. + Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ + ------------------- + begin : 2021-02-21 + copyright : (C) 2021 by Abdul Raheem Siddiqui + email : ars.work.ce@gmail.com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +""" + +__author__ = "Abdul Raheem Siddiqui" +__date__ = "2021-02-21" +__copyright__ = "(C) 2021 by Abdul Raheem Siddiqui" + +# This will get replaced with a git SHA1 when you do a git archive + +__revision__ = "$Format:%H$" + +from qgis.PyQt.QtCore import QCoreApplication +from qgis.core import ( + QgsProcessing, + QgsFeatureSink, + QgsProcessingAlgorithm, + QgsProcessingParameterFeatureSource, + QgsProcessingParameterFeatureSink, +) + + +class WeightedAverageAnalysisAlgorithm(QgsProcessingAlgorithm): + """ + This is an example algorithm that takes a vector layer and + creates a new identical one. + + It is meant to be used as an example of how to create your own + algorithms and explain methods and variables used to do it. An + algorithm like this will be available in all elements, and there + is not need for additional work. + + All Processing algorithms should extend the QgsProcessingAlgorithm + class. + """ + + # Constants used to refer to parameters and outputs. They will be + # used when calling the algorithm from another algorithm, or when + # calling from the QGIS console. + + OUTPUT = "OUTPUT" + INPUT = "INPUT" + + def initAlgorithm(self, config=None): + self.addParameter( + QgsProcessingParameterVectorLayer( + "inputlayer", + "Input Layer", + types=[QgsProcessing.TypeVectorPolygon], + defaultValue=None, + ) + ) + self.addParameter( + QgsProcessingParameterVectorLayer( + "overlaylayer", + "Overlay Layer", + types=[QgsProcessing.TypeVectorPolygon], + defaultValue=None, + ) + ) + self.addParameter( + QgsProcessingParameterField( + "IDFieldMustnothaveduplicates", + "ID Field [Must not have duplicates]", + optional=True, + type=QgsProcessingParameterField.Any, + parentLayerParameterName="inputlayer", + allowMultiple=False, + defaultValue="", + ) + ) + self.addParameter( + QgsProcessingParameterField( + "fieldtoaverage", + "Field to Average", + type=QgsProcessingParameterField.Numeric, + parentLayerParameterName="overlaylayer", + allowMultiple=False, + defaultValue=None, + ) + ) + self.addParameter( + QgsProcessingParameterField( + "AdditionalFields", + "Additional Fields to keep for HTML Table", + optional=True, + type=QgsProcessingParameterField.Any, + parentLayerParameterName="overlaylayer", + allowMultiple=True, + ) + ) + self.addParameter( + QgsProcessingParameterBoolean( + "VERBOSE_LOG", "Verbose logging", optional=True, defaultValue=False + ) + ) + self.addParameter( + QgsProcessingParameterFeatureSink( + "result", + "Result", + type=QgsProcessing.TypeVectorAnyGeometry, + createByDefault=True, + supportsAppend=True, + defaultValue=None, + ) + ) + self.addParameter( + QgsProcessingParameterFeatureSink( + "Intersected", + "Intersected", + type=QgsProcessing.TypeVectorAnyGeometry, + createByDefault=True, + supportsAppend=True, + defaultValue=None, + ) + ) + self.addParameter( + QgsProcessingParameterField( + "NameorID", # to do: reduce name length + "Name or ID Field [Must not have duplicates]", + optional=True, + type=QgsProcessingParameterField.Any, + parentLayerParameterName="inputlayer", + allowMultiple=False, + defaultValue="", + ) + ) + + def processAlgorithm(self, parameters, context, model_feedback): + # Use a multi-step feedback, so that individual child algorithm progress reports are adjusted for the + # overall progress through the model + feedback = QgsProcessingMultiStepFeedback(7, model_feedback) + results = {} + outputs = {} + + # add_ID_field to input layer + alg_params = { + "FIELD_NAME": "__Unique_ID__", + "GROUP_FIELDS": [""], + "INPUT": parameters["inputlayer"], + "SORT_ASCENDING": True, + "SORT_EXPRESSION": "", + "SORT_NULLS_FIRST": False, + "START": 0, + "OUTPUT": QgsProcessing.TEMPORARY_OUTPUT, + } + outputs["Add_id_field"] = processing.run( + "native:addautoincrementalfield", + alg_params, + context=context, + feedback=feedback, + is_child_algorithm=True, + ) + + feedback.setCurrentStep(1) + if feedback.isCanceled(): + return {} + + # add_area_field to input layer + alg_params = { + "FIELD_LENGTH": 0, + "FIELD_NAME": "__Area_SPW__", + "FIELD_PRECISION": 0, + "FIELD_TYPE": 0, + "FORMULA": "area($geometry)", + "INPUT": outputs["Add_id_field"]["OUTPUT"], + "OUTPUT": QgsProcessing.TEMPORARY_OUTPUT, + } + outputs["Add_area_field"] = processing.run( + "native:fieldcalculator", + alg_params, + context=context, + feedback=feedback, + is_child_algorithm=True, + ) + + feedback.setCurrentStep(2) + if feedback.isCanceled(): + return {} + + # intersection between input and overlay layer + alg_params = { + "INPUT": outputs["Add_area_field"]["OUTPUT"], + "INPUT_FIELDS": [""], + "OVERLAY": parameters["overlaylayer"], + "OVERLAY_FIELDS": [str(parameters["fieldtoaverage"])] + + parameters["AdditionalFields"], + "OVERLAY_FIELDS_PREFIX": "", + "OUTPUT": QgsProcessing.TEMPORARY_OUTPUT, + } + outputs["Intersection"] = processing.run( + "native:intersection", + alg_params, + context=context, + feedback=feedback, + is_child_algorithm=True, + ) + + feedback.setCurrentStep(3) + if feedback.isCanceled(): + return {} + + # add_Weight + alg_params = { + "FIELD_LENGTH": 0, + "FIELD_NAME": parameters["fieldtoaverage"] + "_Area", + "FIELD_PRECISION": 0, + "FIELD_TYPE": 0, + "FORMULA": ' "' + parameters["fieldtoaverage"] + '" * area($geometry)', + "INPUT": outputs["Intersection"]["OUTPUT"], + "OUTPUT": parameters["Intersected"] + #'OUTPUT': QgsProcessing.TEMPORARY_OUTPUT + } + outputs["Add_Weight"] = processing.run( + "native:fieldcalculator", + alg_params, + context=context, + feedback=feedback, + is_child_algorithm=True, + ) + + feedback.setCurrentStep(4) + if feedback.isCanceled(): + return {} + + # area_average + alg_params = { + "FIELD_LENGTH": 0, + "FIELD_NAME": "Weighted_" + parameters["fieldtoaverage"], + "FIELD_PRECISION": 0, + "FIELD_TYPE": 0, + "FORMULA": ' sum("' + parameters["fieldtoaverage"] + "_Area" + '","__Unique_ID__")/"__Area_SPW__"', + "INPUT": outputs["Add_Weight"]["OUTPUT"], + "OUTPUT": QgsProcessing.TEMPORARY_OUTPUT, + } + outputs["area_average"] = processing.run( + "native:fieldcalculator", + alg_params, + context=context, + feedback=feedback, + is_child_algorithm=True, + ) + + feedback.setCurrentStep(5) + if feedback.isCanceled(): + return {} + + # Dissolve + alg_params = { + "FIELD": ["__Unique_ID__"], + "INPUT": outputs["area_average"]["OUTPUT"], + "OUTPUT": QgsProcessing.TEMPORARY_OUTPUT, + } + outputs["Dissolve"] = processing.run( + "native:dissolve", + alg_params, + context=context, + feedback=feedback, + is_child_algorithm=True, + ) + + feedback.setCurrentStep(6) + if feedback.isCanceled(): + return {} + + input_layer = self.parameterAsVectorLayer(parameters, "inputlayer", context) + result_name = input_layer.name() + "_" + parameters["fieldtoaverage"] + + parameters["result"].destinationName = result_name + + # in input layer for 'Drop field(s) for Report' add Area and area as % + + # Drop field(s) for Report + alg_params = { + "COLUMN": ["__Unique_ID__", "__Area_SPW__"] + + [ + parameters["fieldtoaverage"] + ] # to do: drop all fields in input layer except id field + + [parameters["fieldtoaverage"] + "_Area"], + "INPUT": outputs["Add_Weight"]["OUTPUT"], + "OUTPUT": parameters["result"], + } + outputs[result_name] = processing.run( + "qgis:deletecolumn", + alg_params, + context=context, + feedback=feedback, + is_child_algorithm=True, + ) + outputs[result_name]["OUTPUT"] = result_name + results["result"] = outputs[result_name]["OUTPUT"] + + # Drop field(s) for Result + alg_params = { + "COLUMN": ["__Unique_ID__", "__Area_SPW__"] + + [parameters["fieldtoaverage"]] + + parameters["AdditionalFields"] + + [parameters["fieldtoaverage"] + "_Area"], + "INPUT": outputs["Dissolve"]["OUTPUT"], + "OUTPUT": parameters["result"], + } + outputs[result_name] = processing.run( + "qgis:deletecolumn", + alg_params, + context=context, + feedback=feedback, + is_child_algorithm=True, + ) + outputs[result_name]["OUTPUT"] = result_name + results["result"] = outputs[result_name]["OUTPUT"] + + return results + + 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 "Weighted Average Analysis" + + def displayName(self): + """ + Returns the translated algorithm name, which should be used for any + user-visible display of the algorithm name. + """ + return self.tr(self.name()) + + def group(self): + """ + Returns the name of the group this algorithm belongs to. This string + should be localised. + """ + return self.tr(self.groupId()) + + 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 "" + + def tr(self, string): + return QCoreApplication.translate("Processing", string) + + def createInstance(self): + return WeightedAverageAnalysisAlgorithm() diff --git a/weighted_average_analysis_provider.py b/weighted_average_analysis_provider.py new file mode 100644 index 0000000..a3df989 --- /dev/null +++ b/weighted_average_analysis_provider.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- + +""" +/*************************************************************************** + WeightedAverageAnalysis + A QGIS plugin + Area Weighted Average Analysis. + Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ + ------------------- + begin : 2021-02-21 + copyright : (C) 2021 by Abdul Raheem Siddiqui + email : ars.work.ce@gmail.com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +""" + +__author__ = 'Abdul Raheem Siddiqui' +__date__ = '2021-02-21' +__copyright__ = '(C) 2021 by Abdul Raheem Siddiqui' + +# This will get replaced with a git SHA1 when you do a git archive + +__revision__ = '$Format:%H$' + +from qgis.core import QgsProcessingProvider +from .weighted_average_analysis_algorithm import WeightedAverageAnalysisAlgorithm + + +class WeightedAverageAnalysisProvider(QgsProcessingProvider): + + def __init__(self): + """ + Default constructor. + """ + QgsProcessingProvider.__init__(self) + + def unload(self): + """ + Unloads the provider. Any tear-down steps required by the provider + should be implemented here. + """ + pass + + def loadAlgorithms(self): + """ + Loads all algorithms belonging to this provider. + """ + self.addAlgorithm(WeightedAverageAnalysisAlgorithm()) + # add additional algorithms here + # self.addAlgorithm(MyOtherAlgorithm()) + + def id(self): + """ + Returns the unique provider id, used for identifying the provider. This + string should be a unique, short, character only string, eg "qgis" or + "gdal". This string should not be localised. + """ + return 'Auzaar' + + def name(self): + """ + Returns the provider name, which is used to describe the provider + within the GUI. + + This string should be short (e.g. "Lastools") and localised. + """ + return self.tr('Auzaar') + + def icon(self): + """ + Should return a QIcon which is used for your provider inside + the Processing toolbox. + """ + return QgsProcessingProvider.icon(self) + + def longName(self): + """ + Returns the a longer version of the provider name, which can include + extra details such as version numbers. E.g. "Lastools LIDAR tools + (version 2.2.1)". This string should be localised. The default + implementation returns the same string as name(). + """ + return self.name() From 74afd0724205fd303dfdfd5d543df183545e8609 Mon Sep 17 00:00:00 2001 From: Abdul Date: Mon, 22 Feb 2021 20:30:10 -0500 Subject: [PATCH 05/15] rename to area weighted average --- Makefile | 10 +- README.html | 72 ++++++----- README.md | 4 +- README.txt | 6 +- __init__.py | 17 +-- ...ge_analysis.py => area_weighted_average.py | 10 +- ...m.py => area_weighted_average_algorithm.py | 13 +- ...average.py => area_weighted_average_old.py | 0 ...er.py => area_weighted_average_provider.py | 23 ++-- help/source/conf.py | 120 ++++++++++-------- help/source/index.rst | 4 +- metadata.txt | 10 +- pb_tool.cfg | 6 +- 13 files changed, 156 insertions(+), 139 deletions(-) rename weighted_average_analysis.py => area_weighted_average.py (89%) rename weighted_average_analysis_algorithm.py => area_weighted_average_algorithm.py (97%) rename weighted_average.py => area_weighted_average_old.py (100%) rename weighted_average_analysis_provider.py => area_weighted_average_provider.py (85%) diff --git a/Makefile b/Makefile index 23975d3..c599889 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ #/*************************************************************************** -# WeightedAverageAnalysis +# AreaWeightedAverage # -# Area Weighted Average Analysis. +# Area Area Weighted Average. # ------------------- # begin : 2021-02-21 # git sha : $Format:%H$ @@ -38,13 +38,13 @@ LOCALES = # translation SOURCES = \ __init__.py \ - weighted_average_analysis.py + area_weighted_average.py -PLUGINNAME = weighted_average_analysis +PLUGINNAME = area_weighted_average PY_FILES = \ __init__.py \ - weighted_average_analysis.py + area_weighted_average.py UI_FILES = diff --git a/README.html b/README.html index 01a259b..9e1cc91 100644 --- a/README.html +++ b/README.html @@ -1,39 +1,43 @@ + -

Plugin Builder Results

+

Plugin Builder Results

-Congratulations! You just built a plugin for QGIS!

+ Congratulations! You just built a plugin for QGIS!

-
-Your plugin WeightedAverageAnalysis was created in:
-  C:/Users/abdul/Documents\weighted_average_analysis -

-Your QGIS plugin directory is located at:
-  C:/Users/abdul/AppData/Roaming/QGIS/QGIS3/profiles/default/python/plugins -

-

What's Next

-
    -
  1. Test the generated sources using make test (or run tests from your IDE) -
  2. Copy the entire directory containing your new plugin to the QGIS plugin directory (see Notes below) -
  3. Test the plugin by enabling it in the QGIS plugin manager and enabling the provider in the Processing Options -
  4. Customize it by editing the implementation file weighted_average_analysis_algorithm.py -
-Notes: -
    -
  • You can use the Makefile to compile and deploy when you - make changes. This requires GNU make (gmake). The Makefile is ready to use, however you - will have to edit it to add addional Python source files, dialogs, and translations. -
  • You can also use pb_tool to compile and deploy your plugin. Tweak the pb_tool.cfg file included with your plugin as you add files. Install pb_tool using - pip or easy_install. See http://loc8.cc/pb_tool for more information. -
-
-
-

-For information on writing PyQGIS code, see http://loc8.cc/pyqgis_resources for a list of resources. -

-
-

-©2011-2018 GeoApt LLC - geoapt.com -

+
+ Your plugin AreaWeightedAverage was created in:
+   C:/Users/abdul/Documents\area_weighted_average +

+ Your QGIS plugin directory is located at:
+   C:/Users/abdul/AppData/Roaming/QGIS/QGIS3/profiles/default/python/plugins +

+

What's Next

+
    +
  1. Test the generated sources using make test (or run tests from your IDE) +
  2. Copy the entire directory containing your new plugin to the QGIS plugin directory (see Notes below) +
  3. Test the plugin by enabling it in the QGIS plugin manager and enabling the provider in the Processing + Options +
  4. Customize it by editing the implementation file area_weighted_average_algorithm.py +
+ Notes: +
    +
  • You can use the Makefile to compile and deploy when you + make changes. This requires GNU make (gmake). The Makefile is ready to use, however you + will have to edit it to add addional Python source files, dialogs, and translations. +
  • You can also use pb_tool to compile and deploy your plugin. Tweak the pb_tool.cfg file + included with your plugin as you add files. Install pb_tool using + pip or easy_install. See http://loc8.cc/pb_tool for more information. +
+
+
+

+ For information on writing PyQGIS code, see http://loc8.cc/pyqgis_resources for a list of resources. +

+
+

+ ©2011-2018 GeoApt LLC - geoapt.com +

- + + \ No newline at end of file diff --git a/README.md b/README.md index e7e0ffa..8bcb8b8 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ -# Weighted Average Analysis -Area Weighted Average Analysis. Given an input polygon layer and overlay polygon layer, calculate weighted average of overlay parameters on input layer. +# Area Weighted Average +Area Area Weighted Average. Given an input polygon layer and overlay polygon layer, calculate weighted average of overlay parameters on input layer. diff --git a/README.txt b/README.txt index 127cc49..97207fb 100644 --- a/README.txt +++ b/README.txt @@ -1,7 +1,7 @@ Plugin Builder Results -Your plugin WeightedAverageAnalysis was created in: - C:/Users/abdul/Documents\weighted_average_analysis +Your plugin AreaWeightedAverage was created in: + C:/Users/abdul/Documents\area_weighted_average Your QGIS plugin directory is located at: C:/Users/abdul/AppData/Roaming/QGIS/QGIS3/profiles/default/python/plugins @@ -15,7 +15,7 @@ What's Next: * Test the plugin by enabling it in the QGIS plugin manager - * Customize it by editing the implementation file: ``weighted_average_analysis.py`` + * Customize it by editing the implementation file: ``area_weighted_average.py`` * You can use the Makefile to compile your Ui and resource files when you make changes. This requires GNU make (gmake) diff --git a/__init__.py b/__init__.py index d9a0438..6294523 100644 --- a/__init__.py +++ b/__init__.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- """ /*************************************************************************** - WeightedAverageAnalysis + AreaWeightedAverage A QGIS plugin - Area Weighted Average Analysis. + Area Area Weighted Average. Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ ------------------- begin : 2021-02-21 @@ -22,18 +22,19 @@ This script initializes the plugin, making it known to QGIS. """ -__author__ = 'Abdul Raheem Siddiqui' -__date__ = '2021-02-21' -__copyright__ = '(C) 2021 by Abdul Raheem Siddiqui' +__author__ = "Abdul Raheem Siddiqui" +__date__ = "2021-02-21" +__copyright__ = "(C) 2021 by Abdul Raheem Siddiqui" # noinspection PyPep8Naming def classFactory(iface): # pylint: disable=invalid-name - """Load WeightedAverageAnalysis class from file WeightedAverageAnalysis. + """Load AreaWeightedAverage class from file AreaWeightedAverage. :param iface: A QGIS interface instance. :type iface: QgsInterface """ # - from .weighted_average_analysis import WeightedAverageAnalysisPlugin - return WeightedAverageAnalysisPlugin() + from .area_weighted_average import AreaWeightedAveragePlugin + + return AreaWeightedAveragePlugin() diff --git a/weighted_average_analysis.py b/area_weighted_average.py similarity index 89% rename from weighted_average_analysis.py rename to area_weighted_average.py index bf858a1..ff0dd14 100644 --- a/weighted_average_analysis.py +++ b/area_weighted_average.py @@ -2,9 +2,9 @@ """ /*************************************************************************** - WeightedAverageAnalysis + AreaWeightedAverage A QGIS plugin - Area Weighted Average Analysis. + Area Area Weighted Average. Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ ------------------- begin : 2021-02-21 @@ -35,7 +35,7 @@ import inspect from qgis.core import QgsProcessingAlgorithm, QgsApplication -from .weighted_average_analysis_provider import WeightedAverageAnalysisProvider +from .area_weighted_average_provider import AreaWeightedAverageProvider cmd_folder = os.path.split(inspect.getfile(inspect.currentframe()))[0] @@ -43,13 +43,13 @@ sys.path.insert(0, cmd_folder) -class WeightedAverageAnalysisPlugin(object): +class AreaWeightedAveragePlugin(object): def __init__(self): self.provider = None def initProcessing(self): """Init Processing provider for QGIS >= 3.8.""" - self.provider = WeightedAverageAnalysisProvider() + self.provider = AreaWeightedAverageProvider() QgsApplication.processingRegistry().addProvider(self.provider) def initGui(self): diff --git a/weighted_average_analysis_algorithm.py b/area_weighted_average_algorithm.py similarity index 97% rename from weighted_average_analysis_algorithm.py rename to area_weighted_average_algorithm.py index 6471c03..7d06ee1 100644 --- a/weighted_average_analysis_algorithm.py +++ b/area_weighted_average_algorithm.py @@ -2,9 +2,9 @@ """ /*************************************************************************** - WeightedAverageAnalysis + AreaWeightedAverage A QGIS plugin - Area Weighted Average Analysis. + Area Area Weighted Average. Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ ------------------- begin : 2021-02-21 @@ -37,10 +37,13 @@ QgsProcessingAlgorithm, QgsProcessingParameterFeatureSource, QgsProcessingParameterFeatureSink, + QgsProcessingParameterVectorLayer, + QgsProcessingParameterField, + QgsProcessingParameterBoolean, ) -class WeightedAverageAnalysisAlgorithm(QgsProcessingAlgorithm): +class AreaWeightedAverageAlgorithm(QgsProcessingAlgorithm): """ This is an example algorithm that takes a vector layer and creates a new identical one. @@ -340,7 +343,7 @@ def name(self): lowercase alphanumeric characters only and no spaces or other formatting characters. """ - return "Weighted Average Analysis" + return "Area Weighted Average" def displayName(self): """ @@ -370,4 +373,4 @@ def tr(self, string): return QCoreApplication.translate("Processing", string) def createInstance(self): - return WeightedAverageAnalysisAlgorithm() + return AreaWeightedAverageAlgorithm() diff --git a/weighted_average.py b/area_weighted_average_old.py similarity index 100% rename from weighted_average.py rename to area_weighted_average_old.py diff --git a/weighted_average_analysis_provider.py b/area_weighted_average_provider.py similarity index 85% rename from weighted_average_analysis_provider.py rename to area_weighted_average_provider.py index a3df989..674d31b 100644 --- a/weighted_average_analysis_provider.py +++ b/area_weighted_average_provider.py @@ -2,9 +2,9 @@ """ /*************************************************************************** - WeightedAverageAnalysis + AreaWeightedAverage A QGIS plugin - Area Weighted Average Analysis. + Area Area Weighted Average. Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ ------------------- begin : 2021-02-21 @@ -22,20 +22,19 @@ ***************************************************************************/ """ -__author__ = 'Abdul Raheem Siddiqui' -__date__ = '2021-02-21' -__copyright__ = '(C) 2021 by Abdul Raheem Siddiqui' +__author__ = "Abdul Raheem Siddiqui" +__date__ = "2021-02-21" +__copyright__ = "(C) 2021 by Abdul Raheem Siddiqui" # This will get replaced with a git SHA1 when you do a git archive -__revision__ = '$Format:%H$' +__revision__ = "$Format:%H$" from qgis.core import QgsProcessingProvider -from .weighted_average_analysis_algorithm import WeightedAverageAnalysisAlgorithm +from .area_weighted_average_algorithm import AreaWeightedAverageAlgorithm -class WeightedAverageAnalysisProvider(QgsProcessingProvider): - +class AreaWeightedAverageProvider(QgsProcessingProvider): def __init__(self): """ Default constructor. @@ -53,7 +52,7 @@ def loadAlgorithms(self): """ Loads all algorithms belonging to this provider. """ - self.addAlgorithm(WeightedAverageAnalysisAlgorithm()) + self.addAlgorithm(AreaWeightedAverageAlgorithm()) # add additional algorithms here # self.addAlgorithm(MyOtherAlgorithm()) @@ -63,7 +62,7 @@ def id(self): string should be a unique, short, character only string, eg "qgis" or "gdal". This string should not be localised. """ - return 'Auzaar' + return "Area Weighted Average" def name(self): """ @@ -72,7 +71,7 @@ def name(self): This string should be short (e.g. "Lastools") and localised. """ - return self.tr('Auzaar') + return self.tr("Area Weighted Average") def icon(self): """ diff --git a/help/source/conf.py b/help/source/conf.py index 748d054..0279fc7 100644 --- a/help/source/conf.py +++ b/help/source/conf.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# WeightedAverageAnalysis documentation build configuration file, created by +# AreaWeightedAverage documentation build configuration file, created by # sphinx-quickstart on Sun Feb 12 17:11:03 2012. # # This file is execfile()d with the current directory set to its containing dir. @@ -16,194 +16,199 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +# sys.path.insert(0, os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.todo', 'sphinx.ext.imgmath', 'sphinx.ext.viewcode'] +extensions = ["sphinx.ext.todo", "sphinx.ext.imgmath", "sphinx.ext.viewcode"] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'WeightedAverageAnalysis' -copyright = u'2013, Abdul Raheem Siddiqui' +project = u"AreaWeightedAverage" +copyright = u"2013, Abdul Raheem Siddiqui" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '0.1' +version = "0.1" # The full version, including alpha/beta/rc tags. -release = '0.1' +release = "0.1" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_TemplateModuleNames = True +# add_TemplateModuleNames = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +html_theme = "default" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'TemplateClassdoc' +htmlhelp_basename = "TemplateClassdoc" # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). -#latex_paper_size = 'letter' +# latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). -#latex_font_size = '10pt' +# latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'WeightedAverageAnalysis.tex', u'WeightedAverageAnalysis Documentation', - u'Abdul Raheem Siddiqui', 'manual'), + ( + "index", + "AreaWeightedAverage.tex", + u"AreaWeightedAverage Documentation", + u"Abdul Raheem Siddiqui", + "manual", + ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Additional stuff for the LaTeX preamble. -#latex_preamble = '' +# latex_preamble = '' # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output -------------------------------------------- @@ -211,6 +216,11 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'TemplateClass', u'WeightedAverageAnalysis Documentation', - [u'Abdul Raheem Siddiqui'], 1) + ( + "index", + "TemplateClass", + u"AreaWeightedAverage Documentation", + [u"Abdul Raheem Siddiqui"], + 1, + ) ] diff --git a/help/source/index.rst b/help/source/index.rst index 75d0134..12ac9a4 100644 --- a/help/source/index.rst +++ b/help/source/index.rst @@ -1,9 +1,9 @@ -.. WeightedAverageAnalysis documentation master file, created by +.. AreaWeightedAverage documentation master file, created by sphinx-quickstart on Sun Feb 12 17:11:03 2012. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -Welcome to WeightedAverageAnalysis's documentation! +Welcome to AreaWeightedAverage's documentation! ============================================ Contents: diff --git a/metadata.txt b/metadata.txt index 6631888..d87a943 100644 --- a/metadata.txt +++ b/metadata.txt @@ -3,17 +3,17 @@ # This file should be included when you package your plugin.# Mandatory items: [general] -name=Weighted Average Analysis +name=Area Weighted Average qgisMinimumVersion=3.6 -description=Area Weighted Average Analysis. +description=Area Area Weighted Average. version=0.1 author=Abdul Raheem Siddiqui email=ars.work.ce@gmail.com about=Given an input polygon layer and overlay polygon layer, calculate weighted average of overlay parameters on input layer and generate report. -tracker=https://github.com/ar-siddiqui/weighted_average_analysis/issues -repository=https://github.com/ar-siddiqui/weighted_average_analysis +tracker=https://github.com/ar-siddiqui/area_weighted_average/issues +repository=https://github.com/ar-siddiqui/area_weighted_average # End of mandatory metadata # Recommended items: @@ -25,7 +25,7 @@ hasProcessingProvider=yes # Tags are comma separated with spaces allowed tags=weighted, spatial, average, mean, analysis, report, overlap, spatial join -homepage=https://github.com/ar-siddiqui/weighted_average_analysis +homepage=https://github.com/ar-siddiqui/area_weighted_average category=Analysis icon=icon.png # experimental flag diff --git a/pb_tool.cfg b/pb_tool.cfg index ee58447..f45f1eb 100644 --- a/pb_tool.cfg +++ b/pb_tool.cfg @@ -1,5 +1,5 @@ #/*************************************************************************** -# WeightedAverageAnalysis +# AreaWeightedAverage # # Configuration file for plugin builder tool (pb_tool) # Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ @@ -39,7 +39,7 @@ [plugin] # Name of the plugin. This is the name of the directory that will # be created in .qgis2/python/plugins -name: weighted_average_analysis +name: area_weighted_average # Full path to where you want your plugin directory copied. If empty, # the QGIS default path will be used. Don't include the plugin name in @@ -48,7 +48,7 @@ plugin_path: [files] # Python files that should be deployed with the plugin -python_files: __init__.py weighted_average_analysis.py +python_files: __init__.py area_weighted_average.py # The main dialog file that is loaded (not compiled) main_dialog: From 2478cd184cda233c234d957d548b1f81f9d8c294 Mon Sep 17 00:00:00 2001 From: Abdul Date: Mon, 22 Feb 2021 21:04:39 -0500 Subject: [PATCH 06/15] add logo and menu option --- __init__.py | 2 +- area_weighted_average.py | 19 ++++++++++++++++++- area_weighted_average_algorithm.py | 8 ++++++++ area_weighted_average_provider.py | 8 +++++++- logo.png | Bin 0 -> 11807 bytes 5 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 logo.png diff --git a/__init__.py b/__init__.py index 6294523..b1c1518 100644 --- a/__init__.py +++ b/__init__.py @@ -37,4 +37,4 @@ def classFactory(iface): # pylint: disable=invalid-name # from .area_weighted_average import AreaWeightedAveragePlugin - return AreaWeightedAveragePlugin() + return AreaWeightedAveragePlugin(iface) diff --git a/area_weighted_average.py b/area_weighted_average.py index ff0dd14..4d0b4fc 100644 --- a/area_weighted_average.py +++ b/area_weighted_average.py @@ -30,6 +30,11 @@ __revision__ = "$Format:%H$" +from qgis.PyQt.QtWidgets import QAction +from qgis.PyQt.QtGui import QIcon + +from qgis.core import QgsProcessingAlgorithm, QgsApplication +import processing import os import sys import inspect @@ -44,8 +49,9 @@ class AreaWeightedAveragePlugin(object): - def __init__(self): + def __init__(self, iface): self.provider = None + self.iface = iface def initProcessing(self): """Init Processing provider for QGIS >= 3.8.""" @@ -55,5 +61,16 @@ def initProcessing(self): def initGui(self): self.initProcessing() + icon = os.path.join(os.path.join(cmd_folder, "logo.png")) + self.action = QAction( + QIcon(icon), u"Area Weighted Average", self.iface.mainWindow() + ) + self.action.triggered.connect(self.run) + self.iface.addPluginToMenu(u"&Area Weighted Average", self.action) + def unload(self): QgsApplication.processingRegistry().removeProvider(self.provider) + self.iface.removePluginMenu(u"&Area Weighted Average", self.action) + + def run(self): + processing.execAlgorithmDialog("Area Weighted Average:Area Weighted Average") diff --git a/area_weighted_average_algorithm.py b/area_weighted_average_algorithm.py index 7d06ee1..a0ca421 100644 --- a/area_weighted_average_algorithm.py +++ b/area_weighted_average_algorithm.py @@ -30,6 +30,9 @@ __revision__ = "$Format:%H$" +import os +import inspect +from qgis.PyQt.QtGui import QIcon from qgis.PyQt.QtCore import QCoreApplication from qgis.core import ( QgsProcessing, @@ -374,3 +377,8 @@ def tr(self, string): def createInstance(self): return AreaWeightedAverageAlgorithm() + + def icon(self): + cmd_folder = os.path.split(inspect.getfile(inspect.currentframe()))[0] + icon = QIcon(os.path.join(os.path.join(cmd_folder, "logo.png"))) + return icon \ No newline at end of file diff --git a/area_weighted_average_provider.py b/area_weighted_average_provider.py index 674d31b..2510d1d 100644 --- a/area_weighted_average_provider.py +++ b/area_weighted_average_provider.py @@ -30,6 +30,10 @@ __revision__ = "$Format:%H$" +import os +import inspect +from qgis.PyQt.QtGui import QIcon + from qgis.core import QgsProcessingProvider from .area_weighted_average_algorithm import AreaWeightedAverageAlgorithm @@ -78,7 +82,9 @@ def icon(self): Should return a QIcon which is used for your provider inside the Processing toolbox. """ - return QgsProcessingProvider.icon(self) + cmd_folder = os.path.split(inspect.getfile(inspect.currentframe()))[0] + icon = QIcon(os.path.join(os.path.join(cmd_folder, "logo.png"))) + return icon def longName(self): """ diff --git a/logo.png b/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..ee40281c9fdffe653573de4152954c4285c32064 GIT binary patch literal 11807 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�|wY|V!t$;ctSe*^+`pav=sF3XNAq0EVx`K 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 Date: Sun, 28 Feb 2021 21:14:54 -0500 Subject: [PATCH 07/15] report as layer --- .gitignore | 1 + area_weighted_average_algorithm.py | 239 ++++++++++++++++++++--------- area_weighted_average_old.py | 2 - logo.png | Bin 11807 -> 44981 bytes logo.svg | 1 + logo2.png | Bin 0 -> 11978 bytes 6 files changed, 171 insertions(+), 72 deletions(-) create mode 100644 logo.svg create mode 100644 logo2.png diff --git a/.gitignore b/.gitignore index 1e3db99..e16dd00 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ Table.jpeg +area_weighted_average_algorithm.py diff --git a/area_weighted_average_algorithm.py b/area_weighted_average_algorithm.py index a0ca421..11cd13d 100644 --- a/area_weighted_average_algorithm.py +++ b/area_weighted_average_algorithm.py @@ -32,6 +32,7 @@ import os import inspect +import processing from qgis.PyQt.QtGui import QIcon from qgis.PyQt.QtCore import QCoreApplication from qgis.core import ( @@ -43,6 +44,9 @@ QgsProcessingParameterVectorLayer, QgsProcessingParameterField, QgsProcessingParameterBoolean, + QgsProcessingMultiStepFeedback, + QgsProcessingParameterDefinition, + QgsProcessingParameterFileDestination, ) @@ -84,17 +88,19 @@ def initAlgorithm(self, config=None): defaultValue=None, ) ) - self.addParameter( - QgsProcessingParameterField( - "IDFieldMustnothaveduplicates", - "ID Field [Must not have duplicates]", - optional=True, - type=QgsProcessingParameterField.Any, - parentLayerParameterName="inputlayer", - allowMultiple=False, - defaultValue="", - ) + + param = QgsProcessingParameterField( + "identifierfieldforreport", + "Identifier Field for Report", + optional=True, + type=QgsProcessingParameterField.Any, + parentLayerParameterName="inputlayer", + allowMultiple=False, + defaultValue="", ) + param.setFlags(param.flags() | QgsProcessingParameterDefinition.FlagAdvanced) + self.addParameter(param) + self.addParameter( QgsProcessingParameterField( "fieldtoaverage", @@ -105,50 +111,44 @@ def initAlgorithm(self, config=None): defaultValue=None, ) ) - self.addParameter( - QgsProcessingParameterField( - "AdditionalFields", - "Additional Fields to keep for HTML Table", - optional=True, - type=QgsProcessingParameterField.Any, - parentLayerParameterName="overlaylayer", - allowMultiple=True, - ) - ) - self.addParameter( - QgsProcessingParameterBoolean( - "VERBOSE_LOG", "Verbose logging", optional=True, defaultValue=False - ) + + param = QgsProcessingParameterField( + "additionalfields", + "Additional Fields to Keep for Report", + optional=True, + type=QgsProcessingParameterField.Any, + parentLayerParameterName="overlaylayer", + allowMultiple=True, ) + param.setFlags(param.flags() | QgsProcessingParameterDefinition.FlagAdvanced) + self.addParameter(param) + self.addParameter( QgsProcessingParameterFeatureSink( "result", "Result", type=QgsProcessing.TypeVectorAnyGeometry, createByDefault=True, - supportsAppend=True, defaultValue=None, ) ) self.addParameter( QgsProcessingParameterFeatureSink( - "Intersected", - "Intersected", + "reportaslayer", + "Report as Layer", type=QgsProcessing.TypeVectorAnyGeometry, createByDefault=True, - supportsAppend=True, defaultValue=None, ) ) + self.addParameter( - QgsProcessingParameterField( - "NameorID", # to do: reduce name length - "Name or ID Field [Must not have duplicates]", - optional=True, - type=QgsProcessingParameterField.Any, - parentLayerParameterName="inputlayer", - allowMultiple=False, - defaultValue="", + QgsProcessingParameterFileDestination( + "ReportasHTML", + self.tr("Report as HTML"), + self.tr("HTML files (*.html)"), + None, + True, ) ) @@ -161,7 +161,7 @@ def processAlgorithm(self, parameters, context, model_feedback): # add_ID_field to input layer alg_params = { - "FIELD_NAME": "__Unique_ID__", + "FIELD_NAME": "F_ID", "GROUP_FIELDS": [""], "INPUT": parameters["inputlayer"], "SORT_ASCENDING": True, @@ -185,7 +185,7 @@ def processAlgorithm(self, parameters, context, model_feedback): # add_area_field to input layer alg_params = { "FIELD_LENGTH": 0, - "FIELD_NAME": "__Area_SPW__", + "FIELD_NAME": "Area_AWA", "FIELD_PRECISION": 0, "FIELD_TYPE": 0, "FORMULA": "area($geometry)", @@ -193,7 +193,7 @@ def processAlgorithm(self, parameters, context, model_feedback): "OUTPUT": QgsProcessing.TEMPORARY_OUTPUT, } outputs["Add_area_field"] = processing.run( - "native:fieldcalculator", + "qgis:fieldcalculator", alg_params, context=context, feedback=feedback, @@ -204,13 +204,34 @@ def processAlgorithm(self, parameters, context, model_feedback): if feedback.isCanceled(): return {} + # dissolve all overlay fields so as not to repeat record in reporting + alg_params = { + "FIELD": [parameters["fieldtoaverage"]] + + [ + field + for field in parameters["additionalfields"] + if field != str(parameters["fieldtoaverage"]) + ], + "INPUT": parameters["overlaylayer"], + "OUTPUT": QgsProcessing.TEMPORARY_OUTPUT, + } + outputs["Dissolve"] = processing.run( + "native:dissolve", + alg_params, + context=context, + feedback=feedback, + is_child_algorithm=True, + ) + # intersection between input and overlay layer + # delete no field in input layer and all fields in overlay layer + # except field to average and additional fields alg_params = { "INPUT": outputs["Add_area_field"]["OUTPUT"], "INPUT_FIELDS": [""], - "OVERLAY": parameters["overlaylayer"], + "OVERLAY": outputs["Dissolve"]["OUTPUT"], "OVERLAY_FIELDS": [str(parameters["fieldtoaverage"])] - + parameters["AdditionalFields"], + + parameters["additionalfields"], "OVERLAY_FIELDS_PREFIX": "", "OUTPUT": QgsProcessing.TEMPORARY_OUTPUT, } @@ -234,11 +255,10 @@ def processAlgorithm(self, parameters, context, model_feedback): "FIELD_TYPE": 0, "FORMULA": ' "' + parameters["fieldtoaverage"] + '" * area($geometry)', "INPUT": outputs["Intersection"]["OUTPUT"], - "OUTPUT": parameters["Intersected"] - #'OUTPUT': QgsProcessing.TEMPORARY_OUTPUT + "OUTPUT": QgsProcessing.TEMPORARY_OUTPUT, } outputs["Add_Weight"] = processing.run( - "native:fieldcalculator", + "qgis:fieldcalculator", alg_params, context=context, feedback=feedback, @@ -256,12 +276,12 @@ def processAlgorithm(self, parameters, context, model_feedback): "FIELD_PRECISION": 0, "FIELD_TYPE": 0, "FORMULA": ' sum("' + parameters["fieldtoaverage"] + "_Area" - '","__Unique_ID__")/"__Area_SPW__"', + '","F_ID")/"Area_AWA"', "INPUT": outputs["Add_Weight"]["OUTPUT"], "OUTPUT": QgsProcessing.TEMPORARY_OUTPUT, } outputs["area_average"] = processing.run( - "native:fieldcalculator", + "qgis:fieldcalculator", alg_params, context=context, feedback=feedback, @@ -272,13 +292,13 @@ def processAlgorithm(self, parameters, context, model_feedback): if feedback.isCanceled(): return {} - # Dissolve + # remerge input layer elements alg_params = { - "FIELD": ["__Unique_ID__"], + "FIELD": ["F_ID"], "INPUT": outputs["area_average"]["OUTPUT"], "OUTPUT": QgsProcessing.TEMPORARY_OUTPUT, } - outputs["Dissolve"] = processing.run( + outputs["Dissolve2"] = processing.run( "native:dissolve", alg_params, context=context, @@ -292,49 +312,128 @@ def processAlgorithm(self, parameters, context, model_feedback): input_layer = self.parameterAsVectorLayer(parameters, "inputlayer", context) result_name = input_layer.name() + "_" + parameters["fieldtoaverage"] - parameters["result"].destinationName = result_name - # in input layer for 'Drop field(s) for Report' add Area and area as % - - # Drop field(s) for Report + # drop field(s) for Result + # alg_params = { - "COLUMN": ["__Unique_ID__", "__Area_SPW__"] + "COLUMN": ["F_ID", "Area_AWA"] + + [parameters["fieldtoaverage"]] + [ - parameters["fieldtoaverage"] - ] # to do: drop all fields in input layer except id field + field + for field in parameters["additionalfields"] + if field != str(parameters["fieldtoaverage"]) + ] + [parameters["fieldtoaverage"] + "_Area"], - "INPUT": outputs["Add_Weight"]["OUTPUT"], + "INPUT": outputs["Dissolve2"]["OUTPUT"], "OUTPUT": parameters["result"], } - outputs[result_name] = processing.run( + outputs["Drop1"] = processing.run( "qgis:deletecolumn", alg_params, context=context, feedback=feedback, is_child_algorithm=True, ) - outputs[result_name]["OUTPUT"] = result_name - results["result"] = outputs[result_name]["OUTPUT"] + results["result"] = outputs["Drop1"]["OUTPUT"] - # Drop field(s) for Result - alg_params = { - "COLUMN": ["__Unique_ID__", "__Area_SPW__"] + # Reporting + + # in input layer for 'Drop field(s) for Report' add Area and area as % + + # # Drop field(s) for Report + + int_layer = context.takeResultLayer(outputs["Add_Weight"]["OUTPUT"]) + all_fields = [f.name() for f in int_layer.fields()] + fields_to_keep = ( + ["F_ID"] + + [ + field + for field in parameters["additionalfields"] + if field != str(parameters["fieldtoaverage"]) + ] + [parameters["fieldtoaverage"]] - + parameters["AdditionalFields"] - + [parameters["fieldtoaverage"] + "_Area"], - "INPUT": outputs["Dissolve"]["OUTPUT"], - "OUTPUT": parameters["result"], + + [parameters["identifierfieldforreport"]] + ) + fields_to_drop = [f for f in all_fields if f not in fields_to_keep] + alg_params = { + "COLUMN": fields_to_drop, + "INPUT": int_layer, + "OUTPUT": QgsProcessing.TEMPORARY_OUTPUT, } - outputs[result_name] = processing.run( + outputs["Drop2"] = processing.run( "qgis:deletecolumn", alg_params, context=context, feedback=feedback, is_child_algorithm=True, ) - outputs[result_name]["OUTPUT"] = result_name - results["result"] = outputs[result_name]["OUTPUT"] + + feedback.setCurrentStep(2) + if feedback.isCanceled(): + return {} + + # update area + alg_params = { + "FIELD_LENGTH": 0, + "FIELD_NAME": "Area_CRS_Units", + "FIELD_PRECISION": 0, + "FIELD_TYPE": 0, + "FORMULA": "area($geometry)", + "INPUT": outputs["Drop2"]["OUTPUT"], + "OUTPUT": QgsProcessing.TEMPORARY_OUTPUT, + } + outputs["update_area"] = processing.run( + "qgis:fieldcalculator", + alg_params, + context=context, + feedback=feedback, + is_child_algorithm=True, + ) + + feedback.setCurrentStep(6) + if feedback.isCanceled(): + return {} + + parameters["reportaslayer"].destinationName = "Report as Layer" + # add area % + alg_params = { + "FIELD_LENGTH": 0, + "FIELD_NAME": "Area_Prcnt", + "FIELD_PRECISION": 0, + "FIELD_TYPE": 0, + "FORMULA": ' "Area_CRS_Units" *100/ sum( "Area_CRS_Units" , "F_ID" )', + "INPUT": outputs["update_area"]["OUTPUT"], + "OUTPUT": parameters["reportaslayer"], + } + outputs["area_prcnt"] = processing.run( + "qgis:fieldcalculator", + alg_params, + context=context, + feedback=feedback, + is_child_algorithm=True, + ) + + feedback.setCurrentStep(7) + if feedback.isCanceled(): + return {} + + results["reportaslayer"] = outputs["area_prcnt"]["OUTPUT"] + + # Drop geometries + + alg_params = { + "INPUT": outputs["Add_Weight"]["OUTPUT"], + "OUTPUT": parameters["ReportasTable"], + } + outputs["DropGeometries"] = processing.run( + "native:dropgeometries", + alg_params, + context=context, + feedback=feedback, + is_child_algorithm=True, + ) + results["ReportasTable"] = outputs["DropGeometries"]["OUTPUT"] return results diff --git a/area_weighted_average_old.py b/area_weighted_average_old.py index 527ab4d..5dd60b5 100644 --- a/area_weighted_average_old.py +++ b/area_weighted_average_old.py @@ -75,7 +75,6 @@ def initAlgorithm(self, config=None): "Result", type=QgsProcessing.TypeVectorAnyGeometry, createByDefault=True, - supportsAppend=True, defaultValue=None, ) ) @@ -85,7 +84,6 @@ def initAlgorithm(self, config=None): "Intersected", type=QgsProcessing.TypeVectorAnyGeometry, createByDefault=True, - supportsAppend=True, defaultValue=None, ) ) diff --git a/logo.png b/logo.png index ee40281c9fdffe653573de4152954c4285c32064..33f1d553d59732d947e97e689c9ae880a6db2b95 100644 GIT binary patch literal 44981 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�|wY|V!t$;ctSe*^+`pav=sF3XNAq0EVx`K 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 \ No newline at end of file diff --git a/logo2.png b/logo2.png new file mode 100644 index 0000000000000000000000000000000000000000..b7c4996f733cbb82cbc675f7a23a0e7e344a2697 GIT binary patch literal 11978 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%0AU##3bQEecfJEaHH9L@e@$<<}oJC72*AD7a34X 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 c3oQ=(KO%_e#6^kkSO5S307*qoM6N<$f;4Av8vp Date: Sun, 28 Feb 2021 21:16:08 -0500 Subject: [PATCH 08/15] ignore vs code settings --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index e16dd00..bce9457 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ Table.jpeg area_weighted_average_algorithm.py +.vscode/settings.json From 21ebad2a231450c37f050ebdb1f63d1b066c77d2 Mon Sep 17 00:00:00 2001 From: Abdul Date: Sun, 28 Feb 2021 21:26:51 -0500 Subject: [PATCH 09/15] initiate dataframe --- area_weighted_average_algorithm.py | 29 ++++++++++++++++++++++++++--- logo2.png | Bin 11978 -> 0 bytes 2 files changed, 26 insertions(+), 3 deletions(-) delete mode 100644 logo2.png diff --git a/area_weighted_average_algorithm.py b/area_weighted_average_algorithm.py index 11cd13d..3f13fe5 100644 --- a/area_weighted_average_algorithm.py +++ b/area_weighted_average_algorithm.py @@ -31,6 +31,7 @@ __revision__ = "$Format:%H$" import os +import tempfile import inspect import processing from qgis.PyQt.QtGui import QIcon @@ -47,8 +48,17 @@ QgsProcessingMultiStepFeedback, QgsProcessingParameterDefinition, QgsProcessingParameterFileDestination, + QgsVectorFileWriter, ) +try: + import pandas as pd +except ImportError: + import pip + + pip.main(["install", "pandas"]) + import pandas as pd + class AreaWeightedAverageAlgorithm(QgsProcessingAlgorithm): """ @@ -423,8 +433,8 @@ def processAlgorithm(self, parameters, context, model_feedback): # Drop geometries alg_params = { - "INPUT": outputs["Add_Weight"]["OUTPUT"], - "OUTPUT": parameters["ReportasTable"], + "INPUT": outputs["area_prcnt"]["OUTPUT"], + "OUTPUT": QgsProcessing.TEMPORARY_OUTPUT, } outputs["DropGeometries"] = processing.run( "native:dropgeometries", @@ -433,7 +443,20 @@ def processAlgorithm(self, parameters, context, model_feedback): feedback=feedback, is_child_algorithm=True, ) - results["ReportasTable"] = outputs["DropGeometries"]["OUTPUT"] + + with tempfile.TemporaryDirectory() as td: + f_name = os.path.join(td, "report_html.csv") + + report_layer = context.takeResultLayer(outputs["DropGeometries"]["OUTPUT"]) + + QgsVectorFileWriter.writeAsVectorFormat( + report_layer, + f_name, + fileEncoding="utf-8", + driverName="CSV", + ) + data = pd.read_csv(f_name) + feedback.pushInfo(data.head().to_string()) return results diff --git a/logo2.png b/logo2.png deleted file mode 100644 index b7c4996f733cbb82cbc675f7a23a0e7e344a2697..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11978 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%0AU##3bQEecfJEaHH9L@e@$<<}oJC72*AD7a34X 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 c3oQ=(KO%_e#6^kkSO5S307*qoM6N<$f;4Av8vp Date: Wed, 3 Mar 2021 20:51:14 -0500 Subject: [PATCH 10/15] add HTML Table and appeal --- area_weighted_average_algorithm.py | 188 +++++++++++++++++++++++------ pandas_part.py | 31 ----- usage_counter.log | 1 + 3 files changed, 149 insertions(+), 71 deletions(-) delete mode 100644 pandas_part.py create mode 100644 usage_counter.log diff --git a/area_weighted_average_algorithm.py b/area_weighted_average_algorithm.py index 3f13fe5..1b4686f 100644 --- a/area_weighted_average_algorithm.py +++ b/area_weighted_average_algorithm.py @@ -34,6 +34,9 @@ import tempfile import inspect import processing +import codecs + +from tempfile import NamedTemporaryFile from qgis.PyQt.QtGui import QIcon from qgis.PyQt.QtCore import QCoreApplication from qgis.core import ( @@ -51,14 +54,6 @@ QgsVectorFileWriter, ) -try: - import pandas as pd -except ImportError: - import pip - - pip.main(["install", "pandas"]) - import pandas as pd - class AreaWeightedAverageAlgorithm(QgsProcessingAlgorithm): """ @@ -154,7 +149,7 @@ def initAlgorithm(self, config=None): self.addParameter( QgsProcessingParameterFileDestination( - "ReportasHTML", + "reportasHTML", self.tr("Report as HTML"), self.tr("HTML files (*.html)"), None, @@ -162,6 +157,14 @@ def initAlgorithm(self, config=None): ) ) + # read usage + with open(os.path.join(cmd_folder, "usage_counter.log"), "r") as f: + counter = int(f.readline()) + + # check if counter is milestone + if (counter + 1) % 25 == 0: + self.addOutput(QgsProcessingOutputHtml("Message", "Area Weighted Average")) + def processAlgorithm(self, parameters, context, model_feedback): # Use a multi-step feedback, so that individual child algorithm progress reports are adjusted for the # overall progress through the model @@ -177,7 +180,7 @@ def processAlgorithm(self, parameters, context, model_feedback): "SORT_ASCENDING": True, "SORT_EXPRESSION": "", "SORT_NULLS_FIRST": False, - "START": 0, + "START": 1, "OUTPUT": QgsProcessing.TEMPORARY_OUTPUT, } outputs["Add_id_field"] = processing.run( @@ -280,9 +283,10 @@ def processAlgorithm(self, parameters, context, model_feedback): return {} # area_average + weighted_field = "Weighted_" + parameters["fieldtoaverage"] alg_params = { "FIELD_LENGTH": 0, - "FIELD_NAME": "Weighted_" + parameters["fieldtoaverage"], + "FIELD_NAME": weighted_field, "FIELD_PRECISION": 0, "FIELD_TYPE": 0, "FORMULA": ' sum("' + parameters["fieldtoaverage"] + "_Area" @@ -353,10 +357,10 @@ def processAlgorithm(self, parameters, context, model_feedback): # # Drop field(s) for Report - int_layer = context.takeResultLayer(outputs["Add_Weight"]["OUTPUT"]) + int_layer = context.takeResultLayer(outputs["area_average"]["OUTPUT"]) all_fields = [f.name() for f in int_layer.fields()] fields_to_keep = ( - ["F_ID"] + ["F_ID", weighted_field] + [ field for field in parameters["additionalfields"] @@ -430,33 +434,107 @@ def processAlgorithm(self, parameters, context, model_feedback): results["reportaslayer"] = outputs["area_prcnt"]["OUTPUT"] - # Drop geometries - - alg_params = { - "INPUT": outputs["area_prcnt"]["OUTPUT"], - "OUTPUT": QgsProcessing.TEMPORARY_OUTPUT, - } - outputs["DropGeometries"] = processing.run( - "native:dropgeometries", - alg_params, - context=context, - feedback=feedback, - is_child_algorithm=True, - ) - - with tempfile.TemporaryDirectory() as td: - f_name = os.path.join(td, "report_html.csv") - - report_layer = context.takeResultLayer(outputs["DropGeometries"]["OUTPUT"]) - - QgsVectorFileWriter.writeAsVectorFormat( - report_layer, - f_name, - fileEncoding="utf-8", - driverName="CSV", + output_file = self.parameterAsFileOutput(parameters, "reportasHTML", context) + + # create HTML report + if output_file: + + try: + try: + import pandas as pd + except ImportError: + import pathlib as pl + import subprocess + import sys + + qgis_Path = pl.Path(sys.executable) + qgis_python_path = (qgis_Path.parent / "python3.exe").as_posix() + + subprocess.check_call( + [qgis_python_path, "-m", "pip", "install", "--user", "pandas"] + ) + import pandas as pd + + feedback.pushInfo( + "python library pandas was installed for qgis python" + ) + except: + feedback.reportError( + "Failed to import pandas. Tried installing pandas but failed.\nPlease manually install pandas for the python that comes with your QGIS.", + True, + ) + return results + + # Drop geometries + alg_params = { + "INPUT": outputs["area_prcnt"]["OUTPUT"], + "OUTPUT": QgsProcessing.TEMPORARY_OUTPUT, + } + outputs["DropGeometries"] = processing.run( + "native:dropgeometries", + alg_params, + context=context, + feedback=feedback, + is_child_algorithm=True, ) - data = pd.read_csv(f_name) - feedback.pushInfo(data.head().to_string()) + + with tempfile.TemporaryDirectory() as td: + f_name = os.path.join(td, "report_df.csv") + + report_layer = context.takeResultLayer( + outputs["DropGeometries"]["OUTPUT"] + ) + + QgsVectorFileWriter.writeAsVectorFormat( + report_layer, + f_name, + fileEncoding="utf-8", + driverName="CSV", + ) + df = pd.read_csv(f_name) + + total_FIDs = df["F_ID"].max() + + ident_name = parameters["identifierfieldforreport"] + weighted_field = "Weighted_" + parameters["fieldtoaverage"] + html = "" + for i in range(1, total_FIDs): + df_sub = df.loc[df["F_ID"] == i] + df_sub.reset_index(inplace=True, drop=True) + avg_value = df_sub.at[0, weighted_field] + if ident_name: + feature_name = df_sub.at[0, ident_name] + df_sub.drop( + columns=["F_ID", ident_name, weighted_field], inplace=True + ) + html += f"

{i}. {feature_name}
{weighted_field}: {avg_value}
Count of intersecting features: {len(df_sub.index)}

\n" + else: + df_sub.drop(columns=["F_ID", weighted_field], inplace=True) + html += f"

Feature ID: {i}
{weighted_field}: {avg_value}
Count of intersecting features: {len(df_sub.index)}

\n" + html += f"{df_sub.to_html(bold_rows=False, index=False)}
\n" + + with codecs.open(output_file, "w", encoding="utf-8") as f: + f.write("\n") + f.write( + '\n' + ) + f.write(html) + f.write("\n") + + results["reportasHTML"] = output_file + + # log usage + with open(os.path.join(cmd_folder, "usage_counter.log"), "r+") as f: + counter = int(f.readline()) + f.seek(0) + f.write(str(counter + 1)) + + # check if counter is a milestone + if (counter + 1) % 25 == 0: + appeal_file = NamedTemporaryFile("w", suffix=".html", delete=False) + self.createHTML(appeal_file.name, counter + 1) + results["Message"] = appeal_file.name return results @@ -503,4 +581,34 @@ def createInstance(self): def icon(self): cmd_folder = os.path.split(inspect.getfile(inspect.currentframe()))[0] icon = QIcon(os.path.join(os.path.join(cmd_folder, "logo.png"))) - return icon \ No newline at end of file + return icon + + def createHTML(self, outputFile, counter): + with codecs.open(outputFile, "w", encoding="utf-8") as f: + f.write( + f""" + + + + + + + +


WOW! You have used the Area Weighted Average + Plugin {counter} + times already.
If you would like to get any GIS task automated for your organization please contact me at + ars.work.ce@gmail.com
+ If this plugin has saved your time, please consider making a personal or organizational donation of any value to the developer.

+
+
+ + + + + +
+ + +""" + ) diff --git a/pandas_part.py b/pandas_part.py deleted file mode 100644 index f0a24e7..0000000 --- a/pandas_part.py +++ /dev/null @@ -1,31 +0,0 @@ -# import tempfile -# import os - -# try: -# import pandas as pd -# except ImportError: -# import pip - -# pip.main(["install", "pandas"]) -# import pandas as pd - - -# with tempfile.TemporaryDirectory() as td: -# f_name = os.path.join(td, "test.csv") - -# layer = iface.activeLayer() - -# QgsVectorFileWriter.writeAsVectorFormat( -# layer, -# f_name, -# fileEncoding="utf-8", -# driverName="CSV", -# ) -# print(f_name) -# data = pd.read_csv(f_name) -# print(data.head()) -# print(f_name) - -# import pip - -# pip.main() \ No newline at end of file diff --git a/usage_counter.log b/usage_counter.log new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/usage_counter.log @@ -0,0 +1 @@ +0 \ No newline at end of file From 57d733a066af59fdf9abb9e7a799f4b85014e876 Mon Sep 17 00:00:00 2001 From: Abdul Date: Wed, 3 Mar 2021 20:53:08 -0500 Subject: [PATCH 11/15] add cmd folder --- area_weighted_average_algorithm.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/area_weighted_average_algorithm.py b/area_weighted_average_algorithm.py index 1b4686f..e53bb79 100644 --- a/area_weighted_average_algorithm.py +++ b/area_weighted_average_algorithm.py @@ -31,6 +31,7 @@ __revision__ = "$Format:%H$" import os +import sys import tempfile import inspect import processing @@ -54,6 +55,9 @@ QgsVectorFileWriter, ) +cmd_folder = os.path.split(inspect.getfile(inspect.currentframe()))[0] +sys.path.append(cmd_folder) + class AreaWeightedAverageAlgorithm(QgsProcessingAlgorithm): """ @@ -445,7 +449,6 @@ def processAlgorithm(self, parameters, context, model_feedback): except ImportError: import pathlib as pl import subprocess - import sys qgis_Path = pl.Path(sys.executable) qgis_python_path = (qgis_Path.parent / "python3.exe").as_posix() From 10b1996acfe6b3a77e8ea2245095e211bdd9d304 Mon Sep 17 00:00:00 2001 From: Abdul Date: Wed, 3 Mar 2021 21:08:06 -0500 Subject: [PATCH 12/15] change logo to icon --- area_weighted_average.py | 2 +- area_weighted_average_provider.py | 2 +- logo.png => icon.png | Bin logo.svg => icon.svg | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename logo.png => icon.png (100%) rename logo.svg => icon.svg (100%) diff --git a/area_weighted_average.py b/area_weighted_average.py index 4d0b4fc..81e0704 100644 --- a/area_weighted_average.py +++ b/area_weighted_average.py @@ -61,7 +61,7 @@ def initProcessing(self): def initGui(self): self.initProcessing() - icon = os.path.join(os.path.join(cmd_folder, "logo.png")) + icon = os.path.join(os.path.join(cmd_folder, "icon.png")) self.action = QAction( QIcon(icon), u"Area Weighted Average", self.iface.mainWindow() ) diff --git a/area_weighted_average_provider.py b/area_weighted_average_provider.py index 2510d1d..2b79706 100644 --- a/area_weighted_average_provider.py +++ b/area_weighted_average_provider.py @@ -83,7 +83,7 @@ def icon(self): the Processing toolbox. """ cmd_folder = os.path.split(inspect.getfile(inspect.currentframe()))[0] - icon = QIcon(os.path.join(os.path.join(cmd_folder, "logo.png"))) + icon = QIcon(os.path.join(os.path.join(cmd_folder, "icon.png"))) return icon def longName(self): diff --git a/logo.png b/icon.png similarity index 100% rename from logo.png rename to icon.png diff --git a/logo.svg b/icon.svg similarity index 100% rename from logo.svg rename to icon.svg From a2e2e5d8a5f82605ed176dfe43a619d5e154c02d Mon Sep 17 00:00:00 2001 From: Abdul Date: Thu, 4 Mar 2021 00:00:54 -0500 Subject: [PATCH 13/15] add help --- area_weighted_average_algorithm.py | 117 ++++++++++++++++++++--------- 1 file changed, 81 insertions(+), 36 deletions(-) diff --git a/area_weighted_average_algorithm.py b/area_weighted_average_algorithm.py index e53bb79..ee9806f 100644 --- a/area_weighted_average_algorithm.py +++ b/area_weighted_average_algorithm.py @@ -53,6 +53,7 @@ QgsProcessingParameterDefinition, QgsProcessingParameterFileDestination, QgsVectorFileWriter, + QgsProcessingOutputHtml, ) cmd_folder = os.path.split(inspect.getfile(inspect.currentframe()))[0] @@ -178,7 +179,7 @@ def processAlgorithm(self, parameters, context, model_feedback): # add_ID_field to input layer alg_params = { - "FIELD_NAME": "F_ID", + "FIELD_NAME": "input_feat_id", "GROUP_FIELDS": [""], "INPUT": parameters["inputlayer"], "SORT_ASCENDING": True, @@ -202,7 +203,7 @@ def processAlgorithm(self, parameters, context, model_feedback): # add_area_field to input layer alg_params = { "FIELD_LENGTH": 0, - "FIELD_NAME": "Area_AWA", + "FIELD_NAME": "area_awa", "FIELD_PRECISION": 0, "FIELD_TYPE": 0, "FORMULA": "area($geometry)", @@ -267,7 +268,7 @@ def processAlgorithm(self, parameters, context, model_feedback): # add_Weight alg_params = { "FIELD_LENGTH": 0, - "FIELD_NAME": parameters["fieldtoaverage"] + "_Area", + "FIELD_NAME": parameters["fieldtoaverage"] + "_area", "FIELD_PRECISION": 0, "FIELD_TYPE": 0, "FORMULA": ' "' + parameters["fieldtoaverage"] + '" * area($geometry)', @@ -287,14 +288,14 @@ def processAlgorithm(self, parameters, context, model_feedback): return {} # area_average - weighted_field = "Weighted_" + parameters["fieldtoaverage"] + weighted_field = "weighted_" + parameters["fieldtoaverage"] alg_params = { "FIELD_LENGTH": 0, "FIELD_NAME": weighted_field, "FIELD_PRECISION": 0, "FIELD_TYPE": 0, - "FORMULA": ' sum("' + parameters["fieldtoaverage"] + "_Area" - '","F_ID")/"Area_AWA"', + "FORMULA": ' sum("' + parameters["fieldtoaverage"] + "_area" + '","input_feat_id")/"area_awa"', "INPUT": outputs["Add_Weight"]["OUTPUT"], "OUTPUT": QgsProcessing.TEMPORARY_OUTPUT, } @@ -312,7 +313,7 @@ def processAlgorithm(self, parameters, context, model_feedback): # remerge input layer elements alg_params = { - "FIELD": ["F_ID"], + "FIELD": ["input_feat_id"], "INPUT": outputs["area_average"]["OUTPUT"], "OUTPUT": QgsProcessing.TEMPORARY_OUTPUT, } @@ -333,16 +334,15 @@ def processAlgorithm(self, parameters, context, model_feedback): parameters["result"].destinationName = result_name # drop field(s) for Result - # alg_params = { - "COLUMN": ["F_ID", "Area_AWA"] + "COLUMN": ["input_feat_id", "area_awa"] + [parameters["fieldtoaverage"]] + [ field for field in parameters["additionalfields"] if field != str(parameters["fieldtoaverage"]) ] - + [parameters["fieldtoaverage"] + "_Area"], + + [parameters["fieldtoaverage"] + "_area"], "INPUT": outputs["Dissolve2"]["OUTPUT"], "OUTPUT": parameters["result"], } @@ -357,14 +357,11 @@ def processAlgorithm(self, parameters, context, model_feedback): # Reporting - # in input layer for 'Drop field(s) for Report' add Area and area as % - - # # Drop field(s) for Report - + # Drop field(s) for Report int_layer = context.takeResultLayer(outputs["area_average"]["OUTPUT"]) all_fields = [f.name() for f in int_layer.fields()] fields_to_keep = ( - ["F_ID", weighted_field] + ["input_feat_id", weighted_field] + [ field for field in parameters["additionalfields"] @@ -393,11 +390,11 @@ def processAlgorithm(self, parameters, context, model_feedback): # update area alg_params = { - "FIELD_LENGTH": 0, - "FIELD_NAME": "Area_CRS_Units", - "FIELD_PRECISION": 0, + "FIELD_LENGTH": 20, + "FIELD_NAME": "area_crs_units", + "FIELD_PRECISION": 5, "FIELD_TYPE": 0, - "FORMULA": "area($geometry)", + "FORMULA": "round(area($geometry),5)", "INPUT": outputs["Drop2"]["OUTPUT"], "OUTPUT": QgsProcessing.TEMPORARY_OUTPUT, } @@ -416,13 +413,13 @@ def processAlgorithm(self, parameters, context, model_feedback): parameters["reportaslayer"].destinationName = "Report as Layer" # add area % alg_params = { - "FIELD_LENGTH": 0, - "FIELD_NAME": "Area_Prcnt", - "FIELD_PRECISION": 0, + "FIELD_LENGTH": 9, + "FIELD_NAME": "area_prcnt", + "FIELD_PRECISION": 5, "FIELD_TYPE": 0, - "FORMULA": ' "Area_CRS_Units" *100/ sum( "Area_CRS_Units" , "F_ID" )', + "FORMULA": ' round("area_crs_units" *100/ sum( "area_crs_units" , "input_feat_id" ),5)', "INPUT": outputs["update_area"]["OUTPUT"], - "OUTPUT": parameters["reportaslayer"], + "OUTPUT": QgsProcessing.TEMPORARY_OUTPUT, } outputs["area_prcnt"] = processing.run( "qgis:fieldcalculator", @@ -436,7 +433,23 @@ def processAlgorithm(self, parameters, context, model_feedback): if feedback.isCanceled(): return {} - results["reportaslayer"] = outputs["area_prcnt"]["OUTPUT"] + # Order by expression + alg_params = { + "ASCENDING": True, + "EXPRESSION": ' "input_feat_id" + area_prcnt" ', + "INPUT": outputs["area_prcnt"]["OUTPUT"], + "NULLS_FIRST": False, + "OUTPUT": parameters["reportaslayer"], + } + outputs["OrderByExpression"] = processing.run( + "native:orderbyexpression", + alg_params, + context=context, + feedback=feedback, + is_child_algorithm=True, + ) + + results["reportaslayer"] = outputs["OrderByExpression"]["OUTPUT"] output_file = self.parameterAsFileOutput(parameters, "reportasHTML", context) @@ -447,6 +460,9 @@ def processAlgorithm(self, parameters, context, model_feedback): try: import pandas as pd except ImportError: + feedback.pushInfo( + "Python library pandas was not found. Installing pandas to QGIS python ..." + ) import pathlib as pl import subprocess @@ -459,7 +475,7 @@ def processAlgorithm(self, parameters, context, model_feedback): import pandas as pd feedback.pushInfo( - "python library pandas was installed for qgis python" + "Python library pandas was successfully installed for QGIS python" ) except: feedback.reportError( @@ -494,27 +510,30 @@ def processAlgorithm(self, parameters, context, model_feedback): fileEncoding="utf-8", driverName="CSV", ) - df = pd.read_csv(f_name) - total_FIDs = df["F_ID"].max() + df = pd.read_csv(f_name) + total_FIDs = df["input_feat_id"].max() ident_name = parameters["identifierfieldforreport"] - weighted_field = "Weighted_" + parameters["fieldtoaverage"] html = "" + df.sort_values(by="area_prcnt", ascending=False, inplace=True) + pd.set_option("display.float_format", "{:.5f}".format) + for i in range(1, total_FIDs): - df_sub = df.loc[df["F_ID"] == i] + df_sub = df.loc[df["input_feat_id"] == i] df_sub.reset_index(inplace=True, drop=True) avg_value = df_sub.at[0, weighted_field] if ident_name: feature_name = df_sub.at[0, ident_name] df_sub.drop( - columns=["F_ID", ident_name, weighted_field], inplace=True + columns=["input_feat_id", ident_name, weighted_field], + inplace=True, ) - html += f"

{i}. {feature_name}
{weighted_field}: {avg_value}
Count of intersecting features: {len(df_sub.index)}

\n" + html += f"

{i}. {feature_name}
{weighted_field}: {avg_value}
count of distinct intersecting features: {len(df_sub.index)}

\n" else: - df_sub.drop(columns=["F_ID", weighted_field], inplace=True) - html += f"

Feature ID: {i}
{weighted_field}: {avg_value}
Count of intersecting features: {len(df_sub.index)}

\n" - html += f"{df_sub.to_html(bold_rows=False, index=False)}
\n" + df_sub.drop(columns=["input_feat_id", weighted_field], inplace=True) + html += f"

Feature ID: {i}
{weighted_field}: {avg_value}
count of distinct intersecting features: {len(df_sub.index)}

\n" + html += f"{df_sub.to_html(bold_rows=False, index=False, justify='left')}
\n" with codecs.open(output_file, "w", encoding="utf-8") as f: f.write("\n") @@ -583,9 +602,35 @@ def createInstance(self): def icon(self): cmd_folder = os.path.split(inspect.getfile(inspect.currentframe()))[0] - icon = QIcon(os.path.join(os.path.join(cmd_folder, "logo.png"))) + icon = QIcon(os.path.join(os.path.join(cmd_folder, "icon.png"))) return icon + def shortHelpString(self): + return """

Algorithm Description

+

This algorithm performs spatial area weighted average analysis on an input polygon layer given an attribute in the overlay polygon layer. Each feature in the input layer will be assigned a spatial area weighted average value of the overlay field. A report of the analysis is generated as a GIS Layer and as HTML.

+

Input Parameters

+

Input Layer

+

Polygon layer for which area weighted average will be calculated.

+

Overlay Layer

+

Polygon layer overlapping the Input Layer.

+

Field to Average

+

Single numeric field in the Overlay Layer.

+

Identifier Field for Report [optional]

+

Name or ID field in the Input Layer. This field will be used to identify features in the report.

+

Additional Fields to Keep for Report

+

Fields in the Overlay Layer that will be included in the reports.

+

Outputs

+

Result

+

Input layer but with the additional attribute of weighted value.

+

Report as Layer

+

Report as a GIS layer.

+

Report as HTML [optional]

+

Report containing feature-wise breakdown of the analysis.

+

Algorithm author: Abdul Raheem Siddiqui

Help author: Abdul Raheem Siddiqui

Algorithm version: 0.1

Contact email: ars.work.ce@gmail.com

""" + + def helpUrl(self): + return "mailto:ars.work.ce@gmail.com" + def createHTML(self, outputFile, counter): with codecs.open(outputFile, "w", encoding="utf-8") as f: f.write( From 90475890a51062cd9f37ce46fffdb9c214b19cd9 Mon Sep 17 00:00:00 2001 From: Abdul Date: Thu, 4 Mar 2021 00:17:08 -0500 Subject: [PATCH 14/15] renumber feeback steps --- area_weighted_average_algorithm.py | 33 +++- area_weighted_average_old.py | 301 ----------------------------- 2 files changed, 25 insertions(+), 309 deletions(-) delete mode 100644 area_weighted_average_old.py diff --git a/area_weighted_average_algorithm.py b/area_weighted_average_algorithm.py index ee9806f..4ee8e78 100644 --- a/area_weighted_average_algorithm.py +++ b/area_weighted_average_algorithm.py @@ -173,7 +173,7 @@ def initAlgorithm(self, config=None): def processAlgorithm(self, parameters, context, model_feedback): # Use a multi-step feedback, so that individual child algorithm progress reports are adjusted for the # overall progress through the model - feedback = QgsProcessingMultiStepFeedback(7, model_feedback) + feedback = QgsProcessingMultiStepFeedback(13, model_feedback) results = {} outputs = {} @@ -241,6 +241,10 @@ def processAlgorithm(self, parameters, context, model_feedback): is_child_algorithm=True, ) + feedback.setCurrentStep(3) + if feedback.isCanceled(): + return {} + # intersection between input and overlay layer # delete no field in input layer and all fields in overlay layer # except field to average and additional fields @@ -261,7 +265,7 @@ def processAlgorithm(self, parameters, context, model_feedback): is_child_algorithm=True, ) - feedback.setCurrentStep(3) + feedback.setCurrentStep(4) if feedback.isCanceled(): return {} @@ -283,7 +287,7 @@ def processAlgorithm(self, parameters, context, model_feedback): is_child_algorithm=True, ) - feedback.setCurrentStep(4) + feedback.setCurrentStep(5) if feedback.isCanceled(): return {} @@ -307,7 +311,7 @@ def processAlgorithm(self, parameters, context, model_feedback): is_child_algorithm=True, ) - feedback.setCurrentStep(5) + feedback.setCurrentStep(6) if feedback.isCanceled(): return {} @@ -325,7 +329,7 @@ def processAlgorithm(self, parameters, context, model_feedback): is_child_algorithm=True, ) - feedback.setCurrentStep(6) + feedback.setCurrentStep(7) if feedback.isCanceled(): return {} @@ -353,6 +357,11 @@ def processAlgorithm(self, parameters, context, model_feedback): feedback=feedback, is_child_algorithm=True, ) + + feedback.setCurrentStep(8) + if feedback.isCanceled(): + return {} + results["result"] = outputs["Drop1"]["OUTPUT"] # Reporting @@ -384,7 +393,7 @@ def processAlgorithm(self, parameters, context, model_feedback): is_child_algorithm=True, ) - feedback.setCurrentStep(2) + feedback.setCurrentStep(9) if feedback.isCanceled(): return {} @@ -406,7 +415,7 @@ def processAlgorithm(self, parameters, context, model_feedback): is_child_algorithm=True, ) - feedback.setCurrentStep(6) + feedback.setCurrentStep(10) if feedback.isCanceled(): return {} @@ -429,7 +438,7 @@ def processAlgorithm(self, parameters, context, model_feedback): is_child_algorithm=True, ) - feedback.setCurrentStep(7) + feedback.setCurrentStep(11) if feedback.isCanceled(): return {} @@ -449,6 +458,10 @@ def processAlgorithm(self, parameters, context, model_feedback): is_child_algorithm=True, ) + feedback.setCurrentStep(12) + if feedback.isCanceled(): + return {} + results["reportaslayer"] = outputs["OrderByExpression"]["OUTPUT"] output_file = self.parameterAsFileOutput(parameters, "reportasHTML", context) @@ -497,6 +510,10 @@ def processAlgorithm(self, parameters, context, model_feedback): is_child_algorithm=True, ) + feedback.setCurrentStep(13) + if feedback.isCanceled(): + return {} + with tempfile.TemporaryDirectory() as td: f_name = os.path.join(td, "report_df.csv") diff --git a/area_weighted_average_old.py b/area_weighted_average_old.py deleted file mode 100644 index 5dd60b5..0000000 --- a/area_weighted_average_old.py +++ /dev/null @@ -1,301 +0,0 @@ -""" -Model exported as python. -Name : Weighted Average -Group : Tests -With QGIS : 31601 -""" - -from qgis.core import QgsProcessing -from qgis.core import QgsProcessingAlgorithm -from qgis.core import QgsProcessingMultiStepFeedback -from qgis.core import QgsProcessingParameterField -from qgis.core import QgsProcessingParameterVectorLayer -from qgis.core import QgsProcessingParameterBoolean -from qgis.core import QgsProcessingParameterFeatureSink -import processing - - -class WeightedAverage(QgsProcessingAlgorithm): - def initAlgorithm(self, config=None): - self.addParameter( - QgsProcessingParameterVectorLayer( - "inputlayer", - "Input Layer", - types=[QgsProcessing.TypeVectorPolygon], - defaultValue=None, - ) - ) - self.addParameter( - QgsProcessingParameterVectorLayer( - "overlaylayer", - "Overlay Layer", - types=[QgsProcessing.TypeVectorPolygon], - defaultValue=None, - ) - ) - self.addParameter( - QgsProcessingParameterField( - "IDFieldMustnothaveduplicates", - "ID Field [Must not have duplicates]", - optional=True, - type=QgsProcessingParameterField.Any, - parentLayerParameterName="inputlayer", - allowMultiple=False, - defaultValue="", - ) - ) - self.addParameter( - QgsProcessingParameterField( - "fieldtoaverage", - "Field to Average", - type=QgsProcessingParameterField.Numeric, - parentLayerParameterName="overlaylayer", - allowMultiple=False, - defaultValue=None, - ) - ) - self.addParameter( - QgsProcessingParameterField( - "AdditionalFields", - "Additional Fields to keep for HTML Table", - optional=True, - type=QgsProcessingParameterField.Any, - parentLayerParameterName="overlaylayer", - allowMultiple=True, - ) - ) - self.addParameter( - QgsProcessingParameterBoolean( - "VERBOSE_LOG", "Verbose logging", optional=True, defaultValue=False - ) - ) - self.addParameter( - QgsProcessingParameterFeatureSink( - "result", - "Result", - type=QgsProcessing.TypeVectorAnyGeometry, - createByDefault=True, - defaultValue=None, - ) - ) - self.addParameter( - QgsProcessingParameterFeatureSink( - "Intersected", - "Intersected", - type=QgsProcessing.TypeVectorAnyGeometry, - createByDefault=True, - defaultValue=None, - ) - ) - self.addParameter( - QgsProcessingParameterField( - "NameorID", # to do: reduce name length - "Name or ID Field [Must not have duplicates]", - optional=True, - type=QgsProcessingParameterField.Any, - parentLayerParameterName="inputlayer", - allowMultiple=False, - defaultValue="", - ) - ) - - def processAlgorithm(self, parameters, context, model_feedback): - # Use a multi-step feedback, so that individual child algorithm progress reports are adjusted for the - # overall progress through the model - feedback = QgsProcessingMultiStepFeedback(7, model_feedback) - results = {} - outputs = {} - - # add_ID_field to input layer - alg_params = { - "FIELD_NAME": "__Unique_ID__", - "GROUP_FIELDS": [""], - "INPUT": parameters["inputlayer"], - "SORT_ASCENDING": True, - "SORT_EXPRESSION": "", - "SORT_NULLS_FIRST": False, - "START": 0, - "OUTPUT": QgsProcessing.TEMPORARY_OUTPUT, - } - outputs["Add_id_field"] = processing.run( - "native:addautoincrementalfield", - alg_params, - context=context, - feedback=feedback, - is_child_algorithm=True, - ) - - feedback.setCurrentStep(1) - if feedback.isCanceled(): - return {} - - # add_area_field to input layer - alg_params = { - "FIELD_LENGTH": 0, - "FIELD_NAME": "__Area_SPW__", - "FIELD_PRECISION": 0, - "FIELD_TYPE": 0, - "FORMULA": "area($geometry)", - "INPUT": outputs["Add_id_field"]["OUTPUT"], - "OUTPUT": QgsProcessing.TEMPORARY_OUTPUT, - } - outputs["Add_area_field"] = processing.run( - "native:fieldcalculator", - alg_params, - context=context, - feedback=feedback, - is_child_algorithm=True, - ) - - feedback.setCurrentStep(2) - if feedback.isCanceled(): - return {} - - # intersection between input and overlay layer - alg_params = { - "INPUT": outputs["Add_area_field"]["OUTPUT"], - "INPUT_FIELDS": [""], - "OVERLAY": parameters["overlaylayer"], - "OVERLAY_FIELDS": [str(parameters["fieldtoaverage"])] - + parameters["AdditionalFields"], - "OVERLAY_FIELDS_PREFIX": "", - "OUTPUT": QgsProcessing.TEMPORARY_OUTPUT, - } - outputs["Intersection"] = processing.run( - "native:intersection", - alg_params, - context=context, - feedback=feedback, - is_child_algorithm=True, - ) - - feedback.setCurrentStep(3) - if feedback.isCanceled(): - return {} - - # add_Weight - alg_params = { - "FIELD_LENGTH": 0, - "FIELD_NAME": parameters["fieldtoaverage"] + "_Area", - "FIELD_PRECISION": 0, - "FIELD_TYPE": 0, - "FORMULA": ' "' + parameters["fieldtoaverage"] + '" * area($geometry)', - "INPUT": outputs["Intersection"]["OUTPUT"], - "OUTPUT": parameters["Intersected"] - #'OUTPUT': QgsProcessing.TEMPORARY_OUTPUT - } - outputs["Add_Weight"] = processing.run( - "native:fieldcalculator", - alg_params, - context=context, - feedback=feedback, - is_child_algorithm=True, - ) - - feedback.setCurrentStep(4) - if feedback.isCanceled(): - return {} - - # area_average - alg_params = { - "FIELD_LENGTH": 0, - "FIELD_NAME": "Weighted_" + parameters["fieldtoaverage"], - "FIELD_PRECISION": 0, - "FIELD_TYPE": 0, - "FORMULA": ' sum("' + parameters["fieldtoaverage"] + "_Area" - '","__Unique_ID__")/"__Area_SPW__"', - "INPUT": outputs["Add_Weight"]["OUTPUT"], - "OUTPUT": QgsProcessing.TEMPORARY_OUTPUT, - } - outputs["area_average"] = processing.run( - "native:fieldcalculator", - alg_params, - context=context, - feedback=feedback, - is_child_algorithm=True, - ) - - feedback.setCurrentStep(5) - if feedback.isCanceled(): - return {} - - # Dissolve - alg_params = { - "FIELD": ["__Unique_ID__"], - "INPUT": outputs["area_average"]["OUTPUT"], - "OUTPUT": QgsProcessing.TEMPORARY_OUTPUT, - } - outputs["Dissolve"] = processing.run( - "native:dissolve", - alg_params, - context=context, - feedback=feedback, - is_child_algorithm=True, - ) - - feedback.setCurrentStep(6) - if feedback.isCanceled(): - return {} - - input_layer = self.parameterAsVectorLayer(parameters, "inputlayer", context) - result_name = input_layer.name() + "_" + parameters["fieldtoaverage"] - - parameters["result"].destinationName = result_name - - # in input layer for 'Drop field(s) for Report' add Area and area as % - - # Drop field(s) for Report - alg_params = { - "COLUMN": ["__Unique_ID__", "__Area_SPW__"] - + [ - parameters["fieldtoaverage"] - ] # to do: drop all fields in input layer except id field - + [parameters["fieldtoaverage"] + "_Area"], - "INPUT": outputs["Add_Weight"]["OUTPUT"], - "OUTPUT": parameters["result"], - } - outputs[result_name] = processing.run( - "qgis:deletecolumn", - alg_params, - context=context, - feedback=feedback, - is_child_algorithm=True, - ) - outputs[result_name]["OUTPUT"] = result_name - results["result"] = outputs[result_name]["OUTPUT"] - - # Drop field(s) for Result - alg_params = { - "COLUMN": ["__Unique_ID__", "__Area_SPW__"] - + [parameters["fieldtoaverage"]] - + parameters["AdditionalFields"] - + [parameters["fieldtoaverage"] + "_Area"], - "INPUT": outputs["Dissolve"]["OUTPUT"], - "OUTPUT": parameters["result"], - } - outputs[result_name] = processing.run( - "qgis:deletecolumn", - alg_params, - context=context, - feedback=feedback, - is_child_algorithm=True, - ) - outputs[result_name]["OUTPUT"] = result_name - results["result"] = outputs[result_name]["OUTPUT"] - - return results - - def name(self): - return "Weighted Average" - - def displayName(self): - return "Weighted Average" - - def group(self): - return "Tests" - - def groupId(self): - return "Tests" - - def createInstance(self): - return WeightedAverage() From 50c123a2d6225884ee213bc2f6a73743de58245f Mon Sep 17 00:00:00 2001 From: Abdul Date: Thu, 4 Mar 2021 00:52:48 -0500 Subject: [PATCH 15/15] cleanup repository --- README.html | 43 ------------------------ README.md | 52 ++++++++++++++++++++++++++++-- README.txt | 26 --------------- area_weighted_average_algorithm.py | 10 +++--- metadata.txt | 6 ++-- 5 files changed, 58 insertions(+), 79 deletions(-) delete mode 100644 README.html delete mode 100644 README.txt diff --git a/README.html b/README.html deleted file mode 100644 index 9e1cc91..0000000 --- a/README.html +++ /dev/null @@ -1,43 +0,0 @@ - - - -

Plugin Builder Results

- - Congratulations! You just built a plugin for QGIS!

- -
- Your plugin AreaWeightedAverage was created in:
-   C:/Users/abdul/Documents\area_weighted_average -

- Your QGIS plugin directory is located at:
-   C:/Users/abdul/AppData/Roaming/QGIS/QGIS3/profiles/default/python/plugins -

-

What's Next

-
    -
  1. Test the generated sources using make test (or run tests from your IDE) -
  2. Copy the entire directory containing your new plugin to the QGIS plugin directory (see Notes below) -
  3. Test the plugin by enabling it in the QGIS plugin manager and enabling the provider in the Processing - Options -
  4. Customize it by editing the implementation file area_weighted_average_algorithm.py -
- Notes: -
    -
  • You can use the Makefile to compile and deploy when you - make changes. This requires GNU make (gmake). The Makefile is ready to use, however you - will have to edit it to add addional Python source files, dialogs, and translations. -
  • You can also use pb_tool to compile and deploy your plugin. Tweak the pb_tool.cfg file - included with your plugin as you add files. Install pb_tool using - pip or easy_install. See http://loc8.cc/pb_tool for more information. -
-
-
-

- For information on writing PyQGIS code, see http://loc8.cc/pyqgis_resources for a list of resources. -

-
-

- ©2011-2018 GeoApt LLC - geoapt.com -

- - - \ No newline at end of file diff --git a/README.md b/README.md index 8bcb8b8..bb931d2 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,50 @@ -# Area Weighted Average -Area Area Weighted Average. Given an input polygon layer and overlay polygon layer, calculate weighted average of overlay parameters on input layer. +![area weighted average icon](icon.png) + +# Area-Weighted-Average +Plugin to perform Area Weighted Average analysis in QGIS. + +## Algorithm Description +This algorithm performs spatial area weighted average analysis on an input polygon layer given an attribute in the overlay polygon layer. Each feature in the input layer will be assigned a spatial area weighted average value of the overlay field. A report of the analysis is generated as a GIS Layer and as HTML. + +## Input Parameters + +- Input Layer: + Polygon layer for which area weighted average will be calculated. + +- Overlay Layer: + Polygon layer overlapping the Input Layer. + +- Field to Average: + Single numeric field in the Overlay Layer. + +- Identifier Field for Report [optional]: + Name or ID field in the Input Layer. This field will be used to identify features in the report. + +- Additional Fields to Keep for Report: + Fields in the Overlay Layer that will be included in the reports. + +## Outputs + +- Result: + Input layer but with the additional attribute of weighted value. + +- Report as Layer: + Report as a GIS layer. + +- Report as HTML [optional]: + Report containing feature-wise breakdown. + +## About + +Algorithm author: Abdul Raheem Siddiqui +Help author: Abdul Raheem Siddiqui +Algorithm version: 0.1 +Contact email: ars.work.ce@gmail.com + +## Donate + +

If this plugin is useful for you, please consider making a donation of any value to the developer.

+ + + Donate + \ No newline at end of file diff --git a/README.txt b/README.txt deleted file mode 100644 index 97207fb..0000000 --- a/README.txt +++ /dev/null @@ -1,26 +0,0 @@ -Plugin Builder Results - -Your plugin AreaWeightedAverage was created in: - C:/Users/abdul/Documents\area_weighted_average - -Your QGIS plugin directory is located at: - C:/Users/abdul/AppData/Roaming/QGIS/QGIS3/profiles/default/python/plugins - -What's Next: - - * Copy the entire directory containing your new plugin to the QGIS plugin - directory - - * Run the tests (``make test``) - - * Test the plugin by enabling it in the QGIS plugin manager - - * Customize it by editing the implementation file: ``area_weighted_average.py`` - - * You can use the Makefile to compile your Ui and resource files when - you make changes. This requires GNU make (gmake) - -For more information, see the PyQGIS Developer Cookbook at: -http://www.qgis.org/pyqgis-cookbook/index.html - -(C) 2011-2018 GeoApt LLC - geoapt.com diff --git a/area_weighted_average_algorithm.py b/area_weighted_average_algorithm.py index 4ee8e78..4c12dc9 100644 --- a/area_weighted_average_algorithm.py +++ b/area_weighted_average_algorithm.py @@ -510,9 +510,9 @@ def processAlgorithm(self, parameters, context, model_feedback): is_child_algorithm=True, ) - feedback.setCurrentStep(13) - if feedback.isCanceled(): - return {} + feedback.setCurrentStep(13) + if feedback.isCanceled(): + return {} with tempfile.TemporaryDirectory() as td: f_name = os.path.join(td, "report_df.csv") @@ -550,7 +550,7 @@ def processAlgorithm(self, parameters, context, model_feedback): else: df_sub.drop(columns=["input_feat_id", weighted_field], inplace=True) html += f"

Feature ID: {i}
{weighted_field}: {avg_value}
count of distinct intersecting features: {len(df_sub.index)}

\n" - html += f"{df_sub.to_html(bold_rows=False, index=False, justify='left')}
\n" + html += f"{df_sub.to_html(bold_rows=False, index=False, na_rep='Null',justify='left')}
\n" with codecs.open(output_file, "w", encoding="utf-8") as f: f.write("\n") @@ -667,7 +667,7 @@ def createHTML(self, outputFile, counter):
- + diff --git a/metadata.txt b/metadata.txt index d87a943..f2ccb2a 100644 --- a/metadata.txt +++ b/metadata.txt @@ -5,12 +5,12 @@ [general] name=Area Weighted Average qgisMinimumVersion=3.6 -description=Area Area Weighted Average. +description=Area Weighted Average. version=0.1 author=Abdul Raheem Siddiqui email=ars.work.ce@gmail.com -about=Given an input polygon layer and overlay polygon layer, calculate weighted average of overlay parameters on input layer and generate report. +about=This plugin performs spatial area weighted average analysis on an input polygon layer given an attribute in the overlay polygon layer. Each feature in the input layer will be assigned a spatial area weighted average value of the overlay field. A report of the analysis is generated as a GIS Layer and as HTML. tracker=https://github.com/ar-siddiqui/area_weighted_average/issues repository=https://github.com/ar-siddiqui/area_weighted_average @@ -23,7 +23,7 @@ hasProcessingProvider=yes # changelog= # Tags are comma separated with spaces allowed -tags=weighted, spatial, average, mean, analysis, report, overlap, spatial join +tags=weighted, spatial, average, mean, analysis, report, overlap, spatial join, area, mean homepage=https://github.com/ar-siddiqui/area_weighted_average category=Analysis