# Building Map Based Apps - The Camino Part 2

Continuing on from : [Building map based apps - The Camino](https://blog.circumblue.com/building-map-based-apps-the-camino)

I needed a simple app on my phone that would enable me to take a photo and upload it to storage (if connectivity was available) OR select photos that I took during the day and upload them in batches once I had reached my accommodation.

Enter a simple .NET MAUI application:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1696018040964/e2da2074-98a2-479b-9d02-42a60e6ef97b.png align="center")

The *Check Location* button was really just there to wake up the GPS and force it to take a reading before the photo. As I did not have a local SIM and was relying on WiFi, the GPS sometimes needed some encouragement.

The code for this was simple and well documented ([Geolocation - .NET MAUI | Microsoft Learn](https://learn.microsoft.com/en-us/dotnet/maui/platform-integration/device/geolocation?tabs=android)).

```csharp
   public async Task GetCurrentLocation()
    {
        try
        {
            _isCheckingLocation = true;
            GeolocationRequest request = new GeolocationRequest(GeolocationAccuracy.Medium, TimeSpan.FromSeconds(10));
            _cancelTokenSource = new CancellationTokenSource();
            Location location = await Geolocation.Default.GetLocationAsync(request, _cancelTokenSource.Token);

            txtLongitude.Text = "Longitude : " + location.Longitude;
            txtLatitude.Text = "Latitude : " + location.Latitude;
            txtAltitude.Text = "Altitude : " + location.Altitude;

        }
        catch (Exception ex)
        {
            txtLongitude.Text = "Error";
            txtLatitude.Text = "Error";
            txtAltitude.Text = "Error";
        }
        finally
        {
            _isCheckingLocation = false;
        }
    }
```

Then to take a photo :

```csharp
  public async void TakePhoto(object sender, EventArgs e)
    {
        txtResult.Text = "";
        //force phone to update location.
        await GetCurrentLocation();

        if (MediaPicker.Default.IsCaptureSupported)
        {
            FileResult photo = await MediaPicker.Default.CapturePhotoAsync();
            if (photo != null)
            {
                txtResult.Text = "Uploading";                         
                // upload the file to Azure Storage
                await UploadFromFileAsync(photo.FullPath);
                txtResult.Text = "Upload Complete :" + photo.FileName;
            }
        }
    }
```

A similar mechanism to pick an existing photo from the gallery and upload it:

```csharp
   public async void Upload(object sender, EventArgs e)
    {
        //await GetCurrentLocation();
        txtResult.Text = "";
        if (MediaPicker.Default.IsCaptureSupported)
        {
            FileResult photo = await MediaPicker.Default.PickPhotoAsync();
            if (photo != null)
            {
                txtResult.Text = "Uploading";
                await UploadFromFileAsync(photo.FullPath);
                txtResult.Text = "Upload Complete :" + photo.FileName;
            }
        }
    }
```

with the only difference being the use of *PickPhotoAsync* vs. *CapturePhotoAsync.*

For the upload to Azure storage :

```csharp
  public async Task UploadFromFileAsync(string localFilePath)
    {
        Uri container_uri = new Uri(BlobSASURL);
        BlobContainerClient bc = new BlobContainerClient(container_uri);

        string fileName = Path.GetFileName(localFilePath);
        BlobClient blobClient = bc.GetBlobClient(fileName);
        try
        {
            BlobContentInfo r = await blobClient.UploadAsync(localFilePath, true);
        } catch(Exception e) {
            txtResult.Text = e.Message;
        };  
    }
```

Issuing and managing blob access keys can be a chicken and egg situation, and a more comprehensive solution would probably go via an API gateway and avoid direct access to the storage account at all. But for my needs, this worked fine.

I created a Shared Access Signature (*BlobSASURL* above) that had *create* rights on the container and nothing else. This means that new files could be uploaded, but nothing existing could be read or changed.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1696029951766/344c6f59-1633-4e05-b5b9-4168c0bb2b2e.png align="center")

With this app deployed to my phone, I can snap away, with or without connectivity. At the end of each day, I selected the more interesting or representative photos and uploaded them. They appeared on the storage account like this:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1696030742388/7c6718f3-4a79-418e-8cc8-3e6295197134.png align="center")

Several easy enhancements here would allow :

* Tagging each photo with a short description or other metadata.
    
* Enabling different trips or events which would then be selectable and displayed on separate maps.
    
