Navigation Menu

Skip to content

Commit

Permalink
Merge pull request #3782 from nyalldawson/heatmap
Browse files Browse the repository at this point in the history
Port heatmap plugin code to analysis, add processing alg
  • Loading branch information
nyalldawson committed Dec 7, 2016
2 parents c558d51 + 034cc65 commit e1a588e
Show file tree
Hide file tree
Showing 26 changed files with 1,239 additions and 2,111 deletions.
1 change: 0 additions & 1 deletion debian/qgis.install
Expand Up @@ -8,7 +8,6 @@ usr/lib/qgis/plugins/librasterterrainplugin.so
usr/lib/qgis/plugins/libspatialqueryplugin.so
usr/lib/qgis/plugins/libofflineeditingplugin.so
usr/lib/qgis/plugins/libroadgraphplugin.so
usr/lib/qgis/plugins/libheatmapplugin.so
usr/lib/qgis/plugins/libtopolplugin.so
usr/lib/qgis/plugins/libgeometrycheckerplugin.so
usr/lib/qgis/qgis_help
Expand Down
File renamed without changes
1 change: 0 additions & 1 deletion ms-windows/osgeo4w/package.cmd
Expand Up @@ -385,7 +385,6 @@ tar -C %OSGEO4W_ROOT% -cjf %ARCH%/release/qgis/%PACKAGENAME%/%PACKAGENAME%-%VERS
"apps/%PACKAGENAME%/plugins/evis.dll" ^
"apps/%PACKAGENAME%/plugins/georefplugin.dll" ^
"apps/%PACKAGENAME%/plugins/gpsimporterplugin.dll" ^
"apps/%PACKAGENAME%/plugins/heatmapplugin.dll" ^
"apps/%PACKAGENAME%/plugins/interpolationplugin.dll" ^
"apps/%PACKAGENAME%/plugins/offlineeditingplugin.dll" ^
"apps/%PACKAGENAME%/plugins/oracleplugin.dll" ^
Expand Down
1 change: 0 additions & 1 deletion ms-windows/plugins.nsh
Expand Up @@ -14,7 +14,6 @@ WriteRegStr HKEY_CURRENT_USER "Software\QGIS\QGIS3\Plugins" "georefplugin" "true
WriteRegStr HKEY_CURRENT_USER "Software\QGIS\QGIS3\Plugins" "globeplugin" "false"
WriteRegStr HKEY_CURRENT_USER "Software\QGIS\QGIS3\Plugins" "gpsimporterplugin" "true"
WriteRegStr HKEY_CURRENT_USER "Software\QGIS\QGIS3\Plugins" "grassplugin" "true"
WriteRegStr HKEY_CURRENT_USER "Software\QGIS\QGIS3\Plugins" "heatmapplugin" "true"
WriteRegStr HKEY_CURRENT_USER "Software\QGIS\QGIS3\Plugins" "interpolationplugin" "true"
WriteRegStr HKEY_CURRENT_USER "Software\QGIS\QGIS3\Plugins" "offlineeditingplugin" "true"
WriteRegStr HKEY_CURRENT_USER "Software\QGIS\QGIS3\Plugins" "oracleplugin" "true"
Expand Down
1 change: 1 addition & 0 deletions python/analysis/analysis.sip
Expand Up @@ -44,6 +44,7 @@
%Include raster/qgsderivativefilter.sip
%Include raster/qgsaspectfilter.sip
%Include raster/qgshillshadefilter.sip
%Include raster/qgskde.sip
%Include raster/qgsninecellfilter.sip
%Include raster/qgsrastercalcnode.sip
%Include raster/qgsrastercalculator.sip
Expand Down
104 changes: 104 additions & 0 deletions python/analysis/raster/qgskde.sip
@@ -0,0 +1,104 @@
/**
* \class QgsKernelDensityEstimation
* \ingroup analysis
* Performs Kernel Density Estimation ("heatmap") calculations on a vector layer.
* @note added in QGIS 3.0
*/
class QgsKernelDensityEstimation
{
%TypeHeaderCode
#include <qgskde.h>
%End

public:

//! Kernel shape type
enum KernelShape
{
KernelQuartic, //!< Quartic kernel
KernelTriangular, //!< Triangular kernel
KernelUniform, //!< Uniform (flat) kernel
KernelTriweight, //!< Triweight kernel
KernelEpanechnikov, //!< Epanechnikov kernel
};

//! Output values type
enum OutputValues
{
OutputRaw, //!< Output the raw KDE values
OutputScaled, //!< Output mathematically correct scaled values
};

//! Result of operation
enum Result
{
Success, //!< Operation completed successfully
DriverError, //!< Could not open the driver for the specified format
InvalidParameters, //!< Input parameters were not valid
FileCreationError, //!< Error creating output file
RasterIoError, //!< Error writing to raster
};

//! KDE parameters
struct Parameters
{
//! Vector point layer
QgsVectorLayer* vectorLayer;

//! Fixed radius, in map units
double radius;

//! Field for radius, or empty if using a fixed radius
QString radiusField;

//! Field name for weighting field, or empty if not using weights
QString weightField;

//! Size of pixel in output file
double pixelSize;

//! Kernel shape
QgsKernelDensityEstimation::KernelShape shape;

//! Decay ratio (Triangular kernels only)
double decayRatio;

//! Type of output value
QgsKernelDensityEstimation::OutputValues outputValues;
};

/**
* Constructor for QgsKernelDensityEstimation. Requires a Parameters object specifying the options to use
* to generate the surface. The output path and file format are also required.
*/
QgsKernelDensityEstimation( const Parameters& parameters, const QString& outputFile, const QString& outputFormat );

/**
* Runs the KDE calculation across the whole layer at once. Either call this method, or manually
* call run(), addFeature() and finalise() separately.
*/
Result run();

/**
* Prepares the output file for writing and setups up the surface calculation. This must be called
* before adding features via addFeature().
* @see addFeature()
* @see finalise()
*/
Result prepare();

/**
* Adds a single feature to the KDE surface. prepare() must be called before adding features.
* @see prepare()
* @see finalise()
*/
Result addFeature( const QgsFeature& feature );

/**
* Finalises the output file. Must be called after adding all features via addFeature().
* @see prepare()
* @see addFeature()
*/
Result finalise();

};
166 changes: 166 additions & 0 deletions python/plugins/processing/algs/qgis/Heatmap.py
@@ -0,0 +1,166 @@
# -*- 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

