Table of Contents |
---|
Intro
By default, Cesium cannot generate heatmaps, so that third-party solutions must be used to implement this functionality in the viewer.
There is a library called heatmap.js that calculates a represents heatmaps from different input types, such as mouse position on the screen, where you click or even coordinates in the browser's own canvas.
...
From this library, and from the idea of representing points in the canvas, a different library called CesiumHeatmap.js was developed. This one allows you to generate heatmaps in the viewer itself, from point coordinates and a value representing a point's weight.
At the end of the tutorial, using an ontology of points, you can generate a heatmap like this one:
Iframe | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
Info |
---|
Eh! This is no photo, but an interactive map. Try to browse through it. |
Library installation
The first step will be download and uncompressing the library, or cloning the repository (https://github.com/manuelnas/CesiumHeatmap.git) in the project folder containing the viewer.
...
The downloaded/cloned file has the following files:
CesiumHeatmap.js
: the library to aggregate heatmaps usingheatmap.js
(included) and the functionsCesium.Entity.Rectangle
orCesium.SingleTileImageryProvider
, the later will be used in this tutorial.HeatmapImageryProvider.js
: another library to aggregate heatmaps, also usingheatmap.js
(also including it) through customization of theCesium.ImageryProvider
function, which will not be studied for the moment.LICENSE
: a generic document with the libraries's copyright.package.json
: file with the repository's address.README.md
: file with the libraries0 description, along with an example of use ofCesiumHeatmap.js
.
To use the library in the Cesium viewer, you must insert the CesiumHeatmap.js
library after the Cesium.js
library call, as shown next:
...
...
Insert CesiumHeatmap.js library
Code Block | ||
---|---|---|
| ||
<script src="https://www.onesaitplatform.online/web/Cesium160/Cesium.js"></script> <script src="js/heatmap/CesiumHeatmap.js"></script> <style> @import url(https://www.onesaitplatform.online/web/Cesium160/Widgets/widgets.css); |
Having done this, you can start generating heatmaps.
Functioning philosophy
To generate a heatmap, you need a series of point-type entities (It does not work with lines or polygons), with defined geospace coordinates (usually longitude and latitude), and a field including each point's value or weight respect to the other points.
For this tutorial, we created a series of points, each of them with a value. These points, represented on a map along with their values, are seen in the following way:
...
The coordinates and values of all these points, which total 30, are stored in an ontology already loaded in the platform and served as a REST API. Each entity has the following structure:
Ontology entity example structure
Code Block | ||||||||
---|---|---|---|---|---|---|---|---|
| ||||||||
{ "type": "Feature", "properties": { "id": 2, "value": 22 }, "geometry": { "type": "Point", coordinates": [ -15.432677385995223, 28.138219879738312 ] } } |
In this structure, we have defined fields such as longitude (geometry.coordinates[0]
), latitude (geometry.coordinates[1]
) and the value assigned to the point (properties.value
).
Tip |
---|
...
heatmapSampleDataWhat? You don't know yet how you can generate an ontology and serve it as a REST API service? You're lucky, we explain it right here. And, if you want to try and create your own REST API for this example, here you have the JSON file: heatmapSampleData.json |
Besides the points with values, the heatmap generation also requires you to define the study area, understanding it as a rectangle delimiting the points to be considered. Thus, for the point series in the tutorial, a possible bounding rectangle can be this one:
...
Info |
---|
...
Bounding rectangleThis is the rectangle referenced when we say that this library uses the |
We are not interested in the rectangle's geometries, but in the upper, lower, left and right limit coordinates. This, while easy to get if you use a GIS program, can be harder if you have to do it manually. We suggest you to take the "higher" point, the increase the value of its latitude coordinate (geometry.coordinates[1]
), so the limit is a bit to its north. Repeat it for each of the sides to get the coordinates.
Another possibility, somewhat simpler but more technical, is drawing two points: One corresponding to the higher left corner, and the other to the lower right. These two point give you four coordinates, corresponding to the four corners in the diagram. See it in the following example:
...
Tip |
---|
heatmapSampleDataTry drawing some point in geojson.io and you'll see its coordinates appearing in the JSON editor in the map's right. Remember the first coordinate is longitude (east-west) and the second one is latitude (north-south). |
And that's it. With this idea in mind, generating heatmaps in Cesium is quite easy.
Library use (or how I can, at last, create a heatmap in Cesium)
Having seen the theory, we can start generating a heatmap like the one in the previous example.
Firstly we need to get the data of the point entities. These are in an ontology served by a REST service. To do this, we need to use AJAX, so in the webpage's <head>
, you must include a call to a jQuery library:
Include the jQuery library
Code Block | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|
| ||||||||||
<script <script src="https://www.onesaitplatform.online/web/Cesium160/Cesium.js"></script> <script src="js/heatmap/CesiumHeatmap.js"></script> <script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous" ></script> |
Next, in the Cesium viewer's Javascript code, you must call the REST service using the following code:
...
...
Call the REST service using AJAX
Code Block | ||
---|---|---|
| ||
jQuery.ajax({ /** REST service URL */ url: 'https://www.onesaitplatform.online/api-manager/server/api/v1/api_tuto_cesium_heatmap/getPointFeatures', /** Header with token to access the API */ headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'X-OP-APIKey': 'user_token' /** API service type */ type: 'GET', /** Content offered by the API; in this case, the ontology, it's in JSON format */ contentType: 'application/json', /** If the token is valid, information is recovered */ success: function(jsonData) { }, /** If the token is not valid, launch error */ error: function(data, errorThrown) { alert('A problem reading the ontology has been detected.') } }) |
Warning |
---|
...
Ontology useBear in mind that the URL refers to the API generated for this example. If you want to generate a heatmap with your own data, you must create your own REST service and include its URL. |
Warning |
---|
...
Personal token useRemember to replace 'user_token' (line 10 in the previous code) with your personal token. |
Tip |
---|
...
Code managementAll the code from here onward will be in the function returning the jsonData (from line 20 in the previous code). |
We will now define a variable containing an object with the previously-defined rectangle's corners:
...
Defining the rectangle's corners
Code Block | ||
---|---|---|
| ||
/** Defining the study area's boundaries (corners)*/ let extent = { west: -15.4522, south: 28.1276, east: -15.417, north: 28.1534 } |
We will also create another variable defining the heatmap's configuration properties. For now, we will specify the most basic ones; we will later give details on how to customize the heatmap.
...
Configuring the heatmap's properties
Code Block | ||
---|---|---|
| ||
/** Defining heatmap's properties */ let heatmapSetup = { radius: 150, maxOpacity: 0.8, minOpacity: 0.2, blur: 0.75 } |
Info |
---|
...
Configuration optionsIf you are interested, you can see all the generic customization options in heatmap.js's documentation. |
Next, generate an instance of CesiumHeatmap using CesiumHeatmap.create()
. This instance will be configured with three members:
The map's viewer: understood as the 'viewer' object that defines the viewer.
The interpolation limits: corresponding with the previously defined rectangle's limits.
A number of configuration properties: that we have just created.
So, a basic instance can be defined as:
Creating CesiumHeatmap instance
Code Block | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|
| ||||||||||
/** Generating heatmap instance */ let heatMap = CesiumHeatmap.create(viewer, extent, heatmapSetup) |
...
Now, you only need to add the data to the viewer, specifying the viewer's coordinate's format. There are two options:
setData()
: for data in X & Y coordinates (UTM style).setWGS84Data()
: for data in longitude and latitude coordinates.
In both cases, the function gets input parameters:
Maximum and minimum interpolation value: for a range of given values, the more the maximum and minimum values are near the points' maximum and minimum values, the more the differentiation visual effect will be in the interpolation (the colors will be more extreme).
List of point entities: with their weight values.
For the example data, as they are defined with longitude and latitude coordinates, it will be something like:
...
Adding heatmap to viewer
Code Block | ||
---|---|---|
| ||
/** Add heatmap to viewer, considering input data as longitude and latitude (WGS84) */ heatMap.setWGS84Data(15, 26, data) |
In this case, 15 is the minimum value to be considered in the interpolation, 26 is the maximum value, and data are the values of the ontology points, following the format{ x: longitude, y: latitude, value: weight }
.
To get those values, you can generate a function that iterates each element in the ontology, returning a list with the coordinates and weight values, and then gives this list to the heatmap generating function. In the case of the example ontology, the used function is this:
...
, the used function is this:
Function iterating the JSON and extracting the needed data
Code Block | ||
---|---|---|
| ||
function getData(values) { /** List containing coordinates and values of each point */ let data = [] /** Iterate by every entity in the JSON */ for (let feature of values.features) { /** Define fields: longitude, latitude and weight */ let longitude = feature.geometry.coordinates[0] let latitude = feature.geometry.coordinates[1] let weight = feature.properties.value /** Add each field to the list */ data.push({ x: longitude, y: latitude, value: weight }) } /** Return point list */ return data } |
...
Then, the function to add data looks like this, understanding as jsonData the object returning the function where the code is:
...
...
Add heatmap to viewer
Code Block | ||
---|---|---|
| ||
/** Add heatmap to viewer, considering input data as longitude and latitude (WGS84) */ heatMapInstance.setWGS84Data(15, 26, getData(jsonData)) |
Having done that, when we view the map, the heatmap should look like this:
Iframe | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
Tip |
---|
See bigger mapYou want to see this map bigger? We've uploaded it to the Platform. Have a look and don't worry about the source code; it's at the end of this tutorial. |
Heatmap configuration
The heatmap customization options are the following ones:
useEntitiesIfAvailable
: specify whether entities will be used to generate the heatmap(true
) or an image provider will always be used (false
). The default value istrue
.minCanvasSize
: minimum size, in pixels, for the color map canvas. The default value is700
.maxCanvasSize
: maximum size, in pixels, for the color map canvas. The default value is2000
.radius
: each point's interpolation size. If the radius is not enough and does not touch another point, it gives a series of concentric circles.radiusFactor
: size factor used if no radius is specified. The value is the higher height and width, divided by this value to generate the radius value to be used. The default value is60
.spacingFactor
: additional space around the analysis area's borders. The extra space is calculated by multiplying the points' radius by this value. The default value is1.5
.maxOpacity
: maximum used opacity value. The default value is0.8
.minOpacity
: minimum used opacity value. The default value is0.1
.blur
: blur value to be represented. The default value is0.85
.gradient
: a set of colors and limit conforming the map's color gradient. The default value is:'.3': 'blue',
'.65': 'yellow',
'.8': 'orange',
'.95': 'red'
...
Unless specifically required, it's best to interact only with radius, maximum and minimum opacity, blur and gradient, because the other properties do not give such a direct visual result and modifying them can generate abominations depending on the values to be shown in the heatmap (or alternatively, to not detecting any obvious change).
...
About the heatmap's symbology, this is achieved by modifying the gradient's options. Each gradient range is defined by the gradient's cut-off value (under a color, over another color; .65 means, for instance, that yellow is under this color and orange over it), and the value of the color to be used. This color is based on CSS, meaning that it accepts both the color's name (blue
) and the color's hexadecimal value (#0000FF
).
We will now see the heatmap with different gradient values:
...
Info | |
---|---|
title | Gradient cut-off valuesYou can do as many color leaps as you want; two, three, nine, etc. You must bear in mind that the percentage progression, from 0 to 100 (or 0 to 1, as Cesium work), must be followed → 0.1, 0.2, 0.45, 0.50, 0.65, 0.80, 0.90 |
Viewer code
This is the code used in the example viewer.
...
Código del visor al completo
Code Block | ||
---|---|---|
| ||
/** Viewer definition */ const viewer = new Cesium.Viewer('cesiumContainer', { /** Lower left ball widget off */ animation: false, /** Start button */ homeButton: false, /** Base map selector widget */ baseLayerPicker: false, /** Full screen button */ fullscreenButton: false, /** Geocoder box */ geocoder: false, /** Information box */ infoBox: false, /** Base map */ imageryProvider: Cesium.createOpenStreetMapImageryProvider({ url: 'https://a.tile.openstreetmap.org/' }), /** Navigation help button */ navigationHelpButton: false, /** Button to select modes 2D/2,5D/3D */ sceneModePicker: false, /** Lower time bar widget */ timeline: false, /** Force scene mode */ sceneMode: Cesium.SceneMode.SCENE3D, /** Green box appears when selecting entities */ selectionIndicator: false }) /** Remove HDR effect of version 1.52 */ viewer.scene.highDynamicRange = false jQuery.ajax({ /** URL to Rest service */ url: 'https://www.onesaitplatform.online/api-manager/server/api/v1/api_tuto_cesium_heatmap/getPointFeatures', /** Header with token to access the API */ headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'X-OP-APIKey': 'user_token' }, /** API service type */ type: 'GET', /** Content offered by the API; in this case it's a JSON */ contentType: 'application/json', /** If the token is valid, ontology information is recovered * then served to work with it */ success: function(jsonData) { /** Defining the study area's boundaries (corners)*/ let extent = { west: -15.4522, south: 28.1276, east: -15.417, north: 28.1534 } /** Defining heatmap's properties */ let heatmapSetup = { radius: 150, maxOpacity: 0.8, minOpacity: 0.2, blur: 0.75 } /** Function that iterate over the JSON and extracts the coordinates and value data */ function getData(values) { /** List with the coordinates and values of each point */ let data = [] /** Iterate by every entity in the JSON */ for (let feature of values.features) { /** Define fields: longitude, latitude and weight */ let longitude = feature.geometry.coordinates[0] let latitude = feature.geometry.coordinates[1] let weight = feature.properties.value /** Add each field to the list */ data.push({ x: longitude, y: latitude, value: weight }) } /** Return point list */ return data } /** Generating heatmap instance */ let heatMap = CesiumHeatmap.create(viewer, extent, heatmapSetup) /** Adding heatmap to viewer */ heatMap.setWGS84Data(15, 26, getData(jsonData)) /** Zoom on the heatmap */ viewer.camera.flyTo({ destination: Cesium.Cartesian3.fromDegrees( -15.4301002359, 28.1405417072, 6000.0 ), orientation: { heading: Cesium.Math.toRadians(0), pitch: Cesium.Math.toRadians(-90.0), roll: 0.0 } }) }, /** If the token is not valid, launch error */ error: function(data, errorThrown) { alert('A problem reading the ontology has been detected.') } }) |
...