cesium
cesium copied to clipboard
Support label rotation
As requested on the mailing list: https://groups.google.com/d/msg/cesium-dev/dnqBUH3Fx7M/iswm2vMz8DMJ
This might involve the same difficulties that prevent us from doing label scaling by distance, but I would have to check the label code to know for sure.
Have you gotten around to implemnting this feature yet?
@Nirco96 sorry, we haven't had a chance to add support for this yet. We'd be happy to review a pull request if anyone has time to implement this!
@hpinkos could you please give us some pointers about where to start to implement this?
@lydonchandra I would look at the Label class where individual glyphs are drawn and the LabelCollection where it's all put together.
The labels use billboards under the hood for rotation, so it may be as simple as passing that through to some "parent" billboard that contains all the glyphs, or it may require handling the fact that all the glyphs may be separate billboards.
@lydonchandra thanks for being interested in taking a look at this!
all the glyphs may be separate billboards.
All glyphs are always separate billboards. It's been a while, but I'm pretty sure 99% of the work is simply modifying repositionAllGlyphs
in LabelCollection.js
to take a rotation into account. (and then the plumbing of exposing a rotation property on the Primitive/Entity APIs.)
To start, I would just recommend reviewing repositionAllGlyphs
to understand what it's doing and then picking some angle (like 1/4 pi) and getting that to work. Once you have all labels drawing at that rotation, then it should be easy to thread it through up to the API.
Thanks for the pointers @OmarShehata @mramato
Screenshot from https://sandcastle.cesium.com/index.html?src=Callback%20Property.html&label=Beginner
As all glyphs are in separate billboards,
- Should each glyphCanvas position/rotation be adjusted separately based rotationAngle? In screenshot above, only rotation is adjusted (position is left untouched)
- Also the label's backgroundBillboard (shown when showBackground: true) can be rotated easily as it is just one billboard.
Is copying all glyphs to another billboard (that can be rotated easily) an acceptable solution?
@lydonchandra, unfortunately that's not an acceptable solution. The reason labels are rendered as individual glyphs and not as a single billboard are for runtime performance and memory usage. Obviously it complicates the implementation, but it's necessary technique. Otherwise creating/updating labels would be slower and consume a large amount of texture memory.
@mramato This is extremely necessary feature fo e.g in order to change the angle of labels according to camera position. is this on the roadmap?
@jony89 can you describe your use case for that?
Do you mean rotating the label based on camera distance, around the axis between the camera and the label?
That's one usecase. correct. to change the label position according to the heading of the camera. but in our app we would like to rotate the label for many other reasons. another use case is for measurement tools with labels next to the polyline that measures height.
@jony89 we would happily take a look at a pull request if you or your team is interested in contributing this feature, otherwise it is not on our near term roadmap. See our Contributing Guide to get started if you are interested.
@mramato, on a side note, I am a contributor, yet the last PR I sent got really lil attention https://github.com/CesiumGS/cesium/pull/8591 :\
@jony89 I reviewed that PR and asked you a question back on Feb 26th that you have yet to reply to: https://github.com/CesiumGS/cesium/pull/8591#discussion_r384509512
Is there any news on this?
If anyone is curious, I ended up needing something like this, (I wanted to show bearing and distance for each leg of a flight leg) and created some react components for this, and it works quite well for me. Of course, figuring out the angle to rotate your stuff by will be different than mine.
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}
/>
)}
</>
);
};
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;
};