Continuing on from : 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:
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).
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 :
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:
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 :
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.
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:
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 :
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
Using the ExigLib package by Simon McKenzie gives us this code :
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:
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