Skip to content

Commit

Permalink
chore(web): add support for heatMap in beta (#835)
Browse files Browse the repository at this point in the history
Co-authored-by: Piyush Chauhan <piyuschauhan1004@gmail.com>
Co-authored-by: keiya01 <keiya.s.0210@gmail.com>
  • Loading branch information
3 people committed Dec 19, 2023
1 parent 293335b commit 0c4c026
Show file tree
Hide file tree
Showing 22 changed files with 1,848 additions and 15 deletions.
2 changes: 2 additions & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@
"@sentry/browser": "7.77.0",
"@seznam/compose-react-refs": "1.0.6",
"@turf/turf": "6.5.0",
"@types/d3": "^7.4.3",
"@types/escape-string-regexp": "2.0.1",
"@ungap/event-target": "0.2.4",
"apollo-link-sentry": "3.2.3",
Expand All @@ -130,6 +131,7 @@
"core-js": "3.33.2",
"crypto-js": "4.2.0",
"csv-parse": "5.5.2",
"d3": "^7.8.5",
"date-fns": "2.30.0",
"dayjs": "1.11.10",
"detect-browser": "5.3.0",
Expand Down
187 changes: 187 additions & 0 deletions web/src/beta/lib/core/engines/Cesium/Feature/HeatMap/HeatmapMesh.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
// ref: https://github.com/takram-design-engineering/plateau-view/blob/main/libs/heatmap/src/HeatmapMesh.tsx

import {
ArcType,
BoundingSphere,
EllipsoidSurfaceAppearance,
GeometryInstance,
GroundPrimitive,
PolygonGeometry,
} from "cesium";
import { type MultiPolygon, type Polygon } from "geojson";
import { pick } from "lodash-es";
import {
forwardRef,
useEffect,
useImperativeHandle,
useRef,
useMemo,
useLayoutEffect,
memo,
} from "react";
import { useCesium } from "resium";

import { ComputedFeature, ComputedLayer } from "@reearth/beta/lib/core/mantle";
import { useConstant } from "@reearth/beta/utils/util";

import { attachTag } from "../utils";

import { createColorMapImage } from "./colorMap";
import { viridisColorMapLUT } from "./constants";
import { type MeshImageData } from "./createMeshImageData";
import { createHeatmapMeshMaterial, type HeatmapMeshMaterialOptions } from "./HeatmapMeshMaterial";
import { convertPolygonToHierarchyArray } from "./utils";

export type HeatmapMeshHandle = {
bringToFront: () => void;
sendToBack: () => void;
};

export type HeatmapMeshProps = Omit<HeatmapMeshMaterialOptions, "image"> & {
layer?: ComputedLayer;
feature?: ComputedFeature;
boundingSphere: BoundingSphere;
meshImageData: MeshImageData;
geometry: Polygon | MultiPolygon;
};

export const HeatmapMesh = memo(
forwardRef<HeatmapMeshHandle, HeatmapMeshProps>(
(
{
layer,
feature,
boundingSphere,
meshImageData,
geometry,
colorMapLUT,
bound,
cropBound,
...props
},
ref,
) => {
const { scene } = useCesium();
const groundPrimitives = scene?.primitives;
const primitiveRef = useRef<GroundPrimitive>();

const material = useConstant(() =>
createHeatmapMeshMaterial({
image: meshImageData.image,
width: meshImageData.width,
height: meshImageData.height,
bound,
cropBound,
}),
);

const geometryInstances = useMemo(() => {
return convertPolygonToHierarchyArray(geometry).map(polygonHierarchy => {
const instance = new GeometryInstance({
geometry: new PolygonGeometry({
polygonHierarchy,
arcType: ArcType.GEODESIC,
vertexFormat: EllipsoidSurfaceAppearance.VERTEX_FORMAT,
}),
id: layer?.id,
});

return instance;
});
}, [geometry, layer?.id]);

// Since we expect a single feature, we directly access the first one
const geometryInstance = geometryInstances[0];

useEffect(() => {
if (groundPrimitives?.isDestroyed()) {
return;
}
const primitive =
// TODO: Needs trapezoidal texture projection to accurately map the
// data. See also: https://github.com/CesiumGS/cesium/issues/4164
new GroundPrimitive({
geometryInstances: geometryInstance,
appearance: new EllipsoidSurfaceAppearance({
material,
}),
});
groundPrimitives?.add(primitive);
primitiveRef.current = primitive;

return () => {
if (!groundPrimitives?.isDestroyed()) {
groundPrimitives?.remove(primitive);
}
primitiveRef.current = undefined;
};
}, [geometryInstance, groundPrimitives, material]);

useLayoutEffect(() => {
// Code for attaching tag
if (!primitiveRef.current || primitiveRef.current?.isDestroyed()) return;
attachTag(primitiveRef.current, {
layerId: layer?.id,
featureId: feature?.id,
originalProperties: boundingSphere,
});
}, [layer?.id, feature?.id, boundingSphere]);

useEffect(() => {
material.uniforms.image = meshImageData.image;
}, [meshImageData.image, material]);

Object.assign(material.uniforms, pick(meshImageData, ["width", "height"]));

useEffect(() => {
material.uniforms.colorMap =
colorMapLUT != null
? createColorMapImage(colorMapLUT)
: createColorMapImage(viridisColorMapLUT);
}, [colorMapLUT, material]);

Object.assign(
material.uniforms,
pick(props, [
"minValue",
"maxValue",
"opacity",
"contourSpacing",
"contourThickness",
"contourAlpha",
"logarithmic",
]),
);

useImperativeHandle(
ref,
() => ({
bringToFront: () => {
if (groundPrimitives?.isDestroyed()) {
return;
}

if (groundPrimitives?.contains(primitiveRef)) {
groundPrimitives?.raiseToTop(primitiveRef);
}
},
sendToBack: () => {
if (groundPrimitives?.isDestroyed()) {
return;
}

if (groundPrimitives?.contains(primitiveRef)) {
groundPrimitives?.lowerToBottom(primitiveRef);
}
},
}),
[groundPrimitives],
);

scene?.requestRender();
return null;
},
),
);

HeatmapMesh.displayName = "HeatmapMesh";
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// ref: https://github.com/takram-design-engineering/plateau-view/blob/main/libs/heatmap/src/HeatmapMeshMaterial.ts

import { Cartesian2, Material, Rectangle } from "cesium";

import { Bound, LUT } from "@reearth/beta/lib/core/mantle";

import { createColorMapImage } from "./colorMap";
import { viridisColorMapLUT } from "./constants";
import heatmapMeshMaterial from "./shaders/heatmapMeshMaterial.glsl?raw";
import makeContour from "./shaders/makeContour.glsl?raw";
import sampleBicubic from "./shaders/sampleBicubic.glsl?raw";

export type HeatmapMeshMaterialOptions = {
image: string | HTMLCanvasElement;
width: number;
height: number;
minValue?: number;
maxValue?: number;
bound?: Bound;
cropBound?: Bound;
colorMapLUT?: LUT;
opacity?: number;
contourSpacing?: number;
contourThickness?: number;
contourAlpha?: number;
logarithmic?: boolean;
};

export function createHeatmapMeshMaterial({
image,
width,
height,
minValue = 0,
maxValue = 100,
bound,
cropBound,
colorMapLUT = viridisColorMapLUT,
opacity = 1,
contourSpacing = 10,
contourThickness = 1,
contourAlpha = 0.2,
logarithmic = false,
}: HeatmapMeshMaterialOptions): Material {
const imageScale = new Cartesian2(1, 1);
const imageOffset = new Cartesian2();
const cropRectangle = new Rectangle(
cropBound?.west,
cropBound?.south,
cropBound?.east,
cropBound?.north,
);
const rectangle = new Rectangle(bound?.west, bound?.south, bound?.east, bound?.north);
if (cropBound != null && rectangle != null && cropRectangle != null) {
imageScale.x = cropRectangle.width / rectangle.width;
imageScale.y = cropRectangle.height / rectangle.height;
imageOffset.x = (cropRectangle.west - rectangle.west) / rectangle.width;
imageOffset.y = (cropRectangle.south - rectangle.south) / rectangle.height;
}

return new Material({
fabric: {
type: "HeatmapMesh",
uniforms: {
colorMap: createColorMapImage(colorMapLUT),
image,
imageScale,
imageOffset,
width,
height,
minValue,
maxValue,
opacity,
contourSpacing,
contourThickness,
contourAlpha,
logarithmic,
},
source: [sampleBicubic, makeContour, heatmapMeshMaterial].join("\n"),
},
});
}
47 changes: 47 additions & 0 deletions web/src/beta/lib/core/engines/Cesium/Feature/HeatMap/colorMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// ref: https://github.com/takram-design-engineering/plateau-view/blob/main/libs/color-maps/src/ColorMap.ts

import { interpolate, quantize, rgb, scaleLinear, type ScaleLinear } from "d3";
import invariant from "tiny-invariant";

import type { ColorTuple, LUT } from "@reearth/beta/lib/core/mantle";

export type ColorMapType = "sequential" | "diverging";

function createScaleLinear(lut: LUT): ScaleLinear<ColorTuple, ColorTuple> {
invariant(lut.length > 1);
return scaleLinear<ColorTuple>()
.domain(quantize(interpolate(0, 1), lut.length))
.range(lut)
.clamp(true);
}

export function linearColorMap(lut: LUT, value: number): ColorTuple {
const scaleLinear = createScaleLinear(lut);
const result = scaleLinear(value);
invariant(result != null);
return result;
}

export function countColorMap(lut: LUT): number {
return lut.length;
}

export function quantizeColorMap(lut: LUT, count: number): ColorTuple[] {
invariant(count > 1);
return [...Array(count)].map((_, index) => {
return linearColorMap(lut, index / (count - 1));
});
}

export function createColorMapImage(lut: LUT): HTMLCanvasElement {
const canvas = document.createElement("canvas");
canvas.width = lut.length;
canvas.height = 1;
const context = canvas.getContext("2d");
invariant(context != null);
lut.forEach(([r, g, b], index) => {
context.fillStyle = rgb(r * 255, g * 255, b * 255).toString();
context.fillRect(index, 0, 1, 1);
});
return canvas;
}

0 comments on commit 0c4c026

Please sign in to comment.