I ran into a need for this as I’m making an aviation flight planning application and wanted to show the bearing/distance for each leg along my route:
import React, { useEffect, useState } from 'react';
import { Cartesian3, Color, ScreenSpaceEventHandler } from 'cesium';
import { PolylineEntity } from './PolylineEntity';
import { LabelEntity } from './LabelEntity';
import { useCalcBearingAndDistanceMutation } from '../../../redux/api/vfr3d/navlog.api';
import { BearingAndDistanceResponseDto, Waypoint } from 'vfr3d-shared';
interface PolylineWithLabelProps {
positions: Cartesian3[];
color: Color;
id: string;
width: number;
waypoints: Waypoint[];
onLeftClick: (
event: ScreenSpaceEventHandler.PositionedEvent,
polylinePoints: Cartesian3[]
) => void;
}
export const BearingDistancePolyline: React.FC<PolylineWithLabelProps> = ({
positions,
color,
id,
width,
onLeftClick,
waypoints,
}) => {
const [bearingAndDistance, setBearingAndDistance] = useState<BearingAndDistanceResponseDto>();
const [bearingAndDistanceText, setBearingAndDistanceText] = useState<string>('');
const [calcBearingAndDistance] = useCalcBearingAndDistanceMutation();
const midpoint = Cartesian3.midpoint(positions[0], positions[1], new Cartesian3());
useEffect(() => {
const getBearingAndDistance = async () => {
const bearingAndDistance = await calcBearingAndDistance({
startPoint: waypoints[0],
endPoint: waypoints[1],
}).unwrap();
setBearingAndDistance(bearingAndDistance);
setBearingAndDistanceText(
`TC: ${Math.round(bearingAndDistance.trueCourse)} - Distance: ${Math.round(bearingAndDistance?.distance)}`
);
};
getBearingAndDistance();
}, [calcBearingAndDistance, waypoints]);
return (
<>
<PolylineEntity
positions={positions}
color={color}
id={id}
width={width}
onLeftClick={onLeftClick}
/>
{bearingAndDistance && (
<LabelEntity
position={midpoint}
text={bearingAndDistanceText}
rotation={bearingAndDistance.trueCourse - 90}
/>
)}
</>
);
};
I ended up making something like this
import {
Cartesian3,
Color,
ConstantProperty,
Entity,
PolylineGraphics,
ScreenSpaceEventHandler,
ScreenSpaceEventType,
} from 'cesium';
import { useEffect, useRef } from 'react';
import { useCesium } from 'resium';
interface PolylineEntityProps {
positions: Cartesian3[];
color?: Color;
width?: number;
id: string;
onLeftClick?: (
position: ScreenSpaceEventHandler.PositionedEvent,
polylinePoints: Cartesian3[]
) => void;
}
export const PolylineEntity: React.FC<PolylineEntityProps> = ({
positions,
color = Color.BLUE,
width = 3,
id,
onLeftClick: onLeftClick,
}) => {
const { viewer } = useCesium();
const entityRef = useRef<Entity | null>(null);
useEffect(() => {
if (!viewer) return;
const polylineGraphics = new PolylineGraphics({
positions: new ConstantProperty(positions),
material: color,
width: new ConstantProperty(width),
});
const entity = viewer.entities.add({
polyline: polylineGraphics,
id,
});
entityRef.current = entity;
const handler = new ScreenSpaceEventHandler(viewer.scene.canvas);
if (onLeftClick) {
handler.setInputAction((movement: ScreenSpaceEventHandler.PositionedEvent) => {
const pickedObject = viewer.scene.pick(movement.position);
if (pickedObject && pickedObject.id === entity) {
onLeftClick(movement, positions);
}
}, ScreenSpaceEventType.LEFT_CLICK);
}
return () => {
if (onLeftClick) {
if (viewer && entity) {
viewer.entities.remove(entity);
handler.removeInputAction(ScreenSpaceEventType.LEFT_CLICK);
}
}
};
}, [viewer, positions, color, width, id, onLeftClick]);
return null;
};
// LabelEntity.tsx
import {
Entity,
Color,
ConstantProperty,
NearFarScalar,
Cartesian2,
Property,
Cartesian3,
Plane,
writeTextToCanvas,
PlaneGraphics,
ImageMaterialProperty,
Transforms,
HeadingPitchRoll,
Math,
} from 'cesium';
import React, { useEffect, useRef } from 'react';
import { useCesium } from 'resium';
interface LabelEntityProps {
position: Cartesian3;
text: string;
show?: boolean | Property;
scale?: number | Property;
color?: Color | Property;
rotation?: number;
id?: string;
}
export const LabelEntity: React.FC<LabelEntityProps> = ({
position,
text,
show = true,
scale = 1.0,
color = Color.WHITE,
rotation,
id,
}) => {
const { viewer } = useCesium();
const entityRef = useRef<Entity | null>(null);
useEffect(() => {
if (!viewer) return;
const image = writeTextToCanvas(text, {
backgroundColor: Color.MAGENTA.withAlpha(0.1),
padding: 2,
fill: true,
fillColor: Color.WHITE,
stroke: true,
strokeWidth: 1,
strokeColor: Color.BLACK,
});
if (image) {
const angle = rotation ? rotation : 0;
const orientation = Transforms.headingPitchRollQuaternion(
position,
new HeadingPitchRoll(Math.toRadians(angle), 0, 0)
);
const offsetPosition = Cartesian3.add(
position,
Cartesian3.multiplyByScalar(new Cartesian3(-1, 0, 0), 1000, new Cartesian3()),
new Cartesian3()
);
const entity = viewer.entities.add({
position: offsetPosition,
plane: new PlaneGraphics({
plane: new ConstantProperty(new Plane(Cartesian3.UNIT_Z, 0.0)),
dimensions: new ConstantProperty(new Cartesian2(image?.width * 50, image?.height * 50)),
material: new ImageMaterialProperty({
image: image,
}),
outline: false,
}),
orientation: orientation,
id,
});
entityRef.current = entity;
return () => {
if (viewer && entity) {
viewer.entities.remove(entity);
}
};
}
}, [viewer, position, text, show, scale, color, rotation, id]);
return null;
};