from qgis.PyQt.QtGui import QIcon

from qgis.core import QgsFeatureRequest
from qgis.analysis import QgsKernelDensityEstimation

from processing.core.GeoAlgorithm import GeoAlgorithm
from processing.core.GeoAlgorithmExecutionException import GeoAlgorithmExecutionException
from processing.core.parameters import ParameterVector
from processing.core.parameters import ParameterNumber
from processing.core.parameters import ParameterSelection
from processing.core.parameters import ParameterTableField
from processing.core.outputs import OutputRaster
from processing.tools import dataobjects, vector, raster
from processing.algs.qgis.ui.HeatmapWidgets import HeatmapPixelSizeWidgetWrapper

pluginPath = os.path.split(os.path.split(os.path.dirname(__file__))[0])[0]


class Heatmap(GeoAlgorithm):

INPUT_LAYER = 'INPUT_LAYER'
RADIUS = 'RADIUS'
RADIUS_FIELD = 'RADIUS_FIELD'
WEIGHT_FIELD = 'WEIGHT_FIELD'
PIXEL_SIZE = 'PIXEL_SIZE'

KERNELS = ['Quartic',
'Triangular',
'Uniform',
'Triweight',
'Epanechnikov'
]
KERNEL = 'KERNEL'
DECAY = 'DECAY'
OUTPUT_VALUES = ['Raw',
'Scaled'
]
OUTPUT_VALUE = 'OUTPUT_VALUE'
OUTPUT_LAYER = 'OUTPUT_LAYER'

def getIcon(self):
return QIcon(os.path.join(pluginPath, 'images', 'heatmap.png'))

def defineCharacteristics(self):
self.name, self.i18n_name = self.trAlgorithm('Heatmap (Kernel Density Estimation)')
self.group, self.i18n_group = self.trAlgorithm('Interpolation')
self.tags = self.tr('heatmap,kde,hotspot')

self.addParameter(ParameterVector(self.INPUT_LAYER,
self.tr('Point layer'), [dataobjects.TYPE_VECTOR_POINT]))
self.addParameter(ParameterNumber(self.RADIUS,
self.tr('Radius (layer units)'),
0.0, 9999999999, 100.0))

self.addParameter(ParameterTableField(self.RADIUS_FIELD,
self.tr('Radius from field'), self.INPUT_LAYER, optional=True, datatype=ParameterTableField.DATA_TYPE_NUMBER))

class ParameterHeatmapPixelSize(ParameterNumber):

def __init__(self, name='', description='', parent_layer=None, radius_param=None, radius_field_param=None, minValue=None, maxValue=None,
default=None, optional=False, metadata={}):
ParameterNumber.__init__(self, name, description, minValue, maxValue, default, optional, metadata)
self.parent_layer = parent_layer
self.radius_param = radius_param
self.radius_field_param = radius_field_param

