// Copyright 2019 Esri
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ArcGIS.Desktop.Framework;
using ArcGIS.Desktop.Framework.Contracts;
using ArcGIS.Desktop.Mapping;
using ArcGIS.Core.Data;
using ArcGIS.Core.CIM;
using ArcGIS.Core.Geometry;
using ArcGIS.Desktop.Framework.Threading.Tasks;
using ArcGIS.Desktop.Framework.Dialogs;
namespace AnimationFromPath
{
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
public static class CreateAnimationFromPath
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
{
#region Constants
//determines where intermediate keyframes are created along a segment
private const double LINE_CONSTRAINT_FACTOR = 0.15;
private const double ARC_CONSTRAINT_FACTOR = 0.15;
//minimum length of a straight line segment needed to create intermediate keyframes
//and use additional logic for ignoring rotation on end points. If a straight line segment
//is smaller than this threshold then intermediate keyframes are not created and ignore-rotation-at-end-points
//logic is not used
private const double STRAIGHT_SEGMENT_LENGTH_THRESHOLD = 30;
private const double ANIMATION_APPEND_TIME = 3; //seconds
#endregion
#region Fields/Properties
private static double _keyframeHeading = 0;
private static double _keyframePitch = 0;
private static double Z_CONVERSION_FACTOR = 1;
private static string _selectedMethod = "Keyframes along path";
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
public static string SelectedMethod
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
{
get { return _selectedMethod; }
set { _selectedMethod = value; }
}
private static string _selectedCameraView = "Top down";
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
public static string SelectedCameraView
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
{
get { return _selectedCameraView; }
set { _selectedCameraView = value; }
}
private static double _customPitch = -90;
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
public static double CustomPitch
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
{
get { return _customPitch; }
set { _customPitch = value; }
}
private static double _cameraZOffset = 1000;
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
public static double CameraZOffset
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
{
get { return _cameraZOffset; }
set { _cameraZOffset = value; }
}
private static double _totalDuration = 0;
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
public static double TotalDuration
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
{
get { return _totalDuration; }
set { _totalDuration = value; }
}
private static double _keyEveryNSecond = 1;
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
public static double KeyEveryNSecond
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
{
get { return _keyEveryNSecond; }
set { _keyEveryNSecond = value; }
}
private static MapPoint _targetPoint = null;
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
public static MapPoint TargetPoint
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
{
get { return _targetPoint; }
set { _targetPoint = value; }
}
#endregion
#region Functions
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
public static async Task CreateKeyframes()
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
{
FeatureLayer ftrLayer = null;
MapView mapView = MapView.Active;
if (mapView == null)
return;
var mapSelection = await QueuedTask.Run(() => MapView.Active.Map.GetSelection());
if (mapSelection.Count == 1)
{
var layer = mapSelection.First().Key;
if (layer is FeatureLayer)
{
ftrLayer = (FeatureLayer)layer;
if (ftrLayer.ShapeType != ArcGIS.Core.CIM.esriGeometryType.esriGeometryPolyline)
{
ArcGIS.Desktop.Framework.Dialogs.MessageBox.Show("Select a polyline feature.");
return;
}
int numFtrsSelected = await QueuedTask.Run(() => ftrLayer.GetSelection().GetCount());
if (numFtrsSelected != 1)
{
ArcGIS.Desktop.Framework.Dialogs.MessageBox.Show("Select only one polyline feature.");
return;
}
}
else
{
ArcGIS.Desktop.Framework.Dialogs.MessageBox.Show("Select a polyline feature.");
return;
}
}
else
{
ArcGIS.Desktop.Framework.Dialogs.MessageBox.Show("Select a polyline feature.");
return;
}
if (SelectedCameraView == "Face target" && TargetPoint == null)
{
ArcGIS.Desktop.Framework.Dialogs.MessageBox.Show("Selected view type is - Face target - but a target point is not set.");
return;
}
string oid_fieldName = await QueuedTask.Run(() => ftrLayer.GetTable().GetDefinition().GetObjectIDField());
//get selected polyline
Polyline lineGeom = await QueuedTask.Run<Polyline>(() =>
{
var selectedFtrOID = MapView.Active.Map.GetSelection()[ftrLayer][0];
QueryFilter qf = new QueryFilter();
qf.WhereClause = oid_fieldName + " = " + selectedFtrOID.ToString();
RowCursor result = ftrLayer.GetFeatureClass().Search(qf);
if (result != null && result.MoveNext())
{
Polyline plyLine = null;
using (Feature selectedFtr = result.Current as Feature)
{
plyLine = selectedFtr.GetShape().Clone() as Polyline;
}
return plyLine;
}
return null;
});
//couldn't get the selected feature
if (lineGeom == null)
return;
ProjectionTransformation transformation = await QueuedTask.Run(() => ProjectionTransformation.Create(ftrLayer.GetSpatialReference(), mapView.Map.SpatialReference));
SpatialReference layerSpatRef = await QueuedTask.Run(() => ftrLayer.GetSpatialReference());
if (layerSpatRef.Unit.Name != "Degree")
Z_CONVERSION_FACTOR = layerSpatRef.Unit.ConversionFactor;
//Project target point if method is Face target
if (SelectedCameraView == "Face target")
{
if (TargetPoint != null && TargetPoint.SpatialReference != layerSpatRef)
{
ProjectionTransformation transf_forTarget = await QueuedTask.Run(() => ProjectionTransformation.Create(TargetPoint.SpatialReference, layerSpatRef));
MapPoint projected_targetPoint = (MapPoint)GeometryEngine.Instance.ProjectEx(TargetPoint, transf_forTarget);
TargetPoint = null;
TargetPoint = projected_targetPoint;
}
}
var animation = mapView.Map.Animation;
var cameraTrack = animation.Tracks.OfType<CameraTrack>().First();
var keyframes = cameraTrack.Keyframes;
//Get segment list for line
ReadOnlyPartCollection polylineParts = lineGeom.Parts;
//get total segment count and determine path length
double pathLength = 0;
int segmentCount = 0;
IEnumerator<ReadOnlySegmentCollection> segments = polylineParts.GetEnumerator();
while (segments.MoveNext())
{
ReadOnlySegmentCollection seg = segments.Current;
foreach (Segment s in seg)
{
//pathLength += s.Length;//s.Length returns 2D length
double length3D = Math.Sqrt((s.EndPoint.X - s.StartPoint.X) * (s.EndPoint.X - s.StartPoint.X) +
(s.EndPoint.Y - s.StartPoint.Y) * (s.EndPoint.Y - s.StartPoint.Y) +
(s.EndPoint.Z - s.StartPoint.Z) * (s.EndPoint.Z - s.StartPoint.Z));
pathLength += length3D;
segmentCount += 1;
}
}
//reset heading and pitch
_keyframeHeading = 0;
_keyframePitch = 0;
// Create keyframes based on chosen method
if (SelectedMethod == "Keyframes along path")
{
await CreateKeyframes_AlongPath(mapView, layerSpatRef, transformation, cameraTrack, segments, segmentCount, pathLength);
}
else if (SelectedMethod == "Keyframes every N seconds")
{
await CreateKeyframes_EveryNSeconds(mapView, layerSpatRef, transformation, cameraTrack, segments, segmentCount, pathLength, KeyEveryNSecond);
}
else if (SelectedMethod == "Keyframes only at vertices")
{
await CreateKeyframes_AtVertices(mapView, layerSpatRef, transformation, cameraTrack, lineGeom, segments, segmentCount, pathLength);
}
}
//Use this method for smoother turns at corners. Additionally this method processes straight line segments and arc segments separately
//For arc segments a keyframe is created at every second. However a minimum of 5 keyframes are created for arcs.
//So if arc segment length is less than 5 then we default to at least 5 keyframes. This is an attempt to stick to the path as much as possible.
//For straight line segments, rotation is ignored at end point of each segment except for the end point of the path itself. Two keyframes with rotation
//are created at certain distance (determined by LINE_CONSTRAINT_FACTOR) before and after the end point of each segment. This is an attempt to avoid
//sharp turns at corners along the path.
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
public static async Task CreateKeyframes_AlongPath(MapView mapView, SpatialReference layerSpatRef, ProjectionTransformation transformation,
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
CameraTrack cameraTrack, IEnumerator<ReadOnlySegmentCollection> segments,
int segmentCount, double pathLength)
{
double segmentLength = 0;
int num_iterations = 0;
segments.Reset();
//process each segment depending upon its type - straight line or arc
while (segments.MoveNext())
{
ReadOnlySegmentCollection seg = segments.Current;
double accumulatedDuration = mapView.Map.Animation.Duration.TotalSeconds + ((mapView.Map.Animation.Duration.TotalSeconds > 0) ? ANIMATION_APPEND_TIME : 0); // 0;
foreach (Segment s in seg)
{
double length3D = Math.Sqrt((s.EndPoint.X - s.StartPoint.X) * (s.EndPoint.X - s.StartPoint.X) +
(s.EndPoint.Y - s.StartPoint.Y) * (s.EndPoint.Y - s.StartPoint.Y) +
(s.EndPoint.Z - s.StartPoint.Z) * (s.EndPoint.Z - s.StartPoint.Z));
double segmentDuration = (TotalDuration / pathLength) * length3D;
segmentLength = length3D;
//straight line segments
if (s.SegmentType == SegmentType.Line)
{
MapPoint startPt = await QueuedTask.Run(() => MapPointBuilder.CreateMapPoint(s.StartPoint.X, s.StartPoint.Y, s.StartPoint.Z * Z_CONVERSION_FACTOR, layerSpatRef));
MapPoint endPt = await QueuedTask.Run(() => MapPointBuilder.CreateMapPoint(s.EndPoint.X, s.EndPoint.Y, s.EndPoint.Z * Z_CONVERSION_FACTOR, layerSpatRef));
//we will be creating three intermediate keyframes for staright segments only if segment length is more than a set threshold
//the threshold is just a guess and might have to be altered depending upon the path geometry. Should work for most cases though
MapPoint firstIntPoint = null;
MapPoint midIntPoint = null;
MapPoint lastIntPoint = null;
if (segmentLength >= STRAIGHT_SEGMENT_LENGTH_THRESHOLD)
{
//first intermediate point
firstIntPoint = await CreatePointAlongSegment(startPt, endPt, LINE_CONSTRAINT_FACTOR * segmentLength, layerSpatRef);
//mid point
midIntPoint = await CreatePointAlongSegment(startPt, endPt, 0.5 * segmentLength, layerSpatRef);
//last intermediate point
lastIntPoint = await CreatePointAlongSegment(startPt, endPt, (1 - LINE_CONSTRAINT_FACTOR) * segmentLength, layerSpatRef);
}
//create keyframe at start vertex of path in map space
double timeSpanValue = accumulatedDuration;
TimeSpan keyframeTimespan = TimeSpan.FromSeconds(timeSpanValue);
if (segmentLength >= STRAIGHT_SEGMENT_LENGTH_THRESHOLD)
{
SetPitchAndHeadingForLine(startPt, firstIntPoint);
}
else
{
SetPitchAndHeadingForLine(startPt, endPt);
}
//ignore rotation for all start vertices (which would also be end vertices of previous segments) EXCEPT for the first vertex of path
if (num_iterations == 0 || segmentLength < STRAIGHT_SEGMENT_LENGTH_THRESHOLD)
{
await CreateCameraKeyframe(mapView, startPt, transformation, cameraTrack, keyframeTimespan, _keyframePitch, _keyframeHeading);
}
else
{
await CreateCameraKeyframe(mapView, startPt, transformation, cameraTrack, keyframeTimespan, _keyframePitch, _keyframeHeading, true, false);
}
if (segmentLength > STRAIGHT_SEGMENT_LENGTH_THRESHOLD)
{
//Create a keyframe at PATH_CONSTRAINT_FACTOR distance along the segment from start point
double distanceAlong = LINE_CONSTRAINT_FACTOR * segmentLength;
timeSpanValue = accumulatedDuration + LINE_CONSTRAINT_FACTOR * segmentDuration;
keyframeTimespan = TimeSpan.FromSeconds(timeSpanValue);
SetPitchAndHeadingForLine(firstIntPoint, midIntPoint);
await CreateCameraKeyframe(mapView, firstIntPoint, transformation, cameraTrack, keyframeTimespan, _keyframePitch, _keyframeHeading);
//Create a keyframe at middle of segment
distanceAlong = 0.5 * segmentLength;
timeSpanValue = accumulatedDuration + 0.5 * segmentDuration;
keyframeTimespan = TimeSpan.FromSeconds(timeSpanValue);
SetPitchAndHeadingForLine(midIntPoint, lastIntPoint);
//await CreateCameraKeyframe(mapView, midIntPoint, transformation, cameraTrack, keyframeTimespan, _keyframePitch, _keyframeHeading);
//Create a keyframe at (1 - PATH_CONSTRAINT_FACTOR) distance along the segment from start point
distanceAlong = (1 - LINE_CONSTRAINT_FACTOR) * segmentLength;
timeSpanValue = accumulatedDuration + (1 - LINE_CONSTRAINT_FACTOR) * segmentDuration;
keyframeTimespan = TimeSpan.FromSeconds(timeSpanValue);
SetPitchAndHeadingForLine(lastIntPoint, endPt);
await CreateCameraKeyframe(mapView, lastIntPoint, transformation, cameraTrack, keyframeTimespan, _keyframePitch, _keyframeHeading);
}
//Create a keyframe at end point of segment only for the end point of last segment
//Otherwise we will get duplicate keyframes at end of one segment and start of the next one
if (num_iterations == segmentCount - 1)
{
timeSpanValue = accumulatedDuration + segmentDuration;
keyframeTimespan = TimeSpan.FromSeconds(timeSpanValue);
if (SelectedCameraView == "Face target")
{
SetPitchAndHeadingForLine(endPt, TargetPoint);
}
await CreateCameraKeyframe(mapView, endPt, transformation, cameraTrack, keyframeTimespan, _keyframePitch, _keyframeHeading);
}
}
//processing for arcs - create a keyframe every second for arcs
//we will create a minimum of 5 keyframes along the arc
else if (s.SegmentType == SegmentType.EllipticArc && segmentDuration > 5)
{
EllipticArcSegment ellipArc = s as EllipticArcSegment;
MapPoint startPt = await QueuedTask.Run(() => MapPointBuilder.CreateMapPoint(s.StartPoint.X, s.StartPoint.Y, s.StartPoint.Z, layerSpatRef));
MapPoint endPt = await QueuedTask.Run(() => MapPointBuilder.CreateMapPoint(s.EndPoint.X, s.EndPoint.Y, s.EndPoint.Z, layerSpatRef));
double radius = Math.Sqrt((ellipArc.CenterPoint.X - startPt.X) * (ellipArc.CenterPoint.X - startPt.X) + (ellipArc.CenterPoint.Y - startPt.Y) * (ellipArc.CenterPoint.Y - startPt.Y));
double angle = ellipArc.CentralAngle;
MapPoint centerPt = await QueuedTask.Run(() => MapPointBuilder.CreateMapPoint(ellipArc.CenterPoint.X, ellipArc.CenterPoint.Y, (s.StartPoint.Z + s.EndPoint.Z) / 2, layerSpatRef));
int num_keys = (int)segmentDuration;
MapPoint firstIntPoint = null;
//first intermediate keyframe for arc - needed for setting heading for start vertex
// >2 to account for start and end
if (num_keys > 2)
{
firstIntPoint = await CreatePointAlongArc(startPt, endPt, centerPt, angle / (num_keys - 1), radius, layerSpatRef, ellipArc.IsMinor, ellipArc.IsCounterClockwise);
}
//Create keyframe at start vertex of path in map space
double timeSpanValue = accumulatedDuration;
TimeSpan keyframeTimespan = TimeSpan.FromSeconds(timeSpanValue);
if (firstIntPoint != null)
{
SetPitchAndHeadingForLine(startPt, firstIntPoint);
}
else
{
SetPitchAndHeadingForLine(startPt, endPt);
}
//Ignore rotation for all start vertices EXCEPT for the first vertex of path
if (num_iterations == 0)
{
await CreateCameraKeyframe(mapView, startPt, transformation, cameraTrack, keyframeTimespan, _keyframePitch, _keyframeHeading);
}
else
{
await CreateCameraKeyframe(mapView, startPt, transformation, cameraTrack, keyframeTimespan, _keyframePitch, _keyframeHeading, true, false);
}
//Loop to create intermediate keyframes at each second
for (int i = 0; i < num_keys - 2; i++)
{
MapPoint currentIntPoint = null;
MapPoint nextIntPoint = null;
currentIntPoint = await CreatePointAlongArc(startPt, endPt, centerPt, (angle / (num_keys - 1)) * (i + 1), radius, layerSpatRef, ellipArc.IsMinor, ellipArc.IsCounterClockwise);
if (i < num_keys - 3)
{
nextIntPoint = await CreatePointAlongArc(startPt, endPt, centerPt, (angle / (num_keys - 1)) * (i + 2), radius, layerSpatRef, ellipArc.IsMinor, ellipArc.IsCounterClockwise);
}
else //for the last intermediate keyframe, heading/pitch has to be determined relative to the end point fo segment
{
nextIntPoint = endPt;
}
//timeSpanValue = accumulatedDuration + (i + 1) * 1; //at each second
timeSpanValue = accumulatedDuration + (i + 1) * (segmentDuration / (num_keys - 1));
keyframeTimespan = TimeSpan.FromSeconds(timeSpanValue);
SetPitchAndHeadingForLine(currentIntPoint, nextIntPoint);
await CreateCameraKeyframe(mapView, currentIntPoint, transformation, cameraTrack, keyframeTimespan, _keyframePitch, _keyframeHeading);
}
//Create a keyframe at end point of segment only for the end point of last segment
if (num_iterations == segmentCount - 1)
{
timeSpanValue = accumulatedDuration + segmentDuration;
keyframeTimespan = TimeSpan.FromSeconds(timeSpanValue);
if (SelectedCameraView == "Face target")
{
SetPitchAndHeadingForLine(endPt, TargetPoint);
}
await CreateCameraKeyframe(mapView, endPt, transformation, cameraTrack, keyframeTimespan, _keyframePitch, _keyframeHeading);
}
}
//create a minimum of 5 keyframes along the arc
else if (s.SegmentType == SegmentType.EllipticArc)
{
EllipticArcSegment ellipArc = s as EllipticArcSegment;
MapPoint startPt = await QueuedTask.Run(() => MapPointBuilder.CreateMapPoint(s.StartPoint.X, s.StartPoint.Y, s.StartPoint.Z, layerSpatRef));
MapPoint endPt = await QueuedTask.Run(() => MapPointBuilder.CreateMapPoint(s.EndPoint.X, s.EndPoint.Y, s.EndPoint.Z, layerSpatRef));
double radius = Math.Sqrt((ellipArc.CenterPoint.X - startPt.X) * (ellipArc.CenterPoint.X - startPt.X) + (ellipArc.CenterPoint.Y - startPt.Y) * (ellipArc.CenterPoint.Y - startPt.Y));
double angle = ellipArc.CentralAngle;
MapPoint centerPt = await QueuedTask.Run(() => MapPointBuilder.CreateMapPoint(ellipArc.CenterPoint.X, ellipArc.CenterPoint.Y, (s.StartPoint.Z + s.EndPoint.Z) / 2, layerSpatRef));
//we are creating five intermediate keyframes for arcs
MapPoint firstIntPoint = await CreatePointAlongArc(startPt, endPt, centerPt, angle * ARC_CONSTRAINT_FACTOR, radius, layerSpatRef, ellipArc.IsMinor, ellipArc.IsCounterClockwise);
MapPoint secondIntPoint = await CreatePointAlongArc(startPt, endPt, centerPt, angle * ARC_CONSTRAINT_FACTOR * 2, radius, layerSpatRef, ellipArc.IsMinor, ellipArc.IsCounterClockwise);
MapPoint midIntPoint = await CreatePointAlongArc(startPt, endPt, centerPt, angle * 0.5, radius, layerSpatRef, ellipArc.IsMinor, ellipArc.IsCounterClockwise);
MapPoint secondLastIntPoint = await CreatePointAlongArc(startPt, endPt, centerPt, angle * (1 - ARC_CONSTRAINT_FACTOR * 2), radius, layerSpatRef, ellipArc.IsMinor, ellipArc.IsCounterClockwise);
MapPoint lastIntPoint = await CreatePointAlongArc(startPt, endPt, centerPt, angle * (1 - ARC_CONSTRAINT_FACTOR), radius, layerSpatRef, ellipArc.IsMinor, ellipArc.IsCounterClockwise);
//Create keyframe at start vertex of path in map space
double timeSpanValue = accumulatedDuration;
TimeSpan keyframeTimespan = TimeSpan.FromSeconds(timeSpanValue);
SetPitchAndHeadingForLine(startPt, firstIntPoint);
//Ignore rotation for all start vertices EXCEPT for the first vertex of path
if (num_iterations == 0)
{
await CreateCameraKeyframe(mapView, startPt, transformation, cameraTrack, keyframeTimespan, _keyframePitch, _keyframeHeading);
}
else
{
await CreateCameraKeyframe(mapView, startPt, transformation, cameraTrack, keyframeTimespan, _keyframePitch, _keyframeHeading, true, false);
}
//await CreateCameraKeyframe(mapView, startPt, transformation, cameraTrack, keyframeTimespan, _keyframePitch, _keyframeHeading);
//Create a keyframe at PATH_CONSTRAINT_FACTOR distance along the segment from start point
timeSpanValue = accumulatedDuration + ARC_CONSTRAINT_FACTOR * segmentDuration;
keyframeTimespan = TimeSpan.FromSeconds(timeSpanValue);
SetPitchAndHeadingForLine(firstIntPoint, secondIntPoint);
await CreateCameraKeyframe(mapView, firstIntPoint, transformation, cameraTrack, keyframeTimespan, _keyframePitch, _keyframeHeading);
//Create a keyframe at 2* PATH_CONSTRAINT_FACTOR distance along the segment from start point
timeSpanValue = accumulatedDuration + ARC_CONSTRAINT_FACTOR * 2 * segmentDuration;
keyframeTimespan = TimeSpan.FromSeconds(timeSpanValue);
SetPitchAndHeadingForLine(secondIntPoint, midIntPoint);
await CreateCameraKeyframe(mapView, secondIntPoint, transformation, cameraTrack, keyframeTimespan, _keyframePitch, _keyframeHeading);
//Create a keyframe at middle of segment
timeSpanValue = accumulatedDuration + 0.5 * segmentDuration;
keyframeTimespan = TimeSpan.FromSeconds(timeSpanValue);
SetPitchAndHeadingForLine(midIntPoint, secondLastIntPoint);
await CreateCameraKeyframe(mapView, midIntPoint, transformation, cameraTrack, keyframeTimespan, _keyframePitch, _keyframeHeading);
//Create a keyframe at (1 - PATH_CONSTRAINT_FACTOR * 2) distance along the segment from start point
timeSpanValue = accumulatedDuration + (1 - ARC_CONSTRAINT_FACTOR * 2) * segmentDuration;
keyframeTimespan = TimeSpan.FromSeconds(timeSpanValue);
SetPitchAndHeadingForLine(secondLastIntPoint, lastIntPoint);
await CreateCameraKeyframe(mapView, secondLastIntPoint, transformation, cameraTrack, keyframeTimespan, _keyframePitch, _keyframeHeading);
//Create a keyframe at (1 - PATH_CONSTRAINT_FACTOR) distance along the segment from start point
timeSpanValue = accumulatedDuration + (1 - ARC_CONSTRAINT_FACTOR) * segmentDuration;
keyframeTimespan = TimeSpan.FromSeconds(timeSpanValue);
SetPitchAndHeadingForLine(lastIntPoint, endPt);
await CreateCameraKeyframe(mapView, lastIntPoint, transformation, cameraTrack, keyframeTimespan, _keyframePitch, _keyframeHeading);
//Create a keyframe at end point of segment only for the end point of last segment
if (num_iterations == segmentCount - 1)
{
timeSpanValue = accumulatedDuration + segmentDuration;
keyframeTimespan = TimeSpan.FromSeconds(timeSpanValue);
if (SelectedCameraView == "Face target")
{
SetPitchAndHeadingForLine(endPt, TargetPoint);
}
await CreateCameraKeyframe(mapView, endPt, transformation, cameraTrack, keyframeTimespan, _keyframePitch, _keyframeHeading);
}
}
accumulatedDuration += segmentDuration;
num_iterations++;
}
}
}
//Use this method to create a keyframe at every n-second of the specified animation duration
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
public static async Task CreateKeyframes_EveryNSeconds(MapView mapView, SpatialReference layerSpatRef, ProjectionTransformation transformation,
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
CameraTrack cameraTrack, IEnumerator<ReadOnlySegmentCollection> segments,
int segmentCount, double pathLength, double keyEveryNSecond = 1)
{
double segmentLength = 0;
int numKeysToCreate = (int)(TotalDuration / keyEveryNSecond); //approximately
double createKeyAtDist = pathLength / numKeysToCreate;
double skippedDistance = 0;
double accumulatedDuration = mapView.Map.Animation.Duration.TotalSeconds + ((mapView.Map.Animation.Duration.TotalSeconds > 0) ? ANIMATION_APPEND_TIME : 0); // 0;
int num_iterations = 0;
segments.Reset();
List<MapPoint> pointsForKeyframes = new List<MapPoint>();
MapPoint pathEndPt = null;
//process each segment depending upon its type - straight line or arc
while (segments.MoveNext())
{
ReadOnlySegmentCollection seg = segments.Current;
foreach (Segment s in seg)
{
segmentLength = Math.Sqrt((s.EndPoint.X - s.StartPoint.X) * (s.EndPoint.X - s.StartPoint.X) +
(s.EndPoint.Y - s.StartPoint.Y) * (s.EndPoint.Y - s.StartPoint.Y) +
(s.EndPoint.Z - s.StartPoint.Z) * (s.EndPoint.Z - s.StartPoint.Z));
double segmentDuration = (TotalDuration / pathLength) * segmentLength;
//straight line segments
if (s.SegmentType == SegmentType.Line)
{
MapPoint startPt = await QueuedTask.Run(() => MapPointBuilder.CreateMapPoint(s.StartPoint.X, s.StartPoint.Y, s.StartPoint.Z, layerSpatRef));
MapPoint endPt = await QueuedTask.Run(() => MapPointBuilder.CreateMapPoint(s.EndPoint.X, s.EndPoint.Y, s.EndPoint.Z, layerSpatRef));
//add start of path to points collection
if (num_iterations == 0)
{
pointsForKeyframes.Add(startPt);
}
if (num_iterations == segmentCount - 1 || segmentCount == 1)
{
pathEndPt = endPt; //store path end pt. This will be the last keyframe.
}
double distCoveredAlongSeg = Math.Abs(createKeyAtDist - skippedDistance); //we are accouunting for skipped distances from previous segments
if (distCoveredAlongSeg < segmentLength)
{
MapPoint keyPt = await CreatePointAlongSegment(startPt, endPt, distCoveredAlongSeg, layerSpatRef);
//add point to collection
pointsForKeyframes.Add(keyPt);
//skipped distance is used now, reset to zero
skippedDistance = 0;
//are more keyframes possible for this segment
bool moreKeysPossible = ((segmentLength - distCoveredAlongSeg) >= createKeyAtDist);
while (moreKeysPossible)
{
double keyAtDistAlongSeg = distCoveredAlongSeg + createKeyAtDist;
keyPt = await CreatePointAlongSegment(startPt, endPt, keyAtDistAlongSeg, layerSpatRef);
//add point to collection
pointsForKeyframes.Add(keyPt);
distCoveredAlongSeg += createKeyAtDist;
moreKeysPossible = ((segmentLength - distCoveredAlongSeg) > createKeyAtDist);
}
//if any segment length left then add to skipped distance
skippedDistance += (segmentLength - distCoveredAlongSeg);
}
else
{
//add this segment's length to skipped distance as no keyframe could be created along it
skippedDistance += segmentLength;
}
}
else if (s.SegmentType == SegmentType.EllipticArc)
{
EllipticArcSegment ellipArc = s as EllipticArcSegment;
MapPoint startPt = await QueuedTask.Run(() => MapPointBuilder.CreateMapPoint(s.StartPoint.X, s.StartPoint.Y, s.StartPoint.Z, layerSpatRef));
MapPoint endPt = await QueuedTask.Run(() => MapPointBuilder.CreateMapPoint(s.EndPoint.X, s.EndPoint.Y, s.EndPoint.Z, layerSpatRef));
double radius = Math.Sqrt((ellipArc.CenterPoint.X - startPt.X) * (ellipArc.CenterPoint.X - startPt.X) + (ellipArc.CenterPoint.Y - startPt.Y) * (ellipArc.CenterPoint.Y - startPt.Y));
double angle = ellipArc.CentralAngle;
MapPoint centerPt = await QueuedTask.Run(() => MapPointBuilder.CreateMapPoint(ellipArc.CenterPoint.X, ellipArc.CenterPoint.Y, (s.StartPoint.Z + s.EndPoint.Z) / 2, layerSpatRef));
//add start of path to points collection
if (num_iterations == 0)
{
pointsForKeyframes.Add(startPt);
}
if (num_iterations == segmentCount - 1 || segmentCount == 1)
{
pathEndPt = endPt; //store path end pt. This will be the last keyframe.
}
double distCoveredAlongSeg = Math.Abs(createKeyAtDist - skippedDistance); //we are accouunting for skipped distances from previous segments
if (distCoveredAlongSeg < segmentLength)
{
MapPoint keyPt = await CreatePointAlongArc(startPt, endPt, centerPt, angle * distCoveredAlongSeg / segmentLength, radius, layerSpatRef, ellipArc.IsMinor, ellipArc.IsCounterClockwise);
//add point to collection
pointsForKeyframes.Add(keyPt);
//skipped distance is used now, reset to zero
skippedDistance = 0;
//are more keyframes possible for this segment
bool moreKeysPossible = ((segmentLength - distCoveredAlongSeg) >= createKeyAtDist);
while (moreKeysPossible)
{
double keyAtDistAlongSeg = distCoveredAlongSeg + createKeyAtDist;
keyPt = await CreatePointAlongArc(startPt, endPt, centerPt, angle * keyAtDistAlongSeg / segmentLength, radius, layerSpatRef, ellipArc.IsMinor, ellipArc.IsCounterClockwise);
//add point to collection
pointsForKeyframes.Add(keyPt);
distCoveredAlongSeg += createKeyAtDist;
moreKeysPossible = ((segmentLength - distCoveredAlongSeg) > createKeyAtDist);
}
//if any segment length left then add to skipped distance
skippedDistance += (segmentLength - distCoveredAlongSeg);
}
else
{
//add this segment's length to skipped distance as no keyframe could be created along it
skippedDistance += segmentLength;
}
}
num_iterations++;
}
}
//now iterate over the points list and create keyframes
double timeSpanValue = accumulatedDuration;
TimeSpan keyframeTimespan = TimeSpan.FromSeconds(timeSpanValue);
for (int i = 0; i < pointsForKeyframes.Count; i++)
{
MapPoint currentPt = pointsForKeyframes[i];
MapPoint nextPt = null;
if (i + 1 < pointsForKeyframes.Count)
{
nextPt = pointsForKeyframes[i + 1];
}
else
{
nextPt = pathEndPt;
}
timeSpanValue = i * keyEveryNSecond + accumulatedDuration;
keyframeTimespan = TimeSpan.FromSeconds(timeSpanValue);
SetPitchAndHeadingForLine(currentPt, nextPt);
await CreateCameraKeyframe(mapView, currentPt, transformation, cameraTrack, keyframeTimespan, _keyframePitch, _keyframeHeading);
if (i == pointsForKeyframes.Count - 1 && skippedDistance > 0)
{
keyframeTimespan = TimeSpan.FromSeconds(TotalDuration + accumulatedDuration);
await CreateCameraKeyframe(mapView, pathEndPt, transformation, cameraTrack, keyframeTimespan, _keyframePitch, _keyframeHeading);
}
}
}
//Use this method if you want keyframes ONLY at line vertices. This is good if the line is highly densified.
//However, you will get sharp turns at corners because there is no attempt to smooth the animation
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
public static async Task CreateKeyframes_AtVertices(MapView mapView, SpatialReference layerSpatRef, ProjectionTransformation transformation,
#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
CameraTrack cameraTrack, Polyline lineGeom, IEnumerator<ReadOnlySegmentCollection> segments,
int segmentCount, double pathLength)
{
double segmentLength = 0;
int num_iterations = 0;
segments.Reset();
//process each segment depending upon its type - straight line or arc
while (segments.MoveNext())
{
ReadOnlySegmentCollection seg = segments.Current;
double accumulatedDuration = mapView.Map.Animation.Duration.TotalSeconds + ((mapView.Map.Animation.Duration.TotalSeconds > 0) ? ANIMATION_APPEND_TIME : 0); // 0;
foreach (Segment s in seg)
{
segmentLength = Math.Sqrt((s.EndPoint.X - s.StartPoint.X) * (s.EndPoint.X - s.StartPoint.X) +
(s.EndPoint.Y - s.StartPoint.Y) * (s.EndPoint.Y - s.StartPoint.Y) +
(s.EndPoint.Z - s.StartPoint.Z) * (s.EndPoint.Z - s.StartPoint.Z));
double segmentDuration = (TotalDuration / pathLength) * segmentLength;
MapPoint startPt = await QueuedTask.Run(() => MapPointBuilder.CreateMapPoint(s.StartPoint.X, s.StartPoint.Y, s.StartPoint.Z, layerSpatRef));
MapPoint endPt = await QueuedTask.Run(() => MapPointBuilder.CreateMapPoint(s.EndPoint.X, s.EndPoint.Y, s.EndPoint.Z, layerSpatRef));
//create keyframe at start vertex of path in map space
double timeSpanValue = accumulatedDuration;
TimeSpan keyframeTimespan = TimeSpan.FromSeconds(timeSpanValue);
SetPitchAndHeadingForLine(startPt, endPt);
await CreateCameraKeyframe(mapView, startPt, transformation, cameraTrack, keyframeTimespan, _keyframePitch, _keyframeHeading);
//Create a keyframe at end point of segment only for the end point of last segment
//Otherwise we will get duplicate keyframes at end of one segment and start of the next one
if (num_iterations == segmentCount - 1)
{
timeSpanValue = accumulatedDuration + segmentDuration;
keyframeTimespan = TimeSpan.FromSeconds(timeSpanValue);
if (SelectedCameraView == "Face target")
{
SetPitchAndHeadingForLine(endPt, TargetPoint);
}
await CreateCameraKeyframe(mapView, endPt, transformation, cameraTrack, keyframeTimespan, _keyframePitch, _keyframeHeading);
}
accumulatedDuration += segmentDuration;
num_iterations++;
}
}
}
private static async Task<MapPoint> CreatePointAlongSegment(MapPoint startPt, MapPoint endPt, double distanceFromStartPoint, SpatialReference spatRef)
{
System.Windows.Media.Media3D.Point3D fromPt = new System.Windows.Media.Media3D.Point3D(startPt.X, startPt.Y, startPt.Z);
System.Windows.Media.Media3D.Point3D toPt = new System.Windows.Media.Media3D.Point3D(endPt.X, endPt.Y, endPt.Z);
System.Windows.Media.Media3D.Vector3D lineVec = new System.Windows.Media.Media3D.Vector3D(toPt.X - fromPt.X, toPt.Y - fromPt.Y, toPt.Z - fromPt.Z);
lineVec.Normalize();
System.Windows.Media.Media3D.Point3D ptAlong = new System.Windows.Media.Media3D.Point3D(fromPt.X + distanceFromStartPoint * (lineVec.X),
fromPt.Y + distanceFromStartPoint * (lineVec.Y),
fromPt.Z + distanceFromStartPoint * (lineVec.Z));
MapPoint intermediateKeyframePoint = await QueuedTask.Run(() => MapPointBuilder.CreateMapPoint(ptAlong.X, ptAlong.Y, ptAlong.Z, spatRef));
return intermediateKeyframePoint;
}
private static async Task<MapPoint> CreatePointAlongArc(MapPoint startPt, MapPoint endPt, MapPoint centerPt, double angle, double radius, SpatialReference spatRef, bool arcIsMinor, bool arcIsCounterClockwise)
{
System.Windows.Media.Media3D.Vector3D start = new System.Windows.Media.Media3D.Vector3D(startPt.X - centerPt.X, startPt.Y - centerPt.Y, startPt.Z - centerPt.Z);
System.Windows.Media.Media3D.Vector3D end = new System.Windows.Media.Media3D.Vector3D(endPt.X - centerPt.X, endPt.Y - centerPt.Y, endPt.Z - centerPt.Z);
System.Windows.Media.Media3D.Vector3D normalOfPlane = new System.Windows.Media.Media3D.Vector3D();
normalOfPlane = System.Windows.Media.Media3D.Vector3D.CrossProduct(start, end);
//Two ortho vectors: orthoVec and start
System.Windows.Media.Media3D.Vector3D orthoVec = new System.Windows.Media.Media3D.Vector3D();
orthoVec = System.Windows.Media.Media3D.Vector3D.CrossProduct(normalOfPlane, start);
//If this is not done then half of the keyframes for S-shaped curve are not on the curve
if (arcIsMinor && !arcIsCounterClockwise)
orthoVec.Negate();
//Normalize
start.Normalize();
orthoVec.Normalize();
System.Windows.Media.Media3D.Vector3D ptAlong = new System.Windows.Media.Media3D.Vector3D();
ptAlong = radius * Math.Cos(angle) * start + radius * Math.Sin(angle) * orthoVec;
MapPoint intermediateKeyframePoint = await QueuedTask.Run(() => MapPointBuilder.CreateMapPoint(ptAlong.X + centerPt.X, ptAlong.Y + centerPt.Y, ptAlong.Z + centerPt.Z, spatRef));
return intermediateKeyframePoint;
}
private static async Task CreateCameraKeyframe(MapView mapView, MapPoint orig_cameraPoint, ProjectionTransformation transformation,
CameraTrack cameraTrack, TimeSpan currentTimespanValue, double pitch, double heading, bool ignoreRotation = false, bool ignoreTranslation = false)
{
await QueuedTask.Run(() =>
{
Keyframe keyFrame = null;
MapPoint projected_cameraPoint = (MapPoint)GeometryEngine.Instance.ProjectEx(orig_cameraPoint, transformation);
if (mapView.ViewingMode == MapViewingMode.Map)
{
var camera = new Camera(projected_cameraPoint.X, projected_cameraPoint.Y, CameraZOffset, heading, null, CameraViewpoint.LookAt);
keyFrame = cameraTrack.CreateKeyframe(camera, currentTimespanValue, AnimationTransition.FixedArc, .5);
}
else
{
var camera = new Camera(projected_cameraPoint.X, projected_cameraPoint.Y, (projected_cameraPoint.Z + CameraZOffset), pitch, heading, null, CameraViewpoint.LookAt);
keyFrame = cameraTrack.CreateKeyframe(camera, currentTimespanValue, AnimationTransition.FixedArc, .5);
}
if (ignoreRotation)
{
CameraKeyframe camKey = keyFrame as CameraKeyframe;
camKey.HeadingTransition = AnimationTransition.None;
camKey.RollTransition = AnimationTransition.None;
camKey.PitchTransition = AnimationTransition.None;
}
if (ignoreTranslation)
{
CameraKeyframe camKey = keyFrame as CameraKeyframe;
camKey.XTransition = AnimationTransition.None;
camKey.YTransition = AnimationTransition.None;
camKey.ZTransition = AnimationTransition.None;
}
});
}
private static void SetPitchAndHeadingForLine(MapPoint startPt, MapPoint endPt)
{
if (SelectedCameraView == "Top down")
{
_keyframeHeading = CalculateHeading(startPt, endPt);
_keyframePitch = -90;
}
else if (SelectedCameraView == "Top down - face north")
{
_keyframeHeading = 0;
_keyframePitch = -90;
}
else if (SelectedCameraView == "Custom pitch")
{
_keyframeHeading = CalculateHeading(startPt, endPt);
_keyframePitch = CustomPitch;
}
else if (SelectedCameraView == "View along" || SelectedCameraView == "Face backward")
{
_keyframeHeading = CalculateHeading(startPt, endPt);
_keyframePitch = CalculatePitch(startPt, endPt);
}
else if (SelectedCameraView == "Face target")
{
_keyframeHeading = CalculateHeading(startPt, TargetPoint);
_keyframePitch = CalculatePitch(startPt, TargetPoint);
}
}
private static double CalculateHeading(MapPoint startPt, MapPoint endPt)
{
double dx, dy, dz, angle, heading;
if (SelectedCameraView == "Top down - face north")
{
heading = 0;
}
else
{
dx = endPt.X - startPt.X;
dy = endPt.Y - startPt.Y;
//need to apply z-conversion factor to target Z
dz = (SelectedCameraView == "Face target") ? endPt.Z * Z_CONVERSION_FACTOR - startPt.Z : endPt.Z - startPt.Z;
angle = Math.Atan2(dy, dx);
heading = 180 + (90 + angle * 180 / Math.PI);
if (SelectedCameraView == "Face backward") { heading = heading - 180; }
}
return heading;
}
private static double CalculatePitch(MapPoint startPt, MapPoint endPt)
{
double dx, dy, dz, pitchForLookingAtTarget;
dx = startPt.X - endPt.X;
dy = startPt.Y - endPt.Y;
//dz = startPt.Z - endPt.Z;
//need to apply z-conversion factor to target Z
dz = (SelectedCameraView == "Face target") ? startPt.Z - endPt.Z * Z_CONVERSION_FACTOR : startPt.Z - endPt.Z;
//try dividing by 0.00017 unit conversion factor if units = degree
SpatialReference spatRef = startPt.SpatialReference;
if (spatRef.Unit.Name == "Degree")
{
dx = dx * 111000;
dy = dy * 111000;
}
double test = dz / Math.Sqrt(dx * dx + dy * dy + dz * dz);
pitchForLookingAtTarget = -(90 - Math.Acos(test) * 180 / Math.PI);
return pitchForLookingAtTarget;
}
#endregion
}
}