Maria globe client: Difference between revisions

From Maria GDK Wiki
Jump to navigation Jump to search
()
()
Line 1,503: Line 1,503:
. . .
. . .
</source>
</source>


==== Running with display of 3D settings ====
==== Running with display of 3D settings ====
Line 1,510: Line 1,511:
{| class="wikitable"
{| class="wikitable"
|-
|-
| [[File:3D_Set_P90.png|none|450px|thumb|ma am ]] || [[File:3D_Set_P15.png|none|450px|thumb|ma am ]]
|
[[File:3D_Set_P90.png|450px|Default 3D setting ]]<br>
Default 3D setting
|
[[File:3D_Set_P15.png|450px| Modified ''pitch'' ]]<br>
Modified ''pitch''
|-
|-
| [[File:3D_Set_P15_Y120.png|none|450px|thumb|ma am ]] || [[File:3D_Set_P15_Y120_af.png|none|450px|thumb|ma am ]]
|  
[[File:3D_Set_P15_Y120.png|450px|Modified ''yaw'' ]] <br>
Modified ''yaw''
|  
[[File:3D_Set_P15_Y120_af.png|450px| With auto follow, track as target]]<br>
With auto follow, track as target
|-
|-
| [[File:3D_Set_P15_Y120_af_close.png|none|450px|thumb|ma am ]] || [[File:3D_Set_P15_Y120_R30_af|none|450px|thumb|ma am ]]
|
[[File:3D_Set_P15_Y120_af_close.png|450px|Auto follow - closer ]] <br>
Auto follow - closer
|
[[File:3D_Set_P15_Y120_R30_af.png|450px| Modified ''roll'' ]]<br>
Modified ''roll''
|}
|}
 






[[Category:Creating applications]]
[[Category:Creating applications]]

Revision as of 18:55, 1 October 2019

This page describes how to create a Maria GDK map client utilising MariaGlobeMapControl with 2D and 3D visualisation of map, tracks and draw objects.

Maria Globe Client

General

This page is under construction!
Note
  • You will need to include the TPG.Maria.MariaGlobeMapControl NuGet package (Currently available Teleplan Globe internal only)
  • Sample code is found in the MariaGlobeClient project, in the Sample Projects solution.
  • Note that the sample code is specifying the service connections in code, and not by app.config.
  • For general troubleshooting, see Development troubleshooting

Utilising the globe control

Including MariaGlobeMapControl

Create a WPF Application, and add reference to the TPG.Maria.MariaGlobeMapControl package.
For NuGet details, see Maria Basic Map Client.

Add the MariaGlobeMapControl to the Main window xaml.

<Window x:Class="MariaGlobeClient.MainWindow"
        . . .
        Title="MariaGlobeClient" Height="600" Width="600">
    <Grid>
        <MariaGlobeMapControl x:Name="MariaGlobeCtrl" Background="#E9ECFA"                      
                     Layers="{Binding Layers}"
                     MiniMapLayer="{Binding MapViewModel.MiniMapLayer, Mode=OneWay}"
                     CenterScale="{Binding MapViewModel.CenterScale}" 
                     CenterPosition="{Binding MapViewModel.CenterPosition}"
                     MouseMoveDistanceToStartTracking="0"
                     ZoomOnDblClick="False"
                     DegreeToLockRotateAndScale="2"
                     PercentageToLockScaleOnly="2.0"
                     Is3DMode="False"
                     IsRulerVisible="True" 
                     IsCenterPositionIndicatorEnabled="True"
                     IsMiniMapVisible="True" />
    </Grid>
</Window>

Add view models

  • Add a view model class (MainViewModel) for communication with the Maria component, and another (MapViewModel) for map handling. Both including inheritance of ViewModelBase, for details see Prepare your application for interactions.
  • Set the data context of your client window to the MainViewModel, and implement an event handler for the WindowClosing event. For details see Maria Basic Map Client.

MainViewModel

Extend the inheritances to include the IMariaGlobeMapViewModel and implement the interface.

public class MainViewModel : ViewModelBase, IMariaGlobeMapViewModel
{
    public IGlobeMapManager GlobeMapManager { get; }
    public IDrawObjectLayerFactory DrawObjectLayerFactory { get; private set; }
    public IGlobeMapViewModel GlobeMapViewModel { get; set; }
...

Add the following fields and properties:

. . .
    private IMariaMapLayer _mapLayer;
    private IMariaMapLayer _miniMapLayer;

    public MapViewModel MapViewModel { get; }
    public ObservableCollection<IMariaLayer> Layers { get; set; }
. . .

In the constructor:

  • initialise the Layer property
  • set up the map services
  • create the GlobeMapManager
  • create map layer, mini-map layer and add to the Layers property
  • instanciate the MapViewModel


...
public MainViewModel()
{
    Layers = new ObservableCollection<IMariaLayer>();

    IBindingFactory bindingFactory = new BindingFactory();
    IEndpointAddressFactory endpointAddressFactory = new EndpointAddressFactory();

    // Set up template client
    var mapTemplateServiceClient = new MapTemplateServiceClientFactory(
        bindingFactory,
        endpointAddressFactory).New("http://tpg-mariagdktest:9008/maptemplates", BindingType.BasicHttp);

    // Set up catalog service client and connect to service
    var mapCatalogServiceClient = new MapCatalogServiceClient(
        bindingFactory.New(BindingType.BasicHttp),
        endpointAddressFactory.New("http://tpg-mariagdktest:9008/catalog"));

    GlobeMapManager = new GlobeMapManager(mapCatalogServiceClient, mapTemplateServiceClient);
    GlobeMapManager.TileCacheManager.MaxCacheSize = 250;

    _mapLayer = new MapLayer(GlobeMapManager);
    _miniMapLayer = new MapLayer(GlobeMapManager);
    Layers.Add(_mapLayer);

    MapViewModel = new MapViewModel(_mapLayer, _miniMapLayer);
}

Make the class disposable by including and implementing IDisposable.

...
 public class MainViewModel : ViewModelBase, IMariaGlobeMapViewModel, IDisposable
{
...
    public void Dispose()
    {
        _mapLayer?.Dispose();
        _miniMapLayer?.Dispose();
        GlobeMapViewModel?.Dispose();
    }

MapViewModel

Add the following fields and properties:

...
    private readonly IMariaMapLayer _mapLayer;