* Multi user separation. While the approach above would work for multiple users, all data would be shown on the same map. This may/may not be what is needed.
    

### Processing the image with an Azure function

An Azure function with a blob trigger then took care of processing the image and pulling out all the data required to display it on the map.

The basic function :

```csharp
public class BlobTrigger
    {
        [FunctionName("BlobTrigger")]
        public void Run([BlobTrigger("images/{name}", Connection = "connstring")] Stream myBlob, string name, ILogger log)
        {
            double latitude = 0, longitude = 0;
            DateTime imagedate = DateTime.Now;
            string locationText = GetCoordinate(myBlob, log, ref latitude, ref longitude, ref imagedate);
         
            SaveToDatabase(name, latitude, longitude,imagedate);
        }
```

When deployed, this is executed every time a new file appears. The code calls *GetCoordinate* to pull out the EXIF data and then *SaveToDatabase* to store to SQL.

The longitude and latitude are automatically added to the image and stored in the Exchangeable Image File Format (EXIF) part of the jpeg file. More info : [EXIF and Coordinates | Cartographic Perspectives](https://cartographicperspectives.org/index.php/journal/article/view/cp75-pagett/html)

Using the ExigLib package by [Simon McKenzie](https://www.codeproject.com/Articles/36342/ExifLib-A-Fast-Exif-Data-Extractor-for-NET) gives us this code :

```csharp
private static string GetCoordinate(Stream image, ILogger log, ref double latitude, ref double longitude, ref DateTime imagedate)
{
   log.LogInformation("Extract location information");
   ExifReader exifReader = new ExifReader(image);
   exifReader.GetTagValue(ExifTags.DateTime, out imagedate);

   double[] latitudeComponents, longitudeComponents;
   String latref, longref;
            
   exifReader.GetTagValue(ExifTags.GPSLatitude, out latitudeComponents);
   exifReader.GetTagValue(ExifTags.GPSLatitudeRef, out latref);
   exifReader.GetTagValue(ExifTags.GPSLongitude, out longitudeComponents);
   exifReader.GetTagValue(ExifTags.GPSLongitudeRef, out longref);

   string location = string.Empty;
   latitude = 0;
   longitude = 0;

   if (latitudeComponents == null || longitudeComponents == null) {
       location = "No location data";
   } else {
       latitude = latitudeComponents[0] + latitudeComponents[1] / 60 + latitudeComponents[2] / 3600;
       if (latref == "S") { latitude = -latitude; }
       longitude = longitudeComponents[0] + longitudeComponents[1] / 60 + longitudeComponents[2] / 3600;
       if (longref == "W") { longitude = -longitude; }
       location = $"Latitude: '{latitude}' | Longitude: '{longitude}'";
   }
   
    return location;
}
```

Not the most elegant of code but some fiddling is required to convert DMS into decimal and South or West into negatives.

Finally, a simple routine to add a row to the SQL table for each photo:

```csharp
private static int SaveToDatabase(String filename, double latitude, double longitude, DateTime imagedate)
{
   String Connstring = Environment.GetEnvironmentVariable("SQLConnString");
   System.Data.SqlClient.SqlConnection sqlConnection1 = new System.Data.SqlClient.SqlConnection(Connstring);
   string format = "yyyy-MM-dd HH:mm:ss";

   System.Data.SqlClient.SqlCommand cmd = new System.Data.SqlClient.SqlCommand();
   cmd.CommandType = System.Data.CommandType.Text;
   cmd.CommandText = "INSERT photolocations (Filename, Latitude, Longitude, photodate) VALUES ('" + filename + "'," + latitude + "," + longitude + ",'" + imagedate.ToString(format) + "')";
   cmd.Connection = sqlConnection1;

   sqlConnection1.Open();
   cmd.ExecuteNonQuery();
   sqlConnection1.Close();
   return 0;
}
```

Very simple old-school way of working with SQL - and will be replaced with Cosmos as part of an upcoming project.

Note that the JPEG images themselves stay on the storage account and SQL contains only metadata.

### Summary

At this point we have a mobile application taking photos, geotagging them and uploading them to cloud storage. The photos are then processed to pull out GPS locations and a unique row is added to SQL with date/time/long/lat/filename.

This is everything that is needed to start on the front end.

Continued here: [Building Map Based Apps - The Camino Part 3](https://blog.circumblue.com/building-map-based-apps-the-camino-part-3)
