Wednesday, 13 August 2025

Enhanced IP Geolocation App with GPS Fallback in .NET MAUI

Here's a complete implementation that combines IP geolocation with device GPS for maximum accuracy in a single-screen MAUI application:

1. Project Setup

First, create a new .NET MAUI project and add these NuGet packages:

bash
dotnet add package Newtonsoft.Json
dotnet add package Microsoft.Maui.Controls.Maps
dotnet add package Microsoft.Maui.Essentials

2. MainPage.xaml

xml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:maps="clr-namespace:Microsoft.Maui.Controls.Maps;assembly=Microsoft.Maui.Controls.Maps"
             x:Class="IpGeolocationApp.MainPage"
             Title="Advanced Geolocator">

    <Grid>
        <!-- Map -->
        <maps:Map x:Name="mapControl" 
                 MapType="Street"
                 IsShowingUser="True"/>

        <!-- Information Overlay -->
        <Frame Margin="20" 
               VerticalOptions="Start"
               BackgroundColor="{AppThemeBinding Light=White, Dark=#202020}"
               BorderColor="{AppThemeBinding Light=LightGray, Dark=Gray}"
               CornerRadius="10"
               HasShadow="True">
            
            <StackLayout Spacing="10">
                <Label Text="Advanced Geolocation" 
                       FontSize="Title"
                       FontAttributes="Bold"
                       HorizontalOptions="Center"/>

                <Grid ColumnDefinitions="Auto, *" RowDefinitions="Auto, Auto, Auto, Auto, Auto, Auto">
                    <Label Text="Method:" Grid.Row="0" Grid.Column="0" FontAttributes="Bold"/>
                    <Label x:Name="methodLabel" Grid.Row="0" Grid.Column="1"/>

                    <Label Text="IP Address:" Grid.Row="1" Grid.Column="0" FontAttributes="Bold"/>
                    <Label x:Name="ipLabel" Grid.Row="1" Grid.Column="1"/>

                    <Label Text="Location:" Grid.Row="2" Grid.Column="0" FontAttributes="Bold"/>
                    <Label x:Name="locationLabel" Grid.Row="2" Grid.Column="1"/>

                    <Label Text="Coordinates:" Grid.Row="3" Grid.Column="0" FontAttributes="Bold"/>
                    <Label x:Name="coordinatesLabel" Grid.Row="3" Grid.Column="1"/>

                    <Label Text="ISP:" Grid.Row="4" Grid.Column="0" FontAttributes="Bold"/>
                    <Label x:Name="ispLabel" Grid.Row="4" Grid.Column="1"/>

                    <Label Text="Accuracy:" Grid.Row="5" Grid.Column="0" FontAttributes="Bold"/>
                    <Label x:Name="accuracyLabel" Grid.Row="5" Grid.Column="1"/>
                </Grid>

                <Button Text="Refresh" 
                        Clicked="OnRefreshClicked"
                        HorizontalOptions="Center"
                        Margin="0,10,0,0"/>
            </StackLayout>
        </Frame>

        <!-- Activity Indicator -->
        <ActivityIndicator x:Name="loadingIndicator"
                           IsRunning="False"
                           Color="{AppThemeBinding Light=Blue, Dark=White}"
                           HorizontalOptions="Center"
                           VerticalOptions="Center"/>
    </Grid>
</ContentPage>

3. MainPage.xaml.cs

csharp
using System.Net;
using Newtonsoft.Json;
using Microsoft.Maui.Devices.Sensors;
using Microsoft.Maui.Controls.Maps;
using Microsoft.Maui.Maps;
using Map = Microsoft.Maui.Controls.Maps.Map;

namespace IpGeolocationApp;

public partial class MainPage : ContentPage
{
    private const string GeolocationApiUrl = "https://ipapi.co/json/";
    
    public MainPage()
    {
        InitializeComponent();
        Loaded += OnPageLoaded;
    }

    private async void OnPageLoaded(object sender, EventArgs e)
    {
        await FetchAndDisplayLocation();
    }

    private async void OnRefreshClicked(object sender, EventArgs e)
    {
        await FetchAndDisplayLocation();
    }

    private async Task FetchAndDisplayLocation()
    {
        loadingIndicator.IsRunning = true;
        
        try
        {
            // Try to get GPS location first
            var gpsLocation = await GetGpsLocation();
            
            if (gpsLocation != null)
            {
                // Use GPS location if available
                var locationData = new LocationData
                {
                    Latitude = gpsLocation.Latitude,
                    Longitude = gpsLocation.Longitude,
                    Source = "GPS",
                    Accuracy = $"~{gpsLocation.Accuracy?.ToString("0") ?? "Unknown"} meters"
                };
                
                // Reverse geocode to get address
                await GetAddressFromCoordinates(locationData);
                
                UpdateUI(locationData);
                UpdateMap(locationData);
            }
            else
            {
                // Fall back to IP geolocation
                await FetchIpGeolocation();
            }
        }
        catch (Exception ex)
        {
            await DisplayAlert("Error", $"Failed to get location: {ex.Message}", "OK");
        }
        finally
        {
            loadingIndicator.IsRunning = false;
        }
    }