    public IMariaMapLayer MiniMapLayer { get; private set; }
    public GeoPos CenterPosition { get; set; }      
    public double CenterScale { get; set; }
...

Create a constructor, taking map layer and and mini-map layer as parameters and initialising map and mini-map event handling.

public MapViewModel(IMariaMapLayer mapLayer, IMariaMapLayer miniMapLayer)
{
    _mapLayer = mapLayer;

    if (_mapLayer != null)
        _mapLayer.LayerInitialized += OnMapLayerInitialized;

    MiniMapLayer = miniMapLayer;
    if (MiniMapLayer != null)
        MiniMapLayer.LayerInitialized += OnMiniMapLayerInitialized;
}

Implement the LayerInitialized event handlers for the map and mini map.

...
    private void OnMapLayerInitialized()
    {
         CenterPosition = new GeoPos(59.908358, 10.628190); // TPG - Lysaker
         CenterScale = 500000;

        _mapLayer.ActiveMapTemplate = _mapLayer.ActiveMapTemplates.Any() ? _mapLayer.ActiveMapTemplates.First() : null;
    }

    private void OnMiniMapLayerInitialized()
    {
        MiniMapLayer.ActiveMapTemplate = _mapLayer.ActiveMapTemplates.Any() ? _mapLayer.ActiveMapTemplates.First() : null;
    }
...

Running the globe client

Running your application, the window area should now include 2D map information and mini map - and you should be able to navigate the map!

Globe client with 2D map

To start the globe client in 3D mode, Change the Is3DMode property of the MariaGlobeMapControl to True, and start the application again.

The window area should now include 3D map information - and you should be able to navigate the 3D-map!


Globe client with 3D map

Map interaction

Map utilities

The different map utilities can be turned on/off programatically e.g. through User controlls. For more details about tools and map functionality, see Map interaction client, Map_interaction and Tools interaction

Here is an example of adding check boxes with direct binding to the MariaGlobeMapControl from your main window.

...
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="auto"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>
    <StackPanel Grid.Row="0" Orientation="Horizontal" 
                Visibility="Visible">
        <GroupBox Header="Map Utilities">
            <StackPanel Orientation="Vertical" Visibility="Visible" Margin="5">
                <CheckBox Content="Ruler" Margin="3"
                          ToolTip="Ruler" 
                          IsChecked="{Binding IsRulerVisible, ElementName=MariaGlobeCtrl}" />
                <CheckBox Content="Pan Navigation" Margin="3"
                          ToolTip="Pan Navigation" 
                          IsChecked="{Binding IsPanNavigationVisible, ElementName=MariaGlobeCtrl}" />
                <CheckBox Content="Scale Bar" Margin="3"
                          ToolTip="Center Position, Scale Bar" 
                          IsChecked="{Binding IsScaleBarVisible, ElementName=MariaGlobeCtrl}" />
                <CheckBox Content="Center Mark"  Margin="3"
                          ToolTip="Center Position Indicator"
                          IsChecked="{Binding IsCenterPositionIndicatorEnabled, ElementName=MariaGlobeCtrl}" />
                <CheckBox Content="Mini Map" Margin="3"
                          ToolTip="Mini Map" 
                          IsChecked="{Binding IsMiniMapVisible, ElementName=MariaGlobeCtrl}" />                   
            </StackPanel>
        </GroupBox>
    </StackPanel>
    <Grid Grid.Row="1">
        <mariaglobemapcontrol:MariaGlobeMapControl x:Name="MariaGlobeCtrl" 
                                                   Background="#E9ECFA"                                                   
                                                   Layers="{Binding Layers}"
                                                   MiniMapLayer="{Binding MapViewModel.MiniMapLayer, Mode=OneWay}"
                                                   CenterScale="{Binding MapViewModel.CenterScale}" 
                                                   CenterPosition="{Binding MapViewModel.CenterPosition}"
                                                   Is3DMode="False"
                                                   MouseMoveDistanceToStartTracking="0"
                                                   ZoomOnDblClick="False"
                                                   DegreeToLockRotateAndScale="2"
                                                   PercentageToLockScaleOnly="2.0"
                                                   IsRulerVisible="True" 
                                                   IsCenterPositionIndicatorEnabled="True"
                                                   IsMiniMapVisible="True" />
    </Grid>
</Grid>
...

Map settings

Your map service will most likely contain different map templates, and you would like to select the template to be used.
In we will create a list of templates to chose from, display the current center position and scale, and also choose the display mode - 2D or 3D.

Add the following to your Main Window XAML:

...
<GroupBox Header="Map Settings">
    <Grid Margin="3">
        <Grid.RowDefinitions>
            <RowDefinition Height="auto" />
            <RowDefinition Height="auto" />
            <RowDefinition Height="auto" />
            <RowDefinition Height="auto" />                       
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="auto"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <ComboBox Name="cmbActiveMap" Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Margin="0,0,0,5"
                  Width="Auto" Height="Auto" MinWidth="200" 
                  VerticalAlignment="Top"
                  ItemsSource="{Binding MapViewModel.AvailableMapTemplateNames}" 
                  SelectedItem="{Binding MapViewModel.ActiveMapTemplateName}" 
                  IsSynchronizedWithCurrentItem="true" />

        <Label Grid.Row="1" Grid.Column="0"
               Content="Mode" FontWeight="DemiBold"/>
        <StackPanel Grid.Row="1" Grid.Column="1">
                <RadioButton Content="2D" 
                             IsChecked="{Binding MapViewModel.Is2DMode, Mode=OneWay}" />
                <RadioButton Content="3D"                                      
                             IsChecked="{Binding MapViewModel.Is3DMode}" />
        </StackPanel>

        <Label Grid.Row="2" Grid.Column="0"  FontWeight="DemiBold">
            <TextBlock>
                Center<LineBreak/>
                Scale
            </TextBlock>
        </Label>
        <TextBox Grid.Row="2" Grid.Column="1" VerticalAlignment="Center" 
                 Text="{Binding MapViewModel.CenterScale, StringFormat=N1}" 
                 IsReadOnly="True" />

        <Label Grid.Row="3" Grid.Column="0"  FontWeight="DemiBold">
            <TextBlock>
                Center<LineBreak/>
                Position
            </TextBlock>
        </Label>
        <TextBox Grid.Row="3" Grid.Column="1" VerticalAlignment="Center"
                 Text="{Binding MapViewModel.CenterPosition, Mode=OneWay}" 
                 IsReadOnly="True" />
    </Grid>
</GroupBox>
...

And bind the Is3DMode property of the MariaGlobeMapControl to same value as the Radio button.

... 
<mariaglobemapcontrol:MariaGlobeMapControl x:Name="MariaGlobeCtrl" Background="#E9ECFA"  
                                           Is3DMode="{Binding MapViewModel.Is3DMode}"
                                           Layers="{Binding Layers}" 
...

Extend the MapViewModel with:

  • fields and properties to handle display of available map templates and 2D/3D mode selection
  • initialized event handlers with initialisation and selection of templates
  • CenterPosition and CenterScale properties with notification handling.
...
private Dictionary<string, MapTemplate> _availableMapTemplateDictionary = new Dictionary<string, MapTemplate>();

private double _scale;
private GeoPos _pos;
private bool _is3DMode;
...
public bool Is2DMode { get { return !_is3DMode; } }

public bool Is3DMode
{
    get { return _is3DMode; }
    set
    {
        _is3DMode = value;
        NotifyPropertyChanged(() => Is2DMode);
        NotifyPropertyChanged(() => Is3DMode);
    }
}

public GeoPos CenterPosition
{
    get { return _pos; }
    set
    {
        _pos = value;
        NotifyPropertyChanged(() => CenterPosition);
    }
}

public double CenterScale
{
    get { return _scale; }
    set
    {
        _scale = value;
        NotifyPropertyChanged(() => CenterScale);
    }
}

public MapViewModel(IMariaMapLayer mapLayer, IMariaMapLayer miniMapLayer)
{
    _mapLayer = mapLayer;

    if (_mapLayer != null)
        _mapLayer.LayerInitialized += OnMapLayerInitialized;

    MiniMapLayer = miniMapLayer;
    if (MiniMapLayer != null)
        MiniMapLayer.LayerInitialized += OnMiniMapLayerInitialized;
}

private void OnMapLayerInitialized()
{
    foreach (var template in _mapLayer.ActiveMapTemplates)
    {
        _availableMapTemplateDictionary.Add(template.Name, template);
    }

    NotifyPropertyChanged(() => AvailableMapTemplateNames);

    CenterPosition = new GeoPos(59.908358, 10.628190); // TPG - Lysaker
    CenterScale = 3500000;

    _mapLayer.ActiveMapTemplate = _mapLayer.ActiveMapTemplates.Any() ?
                                    _mapLayer.ActiveMapTemplates.First() : null;

    _mapLayer.ActiveMapTemplate = PreferredMapTemplate();
    NotifyPropertyChanged(() => ActiveMapTemplateName);
}

private void OnMiniMapLayerInitialized()
{
    MiniMapLayer.ActiveMapTemplate = _mapLayer.ActiveMapTemplates.Any() ?
                                    _mapLayer.ActiveMapTemplates.First() : null;
    MiniMapLayer.ActiveMapTemplate = PreferredMapTemplate();
}



public IEnumerable<string> AvailableMapTemplateNames
{
    get
    {
        return _availableMapTemplateDictionary.Keys.ToList();
    }
}

public string ActiveMapTemplateName
{
    get
    {
        if (_mapLayer != null && _mapLayer.ActiveMapTemplate != null)
            return _mapLayer.ActiveMapTemplate.Name;
        return "";
    }
    set
    {
        MapTemplate template = null;

        if (_availableMapTemplateDictionary.ContainsKey(value))
        {
            template = _availableMapTemplateDictionary[value];
        }
        else if (_availableMapTemplateDictionary.Any())
        {
            template = _availableMapTemplateDictionary.Values.First();
        }

        _mapLayer.ActiveMapTemplate = template;
        MiniMapLayer.ActiveMapTemplate = template;
        NotifyPropertyChanged(() => ActiveMapTemplateName);
    }
}

private MapTemplate PreferredMapTemplate()
{
    var preferred = "TOPO";

    if (_availableMapTemplateDictionary.ContainsKey(preferred))
        return _availableMapTemplateDictionary[preferred];

    return _availableMapTemplateDictionary.Values.Any() ? _availableMapTemplateDictionary.Values.First() : null;
}
...

Running with map interaction

Running your application, observe the following:

  • the map utilities are shown/hidden according to the corresponding check boxes
  • the center position and scale are updated when panning/zooming the map
  • the map template drop-down list contains available map templates
  • the map display changes according to selected map template
  • the map display changes according to deleted mode


Preferred map, 2D
Preferred map, 2D

Preferred map, 3D
Preferred map, 3D

Another map, 2D
Another map, 2D

Another map, 3D
Another map, 3D

Draw object interaction

In this section, we will create and update three different draw objects, a area object, a line object, and a point object, and see how they are presented in 2D and 3D mode.

For more details on track handling, see Draw objects, Draw object interaction and Draw objects visualisation

Adding draw object layer and draw object view model

To be able to display draw objects, we need to include a draw object layer in the globe client.

First, add a view model class, DrawObjectViewModel, to handle the draw object interaction:

  • Inheriting ViewModelBase
  • Add a constructor taking a draw layer object as input
  • Create and implement event handlers for:
    • LayerInitialized event
    • ServiceConnected event
public class DrawObjectViewModel : ViewModelBase
{
    private IMariaDrawObjectLayer _drawObjectLayer;
    private IMariaService _drawObjectService;

    public DrawObjectViewModel(DrawObjectLayer drawObjectLayer)
    {
        _drawObjectLayer = drawObjectLayer;
        _drawObjectLayer.LayerInitialized += DrawObjectLayer_LayerInitialized;
        _drawObjectLayer.ServiceConnected += DrawObjectLayer_ServiceConnected;
    }

    private void DrawObjectLayer_LayerInitialized()
    {
        _drawObjectService = new MariaService(
            "DrawObjectService",
            "http://localhost:9008/drawobjects",
            BindingType.BasicHttp);

        _drawObjectLayer.DrawObjectServices = new ObservableCollection<IMariaService> { _drawObjectService };

        _drawObjectLayer.CreateDrawObjectList("local", true);
    }

    private void DrawObjectLayer_ServiceConnected(object sender, MariaServiceEventArgs args)
    {
        _drawObjectLayer.ActiveDrawObjectService = _drawObjectLayer.DrawObjectServices[0];

        _drawObjectLayer.CreateDrawObjectList("test", false);
        _drawObjectLayer.ActiveDrawObjectList = "test";
    }
}

In the MainViewModel, add a draw object layer field, and in the constructor, add creation of the layer and DrawObjectViewModel and add the layer to the Layers list.

...
public DrawObjectViewModel DrawObjectViewModel { get; private set; }

public MainViewModel()
{
    ...
    DrawObjectLayerFactory = new DrawObjectLayerFactory();
    _drawObjectLayer = new DrawObjectLayer(GlobeMapManager, DrawObjectLayerFactory, true)
        { InitializeCreationWorkflows = true };
    DrawObjectViewModel = new DrawObjectViewModel(_drawObjectLayer);
    Layers.Add(_drawObjectLayer);
}
...

Draw object utilities

Add the following GUI elements to your main window:

  • Button for adding/updating a volume object.
  • Button for adding/updating a line object.
  • Button for adding/updating a point object.


The XAML could look something like this:

...
<GroupBox Header="Draw Object" >
    <StackPanel VerticalAlignment="Top">
        <Button Margin="3"                         
                Content="Volume" 
                Command="{Binding DrawObjectViewModel.VolumeTestCmd}"/>
        <Button Margin="3"                         
                Content="Line" 
                Command="{Binding DrawObjectViewModel.LineTestCmd}"/>
        <Button Margin="3"                         
                Content="Point" 
                Command="{Binding DrawObjectViewModel.PointTestCmd}"/>     
    </StackPanel>
</GroupBox>
...
...
...

Implement command handler and command handler delegate in DrawObjectViewModel.

...
public ICommand VolumeTestCmd { get { return new DelegateCommand(DrawObjectViewModel.CreateOrUpdateVolumeObject); } }
public ICommand LineTestCmd { get { return new DelegateCommand(DrawObjectViewModel.CreateOrUpdateLineObject); } }
public ICommand PointTestCmd { get { return new DelegateCommand(DrawObjectViewModel.CreateOrUpdatePointObject); } }

public void CreateOrUpdatePointObject(object obj)
{
    var pos = RandomProvider.GetRandomPosition(_drawObjectLayer.GeoContext.Viewport.GeoRect);

    var altitude = RandomProvider.GetRandomInt(1, 5) * 500.0;

    var pointObject = _drawObjectLayer.DrawObjectFactory.CreateTacticalGraphic("STBOPS.OPN.HJKG.APL");
    pointObject.TacticalDataFields.StandardIdentity = StandardIdentity.Hostile;
    pointObject.TacticalDataFields.Name = "Globe-Point";
    pointObject.Id = new ItemId("local", "Globe-Point");

    pointObject.Points[0] = new GeoPoint() { Latitude = pos.Lat, Longitude = pos.Lon, Altitude = altitude };
    _drawObjectLayer.UpdateStore(pointObject);
}

public void CreateOrUpdateVolumeObject(object obj)
{

    var volumeObject = _drawObjectLayer.DrawObjectFactory.CreateTacticalGraphic("TACGRP.FSUPP.ARS.C2ARS.ACA.IRR");
    volumeObject.TacticalDataFields.StandardIdentity = StandardIdentity.Friendly;
    volumeObject.Id = new ItemId("local", "Globe-Volume");

    var pos = RandomProvider.GetRandomPosition(_drawObjectLayer.GeoContext.Viewport.GeoRect, 0.8);
    var factorLat = _drawObjectLayer.GeoContext.Viewport.GeoRect.DeltaLat * RandomProvider.GetRandomDouble(0.05, 0.1);
    var factorLon = _drawObjectLayer.GeoContext.Viewport.GeoRect.DeltaLon * RandomProvider.GetRandomDouble(0.05, 0.1);

    volumeObject.Points = new GeoPoint[] {
        new GeoPoint() { Latitude = pos.Lat + factorLat, Longitude = pos.Lon + factorLon },
        new GeoPoint() { Latitude = pos.Lat + factorLat, Longitude = pos.Lon - factorLon },
        new GeoPoint() { Latitude = pos.Lat - factorLat, Longitude = pos.Lon - factorLon },
        new GeoPoint() { Latitude = pos.Lat - factorLat, Longitude = pos.Lon + factorLon },
    };

    _drawObjectLayer.UpdateStore(volumeObject);
}

public void CreateOrUpdateLineObject(object obj)
{
    var lineObject = _drawObjectLayer.DrawObjectFactory.CreatePolyLine(new ItemId("local", "Globe-Line"));
    var pt0 = RandomProvider.GetRandomPosition(_drawObjectLayer.GeoContext.Viewport.GeoRect, 0.5);
    var pt1 = RandomProvider.GetRandomPosition(_drawObjectLayer.GeoContext.Viewport.GeoRect, 0.5);
    var pt2 = RandomProvider.GetRandomPosition(_drawObjectLayer.GeoContext.Viewport.GeoRect, 0.5);

    lineObject.Points = new GeoPoint[] {
        new GeoPoint() { Latitude = pt0.Lat, Longitude = pt0.Lon, Altitude = RandomProvider.GetRandomInt(1, 5) * 500.0 },
        new GeoPoint() { Latitude = pt1.Lat, Longitude = pt1.Lon, Altitude = RandomProvider.GetRandomInt(1, 5) * 500.0 },
        new GeoPoint() { Latitude = pt2.Lat, Longitude = pt2.Lon, Altitude = RandomProvider.GetRandomInt(1, 5) * 500.0 },
    };

    lineObject.GenericDataFields.LineColor = Colors.Navy;
    var dashStyle = DashStyles.DashDot;
    lineObject.GenericDataFields.LineDashStyle = dashStyle.Dashes.ToList();
    lineObject.GenericDataFields.LineDashStyleOffset = dashStyle.Offset;
    lineObject.GenericDataFields.LineWidth = 6.0;

    _drawObjectLayer.UpdateStore(lineObject);
}
...

The RandomProvider class is described in context with Map interaction client, track creation.

Draw object styling

Add a draw object style xml file to your project, and include it as a resource. You can find style xml to use here

Add the resource to the draw object layer in the LayerInitialized event handler in DrawObjectViewModel.

...
private void DrawObjectLayer_LayerInitialized()
{
    _drawObjectLayer.StyleXml = Properties.Resources.DrawObjectStyle;
...

Running with draw object interaction

Running your application, observe the following:

  • Pressing the draw object buttons, draw objects created / moved around in the map area.
  • In 2D mode, the objects can also be selected and moved/resized manually.


Draw objects in map, 2D
Draw objects in map, 2D

Draw objects in map, 3D
Draw objects in map, 3D

Track interaction

In this section, we will create and update a track, and observe the presentation in 2D and 3D mode.

For more details on track handling, see Tracks, Track interaction and Track visualisation

Adding draw object layer and draw object view model

To be able to display tracks, we need to include a track layer in the globe client.

First, add a view model class, TrackViewModel, to handle the draw object interaction:

  • Inheriting ViewModelBase
  • Add a constructor taking a draw layer object as input
  • Create and implement event handlers for:
    • LayerInitialized event
    • ServiceConnected event
public class TrackViewModel : ViewModelBase
{
    private TrackLayer _trackLayer;
    private MariaService _trackService;

    public TrackViewModel(TrackLayer trackLayer)
    {
        _trackLayer = trackLayer;

        _trackLayer.LayerInitialized += TrackLayer_LayerInitialized;
        _trackLayer.ServiceConnected += TrackLayer_ServiceConnected;
    }

    private void TrackLayer_LayerInitialized()
    {
        _trackService = new MariaService("TrackService", "http://localhost:9008/tracks", BindingType.BasicHttp);
        _trackLayer.TrackServices = new ObservableCollection<IMariaService> { _trackService };
    }

    private void TrackLayer_ServiceConnected(object sender, MariaServiceEventArgs args)
    {
        _trackLayer.ActiveTrackService = _trackLayer.TrackServices[0];
        _trackLayer.ExtendedTrackLayer.TrackRefreshInterval = 1000;

        _trackLayer.TrackLists = new ObservableCollection<string>(_trackLayer.GetTrackLists());

        if (!_trackLayer.TrackLists.Contains("globeTest"))
            _trackLayer.TrackLists.Add("globeTest");

        _trackLayer.ActiveTrackList = "globeTest";
    }
}

In the MainViewModel, add a track layer field, and in the constructor, add creation of the layer and TrackViewModel, and add the layer to the Layers list.

...
public TrackViewModel TrackViewModel { get; private set; }

public MainViewModel()
{
    ...
    _trackLayer = new TrackLayer(GlobeMapManager);
    TrackViewModel = new TrackViewModel(_trackLayer);
    Layers.Add(_trackLayer);
}
...

Track utilities

Add the following GUI elements to your main window:

  • Button for adding/updating the track
  • Check box to enable/disable automatic track update (simulate movement)

The XAML could look something like this:

...
<GroupBox Header="Track">
    <StackPanel>
        <Button Margin="3"                         
                Content="Track" 
                Command="{Binding TrackViewModel.TrackTestCmd}"/>
        <CheckBox Margin="3"
                  Content="Auto update" 
                  IsChecked="{Binding TrackViewModel.AutoUpdateActive}"/> 
    </StackPanel>
</GroupBox>
...

In TrackViewModel implement:

  • command handler and command handler delegate in for the track update button
  • auto update property with timer, initialised in during the LayerInitialized event handler.
...
private bool _autoUpdateActive;
Timer _autoUpdateTimer;
...
private void TrackLayer_LayerInitialized()
{
...
    _autoUpdateTimer = new Timer(1000);
    _autoUpdateTimer.Elapsed += OnAutoUpdateTimerTick;
}
...
public ICommand TrackTestCmd { get { return new DelegateCommand(CreateOrUpdateTrack, CheckAutoUpdate); } }

public void CreateOrUpdateTrack(object obj)
{
    if (_trackLayer.ActiveTrackList == null)
        return;

    var currentTime = DateTime.UtcNow;

    var strId = "Globe-Track";
    var itemId = new ItemId(_trackLayer.ActiveTrackList, strId);

    var list = _trackLayer.GetTrackData(strId);

    var speed = RandomProvider.GetRandomDouble(100.0, 250.0);
    var course = RandomProvider.GetRandomDouble(0, 360.0);
    var pos = RandomProvider.GetRandomPosition(_trackLayer.GeoContext.Viewport.GeoRect, 0.9);

    var altitude = RandomProvider.GetRandomDouble(500, 2000);
    var roll = RandomProvider.GetRandomDouble(-45.0, 45.0);
    var pitch = RandomProvider.GetRandomDouble(-30.0, 30.0);

    ITrackData trackData;
    if (!list.Any())
    {
        trackData = new TrackData(itemId, pos, course, speed) { ObservationTime = currentTime };
        InitTrackData(ref trackData, strId);
    }
    else
    {
        trackData = list[0];

        var trackAge = trackData.ObservationTime.HasValue ?
            currentTime - trackData.ObservationTime.Value :
            TimeSpan.MaxValue;

        if (trackAge > new TimeSpan(0, 0, 5))
        {
            trackData = new TrackData(itemId, pos, course, speed) { ObservationTime = currentTime };
            InitTrackData(ref trackData, strId);
        }
        else
        {
            trackData.ObservationTime = currentTime;

            trackData.Speed = trackData.Speed.HasValue ?
                RandomProvider.GetRandomDouble(trackData.Speed.Value * 0.8, trackData.Speed.Value * 1.2) :
                speed;
            trackData.Course = trackData.Course.HasValue ?
                RandomProvider.GetRandomDouble(trackData.Course.Value * 0.9, trackData.Course.Value * 1.1) :
                course;

            trackData.Pos = trackData.Pos.HasValue ?
                CalculateNewPos(trackData.Pos.Value, trackData.Speed.Value, trackData.Course.Value, trackAge) :
                pos;

            if (double.TryParse(trackData.Fields["altitude"], out altitude))
            {
                altitude = RandomProvider.GetRandomDouble(altitude * 0.8, altitude * 1.2);
            }

            if (double.TryParse(trackData.Fields["roll"], out roll))
            {
                roll = RandomProvider.GetRandomDouble(roll * 0.9, roll * 1.1);
            }

            if (double.TryParse(trackData.Fields["pitch"], out pitch))
            {
                pitch = RandomProvider.GetRandomDouble(pitch * 0.9, pitch * 1.1);
            }
        }
    }

    trackData.Fields["altitude"] = altitude.ToString();
    trackData.Fields["roll"] = roll.ToString();
    trackData.Fields["pitch"] = pitch.ToString();
    trackData.Fields["heading"] = course.ToString();

    _trackLayer.SetTrackData(trackData);
}

public bool CheckAutoUpdate(object obj)
{
    return !AutoUpdateActive;
}

public bool AutoUpdateActive
{
    get { return _autoUpdateActive; }
    set
    {
        _autoUpdateActive = value;

        if (_autoUpdateActive)
            _autoUpdateTimer.Start();
        else
            _autoUpdateTimer.Stop();
    }
}

private void OnAutoUpdateTimerTick(object sender, ElapsedEventArgs e)
{
    CreateOrUpdateTrack(null);
}

private void InitTrackData(ref ITrackData td, string id)
{
    td.Fields["name"] = id;
    td.Fields["symbol.2525code"] = "SFAPCF----*****";
    td.Fields["identity"] = "Friendly";
    td.Fields["type"] = "F";
}

private GeoPos CalculateNewPos(GeoPos oldPos, double speed, double course, TimeSpan age)
{
    var distance = speed * age.TotalSeconds;
    var newPos = Earth.BearingRangeToPos(oldPos, new BearingRange(course, distance));
    return newPos;
}
...

Track styling

Add a track style xml file to your project, and include it as a resource. You can find style xml to use here

Add the resource to the track layer in the LayerInitialized event handler in TrackViewModel.

...
private void TrackLayer_LayerInitialized()
{
    _trackLayer.StyleXml = Properties.Resources.TrackStyle;
...

Running with track interaction

Running your application, observe the following:

  • Pressing the track button, the track is created/moved around in the map area.
  • The track button is disabled while auto-update is activated.

Track in map, 2D
Track in map, 2D

Track in map, 3D
Track in map, 3D

Auto follow

In this section, we will utilise the globe control auto follow functionality.

2D
  • valid for tracks and all draw object types
  • the specified object will always be displayed with its center point in the center of the screen.
  • if the item position is changed, the map area will be adjusted accordingly.
3D
  • valid for tracks and point objects only

Auto follow control and display

Add the following GUI elements to your main window:

  • Check box to enable/disable auto follow
  • Text box for display of currently followed item

The XAML could look something like this:

...
<GroupBox Header="Auto follow">
    <StackPanel>
        <CheckBox Margin="3"
                  Content="Follow selected"
                  IsChecked="{Binding IsAutoFollowActive}" />
        <Label Content="Currently followed:" FontWeight="DemiBold" />
        <TextBox Margin="3"
                 Text="{Binding AutoFollowItemName, Mode=OneWay}" 
                 IsReadOnly="True"/>
    </StackPanel>
</GroupBox>
...

In MainViewModel implement:

  • Auto follow property,
  • auto update property with timer, initialised in during the LayerInitialized event handler.
...
public bool IsAutoFollowActive
{
    get
    {
        return GlobeMapViewModel.AutoFollow.TargetItem != null;
    }

    set
    {
        if (!(value && GlobeMapViewModel.AutoFollow.FollowSelectedItem()))
            GlobeMapViewModel.AutoFollow.TargetItem = null;

        NotifyPropertyChanged(() => IsAutoFollowActive);
        NotifyPropertyChanged(() => AutoFollowItemName);
    }
}

public string AutoFollowItemName
{
    get
    {
        if (GlobeMapViewModel?.AutoFollow?.TargetItem != null)
        {
            return GlobeMapViewModel.AutoFollow.TargetItem.ToString();
        }

        return "Inactive";
    }
}
...

Running with auto follow

Activating/deactivating auto follow functionality, observe the following:

  • in 3D mode, tracks and point objects only can be selected.
  • the map will re-position to the activated item if the map is panned.
  • the map will re-position to the activated item if the item is moved.
  • the activated item will stay activated also when deselected in the map and when another item is selected.
    • To activate another item - deactivate, then activate the new item.
No auto follow, 2D
No auto follow, 2D
Auto follow volume object, 2D
Auto follow volume object, 2D
Auto follow line object, 2D
Auto follow line object, 2D
Auto follow point object, 2D
Auto follow point object, 2D
Auto follow track, 2D
Auto follow track, 2D
Auto follow point object, 3D
Auto follow point object, 3D
Auto follow track, 3D
Auto follow track, 3D

Advanced map settings

Map rotation

As default, 2D maps are displayed with north up, but you may rotate the map by modifying the map layer rotation parameter - GeoContext.RotateValue.

Add the following components:

  • Text box to display the current rotation value
  • Two buttons, for right (clockwise) add left (counter clockwise).

Add the following to your Main Window XAML:

. . .
<Label Grid.Row="4" Grid.Column="0"
       Content="Rotation" FontWeight="DemiBold"/>

<TextBox Grid.Row="4" Grid.Column="1" Margin="3"
         Text="{Binding MapViewModel.Rotation}" TextAlignment="Right" />

<StackPanel  Grid.Row="5" Grid.Column="1" Orientation="Horizontal" 
             VerticalAlignment="Center" HorizontalAlignment="Center">
    <RepeatButton Margin="3" Width="50" 
                  Content="Left" 
                  Command="{Binding MapViewModel.RotateLeftCmnd}"/>
    <RepeatButton Margin="3" Width="50" 
                  Content="Right" 
                  Command="{Binding MapViewModel.RotateRightCmnd}"/>
</StackPanel>
. . .

And implement corresponding properties and command handlers in MapViewModel:

. . .
public ICommand RotateLeftCmnd { get { return new DelegateCommand(OnRotateLeft); } }
public ICommand RotateRightCmnd { get { return new DelegateCommand(OnRotateRight); } }

private void OnRotateLeft(object obj)
{
    Rotation += 1;
}

private void OnRotateRight(object obj)
{
    Rotation -= 1;
}

public int Rotation
{
    get { return _mapLayer.GeoContext != null ? (int)_mapLayer.GeoContext.RotateValue : 0; }
    set
    {                
        _mapLayer.GeoContext.RotateValue = value;
        NotifyPropertyChanged(() => Rotation);
    }
}
. . .

Pressing the buttons, the map is rotated, and the rotation display is updated accordingly.

Map rotation.

3D map settings

3D Properties

The Globe Client 3D view is the view from a camera towards a target, specified by the following parameters:

Description
Camera yaw Horizontal rotation, degrees - counterclockwise. Roll, pitch and yaw
Camera pitch Front/back rotation, degrees - counterclockwise
Camera roll Left/right rotation, degrees - counterclockwise.
Follow ground

Lock the target altitude is to the elevation of the target position.

Target altitude

Height above sea level (0-elevation) at target position. Changes are not applicable while:

  • auto follow is active, as the target elevation is defined by the item followed.
  • follow ground is active.
Target position

Latitude and longitude. Changes are not applicable while:

  • auto follow is active, as the target position is defined by the item followed.
Target distance Distance from camera to target.


Note
  • 3D rotations are not commutative.
Performing the same rotations in different order will give different results.

Implementation

To visualise how these parameters are connected, add the following elements to your main window:

. . .
<GroupBox Header="3D Settings" 
          IsEnabled="{Binding MapViewModel.Is3DMode}" >
    <Grid >
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="Auto"/>
        </Grid.ColumnDefinitions>

        <Label Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="4"
               Content="Camera" FontWeight="DemiBold" />
        
        <Label Grid.Row="1" Grid.Column="0" 
               Content="Yaw"   />
        <TextBox Grid.Row="1" Grid.Column="1" Margin="3" Width="60"
                 Text="{Binding MapViewModel.CameraYaw, StringFormat=N2}" TextAlignment="Right"                            
                 IsReadOnly="True"/>
        <RepeatButton Grid.Row="1" Grid.Column="2" Width="12" Margin="3" HorizontalAlignment="Center"
                      Content="+" 
                      Command="{Binding MapViewModel.IncreaseYawCmnd}"/>
        <RepeatButton Grid.Row="1" Grid.Column="3" Width="12" Margin="3" HorizontalAlignment="Center"
                      Content="-" 
                      Command="{Binding MapViewModel.DecreaseYawCmnd}"/>
        
        <Label Grid.Row="2" Grid.Column="0"
               Content="Roll" />
        <TextBox Grid.Row="2" Grid.Column="1" Margin="3" Width="60"
                 Text="{Binding MapViewModel.CameraRoll, StringFormat=N2}" TextAlignment="Right" 
                 IsReadOnly="True"/>
        <RepeatButton Grid.Row="2" Grid.Column="2" Width="12" Margin="3" HorizontalAlignment="Center"
                      Content="+" 
                      Command="{Binding MapViewModel.IncreaseRollCmnd}"/>
        <RepeatButton Grid.Row="2" Grid.Column="3" Width="12" Margin="3" HorizontalAlignment="Center"
                      Content="-" 
                      Command="{Binding MapViewModel.DecreaseRollCmnd}"/>
        
        <Label Grid.Row="3" Grid.Column="0"
               Content="Pitch" />
        <TextBox Grid.Row="3" Grid.Column="1" Margin="3" Width="60"
                 Text="{Binding MapViewModel.CameraPitch, StringFormat=N2}" TextAlignment="Right" 
                 IsReadOnly="True"/>
        <RepeatButton Grid.Row="3" Grid.Column="2" Width="12" Margin="3" HorizontalAlignment="Center"
                      Content="+" 
                      Command="{Binding MapViewModel.IncreasePitchCmnd}"/>
        <RepeatButton Grid.Row="3" Grid.Column="3" Width="12" Margin="3" HorizontalAlignment="Center"
                      Content="-" 
                      Command="{Binding MapViewModel.DecreasePitchCmnd}"/>

        <Label Grid.Row="4" Grid.Column="0"
               Content="Target" FontWeight="DemiBold" />
        <CheckBox Grid.Row="4"  Grid.Column="1" Grid.ColumnSpan="3" VerticalAlignment="Center" Margin="3" 
                  Content="Follow ground"
                  IsChecked="{Binding MapViewModel.FollowGround}"/>

        <Label Grid.Row="5" Grid.Column="0"
               Content="Altitude" />
        <TextBox Grid.Row="5" Grid.Column="1" Margin="3" Width="60"
                 Text="{Binding MapViewModel.TargetAltitude, StringFormat=N0}" TextAlignment="Right" 
                 IsReadOnly="True"/>
        <RepeatButton Grid.Row="5" Grid.Column="2" Width="12" Margin="3" HorizontalAlignment="Center"
                      Content="+" 
                      Command="{Binding MapViewModel.IncreaseTargetAltitudeCmnd}"/>
        <RepeatButton Grid.Row="5" Grid.Column="3" Width="12" Margin="3" HorizontalAlignment="Center"
                      Content="-" 
                      Command="{Binding MapViewModel.DecreaseTargetAltitudeCmnd}"/>
        
        <Label Grid.Row="6" Grid.Column="0"
               Content="Distance" />
        <TextBox Grid.Row="6" Grid.Column="1" Margin="3" Width="60"
                 Text="{Binding MapViewModel.TargetDistance, StringFormat=N0}" TextAlignment="Right" 
                 IsReadOnly="True"/>
        <RepeatButton Grid.Row="6" Grid.Column="2" Width="12" Margin="3" HorizontalAlignment="Center"
                      Content="+" 
                      Command="{Binding MapViewModel.IncreaseTargetDistanceCmnd}"/>
        <RepeatButton Grid.Row="6" Grid.Column="3" Width="12" Margin="3" HorizontalAlignment="Center"
                      Content="-" 
                      Command="{Binding MapViewModel.DecreaseTargetDistanceCmnd}"/>

        <Label Grid.Row="7" Grid.Column="0"
               Content="Position" />
        <TextBox Grid.Row="7" Grid.Column="1" Grid.ColumnSpan="3" Margin="3"                              
                 Text="{Binding MapViewModel.TargetPosText, Mode=OneWay}" TextAlignment="Right" 
                 IsReadOnly="True"/>
        <Label Grid.Row="8" Grid.Column="1"
               Content="Latitude"/>
        <RepeatButton Grid.Row="8" Grid.Column="2" Width="12" Margin="3" HorizontalAlignment="Center"
                      Content="+" 
                      Command="{Binding MapViewModel.IncreaseTargetPosLatCmnd}"/>
        <RepeatButton Grid.Row="8" Grid.Column="3" Width="12" Margin="3" HorizontalAlignment="Center"
                      Content="-" 
                      Command="{Binding MapViewModel.DecreaseTargetPosLatCmnd}"/>

        <Label Grid.Row="9" Grid.Column="1"
               Content="Longitude"/>
        <RepeatButton Grid.Row="9" Grid.Column="2" Width="12" Margin="3" HorizontalAlignment="Center"
                      Content="+" 
                      Command="{Binding MapViewModel.IncreaseTargetPosLonCmnd}"/>
        <RepeatButton Grid.Row="9" Grid.Column="3" Width="12" Margin="3" HorizontalAlignment="Center"
                      Content="-" 
                      Command="{Binding MapViewModel.DecreaseTargetPosLonCmnd}"/>
    </Grid>
</GroupBox>
. . .

And add necessary command handlers and properties to MapViewModel

. . .
public ICommand IncreaseYawCmnd { get { return new DelegateCommand(OnIncreaseCameraYaw); } }
public ICommand DecreaseYawCmnd { get { return new DelegateCommand(OnDecreaseCameraYaw); } }

public ICommand IncreaseRollCmnd { get { return new DelegateCommand(OnIncreaseCameraRoll); } }
public ICommand DecreaseRollCmnd { get { return new DelegateCommand(OnDecreaseCameraRoll); } }

public ICommand IncreasePitchCmnd { get { return new DelegateCommand(OnIncreaseCameraPitch); } }
public ICommand DecreasePitchCmnd { get { return new DelegateCommand(OnDecreaseCameraPitch); } }

public ICommand IncreaseTargetDistanceCmnd { get { return new DelegateCommand(OnIncreaseTargetDistance); } }
public ICommand DecreaseTargetDistanceCmnd { get { return new DelegateCommand(OnDecreaseTargetDistance); } }

public ICommand IncreaseTargetAltitudeCmnd { get { return new DelegateCommand(OnIncreaseTargetAlttitude, CanModTargetAltitude); } }
public ICommand DecreaseTargetAltitudeCmnd { get { return new DelegateCommand(OnDecreaseTargetAltitide, CanModTargetAltitude); } }

public ICommand IncreaseTargetPosLatCmnd { get { return new DelegateCommand(OnIncreaseTargetPosLat, CanModTargetPosition); } }
public ICommand DecreaseTargetPosLatCmnd { get { return new DelegateCommand(OnDecreaseTargetPosLat, CanModTargetPosition); } }
public ICommand IncreaseTargetPosLonCmnd { get { return new DelegateCommand(OnIncreaseTargetPosLon, CanModTargetPosition); } }
public ICommand DecreaseTargetPosLonCmnd { get { return new DelegateCommand(OnDecreaseTargetPosLon, CanModTargetPosition); } }

public bool Autofollow
{
    get { return _autoFollow; }
    set
    {
        _autoFollow = value;

        RefreshTargetAndCameraValues();
    }
}

private void OnIncreaseCameraYaw(object obj) { CameraYaw += 1; }
private void OnDecreaseCameraYaw(object obj) { CameraYaw -= 1; }

private void OnIncreaseCameraRoll(object obj) { CameraRoll += 1; }
private void OnDecreaseCameraRoll(object obj) { CameraRoll -= 1; }

private void OnIncreaseCameraPitch(object obj) { CameraPitch += 1; }
private void OnDecreaseCameraPitch(object obj) { CameraPitch -= 1; }

private void OnIncreaseTargetAlttitude(object obj) { TargetAltitude += 10; }
private void OnDecreaseTargetAltitide(object obj) { TargetAltitude -= 10; }

private void OnIncreaseTargetPosLat(object obj) { TargetPos = new GeoPos(TargetPos.Lat + (1.0 / (60 * 60)), TargetPos.Lon ); }
private void OnDecreaseTargetPosLat(object obj) { TargetPos = new GeoPos(TargetPos.Lat - (1.0 / (60 * 60)), TargetPos.Lon ); }
private void OnIncreaseTargetPosLon(object obj) { TargetPos = new GeoPos(TargetPos.Lat, TargetPos.Lon + (1.0 / (60 * 60))); }
private void OnDecreaseTargetPosLon(object obj) { TargetPos = new GeoPos(TargetPos.Lat, TargetPos.Lon - (1.0 / (60 * 60))); }

private bool CanModTargetAltitude(object obj) { return !FollowGround && !Autofollow; }
private bool CanModTargetPosition(object obj) { return !Autofollow; }

private void OnIncreaseTargetDistance(object obj) { TargetDistance += 10; }
private void OnDecreaseTargetDistance(object obj) { TargetDistance -= 10; }

public double CameraYaw
{
    get
    {
        if (GlobeMapViewModel?.Globe3DViewModel?.CameraControl != null)
            return GlobeMapViewModel.Globe3DViewModel.CameraControl.Yaw;
        return 0;
    }
    set
    {
        if (GlobeMapViewModel?.Globe3DViewModel?.CameraControl != null)
        {
            GlobeMapViewModel.Globe3DViewModel.CameraControl.Yaw = value;
        }

        NotifyPropertyChanged(() => CameraYaw);
    }
}

public double CameraRoll
{
    get
    {
        if (GlobeMapViewModel?.Globe3DViewModel?.CameraControl != null)
            return GlobeMapViewModel.Globe3DViewModel.CameraControl.Roll;
        return 0;
    }
    set
    {
        if (GlobeMapViewModel?.Globe3DViewModel?.CameraControl != null)
        {
            GlobeMapViewModel.Globe3DViewModel.CameraControl.Roll = value;
        }

        NotifyPropertyChanged(() => CameraRoll);
    }
}

public double CameraPitch
{
    get
    {
        if (GlobeMapViewModel?.Globe3DViewModel?.CameraControl != null)
            return GlobeMapViewModel.Globe3DViewModel.CameraControl.Pitch;
        return 0;
    }
    set
    {
        if (GlobeMapViewModel?.Globe3DViewModel?.CameraControl != null)
        {
            GlobeMapViewModel.Globe3DViewModel.CameraControl.Pitch = value;
        }

        NotifyPropertyChanged(() => CameraPitch);
    }
}

public string TargetPosText { get { return TargetPos.ToString(); } }

public GeoPos TargetPos
{
    get
    {
        if (GlobeMapViewModel?.Globe3DViewModel?.CameraControl != null)
            return GlobeMapViewModel.Globe3DViewModel.CameraControl.TargetPos;

        return GeoPos.InvalidPos;
    }
    set
    {
        if (GlobeMapViewModel?.Globe3DViewModel?.CameraControl != null)
        {
            GlobeMapViewModel.Globe3DViewModel.CameraControl.TargetPos = value;
        }

        RefreshTargetAndCameraValues();
    }
}

public double TargetAltitude
{
    get
    {
        if (GlobeMapViewModel?.Globe3DViewModel?.CameraControl != null)
            return GlobeMapViewModel.Globe3DViewModel.CameraControl.TargetAltitude;
        return 0;
    }
    set
    {
        if (GlobeMapViewModel?.Globe3DViewModel?.CameraControl != null)
        {
            GlobeMapViewModel.Globe3DViewModel.CameraControl.TargetAltitude = value;
        }

        NotifyPropertyChanged(() => TargetAltitude);
    }
}

public bool FollowGround
{
    get
    {
        if (GlobeMapViewModel?.Globe3DViewModel?.CameraControl != null)
            return GlobeMapViewModel.Globe3DViewModel.CameraControl.FollowGround;
        return true;
    }
    set
    {
        if (GlobeMapViewModel?.Globe3DViewModel?.CameraControl != null)
        {
            GlobeMapViewModel.Globe3DViewModel.CameraControl.FollowGround = value;
        }

        NotifyPropertyChanged(() => FollowGround);
    }
}

public double TargetDistance
{
    get
    {
        if (GlobeMapViewModel?.Globe3DViewModel?.CameraControl != null)
            return GlobeMapViewModel.Globe3DViewModel.CameraControl.TargetDistance;
        return 0;
    }
    set
    {
        if (GlobeMapViewModel?.Globe3DViewModel?.CameraControl != null)
        {
            GlobeMapViewModel.Globe3DViewModel.CameraControl.TargetDistance = value;
        }

        NotifyPropertyChanged(() => TargetDistance);
    }
}
. . .
private void RefreshTargetAndCameraValues()
{ 
    NotifyPropertyChanged(() => FollowGround);
    NotifyPropertyChanged(() => Autofollow);

    NotifyPropertyChanged(() => CameraPitch);
    NotifyPropertyChanged(() => CameraRoll);
    NotifyPropertyChanged(() => CameraYaw);

    NotifyPropertyChanged(() => TargetAltitude);
    NotifyPropertyChanged(() => TargetPos);
    NotifyPropertyChanged(() => TargetPosText);
    NotifyPropertyChanged(() => TargetDistance);
}
. . .

... and update MapViewModel.Autofollow when auto follow has changed in MainViewModel!

. . .
public bool IsAutoFollowActive
{
   . . .
    set
    {
        if (!(value && GlobeMapViewModel.AutoFollow.FollowSelectedItem()))
            GlobeMapViewModel.AutoFollow.TargetItem = null;

        MapViewModel.Autofollow = IsAutoFollowActive;
        NotifyPropertyChanged(() => IsAutoFollowActive);
        NotifyPropertyChanged(() => AutoFollowItemName);
    }
}
. . .


Running with display of 3D settings

Running the Globe client in 3D mode, altering the 3D parameters will give results like this:

Default 3D setting
Default 3D setting

Modified pitch
Modified pitch

Modified yaw
Modified yaw

With auto follow, track as target
With auto follow, track as target

Auto follow - closer
Auto follow - closer

Modified roll
Modified roll