Skip to content

Commit 6676cea

Browse files
committed
Implement RFC108 heatmap support (#4857)
also adds: - rfc86 scaletoken substitutions for PROCESSING entries - proj fastpaths for 3857->4326 reprojections
1 parent 1a32ba5 commit 6676cea

15 files changed

+2535
-1983
lines changed

CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ mapogcfiltercommon.c maprendering.c mapwcs20.c mapogcsld.c
245245
mapresample.c mapwfs.c mapgdal.c mapogcsos.c mapscale.c mapwfs11.c mapwfs20.c
246246
mapgeomtransform.c mapogroutput.c mapsde.c mapwfslayer.c mapagg.cpp mapkml.cpp
247247
mapgeomutil.cpp mapkmlrenderer.cpp fontcache.c textlayout.c maputfgrid.cpp
248-
mapogr.cpp mapcontour.c mapsmoothing.c mapv8.cpp ${REGEX_SOURCES})
248+
mapogr.cpp mapcontour.c mapsmoothing.c mapv8.cpp ${REGEX_SOURCES} kerneldensity.c)
249249

250250
if(BUILD_DYNAMIC)
251251
add_library(mapserver SHARED ${mapserver_SOURCES} ${agg_SOURCES} ${v8_SOURCES})

HISTORY.TXT

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ For a complete change history, please see the Git log comments.
1515
7.0 release (TBD)
1616
-----------------
1717

18+
- Apply RFC86 scaletoken substitutions to layer->PROCESSING entries
19+
20+
- RFC108 Heatmap / Kernel-Density Layers
21+
1822
- RFC104 Bitmap Label removal, replaced with inlined truetype font
1923

2024
- RFC103 Layer Level Character Encoding