    private async Task<Location?> GetGpsLocation()
    {
        try
        {
            var status = await Permissions.CheckStatusAsync<Permissions.LocationWhenInUse>();
            
            if (status != PermissionStatus.Granted)
            {
                status = await Permissions.RequestAsync<Permissions.LocationWhenInUse>();
            }
            
            if (status == PermissionStatus.Granted)
            {
                var request = new GeolocationRequest(GeolocationAccuracy.High);
                return await Geolocation.GetLocationAsync(request);
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"GPS Error: {ex.Message}");
        }
        
        return null;
    }

    private async Task FetchIpGeolocation()
    {
        using var client = new HttpClient();
        var response = await client.GetStringAsync(GeolocationApiUrl);
        var locationData = JsonConvert.DeserializeObject<LocationData>(response);
        
        locationData.Source = "IP Geolocation";
        locationData.Accuracy = "City-level (IP-based)";
        
        UpdateUI(locationData);
        UpdateMap(locationData);
    }

    private async Task GetAddressFromCoordinates(LocationData data)
    {
        try
        {
            var placemarks = await Geocoding.GetPlacemarksAsync(data.Latitude, data.Longitude);
            var placemark = placemarks?.FirstOrDefault();
            
            if (placemark != null)
            {
                data.City = placemark.Locality;
                data.Region = placemark.AdminArea;
                data.CountryName = placemark.CountryName;
                data.Postal = placemark.PostalCode;
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Geocoding error: {ex.Message}");
        }
    }

    private void UpdateUI(LocationData data)
    {
        methodLabel.Text = data.Source;
        ipLabel.Text = data.Ip ?? "Not available";
        locationLabel.Text = $"{data.City ?? "Unknown"}, {data.Region ?? "Unknown"}, {data.CountryName ?? "Unknown"}";
        coordinatesLabel.Text = $"{data.Latitude:0.00000}, {data.Longitude:0.00000}";
        ispLabel.Text = data.Org ?? "Not available";
        accuracyLabel.Text = data.Accuracy ?? "Unknown";
    }

    private void UpdateMap(LocationData data)
    {
        var position = new Location(data.Latitude, data.Longitude);
        var mapSpan = MapSpan.FromCenterAndRadius(position, Distance.FromKilometers(data.Source == "GPS" ? 1 : 10));
        
        mapControl.MoveToRegion(mapSpan);
        
        // Add pin for the location
        mapControl.Pins.Clear();
        mapControl.Pins.Add(new Pin
        {
            Label = $"Your Location ({data.Source})",
            Address = $"{data.City}, {data.CountryName}",
            Location = position,
            Type = data.Source == "GPS" ? PinType.SavedPin : PinType.Place
        });
    }
}

public class LocationData
{
    public string Ip { get; set; }
    public string City { get; set; }
    public string Region { get; set; }
    public string CountryName { get; set; }
    public string Postal { get; set; }
    public double Latitude { get; set; }
    public double Longitude { get; set; }
    public string Org { get; set; }
    public string Source { get; set; }
    public string Accuracy { get; set; }
}

4. Platform-Specific Setup

Android (Platforms/Android/MainApplication.cs)

csharp
// Add these permissions to AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

iOS (Platforms/iOS/Info.plist)

xml
<key>NSLocationWhenInUseUsageDescription</key>
<string>We need your location to show your precise position on the map</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>We need your location to show your precise position on the map</string>

5. Key Features

  1. Hybrid Location Detection:

    • First attempts to get precise GPS location (if permissions granted)

    • Falls back to IP geolocation if GPS unavailable

    • Clearly indicates which method was used

  2. Enhanced Accuracy:

    • GPS provides meter-level accuracy when available

    • IP geolocation provides city-level accuracy as fallback

    • Displays accuracy estimate for both methods

  3. Reverse Geocoding:

    • Converts GPS coordinates to human-readable addresses

    • Provides consistent display format regardless of source

  4. User Experience:

    • Visual distinction between GPS and IP-based locations

    • Automatic zoom level adjustment based on accuracy

    • Different pin types for different location sources

  5. Error Handling:

    • Graceful degradation when features aren't available

    • Permission request handling

    • Network error recovery

6. How It Works

  1. On Launch:

    • App checks for location permissions

    • Attempts to get GPS location first

    • If GPS fails (or denied), falls back to IP geolocation

    • Displays results with source and accuracy information

  2. GPS Mode:

    • Uses device's GPS sensor

    • Provides coordinates with meter-level accuracy

    • Reverse geocodes to get address information

    • Zooms map to 1km radius

  3. IP Mode:

    • Uses ipapi.co service

    • Provides city-level accuracy

    • Shows ISP information

    • Zooms map to 10km radius

  4. Refresh:

    • Button allows manual refresh

    • Re-runs the entire location detection process

This implementation provides the most accurate possible location by combining both techniques while maintaining a clean, single-screen interface. The app automatically uses the best available method and clearly communicates the accuracy level to users.

No comments:

Post a Comment

Complete Guide: Building a Live Cricket Streaming App for 100M Users

Comprehensive guide to building a scalable live cricket streaming platform for 100M users, covering backend infrastructure, streaming techno...