self.addParameter(ParameterHeatmapPixelSize(self.PIXEL_SIZE,
self.tr('Output raster size'), parent_layer=self.INPUT_LAYER, radius_param=self.RADIUS,
radius_field_param=self.RADIUS_FIELD,
minValue=0.0, maxValue=9999999999, default=0.1,
metadata={'widget_wrapper': HeatmapPixelSizeWidgetWrapper}))

self.addParameter(ParameterTableField(self.WEIGHT_FIELD,
self.tr('Weight from field'), self.INPUT_LAYER, optional=True, datatype=ParameterTableField.DATA_TYPE_NUMBER))
self.addParameter(ParameterSelection(self.KERNEL,
self.tr('Kernel shape'), self.KERNELS))
self.addParameter(ParameterNumber(self.DECAY,
self.tr('Decay ratio (Triangular kernels only)'),
-100.0, 100.0, 0.0))
self.addParameter(ParameterSelection(self.OUTPUT_VALUE,
self.tr('Output value scaling'), self.OUTPUT_VALUES))
self.addOutput(OutputRaster(self.OUTPUT_LAYER,
self.tr('Heatmap')))

def processAlgorithm(self, progress):
layer = dataobjects.getObjectFromUri(
self.getParameterValue(self.INPUT_LAYER))

radius = self.getParameterValue(self.RADIUS)
kernel_shape = self.getParameterValue(self.KERNEL)
pixel_size = self.getParameterValue(self.PIXEL_SIZE)
decay = self.getParameterValue(self.DECAY)
output_values = self.getParameterValue(self.OUTPUT_VALUE)
output = self.getOutputValue(self.OUTPUT_LAYER)
output_format = raster.formatShortNameFromFileName(output)
weight_field = self.getParameterValue(self.WEIGHT_FIELD)
radius_field = self.getParameterValue(self.RADIUS_FIELD)

attrs = []

kde_params = QgsKernelDensityEstimation.Parameters()
kde_params.vectorLayer = layer
kde_params.radius = radius
kde_params.pixelSize = pixel_size
# radius field
if radius_field:
kde_params.radiusField = radius_field
attrs.append(layer.fields().lookupField(radius_field))
# weight field
if weight_field:
kde_params.weightField = weight_field
attrs.append(layer.fields().lookupField(weight_field))

kde_params.shape = kernel_shape
kde_params.decayRatio = decay
kde_params.outputValues = output_values

kde = QgsKernelDensityEstimation(kde_params, output, output_format)

if kde.prepare() != QgsKernelDensityEstimation.Success:
raise GeoAlgorithmExecutionException(
self.tr('Could not create destination layer'))

request = QgsFeatureRequest()
request.setSubsetOfAttributes(attrs)
features = vector.features(layer, request)
total = 100.0 / len(features)
for current, f in enumerate(features):
if kde.addFeature(f) != QgsKernelDensityEstimation.Success:
raise GeoAlgorithmExecutionException(
self.tr('Error adding feature to heatmap'))

progress.setPercentage(int(current * total))

if kde.finalise() != QgsKernelDensityEstimation.Success:
raise GeoAlgorithmExecutionException(
self.tr('Could not save destination layer'))
Expand Up @@ -36,6 +36,7 @@
)

from processing.core.GeoAlgorithm import GeoAlgorithm
from processing.core.GeoAlgorithmExecutionException import GeoAlgorithmExecutionException
from processing.core.parameters import ParameterVector
from processing.core.parameters import ParameterSelection
from processing.core.parameters import ParameterNumber
Expand Down
3 changes: 2 additions & 1 deletion python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py
Expand Up @@ -182,6 +182,7 @@
from .CreateAttributeIndex import CreateAttributeIndex
from .DropGeometry import DropGeometry
from .BasicStatistics import BasicStatisticsForField
from .Heatmap import Heatmap

pluginPath = os.path.normpath(os.path.join(
os.path.split(os.path.dirname(__file__))[0], os.pardir))
Expand Down Expand Up @@ -246,7 +247,7 @@ def __init__(self):
RemoveNullGeometry(), ExtractByExpression(), ExtendLines(),
ExtractSpecificNodes(), GeometryByExpression(), SnapGeometriesToLayer(),
PoleOfInaccessibility(), CreateAttributeIndex(), DropGeometry(),
BasicStatisticsForField(), RasterCalculator()
BasicStatisticsForField(), RasterCalculator(), Heatmap()
]

if hasMatplotlib:
Expand Down

0 comments on commit e1a588e

Please sign in to comment.