Skip to content

Commit

Permalink
feat: draw temperature values on heatmaps
Browse files Browse the repository at this point in the history
The feature can be disabled with a new config option.
  • Loading branch information
mKeRix committed May 31, 2020
1 parent ac52e76 commit d2ad9bf
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 32 deletions.
11 changes: 6 additions & 5 deletions docs/integrations/grid-eye.md
Expand Up @@ -74,11 +74,12 @@ When placing your sensor you need to consider a few factors to get reliable resu

### Heatmap

| Name | Type | Default | Description |
| ---------------- | ------ | ------- | ------------------------------------------------------------ |
| `minTemperature` | Number | `16` | Temperature that will be considered the lower bound for the color scale in °C. |
| `maxTemperature` | Number | `30` | Temperature that will be considered the upper bound for the color scale in °C. |
| `rotation` | Number | `0` | The amount of degrees that the heatmap output image should be rotated. Only `0`, `90`, `180` or `270` are supported as values. |
| Name | Type | Default | Description |
| ------------------ | ------- | ------- | ------------------------------------------------------------ |
| `minTemperature` | Number | `16` | Temperature that will be considered the lower bound for the color scale in °C. |
| `maxTemperature` | Number | `30` | Temperature that will be considered the upper bound for the color scale in °C. |
| `rotation` | Number | `0` | The amount of degrees that the heatmap output image should be rotated. Only `0`, `90`, `180` or `270` are supported as values. |
| `drawTemperatures` | Boolean | `true` | Whether the actual temperature values should be drawn on the heatmap or not. |

::: details Example Config

Expand Down
11 changes: 6 additions & 5 deletions docs/integrations/omron-d6t.md
Expand Up @@ -87,11 +87,12 @@ When placing your sensor you need to consider a few factors to get reliable resu

### Heatmap

| Name | Type | Default | Description |
| ---------------- | ------ | ------- | ------------------------------------------------------------ |
| `minTemperature` | Number | `16` | Temperature that will be considered the lower bound for the color scale in °C. |
| `maxTemperature` | Number | `30` | Temperature that will be considered the upper bound for the color scale in °C. |
| `rotation` | Number | `0` | The amount of degrees that the heatmap output image should be rotated. Only `0`, `90`, `180` or `270` are supported as values. |
| Name | Type | Default | Description |
| ------------------ | ------- | ------- | ------------------------------------------------------------ |
| `minTemperature` | Number | `16` | Temperature that will be considered the lower bound for the color scale in °C. |
| `maxTemperature` | Number | `30` | Temperature that will be considered the upper bound for the color scale in °C. |
| `rotation` | Number | `0` | The amount of degrees that the heatmap output image should be rotated. Only `0`, `90`, `180` or `270` are supported as values. |
| `drawTemperatures` | Boolean | `true` | Whether the actual temperature values should be drawn on the heatmap or not. |

::: details Example Config

Expand Down
1 change: 1 addition & 0 deletions src/integrations/thermopile/thermopile-occupancy.config.ts
Expand Up @@ -4,4 +4,5 @@ export class HeatmapOptions {
minTemperature = 16;
maxTemperature = 30;
rotation: RotationOption = 0;
drawTemperatures = true;
}
41 changes: 28 additions & 13 deletions src/integrations/thermopile/thermopile-occupancy.service.spec.ts
@@ -1,8 +1,14 @@
import { HeatmapOptions } from './thermopile-occupancy.config';

const mockContext = {
rotate: jest.fn(),
translate: jest.fn(),
fillRect: jest.fn(),
fillText: jest.fn(),
save: jest.fn(),
restore: jest.fn(),
fillStyle: '',
fontStyle: '',
};
const mockCanvas = {
getContext: jest.fn().mockReturnValue(mockContext),
Expand Down Expand Up @@ -104,14 +110,32 @@ describe('ThermopileOccupancyService', () => {
expect(mockCanvasModule.createCanvas).toHaveBeenCalledWith(250, 250);
});

it('should create rectangles for each pixel', async () => {
await service.generateHeatmap(PRESENCE_TEMPERATURES);
it('should create rectangles with fonts for each pixel', async () => {
await service.generateHeatmap(PRESENCE_TEMPERATURES, undefined, 150, 150);

expect(mockContext.translate).toHaveBeenCalledWith(75, 75);
expect(mockContext.fillRect).toHaveBeenCalledTimes(16);
expect(mockContext.fillRect).toHaveBeenCalledWith(-75, -75, 38, 38);
expect(mockContext.fillRect).toHaveBeenCalledWith(39, 1, 38, 38);
expect(mockContext.fillStyle).toEqual('hsl(111.42857142857143, 100%, 50%)');
expect(mockContext.fillText).toHaveBeenCalledTimes(16);
expect(mockContext.fillText).toHaveBeenCalledWith(
PRESENCE_TEMPERATURES[0][0].toFixed(1),
0,
0
);
});

it('should not draw the temperature text if option is disabled', async () => {
const heatmapOptions = new HeatmapOptions();
heatmapOptions.drawTemperatures = false;
await service.generateHeatmap(
PRESENCE_TEMPERATURES,
heatmapOptions,
150,
150
);

expect(mockContext.fillText).not.toHaveBeenCalled();
});

it('should output a jpeg image buffer', async () => {
Expand All @@ -130,6 +154,7 @@ describe('ThermopileOccupancyService', () => {
rotation: 90,
minTemperature: 16,
maxTemperature: 30,
drawTemperatures: true,
},
100,
200
Expand All @@ -138,14 +163,4 @@ describe('ThermopileOccupancyService', () => {
expect(mockCanvasModule.createCanvas).toHaveBeenCalledWith(200, 100);
expect(mockContext.rotate).toHaveBeenCalledWith(1.5707963267948966);
});

it('should adapt the pixel coloring based on min and max temperatures', async () => {
await service.generateHeatmap(PRESENCE_TEMPERATURES, {
rotation: 0,
minTemperature: 10,
maxTemperature: 40,
});

expect(mockContext.fillStyle).toEqual('hsl(132, 100%, 50%)');
});
});
86 changes: 77 additions & 9 deletions src/integrations/thermopile/thermopile-occupancy.service.ts
Expand Up @@ -109,8 +109,8 @@ export abstract class ThermopileOccupancyService {
async generateHeatmap(
temperatures: number[][],
options = new HeatmapOptions(),
width = 150,
height = 150
width = 280,
height = 280
): Promise<Buffer> {
if (!this.isHeatmapAvailable()) {
throw new Error(
Expand All @@ -125,7 +125,8 @@ export abstract class ThermopileOccupancyService {
: nodeCanvas.createCanvas(width, height);
const ctx = canvas.getContext('2d');
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.rotate((options.rotation * Math.PI) / 180);
const rotationRad = (options.rotation * Math.PI) / 180;
ctx.rotate(rotationRad);

const normed = math.divide(
math.subtract(temperatures, options.minTemperature),
Expand All @@ -134,19 +135,86 @@ export abstract class ThermopileOccupancyService {

for (const [y, row] of normed.entries()) {
for (const [x, value] of row.entries()) {
const h = (1 - _.clamp(value, 0, 1)) * 240;
ctx.fillStyle = `hsl(${h}, 100%, 50%)`;
ctx.fillRect(
-canvas.width / 2 + x * segmentWidth,
-canvas.height / 2 + y * segmentHeight,
const pixelXOrigin = -canvas.width / 2 + x * segmentWidth;
const pixelXCenter = pixelXOrigin + segmentWidth / 2;
const pixelYOrigin = -canvas.height / 2 + y * segmentHeight;
const pixelYCenter = pixelYOrigin + segmentHeight / 2;

ThermopileOccupancyService.drawPixel(
ctx,
pixelXOrigin,
pixelYOrigin,
segmentWidth,
segmentHeight
segmentHeight,
value
);
if (options.drawTemperatures) {
ThermopileOccupancyService.drawTemperature(
ctx,
pixelXCenter,
pixelYCenter,
segmentWidth,
rotationRad,
temperatures[x][y]
);
}
}
}

return canvas.toBuffer('image/jpeg', {
quality: 1,
});
}

/**
* Draws a rectangle representing the pixel temperature with a color.
*
* @param ctx - 2d context of the canvas to draw on
* @param xOrigin - x coordinate of the pixel top-left origin
* @param yOrigin - y coordinate of the pixel top-left origin
* @param width - width of the pixel
* @param height - height of the pixel
* @param normedValue - value representing the temperature within a normed range (between 0 and 1)
*/
private static drawPixel(
ctx: CanvasRenderingContext2D,
xOrigin: number,
yOrigin: number,
width: number,
height: number,
normedValue: number
): void {
const h = (1 - _.clamp(normedValue, 0, 1)) * 240;
ctx.fillStyle = `hsl(${h}, 100%, 50%)`;
ctx.fillRect(xOrigin, yOrigin, width, height);
}

/**
* Draws a text representing the temperature onto a pixel.
*
* @param ctx - 2d context of the canvas to draw on
* @param xCenter - x coordinate of the pixel center
* @param yCenter - y coordinate of the pixel center
* @param width - width of the pixel
* @param rotationRad - rotation that is used for the context in rad
* @param temperature - temperature value to draw
*/
private static drawTemperature(
ctx: CanvasRenderingContext2D,
xCenter: number,
yCenter: number,
width: number,
rotationRad: number,
temperature: number
): void {
ctx.save();
ctx.font = `${Math.round(width / 2.7)}px sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = 'black';
ctx.translate(xCenter, yCenter);
ctx.rotate(-rotationRad);
ctx.fillText(temperature.toFixed(1), 0, 0);
ctx.restore();
}
}

0 comments on commit d2ad9bf

Please sign in to comment.