Follow Path on Transport Network

Animate an object following a path on a transport network. The path is formed from a sequence of coordinates and heading angle samples, as might typically be obtained from a location-tracking service.

To run this example, open the Wrld/Demo/Examples.unity scene, click the Play button, and select Follow Path on Transport Network from the dropdown.

Animate an object following a path on a transport network. The path is formed from a sequence of coordinates and heading angle samples, as might typically be obtained from a location-tracking service.
using System;
using UnityEngine;
using Wrld;
using Wrld.Space;
using Wrld.Transport;

public class FollowPathOnTransportNetwork : MonoBehaviour
{
    class InputSample
    {
        public double LatitudeDegrees;
        public double LongitudeDegrees;
        public double HeadingDegrees;

        public static InputSample Create(double latitudeDegrees, double longitudeDegrees, double heading)
        {
            return new InputSample()
            {
                LatitudeDegrees = latitudeDegrees,
                LongitudeDegrees = longitudeDegrees,
                HeadingDegrees = heading
            };
        }
    };

    public double SamplePeriod = 5.0;

    private readonly InputSample[] m_inputSamples = new InputSample[] {
            InputSample.Create(37.801747, -122.419589, -10.0),
            InputSample.Create(37.802114, -122.418776, 45.0),
            InputSample.Create(37.801477, -122.417845, 170.0),
            InputSample.Create(37.799597, -122.417455, 160.0),
            InputSample.Create(37.799223, -122.418822, 260.0),
            InputSample.Create(37.800270, -122.419288, -10.0)
        };

    private int m_currentInputSampleIndex;
    private TransportPositioner m_transportPositioner;
    private TransportPositionerPointOnGraph m_prevPointOnGraph;
    private TransportPositionerPointOnGraph m_currentPointOnGraph;
    private TransportPathfindResult m_pathfindResult;

    private double m_time;
    private double m_prevSampleTime;
    private double m_currentSampleTime;
    private double m_nextSampleTime;

    private bool m_needPathfind;
    private bool m_needCurrentMatched;

    private GameObject m_capsule;

    private TransportApi m_transportApi;
    private SpacesApi m_spacesApi;

    private void OnEnable()
    {
        m_transportApi = Api.Instance.TransportApi;
        m_spacesApi = Api.Instance.SpacesApi;

        m_currentInputSampleIndex = 0;

        m_time = 0.0;
        m_prevSampleTime = 0.0;
        m_currentSampleTime = 0.0;
        m_nextSampleTime = SamplePeriod;

        m_needCurrentMatched = false;
        m_needPathfind = false;

        m_capsule = CreateCapsule(Color.yellow, 4.0f);
        m_capsule.SetActive(false);

        m_prevPointOnGraph = TransportPositionerPointOnGraph.MakeEmpty();
        m_currentPointOnGraph = TransportPositionerPointOnGraph.MakeEmpty();
        m_pathfindResult = null;

        m_transportPositioner = m_transportApi.CreatePositioner(new TransportPositionerOptionsBuilder()
            .SetInputCoordinates(m_inputSamples[0].LatitudeDegrees, m_inputSamples[0].LongitudeDegrees)
            .Build());
        m_transportPositioner.OnPointOnGraphChanged += OnPointOnGraphChanged;

        SetInputSample();
    }

    private void OnDisable()
    {
        GameObject.Destroy(m_capsule);
        m_transportPositioner.OnPointOnGraphChanged -= OnPointOnGraphChanged;
        m_transportPositioner.Discard();
        m_transportPositioner = null;
        m_prevPointOnGraph = null;
        m_currentPointOnGraph = null;
        m_pathfindResult = null;
    }

    private void Update()
    {
        m_time += Time.deltaTime;
        if (m_time >= m_nextSampleTime)
        {
            NextInput();
        }

        UpdatePointOnPath();
    }


    private void NextInput()
    {
        m_nextSampleTime += SamplePeriod;

        m_currentInputSampleIndex++;
        if (m_currentInputSampleIndex >= m_inputSamples.Length)
        {
            m_currentInputSampleIndex = 0;
        }

        SetInputSample();
    }

    private void SetInputSample()
    {
        var inputSample = m_inputSamples[m_currentInputSampleIndex];
        m_transportPositioner.SetInputCoordinates(inputSample.LatitudeDegrees, inputSample.LongitudeDegrees);
        m_transportPositioner.SetInputHeading(inputSample.HeadingDegrees);
        m_needCurrentMatched = true;
        m_needPathfind = true;
    }

    private void OnPointOnGraphChanged()
    {
        if (m_needCurrentMatched && 
            m_transportPositioner.IsMatched())
        {
            m_prevSampleTime = m_currentSampleTime;
            m_currentSampleTime = m_nextSampleTime;
            m_prevPointOnGraph = new TransportPositionerPointOnGraph(m_currentPointOnGraph);
            m_currentPointOnGraph = m_transportPositioner.GetPointOnGraph();
            m_needCurrentMatched = false;
        }

        if (m_needPathfind &&
            m_currentPointOnGraph.IsMatched &&
            m_prevPointOnGraph.IsMatched)
        {
            var pathfindResult = m_transportApi.FindShortestPath(new TransportPathfindOptionsBuilder()
                .SetPointOnGraphA(m_prevPointOnGraph)
                .SetPointOnGraphB(m_currentPointOnGraph)
                .Build());

            if (pathfindResult.IsPathFound)
            {
                m_needPathfind = false;
                m_pathfindResult = pathfindResult;
            }
        }
    }

    private void UpdatePointOnPath()
    {
        if (m_pathfindResult == null)
        {
            return;
        }

        var parameterizedDistanceAlongPath = CalcParameterizedDistanceAlongPath(m_time, m_prevSampleTime, m_currentSampleTime);
        var pointEcef = m_transportApi.GetPointEcefOnPath(m_pathfindResult, parameterizedDistanceAlongPath);
        var directionEcef = m_transportApi.GetDirectionEcefOnPath(m_pathfindResult, parameterizedDistanceAlongPath);
        var headingDegrees = m_spacesApi.HeadingDegreesFromDirectionAtPoint(directionEcef, pointEcef);

        m_capsule.SetActive(true);
        m_capsule.transform.localPosition = m_spacesApi.GeographicToWorldPoint(LatLongAltitude.FromECEF(pointEcef));
        m_capsule.transform.localEulerAngles = new Vector3(90.0f, (float)headingDegrees, 0.0f);
    }

    private static double CalcParameterizedDistanceAlongPath(double timeNow, double prevSampleTime, double currentSampleTime)
    {
        var delta = currentSampleTime - prevSampleTime;

        if (delta > 0.0)
        {
            return Math.Min(Math.Max((timeNow - prevSampleTime) / delta, 0.0), 1.0);
        }
        else
        {
            return 0.0;
        }
    }

    private GameObject CreateCapsule(Color color, float radius)
    {
        var capsule = GameObject.CreatePrimitive(PrimitiveType.Capsule);
        var material = new Material(Shader.Find("Sprites/Default"));
        material.color = color;
        capsule.GetComponent<Renderer>().material = material;
        capsule.transform.localScale = Vector3.one * radius;
        capsule.transform.parent = this.transform;
        return capsule;
    }
}
v0.8.17