2016-11-17 19:48:46 +10:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
|
|
"""
|
|
|
|
***************************************************************************
|
|
|
|
Heatmap.py
|
|
|
|
---------------------
|
|
|
|
Date : November 2016
|
|
|
|
Copyright : (C) 2016 by Nyall Dawson
|
|
|
|
Email : nyall dot dawson at gmail dot 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__ = 'Nyall Dawson'
|
|
|
|
__date__ = 'November 2016'
|
|
|
|
__copyright__ = '(C) 2016, Nyall Dawson'
|
|
|
|
|
|
|
|
# This will get replaced with a git SHA1 when you do a git archive
|
|
|
|
|
|
|
|
__revision__ = '$Format:%H$'
|
|
|
|
|
|
|
|
import os
|
2017-07-12 10:22:45 +03:00
|
|
|
from collections import OrderedDict
|
2016-11-17 19:48:46 +10:00
|
|
|
|
|
|
|
from qgis.PyQt.QtGui import QIcon
|
|
|
|
|
2017-04-24 14:35:50 +10:00
|
|
|
from qgis.core import (QgsFeatureRequest,
|
2017-08-01 14:49:31 +03:00
|
|
|
QgsRasterFileWriter,
|
2017-07-12 10:22:45 +03:00
|
|
|
QgsProcessing,
|
|
|
|
QgsProcessingException,
|
2017-07-13 20:11:28 +10:00
|
|
|
QgsProcessingParameterFeatureSource,
|
2017-07-12 10:22:45 +03:00
|
|
|
QgsProcessingParameterNumber,
|
|
|
|
QgsProcessingParameterField,
|
|
|
|
QgsProcessingParameterEnum,
|
2017-07-13 12:10:59 +03:00
|
|
|
QgsProcessingParameterDefinition,
|
|
|
|
QgsProcessingParameterRasterDestination)
|
2017-07-12 10:22:45 +03:00
|
|
|
|
2016-11-17 19:48:46 +10:00
|
|
|
from qgis.analysis import QgsKernelDensityEstimation
|
|
|
|
|
2017-06-06 13:41:42 +10:00
|
|
|
from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm
|
2016-11-17 19:48:46 +10:00
|
|
|
|
|
|
|
pluginPath = os.path.split(os.path.split(os.path.dirname(__file__))[0])[0]
|
|
|
|
|
|
|
|
|
2017-05-19 11:27:16 +10:00
|
|
|
class Heatmap(QgisAlgorithm):
|
2016-11-17 19:48:46 +10:00
|
|
|
|
2017-07-14 17:42:31 +03:00
|
|
|
INPUT = 'INPUT'
|
2016-11-17 19:48:46 +10:00
|
|
|
RADIUS = 'RADIUS'
|
|
|
|
RADIUS_FIELD = 'RADIUS_FIELD'
|
|
|
|
WEIGHT_FIELD = 'WEIGHT_FIELD'
|
|
|
|
PIXEL_SIZE = 'PIXEL_SIZE'
|
|
|
|
KERNEL = 'KERNEL'
|
|
|
|
DECAY = 'DECAY'
|
|
|
|
OUTPUT_VALUE = 'OUTPUT_VALUE'
|
2017-07-14 17:42:31 +03:00
|
|
|
OUTPUT = 'OUTPUT'
|
2016-11-17 19:48:46 +10:00
|
|
|
|
2017-03-29 10:42:42 +10:00
|
|
|
def icon(self):
|
2016-11-17 19:48:46 +10:00
|
|
|
return QIcon(os.path.join(pluginPath, 'images', 'heatmap.png'))
|
|
|
|
|
2017-03-29 09:51:13 +10:00
|
|
|
def tags(self):
|
|
|
|
return self.tr('heatmap,kde,hotspot').split(',')
|
|
|
|
|
2017-03-29 12:04:09 +10:00
|
|
|
def group(self):
|
|
|
|
return self.tr('Interpolation')
|
|
|
|
|
2017-07-12 10:22:45 +03:00
|
|
|
def name(self):
|
|
|
|
return 'heatmapkerneldensityestimation'
|
|
|
|
|
|
|
|
def displayName(self):
|
|
|
|
return self.tr('Heatmap (Kernel Density Estimation)')
|
|
|
|
|
2017-05-15 13:40:38 +10:00
|
|
|
def __init__(self):
|
|
|
|
super().__init__()
|
2017-07-10 16:31:14 +10:00
|
|
|
|
|
|
|
def initAlgorithm(self, config=None):
|
2017-07-12 10:22:45 +03:00
|
|
|
self.KERNELS = OrderedDict([(self.tr('Quartic'), QgsKernelDensityEstimation.KernelQuartic),
|
|
|
|
(self.tr('Triangular'), QgsKernelDensityEstimation.KernelTriangular),
|
|
|
|
(self.tr('Uniform'), QgsKernelDensityEstimation.KernelUniform),
|
|
|
|
(self.tr('Triweight'), QgsKernelDensityEstimation.KernelTriweight),
|
|
|
|
(self.tr('Epanechnikov'), QgsKernelDensityEstimation.KernelEpanechnikov)])
|
|
|
|
|
|
|
|
self.OUTPUT_VALUES = OrderedDict([(self.tr('Raw'), QgsKernelDensityEstimation.OutputRaw),
|
|
|
|
(self.tr('Scaled'), QgsKernelDensityEstimation.OutputScaled)])
|
|
|
|
|
2017-07-14 17:42:31 +03:00
|
|
|
self.addParameter(QgsProcessingParameterFeatureSource(self.INPUT,
|
2017-07-13 20:11:28 +10:00
|
|
|
self.tr('Point layer'),
|
|
|
|
[QgsProcessing.TypeVectorPoint]))
|
2017-07-12 10:22:45 +03:00
|
|
|
|
|
|
|
self.addParameter(QgsProcessingParameterNumber(self.RADIUS,
|
|
|
|
self.tr('Radius (layer units)'),
|
|
|
|
QgsProcessingParameterNumber.Double,
|
|
|
|
100.0, False, 0.0, 9999999999.99))
|
|
|
|
|
|
|
|
radius_field_param = QgsProcessingParameterField(self.RADIUS_FIELD,
|
|
|
|
self.tr('Radius from field'),
|
|
|
|
None,
|
2017-07-14 17:42:31 +03:00
|
|
|
self.INPUT,
|
2017-07-12 10:22:45 +03:00
|
|
|
QgsProcessingParameterField.Numeric,
|
|
|
|
optional=True
|
|
|
|
)
|
2017-05-16 16:36:00 +10:00
|
|
|
radius_field_param.setFlags(radius_field_param.flags() | QgsProcessingParameterDefinition.FlagAdvanced)
|
2016-12-07 13:50:12 +10:00
|
|
|
self.addParameter(radius_field_param)
|
2016-12-07 11:59:35 +10:00
|
|
|
|
2017-07-12 10:22:45 +03:00
|
|
|
class ParameterHeatmapPixelSize(QgsProcessingParameterNumber):
|
2016-12-07 11:59:35 +10:00
|
|
|
|
|
|
|
def __init__(self, name='', description='', parent_layer=None, radius_param=None, radius_field_param=None, minValue=None, maxValue=None,
|
2017-07-12 10:22:45 +03:00
|
|
|
default=None, optional=False):
|
|
|
|
QgsProcessingParameterNumber.__init__(self, name, description, QgsProcessingParameterNumber.Double, default, optional, minValue, maxValue)
|
2016-12-07 11:59:35 +10:00
|
|
|
self.parent_layer = parent_layer
|
|
|
|
self.radius_param = radius_param
|
|
|
|
self.radius_field_param = radius_field_param
|
|
|
|
|
2017-08-18 00:37:58 +10:00
|
|
|
def clone(self):
|
|
|
|
copy = ParameterHeatmapPixelSize(self.name(), self.description(), self.parent_layer, self.radius_param, self.radius_field_param, self.minimum(), self.maximum(), self.defaultValue((), self.flags() & QgsProcessingParameterDefinition.FlagOptional))
|
|
|
|
return copy
|
|
|
|
|
2017-07-12 10:22:45 +03:00
|
|
|
pixel_size_param = ParameterHeatmapPixelSize(self.PIXEL_SIZE,
|
|
|
|
self.tr('Output raster size'),
|
2017-07-14 17:42:31 +03:00
|
|
|
parent_layer=self.INPUT,
|
2017-07-12 10:22:45 +03:00
|
|
|
radius_param=self.RADIUS,
|
|
|
|
radius_field_param=self.RADIUS_FIELD,
|
|
|
|
minValue=0.0,
|
|
|
|
maxValue=9999999999,
|
|
|
|
default=0.1)
|
|
|
|
pixel_size_param.setMetadata({
|
|
|
|
'widget_wrapper': {
|
|
|
|
'class': 'processing.algs.qgis.ui.HeatmapWidgets.HeatmapPixelSizeWidgetWrapper'}})
|
|
|
|
self.addParameter(pixel_size_param)
|
|
|
|
|
|
|
|
weight_field_param = QgsProcessingParameterField(self.WEIGHT_FIELD,
|
|
|
|
self.tr('Weight from field'),
|
|
|
|
None,
|
2017-07-14 17:42:31 +03:00
|
|
|
self.INPUT,
|
2017-07-12 10:22:45 +03:00
|
|
|
QgsProcessingParameterField.Numeric,
|
|
|
|
optional=True
|
|
|
|
)
|
2017-05-16 16:36:00 +10:00
|
|
|
weight_field_param.setFlags(weight_field_param.flags() | QgsProcessingParameterDefinition.FlagAdvanced)
|
2016-12-07 13:50:12 +10:00
|
|
|
self.addParameter(weight_field_param)
|
2017-07-12 10:22:45 +03:00
|
|
|
|
|
|
|
keys = list(self.KERNELS.keys())
|
|
|
|
kernel_shape_param = QgsProcessingParameterEnum(self.KERNEL,
|
|
|
|
self.tr('Kernel shape'),
|
|
|
|
keys,
|
|
|
|
allowMultiple=False,
|
|
|
|
defaultValue=0)
|
2017-05-16 16:36:00 +10:00
|
|
|
kernel_shape_param.setFlags(kernel_shape_param.flags() | QgsProcessingParameterDefinition.FlagAdvanced)
|
2016-12-07 13:50:12 +10:00
|
|
|
self.addParameter(kernel_shape_param)
|
2017-07-12 10:22:45 +03:00
|
|
|
|
|
|
|
decay_ratio = QgsProcessingParameterNumber(self.DECAY,
|
|
|
|
self.tr('Decay ratio (Triangular kernels only)'),
|
|
|
|
QgsProcessingParameterNumber.Double,
|
|
|
|
0.0, True, -100.0, 100.0)
|
2017-05-16 16:36:00 +10:00
|
|
|
decay_ratio.setFlags(decay_ratio.flags() | QgsProcessingParameterDefinition.FlagAdvanced)
|
2016-12-07 13:50:12 +10:00
|
|
|
self.addParameter(decay_ratio)
|
2017-07-12 10:22:45 +03:00
|
|
|
|
|
|
|
keys = list(self.OUTPUT_VALUES.keys())
|
|
|
|
output_scaling = QgsProcessingParameterEnum(self.OUTPUT_VALUE,
|
|
|
|
self.tr('Output value scaling'),
|
|
|
|
keys,
|
|
|
|
allowMultiple=False,
|
|
|
|
defaultValue=0)
|
2017-05-16 16:36:00 +10:00
|
|
|
output_scaling.setFlags(output_scaling.flags() | QgsProcessingParameterDefinition.FlagAdvanced)
|
2016-12-07 13:50:12 +10:00
|
|
|
self.addParameter(output_scaling)
|
2017-05-15 13:40:38 +10:00
|
|
|
|
2017-07-14 17:42:31 +03:00
|
|
|
self.addParameter(QgsProcessingParameterRasterDestination(self.OUTPUT, self.tr('Heatmap')))
|
2017-07-13 12:10:59 +03:00
|
|
|
|
2017-05-15 16:19:46 +10:00
|
|
|
def processAlgorithm(self, parameters, context, feedback):
|
2017-07-14 17:42:31 +03:00
|
|
|
source = self.parameterAsSource(parameters, self.INPUT, context)
|
2017-07-12 10:22:45 +03:00
|
|
|
|
|
|
|
radius = self.parameterAsDouble(parameters, self.RADIUS, context)
|
|
|
|
kernel_shape = self.parameterAsEnum(parameters, self.KERNEL, context)
|
|
|
|
pixel_size = self.parameterAsDouble(parameters, self.PIXEL_SIZE, context)
|
|
|
|
decay = self.parameterAsDouble(parameters, self.DECAY, context)
|
|
|
|
output_values = self.parameterAsEnum(parameters, self.OUTPUT_VALUE, context)
|
2017-07-14 17:42:31 +03:00
|
|
|
outputFile = self.parameterAsOutputLayer(parameters, self.OUTPUT, context)
|
2017-08-01 14:49:31 +03:00
|
|
|
output_format = QgsRasterFileWriter.driverForExtension(os.path.splitext(outputFile)[1])
|
2017-07-12 10:22:45 +03:00
|
|
|
weight_field = self.parameterAsString(parameters, self.WEIGHT_FIELD, context)
|
|
|
|
radius_field = self.parameterAsString(parameters, self.RADIUS_FIELD, context)
|
2016-11-17 19:48:46 +10:00
|
|
|
|
|
|
|
attrs = []
|
|
|
|
|
|
|
|
kde_params = QgsKernelDensityEstimation.Parameters()
|
2017-07-13 20:11:28 +10:00
|
|
|
kde_params.source = source
|
2016-11-17 19:48:46 +10:00
|
|
|
kde_params.radius = radius
|
|
|
|
kde_params.pixelSize = pixel_size
|
|
|
|
# radius field
|
|
|
|
if radius_field:
|
|
|
|
kde_params.radiusField = radius_field
|
2017-07-13 20:11:28 +10:00
|
|
|
attrs.append(source.fields().lookupField(radius_field))
|
2016-11-17 19:48:46 +10:00
|
|
|
# weight field
|
|
|
|
if weight_field:
|
|
|
|
kde_params.weightField = weight_field
|
2017-07-13 20:11:28 +10:00
|
|
|
attrs.append(source.fields().lookupField(weight_field))
|
2016-11-17 19:48:46 +10:00
|
|
|
|
|
|
|
kde_params.shape = kernel_shape
|
|
|
|
kde_params.decayRatio = decay
|
|
|
|
kde_params.outputValues = output_values
|
|
|
|
|
2017-07-13 12:10:59 +03:00
|
|
|
kde = QgsKernelDensityEstimation(kde_params, outputFile, output_format)
|
2016-11-17 19:48:46 +10:00
|
|
|
|
|
|
|
if kde.prepare() != QgsKernelDensityEstimation.Success:
|
2017-07-12 10:22:45 +03:00
|
|
|
raise QgsProcessingException(
|
2016-11-17 19:48:46 +10:00
|
|
|
self.tr('Could not create destination layer'))
|
|
|
|
|
|
|
|
request = QgsFeatureRequest()
|
|
|
|
request.setSubsetOfAttributes(attrs)
|
2017-07-13 20:11:28 +10:00
|
|
|
features = source.getFeatures(request)
|
|
|
|
total = 100.0 / source.featureCount() if source.featureCount() else 0
|
2016-11-17 19:48:46 +10:00
|
|
|
for current, f in enumerate(features):
|
2017-07-12 10:22:45 +03:00
|
|
|
if feedback.isCanceled():
|
|
|
|
break
|
|
|
|
|
2016-11-17 19:48:46 +10:00
|
|
|
if kde.addFeature(f) != QgsKernelDensityEstimation.Success:
|
2017-07-13 20:11:37 +10:00
|
|
|
feedback.reportError(self.tr('Error adding feature with ID {} to heatmap').format(f.id()))
|
2016-11-17 19:48:46 +10:00
|
|
|
|
2017-01-06 20:04:00 +10:00
|
|
|
feedback.setProgress(int(current * total))
|
2016-11-17 19:48:46 +10:00
|
|
|
|
|
|
|
if kde.finalise() != QgsKernelDensityEstimation.Success:
|
2017-07-12 10:22:45 +03:00
|
|
|
raise QgsProcessingException(
|
2016-11-17 19:48:46 +10:00
|
|
|
self.tr('Could not save destination layer'))
|
2017-07-12 10:22:45 +03:00
|
|
|
|
2017-07-14 17:42:31 +03:00
|
|
|
return {self.OUTPUT: outputFile}
|