Skip to content

Instantly share code, notes, and snippets.

@clupprich
Created July 3, 2009 08:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save clupprich/140004 to your computer and use it in GitHub Desktop.
Save clupprich/140004 to your computer and use it in GitHub Desktop.
public class HeatMapLayer : DynamicLayer
{
BackgroundWorker renderThread; //background thread used for generating the heat map
ESRI.ArcGIS.Client.Geometry.PointCollection heatMapPoints;
private Envelope fullExtent; //cached value of the calculated full extent
private struct HeatPoint
{
public int X;
public int Y;
}
private struct ThreadSafeGradientStop
{
public double Offset;
public Color Color;
}
/// <summary>
/// Initializes a new instance of the <see cref="HeatMapLayer"/> class.
/// </summary>
public HeatMapLayer()
{
GradientStopCollection stops = new GradientStopCollection();
stops.Add(new GradientStop() { Color = Colors.Transparent, Offset = 0 });
stops.Add(new GradientStop() { Color = Colors.Blue, Offset = .2 });
stops.Add(new GradientStop() { Color = Colors.Red, Offset = .5 });
stops.Add(new GradientStop() { Color = Colors.Yellow, Offset = .8 });
stops.Add(new GradientStop() { Color = Colors.White, Offset = 1 });
Gradient = stops;
HeatMapPoints = new ESRI.ArcGIS.Client.Geometry.PointCollection();
//Create a separate thread for rendering the heatmap layer.
renderThread = new BackgroundWorker() { WorkerReportsProgress = true, WorkerSupportsCancellation = true };
renderThread.ProgressChanged += new ProgressChangedEventHandler(renderThread_ProgressChanged);
renderThread.RunWorkerCompleted += new RunWorkerCompletedEventHandler(renderThread_RunWorkerCompleted);
renderThread.DoWork += new DoWorkEventHandler(renderThread_DoWork);
}
/// <summary>
/// The full extent of the layer.
/// </summary>
public override Envelope FullExtent
{
get
{
if (fullExtent == null && heatMapPoints != null && heatMapPoints.Count > 0)
{
fullExtent = new Envelope();
foreach (MapPoint p in heatMapPoints)
{
fullExtent = fullExtent.Union(p.Extent);
}
}
return fullExtent;
}
protected set { throw new NotSupportedException(); }
}
/// <summary>
/// Identifies the <see cref="Interval"/> dependency property.
/// </summary>
public static readonly DependencyProperty IntervalProperty =
DependencyProperty.Register("Interval", typeof(int), typeof(HeatMapLayer),
new PropertyMetadata(10, OnIntervalPropertyChanged));
/// <summary>
/// Gets or sets the interval.
/// </summary>
public int Intensity
{
get { return (int)GetValue(IntervalProperty); }
set { SetValue(IntervalProperty, value); }
}
/// <summary>
/// IntervalProperty property changed handler.
/// </summary>
/// <param name="d">HeatMapLayer that changed its Interval.</param>
/// <param name="e">DependencyPropertyChangedEventArgs.</param>
private static void OnIntervalPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if ((int)e.NewValue < 1)
throw new ArgumentOutOfRangeException("Intensity");
HeatMapLayer layer = d as HeatMapLayer;
layer.OnLayerChanged();
}
public static readonly DependencyProperty BlurRadiusProperty =
DependencyProperty.Register("BlurRadius", typeof(int), typeof(HeatMapLayer),
new PropertyMetadata(2, OnBlurPropertyChanged));
public int BlurRadius
{
get { return (int)GetValue(BlurRadiusProperty); }
set { SetValue(BlurRadiusProperty, value); }
}
private static void OnBlurPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
HeatMapLayer layer = d as HeatMapLayer;
layer.OnLayerChanged();
}
/// <summary>
/// Identifies the <see cref="Resolution"/> dependency property.
/// </summary>
public static readonly DependencyProperty ResolutionProperty =
DependencyProperty.Register("Resolution", typeof(double), typeof(HeatMapLayer),
new PropertyMetadata(1.0, OnResolutionPropertyChanged));
/// <summary>
/// Gets or sets Resolution factor. Set this &lt; 1 to increase performance.
/// </summary>
public double Resolution
{
get { return (double)GetValue(ResolutionProperty); }
set { SetValue(ResolutionProperty, value); }
}
/// <summary>
/// ResolutionProperty property changed handler.
/// </summary>
/// <param name="d">HeatMapLayer that changed its Resolution.</param>
/// <param name="e">DependencyPropertyChangedEventArgs.</param>
private static void OnResolutionPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
HeatMapLayer layer = d as HeatMapLayer;
double newValue = (double)e.NewValue;
if (newValue <= 0 || newValue > 1)
throw new ArgumentOutOfRangeException("Resolution must be between 0 and 1.");
layer.OnLayerChanged();
}
/// <summary>
/// Gets or sets the heat map points.
/// </summary>
/// <value>The heat map points.</value>
public ESRI.ArcGIS.Client.Geometry.PointCollection HeatMapPoints
{
get { return heatMapPoints; }
set
{
if (heatMapPoints != null)
heatMapPoints.CollectionChanged -= heatMapPoints_CollectionChanged;
heatMapPoints = value;
if (heatMapPoints != null)
heatMapPoints.CollectionChanged += heatMapPoints_CollectionChanged;
fullExtent = null;
}
}
/// <summary>
/// Handles the CollectionChanged event of the heatMapPoints control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="System.Collections.Specialized.NotifyCollectionChangedEventArgs"/> instance containing the event data.</param>
private void heatMapPoints_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
fullExtent = null;
OnLayerChanged();
}
/// <summary>
/// Identifies the <see cref="Gradient"/> dependency property.
/// </summary>
public static readonly DependencyProperty GradientProperty =
DependencyProperty.Register("Gradient", typeof(GradientStopCollection), typeof(HeatMapLayer),
new PropertyMetadata(null, OnGradientPropertyChanged));
/// <summary>
/// Gets or sets the heat map gradient.
/// </summary>
public GradientStopCollection Gradient
{
get { return (GradientStopCollection)GetValue(GradientProperty); }
set { SetValue(GradientProperty, value); }
}
/// <summary>
/// GradientProperty property changed handler.
/// </summary>
/// <param name="d">HeatMapLayer that changed its Gradient.</param>
/// <param name="e">DependencyPropertyChangedEventArgs.</param>
private static void OnGradientPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
HeatMapLayer dp = d as HeatMapLayer;
dp.OnLayerChanged();
}
/// <summary>
/// Gets the source image to display in the dynamic layer. Override this to generate
/// or modify images.
/// </summary>
/// <param name="extent">The extent of the image being request.</param>
/// <param name="width">The width of the image being request.</param>
/// <param name="height">The height of the image being request.</param>
/// <param name="onComplete">The method to call when the image is ready.</param>
/// <seealso cref="OnProgress"/>
protected override void GetSource(Envelope extent, int width, int height, DynamicLayer.OnImageComplete onComplete)
{
if (!IsInitialized)
{
onComplete(null, -1, -1, null);
return;
}
if (renderThread != null && renderThread.IsBusy)
{
renderThread.CancelAsync(); //render already running. Cancel current process.
while (renderThread.IsBusy) //wait for thread to cancel
{
#if SILVERLIGHT
Thread.Sleep(10);
#else
System.Windows.Forms.Application.DoEvents();
#endif
}
}
//Accessing a GradientStop collection from a non-UI thread is not allowed,
//so we used a private class gradient collection
List<ThreadSafeGradientStop> stops = new List<ThreadSafeGradientStop>(Gradient.Count);
foreach (GradientStop stop in Gradient)
{
stops.Add(new ThreadSafeGradientStop() { Color = stop.Color, Offset = stop.Offset });
}
//Gradients must be sorted by offset
stops.Sort((ThreadSafeGradientStop g1, ThreadSafeGradientStop g2) => { return g1.Offset.CompareTo(g2.Offset); });
List<HeatPoint> points = new List<HeatPoint>();
double res = (extent.Width / width) / Resolution;
//adjust extent to include points slightly outside the view so pan won't affect the outcome
Envelope extent2 = new Envelope(
extent.XMin - Intensity * res,
extent.YMin - Intensity * res,
extent.XMax + Intensity * res,
extent.YMax + Intensity * res);
//get points within the extent and transform them to pixel space
foreach (MapPoint p in HeatMapPoints)
{
if (p.X >= extent2.XMin && p.Y >= extent2.YMin &&
p.X <= extent2.XMax && p.Y <= extent2.YMax)
{
points.Add(new HeatPoint()
{
X = (int)Math.Round((p.X - extent.XMin) / res),
Y = (int)Math.Round((extent.YMax - p.Y) / res)
});
}
}
//Start the render thread
renderThread.RunWorkerAsync(
new object[] { extent, width, height, this.Intensity, this.Resolution, stops, points, onComplete, BlurRadius });
}
/// <summary>
/// Stops loading of any pending images
/// </summary>
protected override void Cancel()
{
if (renderThread != null && renderThread.IsBusy)
{
renderThread.CancelAsync();
}
base.Cancel();
}
/// <summary>
/// Handles the DoWork event of the renderThread control. This is where we
/// render the heatmap outside the UI thread.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="System.ComponentModel.DoWorkEventArgs"/> instance
/// containing the event data.</param>
private void renderThread_DoWork(object sender, DoWorkEventArgs e)
{
Log.Debug("Starting to calculating Heatmap.");
DateTime start = DateTime.Now;
BackgroundWorker worker = (BackgroundWorker)sender;
object[] args = (object[])e.Argument;
Envelope extent = (Envelope)args[0];
double res = (double)args[4];
int width = (int)Math.Ceiling((int)args[1] * res);
int height = (int)Math.Ceiling((int)args[2] * res);
int size = (int)Math.Ceiling((int)args[3] * res);
int blurRadius = (int)args[8];
List<ThreadSafeGradientStop> stops = (List<ThreadSafeGradientStop>)args[5];
List<HeatPoint> points = (List<HeatPoint>)args[6];
OnImageComplete onComplete = (OnImageComplete)args[7];
size = size * 2 + 1;
ushort[] matrix = CreateDistanceMatrix(size);
int[] output = new int[width * height];
foreach (HeatPoint p in points)
{
AddPoint(matrix, size, p.X, p.Y, output, width);
if (worker.CancellationPending)
{
e.Cancel = true;
e.Result = null;
return;
}
}
matrix = null;
output = blur(output, width, height, blurRadius);
int max = 0;
foreach (int val in output) //find max - used for scaling the intensity
if (max < val) max = val;
//If we only have single points in the view, don't show them with too much intensity.
if (max < 2)
max = 2;
int[] pixels = new int[width * height];
for (int idx = 0; idx < height; idx++) // Height (y)
{
//int rowstart = ei.GetRowStart(idx);
for (int jdx = 0; jdx < width; jdx++) // Width (x)
{
double v = output[idx * width + jdx] / (float)max;
v = Math.Pow(v, 0.50);
Color c = InterpolateColor((float)v, stops);
pixels[idx * width + jdx] = c.A << 24 | (c.R << 16) | (c.G << 8) | c.B;
}
if (worker.CancellationPending)
{
e.Cancel = true;
e.Result = null;
output = null;
return;
}
//Raise the progress event for each line rendered
worker.ReportProgress((idx + 1) * 100 / height);
}
stops.Clear();
output = null;
// Get stream and set image source
e.Result = new object[] { pixels, width, height, extent, onComplete };
DateTime end = DateTime.Now;
Log.Debug("Finished heatmap calculation ({0} sec).", end - start);
}
#region Blur Functions
private int[] blur(int[] output, int width, int height, int radius)
{
int[] result = new int[width * height];
blur(output, result, width, height, radius);
blur(result, output, height, width, radius);
return output;
}
static void blur(int[] srcPixels, int[] dstPixels,
int width, int height, int radius)
{
int windowSize = radius * 2 + 1;
int radiusPlusOne = radius + 1;
int sumAlpha;
int sumRed;
int sumGreen;
int sumBlue;
int srcIndex = 0;
int dstIndex;
int pixel;
int[] sumLookupTable = new int[256 * windowSize];
for (int i = 0; i < sumLookupTable.Length; i++)
{
sumLookupTable[i] = i / windowSize;
}
int[] indexLookupTable = new int[radiusPlusOne];
if (radius < width)
{
for (int i = 0; i < indexLookupTable.Length; i++)
{
indexLookupTable[i] = i;
}
}
else
{
for (int i = 0; i < width; i++)
{
indexLookupTable[i] = i;
}
for (int i = width; i < indexLookupTable.Length; i++)
{
indexLookupTable[i] = width - 1;
}
}
for (int y = 0; y < height; y++)
{
sumAlpha = sumRed = sumGreen = sumBlue = 0;
dstIndex = y;
pixel = srcPixels[srcIndex];
sumAlpha += radiusPlusOne * ((pixel >> 24) & 0xFF);
sumRed += radiusPlusOne * ((pixel >> 16) & 0xFF);
sumGreen += radiusPlusOne * ((pixel >> 8) & 0xFF);
sumBlue += radiusPlusOne * (pixel & 0xFF);
for (int i = 1; i <= radius; i++)
{
pixel = srcPixels[srcIndex + indexLookupTable[i]];
sumAlpha += (pixel >> 24) & 0xFF;
sumRed += (pixel >> 16) & 0xFF;
sumGreen += (pixel >> 8) & 0xFF;
sumBlue += pixel & 0xFF;
}
for (int x = 0; x < width; x++)
{
dstPixels[dstIndex] = sumLookupTable[sumAlpha] << 24 |
sumLookupTable[sumRed] << 16 |
sumLookupTable[sumGreen] << 8 |
sumLookupTable[sumBlue];
dstIndex += height;
int nextPixelIndex = x + radiusPlusOne;
if (nextPixelIndex >= width)
{
nextPixelIndex = width - 1;
}
int previousPixelIndex = x - radius;
if (previousPixelIndex < 0)
{
previousPixelIndex = 0;
}
int nextPixel = srcPixels[srcIndex + nextPixelIndex];
int previousPixel = srcPixels[srcIndex + previousPixelIndex];
sumAlpha += (nextPixel >> 24) & 0xFF;
sumAlpha -= (previousPixel >> 24) & 0xFF;
sumRed += (nextPixel >> 16) & 0xFF;
sumRed -= (previousPixel >> 16) & 0xFF;
sumGreen += (nextPixel >> 8) & 0xFF;
sumGreen -= (previousPixel >> 8) & 0xFF;
sumBlue += nextPixel & 0xFF;
sumBlue -= previousPixel & 0xFF;
}
srcIndex += width;
}
}
#endregion
/// <summary>
/// Handles the RunWorkerCompleted event of the renderThread control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="System.ComponentModel.RunWorkerCompletedEventArgs"/> instance containing the event data.</param>
private void renderThread_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
Log.Debug("Starting to transform Heatmap.");
DateTime start = DateTime.Now;
if (e.Cancelled || e.Result == null) return;
object[] result = (object[])e.Result;
int[] pixels = (int[])result[0];
int width = (int)result[1];
int height = (int)result[2];
Envelope extent = (Envelope)result[3];
OnImageComplete onComplete = (OnImageComplete)result[4];
BitmapImage image = new BitmapImage();
#if SILVERLIGHT
image.SetSource(ei.GetImageStream());
#else
BitmapSource bitmapSource = BitmapSource.Create(
width,
height,
96,
96,
PixelFormats.Pbgra32,
null,
pixels,
(width * PixelFormats.Pbgra32.BitsPerPixel + 7) / 8);
PngBitmapEncoder encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(bitmapSource));
image.BeginInit();
image.StreamSource = new MemoryStream();
encoder.Save(image.StreamSource);
image.EndInit();
#endif
DateTime end = DateTime.Now;
Log.Debug("Completed heatmap transformation ({0} sec).", end - start);
onComplete(image, width, height, extent);
}
/// <summary>
/// Handles the ProgressChanged event of the renderThread control and fires the layer progress event.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="System.ComponentModel.ProgressChangedEventArgs"/> instance containing the event data.</param>
private void renderThread_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
//Raise the layer progress event
OnProgress(e.ProgressPercentage);
}
/// <summary>
/// Lienarly interpolates a color from a list of colors.
/// </summary>
/// <param name="value">The value relative to the gradient stop offsets.</param>
/// <param name="stops">The color stops sorted by the offset.</param>
/// <returns></returns>
private static Color InterpolateColor(float value, List<ThreadSafeGradientStop> stops)
{
if (value < 1 / 255f)
return Colors.Transparent;
if (stops == null || stops.Count == 0)
return Colors.Black;
if (stops.Count == 1)
return stops[0].Color;
if (stops[0].Offset >= value) //clip to bottom
return stops[0].Color;
else if (stops[stops.Count - 1].Offset <= value) //clip to top
return stops[stops.Count - 1].Color;
int i = 0;
for (i = 1; i < stops.Count; i++)
{
if (stops[i].Offset > value)
{
Color start = stops[i - 1].Color;
Color end = stops[i].Color;
double frac = (value - stops[i - 1].Offset) / (stops[i].Offset - stops[i - 1].Offset);
byte R = (byte)Math.Round((start.R * (1 - frac) + end.R * frac));
byte G = (byte)Math.Round((start.G * (1 - frac) + end.G * frac));
byte B = (byte)Math.Round((start.B * (1 - frac) + end.B * frac));
byte A = (byte)Math.Round((start.A * (1 - frac) + end.A * frac));
return Color.FromArgb(A, R, G, B);
}
}
return stops[stops.Count - 1].Color; //should never happen
}
/// <summary>
/// Adds a heat map point to the intensity matrix.
/// </summary>
/// <param name="distanceMatrix">The distance matrix.</param>
/// <param name="size">The size of the distance matrix.</param>
/// <param name="x">x.</param>
/// <param name="y">y</param>
/// <param name="intensityMap">The intensity map.</param>
/// <param name="width">The width of the intensity map..</param>
private static void AddPoint(ushort[] distanceMatrix, int size, int x, int y, int[] intensityMap, int width)
{
for (int i = 0; i < size * 2 - 1; i++)
{
int start = (y - size + 1 + i) * width + x - size;
for (int j = 0; j < size * 2 - 1; j++)
{
if (j + x - size < 0 || j + x - size >= width) continue;
int idx = start + j;
if (idx < 0 || idx >= intensityMap.Length)
continue;
intensityMap[idx] += distanceMatrix[i * (size * 2 - 1) + j];
}
}
}
/// <summary>
/// Creates the distance matrix.
/// </summary>
/// <param name="size">The size of the matrix (must be and odd number).</param>
/// <returns></returns>
private static ushort[] CreateDistanceMatrix(int size)
{
int width = size * 2 - 1;
ushort[] matrix = new ushort[(int)Math.Pow(width, 2)];
for (int i = 0; i < width; i++)
{
for (int j = 0; j < width; j++)
{
matrix[i * width + j] = (ushort)Math.Max((size - (Math.Sqrt(Math.Pow(i - size + 1, 2) + Math.Pow(j - size + 1, 2)))), 0);
}
}
return matrix;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment