Create a Place Explorer App Using .NET MAUI and ChatGPT

Jollen Moyani - May 25 '23 - - Dev Community

In this blog post, we will explore the process of developing a .NET MAUI application that leverages OpenAI’s ChatGPT APIs to deliver place recommendations based on the user’s location. The application will feature a user-friendly interface where users can input their geographical location, and in return, they will receive information about popular landmarks and attractions nearby.

To get started with developing the application, there are a few prerequisites that need to be fulfilled:

  • OpenAI account : You will need to have an account with OpenAI to access their APIs and obtain an API key.
  • OpenAI API key : Once you have an account, generate an API key from OpenAI. This key will be used to authenticate your requests to their APIs.
  • .NET MAUI installation : Ensure you have the necessary dependencies and tools installed for .NET MAUI application development. This includes having the .NET SDK, MAUI workload, and related libraries set up on your development machine.

Next, I’ll detail how to meet these prerequisites.

Note: To access OpenAI’s APIs in your .NET MAUI application, you need the API key, which is not free. If you are considering a paid account, this blog will give you an idea of how you might utilize it, even if you can’t follow along. Check the pricing page for more details.

How to create an Open AI account

To create an OpenAI account, follow these steps:

  1. Go to the official OpenAI website.
  2. Look for the Sign Up button on the website’s homepage and click on it. This will direct you to the account creation page.
  3. Fill out the required information in the sign-up form.
  4. Verify your email address by clicking on a verification link sent to your registered email.
  5. Once your registration is successful, you should be able to log in to your OpenAI account using the credentials you provided during the sign-up process.

By following these steps, you can create an OpenAI account and gain access to their services.

How to get an OpenAI API key

To obtain an OpenAI API key:

  1. Visit the OpenAI website and sign in using your account credentials.
  2. Navigate to the API section of the OpenAI platform. This can be found in the Account Settings.
  3. Create a new secret API key. This is where you are required to purchase it.

Create a place explorer app with .NET MAUI

Please create a .NET MAUI application and follow the instructions within the project.

Configuring ChatGPT APIs

To integrate the ChatGPT library and call its APIs in your .NET MAUI project, follow these steps:

  1. Reference and bootstrap the ChatGptNet library. Open the Package Manager Console from the Tools menu in Visual Studio, and run the following command to install the ChatGptNet library.
Install-Package ChatGptNet
Enter fullscreen mode Exit fullscreen mode

2.In the MauiProgram.cs file, locate the CreateMauiApp Add the following code.

builder.Services.AddChatGpt(options =>
{
   options.ApiKey = ""; // Your API Key Here;
   options.Organization = null; // Optional
   options.DefaultModel = ChatGptModels.Gpt35Turbo; // Default: ChatGptModels.Gpt35Turbo
   options.MessageLimit = 10; // Default: 10
   options.MessageExpiration = TimeSpan.FromMinutes(5); // Default: 1 hour
});
Enter fullscreen mode Exit fullscreen mode

Add necessary NuGet packages

In this application, I will utilize the Syncfusion .NET MAUI controls, specifically the Maps, Busy Indicator, and Popup controls. These controls will play crucial roles in enhancing the functionality and user experience of the app.

The Maps control will be the centerpiece of the application, allowing users to select a specific location by picking the latitude and longitude. With the Maps control, users will have an interactive and visually appealing interface to navigate and explore different areas.

To provide a seamless user experience, I will incorporate the Busy Indicator control. This control will be activated when the application communicates with the ChatGPT API to retrieve recommendations based on the selected location. The Busy Indicator will display an animated loading indicator, assuring users that the app is working in the background to fetch the desired information.

I will utilize the Popup control to present the output corresponding to the selected location. This control will enable me to display relevant information and recommendations in an organized and visually appealing manner.

Syncfusion .NET MAUI components are available on NuGet.org. To add SfMaps to your project, open the NuGet package manager in Visual Studio, search for Syncfusion.Maui.Maps , and then install it. Similarly, SfBusyIndicator is available in the Syncfusion.Maui.Core NuGet package and SfPopup is available in the Syncfusion.Maui.Popup NuGet package.

Handler registration

Syncfusion.Maui.Core NuGet is a dependent package for all Syncfusion .NET MAUI controls. In the MauiProgram.cs file, register the handler for Syncfusion core.

using ChatGptNet;
using ChatGptNet.Models;
using Microsoft.Extensions.Logging;
using Syncfusion.Maui.Core.Hosting;

namespace ChatGPTinMAUI;

public static class MauiProgram
{
   public static MauiApp CreateMauiApp()
   {
    var builder = MauiApp.CreateBuilder();
    builder.UseMauiApp<App>()

        builder.ConfigureSyncfusionCore();
        builder.Services.AddChatGpt(options =>
        {
            options.ApiKey = ""; // Your API key here; options.Organization = null; // Optional
            options.DefaultModel = ChatGptModels.Gpt35Turbo; // Default: ChatGptModels.Gpt35Turbo
            options.MessageLimit = 10; // Default: 10
            options.MessageExpiration = TimeSpan.FromMinutes(5); // Default: 1 hour
        });
       #if DEBUG
         builder.Logging.AddDebug();
       #endif
         return builder.Build();
    }
}
Enter fullscreen mode Exit fullscreen mode

Designing the UI

Create the required UI:

  1. Declare the required namespaces in the ContentPage , which represents a page in the application’s user interface.

The XAML code begins with declaring the necessary namespaces using the xmlns attribute. The required namespaces are:

  • clr-namespace:Syncfusion.Maui.Maps;assembly=Syncfusion.Maui.Maps ” for the Syncfusion Maps control.
  • clr-namespace:Syncfusion.Maui.Core;assembly=Syncfusion.Maui.Core ” for the Syncfusion BusyIndicator control.
  • clr-namespace:Syncfusion.Maui.Popup;assembly=Syncfusion.Maui.Popup ” for the Syncfusion Popup control.
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:maps="clr-namespace:Syncfusion.Maui.Maps;assembly=Syncfusion.Maui.Maps"
             xmlns:busy="clr-namespace:Syncfusion.Maui.Core;assembly=Syncfusion.Maui.Core"
             xmlns:syncfusion="clr-namespace:Syncfusion.Maui.Popup;assembly=Syncfusion.Maui.Popup"
             x:Class="ChatGPTinMAUI.MainPage">
</ContentPage>

Enter fullscreen mode Exit fullscreen mode

2.Inside the ContentPage, add an instance of the busy:SfBusyIndicator control. Here, I am naming it busyIndicator.

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:maps="clr-namespace:Syncfusion.Maui.Maps;assembly=Syncfusion.Maui.Maps"
             xmlns:busy="clr-namespace:Syncfusion.Maui.Core;assembly=Syncfusion.Maui.Core"
             xmlns:syncfusion="clr-namespace:Syncfusion.Maui.Popup;assembly=Syncfusion.Maui.Popup"
             x:Class="ChatGPTinMAUI.MainPage">
 <busy:SfBusyIndicator x:Name="busyIndicator">
 </busy:SfBusyIndicator>
</ContentPage>
Enter fullscreen mode Exit fullscreen mode

3.Add a ScrollView element to provide vertical scrolling functionality for the content within the page. Within the ScrollView , add a Grid element. The Grid is a container for other controls and allows flexible layout arrangements.

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:maps="clr-namespace:Syncfusion.Maui.Maps;assembly=Syncfusion.Maui.Maps"
             xmlns:busy="clr-namespace:Syncfusion.Maui.Core;assembly=Syncfusion.Maui.Core"
             xmlns:syncfusion="clr-namespace:Syncfusion.Maui.Popup;assembly=Syncfusion.Maui.Popup"
             x:Class="ChatGPTinMAUI.MainPage">
 <busy:SfBusyIndicator x:Name="busyIndicator">
  <ScrollView>
   <Grid>
   </Grid>
  </ScrollView>
 </busy:SfBusyIndicator>
</ContentPage>
Enter fullscreen mode Exit fullscreen mode

4.Inside the grid, add a maps:SfMaps control. This control displays a map using OpenStreetMap tiles. The MapTileLayer element defines the URL template for the map tiles, and the MapZoomPanBehavior specifies the zooming and panning behavior.

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:maps="clr-namespace:Syncfusion.Maui.Maps;assembly=Syncfusion.Maui.Maps"
             xmlns:busy="clr-namespace:Syncfusion.Maui.Core;assembly=Syncfusion.Maui.Core"
             xmlns:syncfusion="clr-namespace:Syncfusion.Maui.Popup;assembly=Syncfusion.Maui.Popup"
             x:Class="ChatGPTinMAUI.MainPage">
 <busy:SfBusyIndicator x:Name="busyIndicator">
  <ScrollView>
   <Grid >
    <maps:SfMaps x:Name="maps">
     <maps:SfMaps.Layer>
      <maps:MapTileLayer UrlTemplate="https://tile.openstreetmap.org/{z}/{x}/{y}.png">
       <maps:MapTileLayer.ZoomPanBehavior>
        <maps:MapZoomPanBehavior MinZoomLevel="3"
                                 MaxZoomLevel="10"
                                 EnableDoubleTapZooming="True"
                                 ZoomLevel="3">
        </maps:MapZoomPanBehavior>
       </maps:MapTileLayer.ZoomPanBehavior>
      </maps:MapTileLayer>
     </maps:SfMaps.Layer>
    </maps:SfMaps>
   </Grid>
  </ScrollView>
 </busy:SfBusyIndicator>
</ContentPage>
Enter fullscreen mode Exit fullscreen mode

5.Inside the Grid , add a syncfusion:SfPopup control. I am naming popupDisplay. It displays a pop-up window when triggered. The SfPopup.ContentTemplate defines the content that will be displayed in the pop-up. It contains a grid with a label displaying the places to visit in the selected location and a scroll view with a label bound to a ContentText property for displaying the content.

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:maps="clr-namespace:Syncfusion.Maui.Maps;assembly=Syncfusion.Maui.Maps"
             xmlns:busy="clr-namespace:Syncfusion.Maui.Core;assembly=Syncfusion.Maui.Core"
             xmlns:syncfusion="clr-namespace:Syncfusion.Maui.Popup;assembly=Syncfusion.Maui.Popup"
             x:Class="ChatGPTinMAUI.MainPage">
 <busy:SfBusyIndicator x:Name="busyIndicator">
  <ScrollView>
   <Grid >
    <maps:SfMaps x:Name="maps">
     <maps:SfMaps.Layer>
      <maps:MapTileLayer UrlTemplate="https://tile.openstreetmap.org/{z}/{x}/{y}.png">
       <maps:MapTileLayer.ZoomPanBehavior>
        <maps:MapZoomPanBehavior MinZoomLevel="3"
                                 MaxZoomLevel="10"
                                 EnableDoubleTapZooming="True"
                                 ZoomLevel="3">
        </maps:MapZoomPanBehavior>
       </maps:MapTileLayer.ZoomPanBehavior>
      </maps:MapTileLayer>
     </maps:SfMaps.Layer>
    </maps:SfMaps>
    <syncfusion:SfPopup x:Name="popupDisplay" 
                        IsOpen="false" 
                        ShowHeader="False"
                        WidthRequest="500"
                        HeightRequest="300">
     <syncfusion:SfPopup.ContentTemplate>
      <DataTemplate>
       <Grid RowDefinitions="20,*">
        <Label Text="5 places to visit in this location :" FontSize="14" FontAttributes="Bold" />
        <ScrollView Grid.Row="1" >
         <Label Text="{Binding ContentText}" Margin="0,10,0,10" />
        </ScrollView>
       </Grid>
      </DataTemplate>
     </syncfusion:SfPopup.ContentTemplate>
    </syncfusion:SfPopup>
   </Grid>
  </ScrollView>
 </busy:SfBusyIndicator>
</ContentPage>
Enter fullscreen mode Exit fullscreen mode

Overall, the XAML code creates a content page with a Busy Indicator control, a scroll view, a Maps control, and a Popup control.

Implementing the code-behind

  1. Declare the following namespaces in the code-behind file:
    • ChatGptNet and ChatGptNet.Models : Namespaces for the ChatGptNet library, which provides functionality for making calls to OpenAI’s ChatGPT API.
    • Syncfusion.Maui.Maps : The Syncfusion Maps control’s namespace for displaying maps.
using ChatGptNet;
using ChatGptNet.Models;
using Syncfusion.Maui.Maps;

Enter fullscreen mode Exit fullscreen mode

The code is within the ChatGPTinMAUI namespace, and the MainPage class is defined as a partial class extending the ContentPage class.

using ChatGptNet;
using ChatGptNet.Models;
using Syncfusion.Maui.Maps;
namespace ChatGPTinMAUI;

public partial class MainPage : ContentPage
{
}

Enter fullscreen mode Exit fullscreen mode

2.Define the Title and ContentText properties. The Title is a bindable property defined using the BindableProperty.Create method. It represents the title of the page and enables binding, animation, and styling. The ContentText property is also a bindable property, representing the content text displayed on the page.

using ChatGptNet;
using ChatGptNet.Models;
using Syncfusion.Maui.Maps;

namespace ChatGPTinMAUI;

public partial class MainPage : ContentPage
{
    public String Title
    {
        get { return (String)GetValue(TitleProperty); }
        set { SetValue(TitleProperty, value); }
    }

    public static readonly BindableProperty TitleProperty =
        BindableProperty.Create("Title", typeof(String), typeof(MainPage), String.Empty);

    public String ContentText
    {
        get { return (String)GetValue(ContentTextProperty); }
        set { SetValue(ContentTextProperty, value); }
    }

    public static readonly BindableProperty ContentTextProperty =
        BindableProperty.Create("ContentText", typeof(String), typeof(MainPage), String.Empty);
}
Enter fullscreen mode Exit fullscreen mode

3.In the MainPage constructor, initialize the page by calling InitializeComponent and subscribe to the Loaded event.

using ChatGptNet;
using ChatGptNet.Models;
using Syncfusion.Maui.Maps;

namespace ChatGPTinMAUI;

public partial class MainPage : ContentPage
{
    public String Title
    {
        get { return (String)GetValue(TitleProperty); }
        set { SetValue(TitleProperty, value); }
    }

    public static readonly BindableProperty TitleProperty =
        BindableProperty.Create("Title", typeof(String), typeof(MainPage), String.Empty);

    public String ContentText
    {
        get { return (String)GetValue(ContentTextProperty); }
        set { SetValue(ContentTextProperty, value); }
    }

    public static readonly BindableProperty ContentTextProperty =
        BindableProperty.Create("ContentText", typeof(String), typeof(MainPage), String.Empty);

    private IChatGptClient _chatGptClient;
    private Guid _sessionGuid = Guid.Empty;
    MapMarkerCollection mapMarkers = new MapMarkerCollection();
    MapMarker mapMarker = new MapMarker();

    public MainPage()
    {
        InitializeComponent();

        this.Loaded += MainPage_Loaded;
        mapMarkers.Add(mapMarker);
        this.BindingContext = this;
    }
}
Enter fullscreen mode Exit fullscreen mode

4.The MainPage_Loaded event handler is invoked when the page is loaded. Retrieve an instance of the IChatGptClient service using dependency injection in this event.

using ChatGptNet;
using ChatGptNet.Models;
using Syncfusion.Maui.Maps;

namespace ChatGPTinMAUI;

public partial class MainPage : ContentPage
{
    public String Title
    {
        get { return (String)GetValue(TitleProperty); }
        set { SetValue(TitleProperty, value); }
    }

    public static readonly BindableProperty TitleProperty =
        BindableProperty.Create("Title", typeof(String), typeof(MainPage), String.Empty);

    public String ContentText
    {
        get { return (String)GetValue(ContentTextProperty); }
        set { SetValue(ContentTextProperty, value); }
    }

     public static readonly BindableProperty ContentTextProperty =
        BindableProperty.Create("ContentText", typeof(String), typeof(MainPage), String.Empty);

    private IChatGptClient _chatGptClient;
    private Guid _sessionGuid = Guid.Empty;
    MapMarkerCollection mapMarkers = new MapMarkerCollection();
    MapMarker mapMarker = new MapMarker();

    public MainPage()
    {
        InitializeComponent();

        this.Loaded += MainPage_Loaded;
        mapMarkers.Add(mapMarker);
        this.BindingContext = this;
    }

     private void MainPage_Loaded(object sender, EventArgs e)
     {
        _chatGptClient = Handler.MauiContext.Services.GetService<IChatGptClient>();
     }
}
Enter fullscreen mode Exit fullscreen mode

5.Add the MainPage_Tapped event handler. The MainPage_Tapped event handler is called when the user taps on the map. It retrieves the latitude and longitude of the tapped location and calls the GetNearByTouristAttraction method.

using ChatGptNet;
using ChatGptNet.Models;
using Syncfusion.Maui.Maps;

namespace ChatGPTinMAUI;

public partial class MainPage : ContentPage
{
    public String Title
    {
        get { return (String)GetValue(TitleProperty); }
        set { SetValue(TitleProperty, value); }
    }

    public static readonly BindableProperty TitleProperty =
        BindableProperty.Create("Title", typeof(String), typeof(MainPage), String.Empty);

    public String ContentText
    {
        get { return (String)GetValue(ContentTextProperty); }
        set { SetValue(ContentTextProperty, value); }
    }

    public static readonly BindableProperty ContentTextProperty =
        BindableProperty.Create("ContentText", typeof(String), typeof(MainPage), String.Empty);

    private IChatGptClient _chatGptClient;
    private Guid _sessionGuid = Guid.Empty;
    MapMarkerCollection mapMarkers = new MapMarkerCollection();
    MapMarker mapMarker = new MapMarker();

    public MainPage()
    {
        InitializeComponent();

        this.Loaded += MainPage_Loaded;
        mapMarkers.Add(mapMarker);

        this.BindingContext = this;
    }

    private void MainPage_Loaded(object sender, EventArgs e)
    {
        _chatGptClient = Handler.MauiContext.Services.GetService<IChatGptClient>();
    }

    private async void MainPage_Tapped(object sender, Syncfusion.Maui.Maps.TappedEventArgs e)
    {
        var latlong = (this.maps.Layer as MapTileLayer).GetLatLngFromPoint(e.Position);
        var geoLocation = "Latitude:"+ latlong.Latitude.ToString() + ",Longitude:" + latlong.Longitude.ToString();

        await GetNearByTouristAttraction(geoLocation);
        this.popupDisplay.Show(e.Position.X, e.Position.Y);
        this.popupDisplay.Show(e.Position.X, e.Position.Y);
    }
}
Enter fullscreen mode Exit fullscreen mode

The GetNearByTouristAttraction method is an asynchronous method that fetches nearby tourist attractions based on the provided geolocation. It displays a loading indicator while the request is being processed. It makes a request to the ChatGPT Client to get recommendations for tourist attractions and then updates the ContentText property with the response.

using ChatGptNet;
using ChatGptNet.Models;
using Syncfusion.Maui.Maps;

namespace ChatGPTinMAUI;

public partial class MainPage : ContentPage
{
    public String Title
    {
        get { return (String)GetValue(TitleProperty); }
        set { SetValue(TitleProperty, value); }
    }

    public static readonly BindableProperty TitleProperty =
        BindableProperty.Create("Title", typeof(String), typeof(MainPage), String.Empty);

    public String ContentText
    {
        get { return (String)GetValue(ContentTextProperty); }
        set { SetValue(ContentTextProperty, value); }
    }

    public static readonly BindableProperty ContentTextProperty =
        BindableProperty.Create("ContentText", typeof(String), typeof(MainPage), String.Empty);

    private IChatGptClient _chatGptClient;
    private Guid _sessionGuid = Guid.Empty;
    MapMarkerCollection mapMarkers = new MapMarkerCollection();
    MapMarker mapMarker = new MapMarker();

    public MainPage()
    {
        InitializeComponent();

        this.Loaded += MainPage_Loaded;
        mapMarkers.Add(mapMarker);
        this.BindingContext = this;
    }

    private void MainPage_Loaded(object sender, EventArgs e)
    {
        _chatGptClient = Handler.MauiContext.Services.GetService<IChatGptClient>();
    }

    private async void MainPage_Tapped(object sender, Syncfusion.Maui.Maps.TappedEventArgs e)
    {
        var latlong = (this.maps.Layer as MapTileLayer).GetLatLngFromPoint(e.Position);
        var geoLocation = "Latitude:"+ latlong.Latitude.ToString() + ",Longitude:" + latlong.Longitude.ToString();

        await GetNearByTouristAttraction(geoLocation);
        this.popupDisplay.Show(e.Position.X, e.Position.Y);
        this.popupDisplay.Show(e.Position.X, e.Position.Y);
    }

    private async Task GetNearByTouristAttraction(string geoLocation)
    {
        this.busyIndicator.IsRunning = true;
        if (string.IsNullOrWhiteSpace(geoLocation))
        {
            await DisplayAlert("Empty location", "No Suggestions", "OK");
            return;
        }

        if (_sessionGuid == Guid.Empty)
        {
            _sessionGuid = Guid.NewGuid();
        }

        var query = "Tell me 5 places near the following location (Each with 20 words) " + geoLocation;
        ChatGptResponse response = await _chatGptClient.AskAsync(_sessionGuid, query);
        this.ContentText = response.GetMessage();
        this.busyIndicator.IsRunning = false;
    }
}
Enter fullscreen mode Exit fullscreen mode

Creating a place explorer app using .NET MAUI and ChatGPT

That’s it; we have successfully created a .NET MAUI application with a map control that displays popular tourist attractions near a selected location.

Reference

For more details, refer to the project on GitHub.

Conclusion

This blog guided you in creating a .NET MAUI application that leverages the power of the OpenAI ChatGPT API to deliver tourist recommendations based on the user’s location.

Feel free to experiment by modifying the prompts to enhance the quality of the suggestions. Additionally, you can explore the option of changing the ChatGptModels enum value within the AddChatGpt method in MauiProgram.cs to observe if different models yield improved outcomes.

Syncfusion’s collection of .NET MAUI controls offers a comprehensive suite of tools that enable developers to create robust and feature-rich applications. With their extensive customization options and intuitive APIs, Syncfusion controls provide seamless integration into the .NET MAUI framework, making building cross-platform applications with enhanced functionality easier.

Please try out the steps in this blog and share your feedback in the comment section below. You can also reach us through our support forum, support portal, or feedback portal. We are always happy to assist you!

Related blogs

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .