Skip to content

Commit

Permalink
Add heatmap layer.
Browse files Browse the repository at this point in the history
  • Loading branch information
francois2metz committed Nov 28, 2021
1 parent 5775928 commit 83a9e53
Show file tree
Hide file tree
Showing 9 changed files with 109 additions and 53 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ const indoorequal = new IndoorEqual(map, { apiKey: 'myKey', spriteBaseUrl: '/ind
* [Parameters](#parameters-1)
* [setStyle](#setstyle)
* [Parameters](#parameters-2)
* [setHeatmapVisible](#setheatmapvisible)
* [Parameters](#parameters-3)
* [IndoorEqual#change:levels](#indoorequalchangelevels)
* [IndoorEqual#levelchange](#indoorequallevelchange)

Expand Down Expand Up @@ -150,6 +152,7 @@ Load the indoor= source and layers in your map.
* `options.spriteBaseUrl` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)?** The base url of the sprite (without .json or .png). If not set, no sprite will be used in the default style.
* `options.url` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)?** Override the default tiles URL (<https://tiles.indoorequal.org/>).
* `options.apiKey` **[string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)?** The API key if you use the default tile URL (get your free key at [indoorequal.com](https://indoorequal.com)).
* `options.heatmap` **[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)?** Should the heatmap layer be visible at start (true : visible, false : hidden). Defaults to true/visible.

Returns **[IndoorEqual](#indoorequal)** `this`

Expand All @@ -161,6 +164,14 @@ Set the style for displayed features. This function takes a feature and resoluti

* `styleFunction` **[function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** the style function

#### setHeatmapVisible

Change the heatmap layer visibility

##### Parameters

* `visible` **[boolean](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean)** True to make it visible, false to hide it

### IndoorEqual#change:levels

Emitted when the list of available levels has been updated
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"test": "jest"
},
"jest": {
"setupFiles": ["./test/setup.js"],
"testEnvironment": "jsdom",
"transformIgnorePatterns": [
"node_modules/(?!(ol)/)"
Expand Down
3 changes: 3 additions & 0 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,12 @@ export default [
'ol/Object',
'ol/control',
'ol/format/MVT',
'ol/layer/Heatmap',
'ol/layer/VectorTile',
'ol/loadingstrategy',
'ol/proj',
'ol/source/TileJSON',
'ol/source/Vector',
'ol/source/VectorTile',
'ol/style',
'ol/tilegrid/TileGrid',
Expand Down
2 changes: 1 addition & 1 deletion src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { getLayer, loadSourceFromTileJSON } from './layer';
export { getLayer, getHeatmapLayer, loadSourceFromTileJSON } from './layer';
export LevelControl from './level_control';
export defaultStyle from './defaultstyle';
export default from './indoorequal';
61 changes: 37 additions & 24 deletions src/indoorequal.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import BaseObject from 'ol/Object';
import debounce from 'debounce';

import { loadSourceFromTileJSON, getLayer } from './layer';
import { loadSourceFromTileJSON, getLayer, getHeatmapLayer, createHeatmapSource } from './layer';
import findAllLevels from './levels';
import defaultStyle from './defaultstyle';

Expand All @@ -13,13 +13,14 @@ import defaultStyle from './defaultstyle';
* @param {string} [options.spriteBaseUrl] The base url of the sprite (without .json or .png). If not set, no sprite will be used in the default style.
* @param {string} [options.url] Override the default tiles URL (https://tiles.indoorequal.org/).
* @param {string} [options.apiKey] The API key if you use the default tile URL (get your free key at [indoorequal.com](https://indoorequal.com)).
* @param {boolean} [options.heatmap] Should the heatmap layer be visible at start (true : visible, false : hidden). Defaults to true/visible.
* @fires change:levels
* @fires change:level
* @return {IndoorEqual} `this`
*/
export default class IndoorEqual extends BaseObject {
constructor(map, options = {}) {
const defaultOpts = { url: 'https://tiles.indoorequal.org/', defaultStyle: true, spriteBaseUrl: null };
const defaultOpts = { url: 'https://tiles.indoorequal.org/', defaultStyle: true, spriteBaseUrl: null, heatmap: true };
const opts = { ...defaultOpts, ...options };
if (opts.url === defaultOpts.url && !opts.apiKey) {
throw 'You must register your apiKey at https://indoorequal.com before and set it as apiKey param.';
Expand All @@ -30,8 +31,9 @@ export default class IndoorEqual extends BaseObject {
this.url = opts.url;
this.apiKey = opts.apiKey;

this._addLayer();
this.styleFunction = opts.defaultStyle ? defaultStyle(this.map, this.layer, opts.spriteBaseUrl) : null;
this._createLayers(opts.heatmap);
this._loadSource();
this.styleFunction = opts.defaultStyle ? defaultStyle(this.map, this.indoorLayer, opts.spriteBaseUrl) : null;
this._changeLayerOnLevelChange();
this._setLayerStyle();
this._resetLevelOnLevelsChange();
Expand All @@ -45,41 +47,52 @@ export default class IndoorEqual extends BaseObject {
this.styleFunction = styleFunction;
}

_addLayer() {
/**
* Change the heatmap layer visibility
* @param {boolean} visible True to make it visible, false to hide it
*/
setHeatmapVisible(visible) {
this.heatmapLayer.setVisible(visible);
}

async _loadSource() {
const urlParams = this.apiKey ? `?key=${this.apiKey}` : '';
this.layer = getLayer();
this.map.addLayer(this.layer);
loadSourceFromTileJSON(`${this.url}${urlParams}`).then((source) => {
this.source = source;
this.layer.setSource(source);
this.layer.setVisible(true);
});
this.source = await loadSourceFromTileJSON(`${this.url}${urlParams}`);

this.indoorLayer.setSource(this.source);
this.heatmapLayer.setSource(createHeatmapSource(this.source));
this._listenForLevels();
}

_createLayers(heatmapVisible) {
this.indoorLayer = getLayer();
this.heatmapLayer = getHeatmapLayer({ visible: heatmapVisible });
[this.indoorLayer, this.heatmapLayer].forEach((layer) => {
this.map.addLayer(layer);
});
}

_listenForLevels() {
this.layer.on('change:source', () => {
const source = this.layer.getSource();
const source = this.source;

const refreshLevels = debounce(() => {
const extent = this.map.getView().calculateExtent(this.map.getSize());
const features = source.getFeaturesInExtent(extent);
this.set('levels', findAllLevels(features));
}, 1000);
const refreshLevels = debounce(() => {
const extent = this.map.getView().calculateExtent(this.map.getSize());
const features = source.getFeaturesInExtent(extent);
this.set('levels', findAllLevels(features));
}, 1000);

source.on('tileloadend', refreshLevels);
this.map.getView().on('change:center', refreshLevels);
});
source.on('tileloadend', refreshLevels);
this.map.getView().on('change:center', refreshLevels);
}

_changeLayerOnLevelChange() {
this.on('change:level', () => {
this.layer.changed();
this.indoorLayer.changed();
});
}

_setLayerStyle() {
this.layer.setStyle((feature, resolution) => {
this.indoorLayer.setStyle((feature, resolution) => {
if (feature.getProperties().level === this.get('level')) {
return this.styleFunction && this.styleFunction(feature, resolution);
}
Expand Down
44 changes: 38 additions & 6 deletions src/layer.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import Feature from 'ol/Feature';
import HeatmapLayer from 'ol/layer/Heatmap';
import MVT from 'ol/format/MVT';
import TileGrid from 'ol/tilegrid/TileGrid';
import TileJSON from 'ol/source/TileJSON';
import VectorSource from 'ol/source/Vector';
import VectorTileLayer from 'ol/layer/VectorTile';
import VectorTileSource from 'ol/source/VectorTile';
import TileJSON from 'ol/source/TileJSON';
import MVT from 'ol/format/MVT';
import { fromLonLat } from 'ol/proj';
import TileGrid from 'ol/tilegrid/TileGrid';
import Feature from 'ol/Feature';
import { tile } from 'ol/loadingstrategy';

const MIN_ZOOM_INDOOR = 17;
const MAX_ZOOM_HEATMAP = MIN_ZOOM_INDOOR;

function extentFromTileJSON(tileJSON) {
const bounds = tileJSON.bounds;
Expand Down Expand Up @@ -73,8 +77,36 @@ export async function loadSourceFromTileJSON(url) {
export function getLayer(options) {
return new VectorTileLayer({
declutter: true,
visible: false,
minZoom: MIN_ZOOM_INDOOR,
...options,
});
}

export function createHeatmapSource(source) {
const tilegrid = source.getTileGrid();
const vectorSource = new VectorSource({
loader(extent, resolution, projection, success, failure) {
const refresh = () => {
const features = source.getFeaturesInExtent(extent);
vectorSource.clear(true);
vectorSource.addFeatures(features);
success(features);
}
source.on('tileloadend', refresh);
refresh();
},
loadingstrategy: tile(tilegrid)
});
return vectorSource;
}

export function getHeatmapLayer(options) {
return new HeatmapLayer({
maxZoom: MAX_ZOOM_HEATMAP,
gradient: [
'rgba(102, 103, 173, 0)',
'rgba(102, 103, 173, 0.2)',
'rgba(102, 103, 173, 0.7)'
],
...options,
});
}
37 changes: 16 additions & 21 deletions test/indoorequal.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
jest.mock('../src/layer');

import Feature from 'ol/Feature';
import IndoorEqual, { getLayer, loadSourceFromTileJSON } from '../src/';
import IndoorEqual, { getLayer, getHeatmapLayer, loadSourceFromTileJSON } from '../src/';

describe('IndoorEqual', () => {
it('get the indoorequal layer with the default url', () => {
Expand All @@ -10,8 +10,7 @@ describe('IndoorEqual', () => {
getLayer.mockReturnValueOnce(getLayerReturn);
loadSourceFromTileJSON.mockReturnValueOnce(new Promise(() => {}));
new IndoorEqual(map, { apiKey: 'test' });
expect(loadSourceFromTileJSON
).toHaveBeenCalledWith('https://tiles.indoorequal.org/?key=test');
expect(loadSourceFromTileJSON).toHaveBeenCalledWith('https://tiles.indoorequal.org/?key=test');
});

it('throws an error if the apiKey is not defined with the default tiles url', () => {
Expand All @@ -29,18 +28,16 @@ describe('IndoorEqual', () => {
expect(loadSourceFromTileJSON).toHaveBeenCalledWith('http://localhost:8090/');
});

it('load and add the the indoorequal layer', () => {
it('load and add the the indoorequal layer and the heatmap layer', () => {
const map = { addLayer: jest.fn() };
const getLayerReturn = { on: jest.fn(), setStyle: jest.fn() };
getLayer.mockReturnValueOnce(getLayerReturn);
loadSourceFromTileJSON.mockReturnValueOnce(new Promise(() => {}));
new IndoorEqual(map, { apiKey: 'test' });
expect(map.addLayer.mock.calls.length).toEqual(1);
expect(map.addLayer.mock.calls.length).toEqual(2);
});

it('expose the available levels', (done) => {
let tileLoadEndCallback;

const map = {
addLayer: jest.fn(),
getView: () => {
Expand All @@ -51,33 +48,31 @@ describe('IndoorEqual', () => {
},
getSize: jest.fn(),
};
const source = {
on: (_eventName, callback) => callback(),
getFeaturesInExtent: () => {
return [
new Feature({ layer: 'area', level: 0 }),
new Feature({ layer: 'area', level: 1 }),
new Feature({ layer: 'area', level: -2 }),
];
}
}
const getLayerReturn = {
on: (_eventName, callback) => callback(),
setStyle: jest.fn(),
setSource: jest.fn(),
setVisible: jest.fn(),
getSource: () => {
return {
on: (_eventName, callback) => tileLoadEndCallback = callback,
getFeaturesInExtent: () => {
return [
new Feature({ layer: 'area', level: 0 }),
new Feature({ layer: 'area', level: 1 }),
new Feature({ layer: 'area', level: -2 }),
];
}
};
}
};
getLayer.mockReturnValueOnce(getLayerReturn);
loadSourceFromTileJSON.mockReturnValueOnce(Promise.resolve());
getHeatmapLayer.mockReturnValueOnce(getLayerReturn);
loadSourceFromTileJSON.mockReturnValueOnce(Promise.resolve(source));
const indoorEqual = new IndoorEqual(map, { apiKey: 'test' });
expect(indoorEqual.get('levels')).toEqual([]);
indoorEqual.on('change:levels', (levels) => {
expect(indoorEqual.get('levels')).toEqual([1, 0, -2]);
done();
});
tileLoadEndCallback();
});

it('reset the level if the current one is not available', () => {
Expand Down
2 changes: 1 addition & 1 deletion test/level_control.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LevelControl } from '../src';
import LevelControl from '../src/level_control';

describe('LevelControl', () => {
it('create a container', () => {
Expand Down
1 change: 1 addition & 0 deletions test/setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
window.URL.createObjectURL = () => {}

0 comments on commit 83a9e53

Please sign in to comment.