Skip to content

Instantly share code, notes, and snippets.

@datadavev
Last active December 17, 2025 17:43
Show Gist options
  • Select an option

  • Save datadavev/f12f5e7f2521e929eed6b44d8ec0d665 to your computer and use it in GitHub Desktop.

Select an option

Save datadavev/f12f5e7f2521e929eed6b44d8ec0d665 to your computer and use it in GitHub Desktop.
H3 cells in Cesium view
import * as Cesium from "cesium";
import * as h3 from "https://cdnjs.cloudflare.com/ajax/libs/h3-js/4.3.0/h3-js.es.min.js";
/**************************************************************************************************
* "Patch" for Cesium camera computeViewRectangle.
*
* This path provides a modified implementation of computeViewRectangle that
* returns the correct view rectangle when the camera is oriented towards the south
* and the horizon is visible. The default implementation truncates the view rectangle
* at a lower corner of the view which can significantly reduce the reported view
* rectangle.
*
* Load this in sandcastle.
*/
const VIEWER = new Cesium.Viewer(
'cesiumContainer', {
timeline: false,
animation: false,
baseLayerPicker: false,
terrain: Cesium.Terrain.fromWorldTerrain(),
});
const TOOLBAR = document.getElementById('toolbar');
const someColors = [
Cesium.Color.GOLD,
Cesium.Color.AQUA,
Cesium.Color.BLUEVIOLET,
Cesium.Color.CHARTREUSE,
Cesium.Color.CORAL,
Cesium.Color.DEEPPINK
];
// The global bounding box latitude and longitude values.
const GLOBAL_RECT = new Cesium.Rectangle(-Cesium.Math.PI, -Cesium.Math.PI_OVER_TWO, Cesium.Math.PI, Cesium.Math.PI_OVER_TWO);
let bbEntity = null;
/**
* Scans vertically at x pixels min_y to max_y to find a pixel that intersect
* the ellipsoid.
*
* In screen space canvas, 0,0 is the top left corner.
* Returns the cartographic coordinates of the picked point or null if
* there is no intersection.
*/
function computeHorizonPointY(camera, ellips, x, min_y, max_y) {
for (let j=min_y; j < max_y; j += 5) {
const cp = camera.pickEllipsoid(new Cesium.Cartesian2(x, j), ellips);
if (cp) {
return ellips.cartesianToCartographic(cp);
}
}
return null;
}
/**
* Compute if point is visible in current view
* p: Cartographic3
*/
function isPositionVisible(camera, ellips, cartesian) {
const frustum = camera.frustum;
const cullingVolume = frustum.computeCullingVolume(
camera.position,
camera.direction,
camera.up
);
const intersection = cullingVolume.computeVisibility(new Cesium.BoundingSphere(cartesian, 0.0));
if (intersection === Cesium.Intersect.INSIDE) {
const globeBoundingSphere = new Cesium.BoundingSphere(
Cesium.Cartesian3.ZERO,
ellips.minimumRadius
);
const occluder = new Cesium.Occluder(
globeBoundingSphere,
camera.position
)
return occluder.isPointVisible(cartesian);
}
return false;
}
/**
* Return 0 if neither visible, -1 for south, 1 for north
*/
function isPoleVisible(camera, ellips) {
const NORTH_POLE = Cesium.Cartographic.toCartesian(
Cesium.Cartographic.fromDegrees(0.0, 90.0, 1.0, new Cesium.Cartographic()),
ellips,
new Cesium.Cartesian3()
);
if (isPositionVisible(camera, ellips, NORTH_POLE)) {
return 1;
}
const SOUTH_POLE = Cesium.Cartographic.toCartesian(
Cesium.Cartographic.fromDegrees(0.0, -90.0, 1.0, new Cesium.Cartographic()),
ellips,
new Cesium.Cartesian3()
);
if (isPositionVisible(camera, ellips, SOUTH_POLE)) {
return -1;
}
return 0;
}
/**
* Computes five cartographic points corresponding with the canvas top left,
* top middle, top right, lower right, and lower left.
*
* The top three points are computed at the intersection of that column of pixels
* with the ellpsoid.
*
* null is returned for any point that does not intersect with the ellpsoid.
*/
function computeViewHorizonPoints(canvas, camera, ellips) {
const xs = [0, Math.floor(canvas.width/2), canvas.width];
const points = [null, null, null, null, null];
for (let i=0; i<3; i++) {
points[i] = computeHorizonPointY(camera, ellips, xs[i], 0, canvas.height);
}
points[3] = computeHorizonPointY(camera, ellips, xs[0], canvas.height-1, canvas.height);
points[4] = computeHorizonPointY(camera, ellips, xs[2], canvas.height-1, canvas.height);
return points;
}
/**
* Given a list of cartographic points, compute
* the bounding rectangle for the points.
*/
function pointsToBoundingRectangle(camera, ellips, points, result) {
for (let i=0; i < points.length; i++) {
if (!points[i]) {
return GLOBAL_RECT;
}
}
result = Cesium.Rectangle.fromCartographicArray(points, result);
const pvisible = isPoleVisible(camera, ellips);
if (pvisible === 1) {
result.west = -Cesium.Math.PI;
result.east = Cesium.Math.PI;
result.north = Cesium.Math.PI_OVER_TWO;
} else if (pvisible === -1) {
result.west = -Cesium.Math.PI;
result.east = Cesium.Math.PI;
result.south = -Cesium.Math.PI_OVER_TWO;
}
return result;
}
/**
* Compute the (approximate) bounding rectangle of the camera view.
*
* returns Cesium.Rectangle
*/
Cesium.Camera.prototype.computeViewRectangle2 = function(ellips, result) {
const points = computeViewHorizonPoints(this._scene.canvas, this, ellips);
return pointsToBoundingRectangle(this, ellips, points, result);
}
function getH3IndexesInBondingBox(minLat, minLng, maxLat, maxLng, resolution) {
// Define the bounding box as a GeoJSON-like polygon:
// The structure is [outer ring, ...holes], and each ring is an array of [lat, lng]
const polygon = [
[
[maxLat, maxLng], // Top Right
[minLat, maxLng], // Bottom Right
[minLat, minLng], // Bottom Left
[maxLat, minLng], // Top Left
[maxLat, maxLng] // Closing the loop (same as Top Right)
]
];
// The h3.polygonToCells function (or h3.polyfill in older v3 library)
// returns an array of H3 indexes whose centroids fall within the polygon.
const h3Indexes = h3.polygonToCells(polygon, resolution);
return h3Indexes;
}
function distanceToLevel(d) {
// really rough estimate of distance to center of view vs zoom level.
const ranges = [
5655046,
2534653,
1123998,
326074,
78393,
30721,
18239,
4639,
1102,
500,
250
];
if (d < 0) {
return 0;
}
for (let i=0; i < ranges.length; i++) {
if (d > ranges[i]) {
return i+1;
}
}
return 12;
}
function createH3GroundPrimitives(h3Indexes, alpha=0.3) {
return h3Indexes.map(index => {
const color = someColors[Math.floor(Math.random()*someColors.length)].withAlpha(alpha);
const boundary = h3.cellToBoundary(index);
const positions = boundary.map(coords => {
return Cesium.Cartesian3.fromDegrees(coords[1], coords[0]);
});
const instance = new Cesium.GeometryInstance({
geometry: new Cesium.PolygonGeometry({
polygonHierarchy: new Cesium.PolygonHierarchy(positions),
height: 0 // Keep at ground level
}),
attributes: {
color: Cesium.ColorGeometryInstanceAttribute.fromColor(color)
}
});
return new Cesium.GroundPrimitive({
geometryInstances: instance,
appearance: new Cesium.PerInstanceColorAppearance({
flat: true // Flat shading often looks better for data overlays
})
});
});
}
const handler = new Cesium.ScreenSpaceEventHandler(VIEWER.canvas);
handler.setInputAction((click) => {
const bb = VIEWER.camera.computeViewRectangle2(VIEWER.scene.globe.ellipsoid, new Cesium.Rectangle());
const minLng = Cesium.Math.toDegrees(bb.west);
const minLat = Cesium.Math.toDegrees(bb.south);
const maxLat = Cesium.Math.toDegrees(bb.north)
const maxLng = Cesium.Math.toDegrees(bb.east);
let h3cells = [];
let distanceToTerrain = -1;
if (bb === GLOBAL_RECT) {
h3cells = h3.getRes0Cells();
} else {
const ray = VIEWER.camera.getPickRay(
new Cesium.Cartesian2(
VIEWER.canvas.width / 2,
VIEWER.canvas.height / 2
)
);
const intersection = VIEWER.scene.globe.pick(ray, VIEWER.scene);
if (intersection) {
distanceToTerrain = Cesium.Cartesian3.distance(VIEWER.camera.position, intersection);
}
h3cells = getH3IndexesInBondingBox(minLat, minLng, maxLat, maxLng, distanceToLevel(distanceToTerrain));
}
TOOLBAR.innerHTML = `<pre>
west: ${minLng.toFixed(2)}
south:${minLat.toFixed(2)}
east:${maxLng.toFixed(2)}
north:${maxLat.toFixed(2)}
distance:${distanceToTerrain.toFixed(0)}
ncells:${h3cells.length}</pre>`;
VIEWER.scene.primitives.removeAll();
if (h3cells.length > 0) {
const primitives = createH3GroundPrimitives(h3cells.slice(0,2000));
primitives.forEach(p => VIEWER.scene.primitives.add(p));
}
}, Cesium.ScreenSpaceEventType.LEFT_UP);
@datadavev
Copy link
Author

datadavev commented Dec 17, 2025

Example in sandcastle.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment