Align text along a fixed heading/polyline

Hello, I’m trying to add a label that aligns with a path, similar to how street names follow the direction of their streets in google maps (they aren’t always horizontal). I don’t want to just rotate the text say 45 degrees, I want to define it’s position and heading so when the user rotates the camera the text orientation changes appropriately.

Is there a way to do this that I haven’t found?

I started by trying to find a basic text rotate and discovered i couldn’t even figure that out. IE something like this:

viewer.entities.add({

position : Cesium.Cartesian3.fromDegrees(-75.1641667, 39.9522222),

label : {

text : ‘Philadelphia’

//rotation : Cesium.Math.toRadians(-45) … or even better heading : Cesium.Math.toRadians(-45)
}

});

``

or this:

var labels = scene.primitives.add(new Cesium.LabelCollection());

var l = labels.add({

position : Cesium.Cartesian3.fromRadians(longitude, latitude, height),

text : ‘Hello World’,

font : ‘24px Helvetica’

//rotation : Cesium.Math.toRadians(-45) … or even better heading : Cesium.Math.toRadians(-45)
});

``

Will I have to Create Pictures of each label in photoshop and import them as an image, then rotate the image (or use it as a material and rotate the entity)? Obviously labor intensive if you have a lot of labels (like street names).

Thanks!

Label rotation isn’t supported yet (but is definitely something we want to support eventually). Your best bet for now is to use the writeTextToCanvas and make a billboard out of it. Label’s can’t do this because they render individual letters, not entire words (which is a huge performance/memory win that makes things like rotation difficult).

Here’s an example:

var viewer = new Cesium.Viewer(‘cesiumContainer’);

var scene = viewer.scene;

var labels = scene.primitives.add(new Cesium.BillboardCollection());

labels.add({

position : Cesium.Cartesian3.fromDegrees(-74,13),

image : Cesium.writeTextToCanvas(‘Test’, { font: ‘24px san-serif’ }),

rotation : Cesium.Math.toRadians(90), //heading

alignedAxis : Cesium.Cartesian3.UNIT_Z //Makes rotation a heading

});

Hello, following up to see if this has been implemented, thanks!

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;
};