-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore(web): add support for heatMap in beta (#835)
Co-authored-by: Piyush Chauhan <piyuschauhan1004@gmail.com> Co-authored-by: keiya01 <keiya.s.0210@gmail.com>
- Loading branch information
1 parent
293335b
commit 0c4c026
Showing
22 changed files
with
1,848 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
187 changes: 187 additions & 0 deletions
187
web/src/beta/lib/core/engines/Cesium/Feature/HeatMap/HeatmapMesh.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; |
81 changes: 81 additions & 0 deletions
81
web/src/beta/lib/core/engines/Cesium/Feature/HeatMap/HeatmapMeshMaterial.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
47
web/src/beta/lib/core/engines/Cesium/Feature/HeatMap/colorMap.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
Oops, something went wrong.