kerneldensity.c

Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
/******************************************************************************
2+
*
3+
* Project: MapServer
4+
* Purpose: KernelDensity layer implementation and related functions.
5+
* Author: Thomas Bonfort and the MapServer team.
6+
*
7+
******************************************************************************
8+
* Copyright (c) 2014 Regents of the University of Minnesota.
9+
*
10+
* Permission is hereby granted, free of charge, to any person obtaining a
11+
* copy of this software and associated documentation files (the "Software"),
12+
* to deal in the Software without restriction, including without limitation
13+
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
14+
* and/or sell copies of the Software, and to permit persons to whom the
15+
* Software is furnished to do so, subject to the following conditions:
16+
*
17+
* The above copyright notice and this permission notice shall be included in
18+
* all copies of this Software or works derived from this Software.
19+
*
20+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
21+
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
23+
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
25+
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
26+
* DEALINGS IN THE SOFTWARE.
27+
*****************************************************************************/
28+
29+
#include "mapserver.h"
30+
#include <float.h>
31+
#ifdef USE_GDAL
32+
33+
#include "gdal.h"
34+
35+
36+
void gaussian_blur(float *values, int width, int height, int radius) {
37+
float *tmp = (float*)msSmallMalloc(width*height*sizeof(float));
38+
int length = radius*2+1;
39+
float *kernel = (float*)msSmallMalloc(length*sizeof(float));
40+
float sigma=radius/3.0;
41+
float a=1.0/ sqrt(2.0*M_PI*sigma*sigma);
42+
float den=2.0*sigma*sigma;
43+
int i,x,y;
44+
45+
for (i=0; i<length; i++) {
46+
float x=i - radius;
47+
float v=a * exp(-(x*x) / den);
48+
kernel[i]=v;
49+
}
50+
memset(tmp,0,width*height*sizeof(float));
51+
52+
for(y=0; y<height; y++) {
53+
float* src_row=values + width*y;
54+
float* dst_row=tmp + width*y;
55+
56+
for(x=radius; x<width-radius; x++) {
57+
float accum=0;
58+
for(i=0; i<length; i++) {
59+
accum+=src_row[x+i-radius] * kernel[i];
60+
}
61+
dst_row[x]=accum;
62+
}
63+
}
64+
65+
for(x=0; x<width; x++) {
66+
float* src_col=tmp+x;
67+
float* dst_col=values+x;
68+
69+
for(y=radius; y<height-radius; y++) {
70+
float accum=0;
71+
for (i=0; i<length; i++) {
72+
accum+=src_col[width*(y+i-radius)] * kernel[i];
73+
}
74+
dst_col[y*width]=accum;
75+
}
76+
}
77+
free(tmp);
78+
free(kernel);
79+
}
80+
81+
82+
int msComputeKernelDensityDataset(mapObj *map, imageObj *image, layerObj *kerneldensity_layer, void **hDSvoid, void **cleanup_ptr) {
83+
84+
int status,layer_idx, i,j, nclasses=0, have_sample=0;
85+
rectObj searchrect;
86+
shapeObj shape;
87+
layerObj *layer;
88+
float *values;
89+
int radius = 10, im_width = image->width, im_height = image->height;
90+
int expand_searchrect=1;
91+
float normalization_scale=0.0;
92+
double invcellsize = 1.0 / map->cellsize, georadius=0;
93+
float valmax=FLT_MIN, valmin=FLT_MAX;
94+
unsigned char *iValues;
95+
GDALDatasetH hDS;
96+
const char *pszProcessing;
97+
int *classgroup = NULL;
98+
99+
assert(kerneldensity_layer->connectiontype == MS_KERNELDENSITY);
100+
*cleanup_ptr = NULL;
101+
102+
if(!kerneldensity_layer->connection || !*kerneldensity_layer->connection) {
103+
msSetError(MS_MISCERR, "msComputeKernelDensityDataset()", "KernelDensity layer has no CONNECTION defined");
104+
return MS_FAILURE;
105+
}
106+
pszProcessing = msLayerGetProcessingKey( kerneldensity_layer, "KERNELDENSITY_RADIUS" );
107+
if(pszProcessing)
108+
radius = atoi(pszProcessing);
109+
else
110+
radius = 10;
111+
112+
pszProcessing = msLayerGetProcessingKey( kerneldensity_layer, "KERNELDENSITY_COMPUTE_BORDERS" );
113+
if(pszProcessing && strcasecmp(pszProcessing,"OFF"))
114+
expand_searchrect = 1;
115+
else
116+
expand_searchrect = 0;
117+
118+
pszProcessing = msLayerGetProcessingKey( kerneldensity_layer, "KERNELDENSITY_NORMALIZATION" );
119+
if(!pszProcessing || !strcasecmp(pszProcessing,"AUTO"))
120+
normalization_scale = 0.0;
121+
else {
122+
normalization_scale = atof(pszProcessing);
123+
if(normalization_scale != 0) {
124+
normalization_scale = 1.0 / normalization_scale;
125+
} else {
126+
normalization_scale = 1.0;
127+
}
128+
}
129+
130+
layer_idx = msGetLayerIndex(map,kerneldensity_layer->connection);
131+
if(layer_idx == -1) {
132+
int nLayers, *aLayers;
133+
aLayers = msGetLayersIndexByGroup(map, kerneldensity_layer->connection, &nLayers);
134+
if(!aLayers || !nLayers) {
135+
msSetError(MS_MISCERR, "KernelDensity layer (%s) references unknown layer (%s)", "msComputeKernelDensityDataset()",
136+
kerneldensity_layer->name,kerneldensity_layer->connection);
137+
return (MS_FAILURE);
138+
}
139+
for(i=0; i<nLayers; i++) {
140+
layer_idx = aLayers[i];
141+
layer = GET_LAYER(map, layer_idx);
142+
if(msScaleInBounds(map->scaledenom, layer->minscaledenom, layer->maxscaledenom))
143+
break;
144+
}
145+
free(aLayers);
146+
if(i == nLayers) {
147+
msSetError(MS_MISCERR, "KernelDensity layer (%s) references no layer for current scale", "msComputeKernelDensityDataset()",
148+
kerneldensity_layer->name);
149+
return (MS_FAILURE);
150+
}
151+
} else {
152+
layer = GET_LAYER(map, layer_idx);
153+
}
154+
/* open the linked layer */
155+
status = msLayerOpen(layer);
156+
if(status != MS_SUCCESS) return MS_FAILURE;
157+
158+
status = msLayerWhichItems(layer, MS_FALSE, NULL);
159+
if(status != MS_SUCCESS) {
160+
msLayerClose(layer);
161+
return MS_FAILURE;
162+
}
163+
164+
/* identify target shapes */
165+
if(layer->transform == MS_TRUE) {
166+
searchrect = map->extent;
167+
if(expand_searchrect) {
168+
georadius = radius * map->cellsize;
169+
searchrect.minx -= georadius;
170+
searchrect.miny -= georadius;
171+
searchrect.maxx += georadius;
172+
searchrect.maxy += georadius;
173+
im_width += 2 * radius;
174+
im_height += 2 * radius;
175+
}
176+
}
177+
else {
178+
searchrect.minx = searchrect.miny = 0;
179+
searchrect.maxx = map->width-1;
180+
searchrect.maxy = map->height-1;
181+
}
182+
183+
#ifdef USE_PROJ
184+
layer->project = msProjectionsDiffer(&(layer->projection), &(map->projection));
185+
if(layer->project)
186+
msProjectRect(&map->projection, &layer->projection, &searchrect); /* project the searchrect to source coords */
187+
#endif
188+
189+
status = msLayerWhichShapes(layer, searchrect, MS_FALSE);
190+
if(status == MS_DONE) { /* no overlap */
191+
msLayerClose(layer);
192+
return MS_SUCCESS;
193+
} else if(status != MS_SUCCESS) {
194+
msLayerClose(layer);
195+
return MS_FAILURE;
196+
}
197+
values = (float*) msSmallCalloc(im_width * im_height, sizeof(float));
198+
199+
if(layer->classgroup && layer->numclasses > 0)
200+
classgroup = msAllocateValidClassGroups(layer, &nclasses);
201+
202+
msInitShape(&shape);
203+
while((status = msLayerNextShape(layer, &shape)) == MS_SUCCESS) {
204+
int l,p,s,c;
205+
double weight = 1.0;
206+
#ifdef USE_PROJ
207+
if(layer->project)
208+
msProjectShape(&layer->projection, &map->projection, &shape);
209+
#endif
210+
211+
/* the weight for the sample is set to 1.0 by default. If the
212+
* layer has some classes defined, we will read the weight from
213+
* the class->style->size (which can be binded to an attribute)
214+
*/
215+
if(layer->numclasses > 0) {
216+
c = msShapeGetClass(layer, map, &shape, classgroup, nclasses);
217+
if((c == -1) || (layer->class[c]->status == MS_OFF)) {
218+
goto nextshape; /* no class matched, skip */
219+
}
220+
for (s = 0; s < layer->class[c]->numstyles; s++) {
221+
if (msScaleInBounds(map->scaledenom,
222+
layer->class[c]->styles[s]->minscaledenom,
223+
layer->class[c]->styles[s]->maxscaledenom)) {
224+
if(layer->class[c]->styles[s]->bindings[MS_STYLE_BINDING_SIZE].index != -1) {
225+
weight = atof(shape.values[layer->class[c]->styles[s]->bindings[MS_STYLE_BINDING_SIZE].index]);
226+
} else {
227+
weight = layer->class[c]->styles[s]->size;
228+
}
229+
break;
230+
}
231+
}
232+
if(s == layer->class[c]->numstyles) {
233+
/* no style in scale bounds */
234+
goto nextshape;
235+
}
236+
}
237+
for(l=0; l<shape.numlines; l++) {
238+
for(p=0; p<shape.line[l].numpoints; p++) {
239+
int x = MS_MAP2IMAGE_XCELL_IC(shape.line[l].point[p].x, map->extent.minx - georadius, invcellsize);
240+
int y = MS_MAP2IMAGE_YCELL_IC(shape.line[l].point[p].y, map->extent.maxy + georadius, invcellsize);
241+
if(x>=0 && y>=0 && x<im_width && y<im_height) {
242+
float *value = values + y * im_width + x;
243+
(*value) += weight;
244+
have_sample = 1;
245+
}
246+
}
247+
}
248+
249+
nextshape:
250+
msFreeShape(&shape);
251+
}
252+
msLayerClose(layer);
253+
if(status == MS_DONE) {
254+
status = MS_SUCCESS;
255+
} else {
256+
status = MS_FAILURE;
257+
}
258+
259+
if(have_sample) { /* no use applying the filtering kernel if we have no samples */
260+
gaussian_blur(values,im_width, im_height, radius);
261+
262+
if(normalization_scale == 0.0) { /* auto normalization */
263+
for (j=radius; j<im_height-radius; j++) {
264+
for (i=radius; i<im_width-radius; i++) {
265+
float val = values[j*im_width + i];
266+
if(val >0 && val>valmax) {
267+
valmax = val;
268+
}
269+
if(val>0 && val<valmin) {
270+
valmin = val;
271+
}
272+
}
273+
}
274+
} else {
275+
valmin = 0;
276+
valmax = normalization_scale;
277+
}
278+
}
279+
280+
281+
if(have_sample && expand_searchrect) {
282+
iValues = msSmallMalloc(image->width*image->height*sizeof(unsigned char));
283+
for (j=0; j<image->height; j++) {
284+
for (i=0; i<image->width; i++) {
285+
float norm=(values[(j+radius)*im_width + i + radius] - valmin) / valmax;
286+
int v=255 * norm;
287+
if (v<0) v=0;
288+
else if (v>255) v=255;
289+
iValues[j*image->width + i] = v;
290+
}
291+
}
292+
} else {
293+
iValues = msSmallCalloc(1,image->width*image->height*sizeof(unsigned char));
294+
if(have_sample) {
295+
for (j=radius; j<image->height-radius; j++) {
296+
for (i=radius; i<image->width-radius; i++) {
297+
float norm=(values[j*im_width + i] - valmin) / valmax;
298+
int v=255 * norm;
299+
if (v<0) v=0;
300+
else if (v>255) v=255;
301+
iValues[j*image->width + i]=v;
302+
}
303+
}
304+
}
305+
}
306+
307+
free(values);
308+
309+
{
310+
char ds_string [1024];
311+
double adfGeoTransform[6];
312+
snprintf(ds_string,1024,"MEM:::DATAPOINTER=%p,PIXELS=%u,LINES=%u,BANDS=1,DATATYPE=Byte,PIXELOFFSET=1,LINEOFFSET=%u",
313+
iValues,image->width,image->height,image->width);
314+
hDS = GDALOpenShared( ds_string, GA_ReadOnly );
315+
if(hDS==NULL) {
316+
msSetError(MS_MISCERR,"msComputeKernelDensityDataset()","failed to create in-memory gdal dataset for interpolated data");
317+
status = MS_FAILURE;
318+
free(iValues);
319+
}
320+
adfGeoTransform[0] = map->extent.minx - map->cellsize * 0.5; /* top left x */
321+
adfGeoTransform[1] = map->cellsize;/* w-e pixel resolution */
322+
adfGeoTransform[2] = 0; /* 0 */
323+
adfGeoTransform[3] = map->extent.maxy + map->cellsize * 0.5;/* top left y */
324+
adfGeoTransform[4] =0; /* 0 */
325+
adfGeoTransform[5] = -map->cellsize;/* n-s pixel resolution (negative value) */
326+
GDALSetGeoTransform(hDS,adfGeoTransform);
327+
*hDSvoid = hDS;
328+
*cleanup_ptr = (void*)iValues;
329+
}
330+
return status;
331+
}
332+
#else
333+
334+
335+
int msComputeKernelDensityDataset(mapObj *map, imageObj *image, layerObj *layer, void **hDSvoid, void **cleanup_ptr) {
336+
msSetError(MS_MISCERR,"msComputeKernelDensityDataset()", "KernelDensity layers require GDAL support, however GDAL support is not compiled in this build");
337+
return MS_FAILURE;
338+
}
339+
340+
#endif
341+
342+
int msCleanupKernelDensityDataset(mapObj *map, imageObj *image, layerObj *layer, void *cleanup_ptr) {
343+
free(cleanup_ptr);
344+
return MS_SUCCESS;
345+
}

0 commit comments

Comments
 (0)