WPF: Mapy i „GPS” w praktyce (INF.04)

Aplikacja MapLocator – adres → współrzędne → mapa → odległość

Po co nam „mapy i GPS” w INF.04?

W podstawie INF.04 pojawia się hasło „lokalizacja / GPS / mapy”. W aplikacjach mobilnych to często znaczy: „pobierz pozycję z czujnika”.
W przypadku WPF (desktop) najczęściej robimy to inaczej: pracujemy na współrzędnych geograficznych i wyświetlamy je na mapie, a lokalizację bierzemy np. z internetu (API).

Czyli: uczysz się tego samego „sensu” GPS (punkt na Ziemi), tylko w wersji desktopowej.


1) Teoria – prosto i po ludzku

1.1 Współrzędne GPS: latitude i longitude

Każde miejsce na świecie można opisać dwoma liczbami:

  • latitude (szerokość) – góra/dół na mapie (północ-południe)
  • longitude (długość) – lewo/prawo (wschód-zachód)

Przykład (Warszawa, okolice centrum):
lat ≈ 52.2297
lon ≈ 21.0122

Te dwie liczby to „GPS” w najczystszej postaci.

1.2 Geokodowanie

Geokodowanie = zamiana adresu na współrzędne.
Np. „Działdowo, Grunwaldzka 38” → lat/lon

Reverse geocoding działa odwrotnie: współrzędne → adres.

1.3 Mapy w aplikacji

Są dwa popularne sposoby:

  1. Osadzamy mapę w przeglądarce (w WPF: WebView2) i ładujemy np. OpenStreetMap.
  2. Używamy specjalnych bibliotek mapowych (bardziej zaawansowane – na później).

My wybieramy wersję 1, bo jest prosta i stabilna na lekcję + INF.04.

1.4 Odległość między punktami (po co to uczniowi?)

Bardzo praktyczne:

  • ile km jest z domu do szkoły,
  • jaka odległość do hotelu, firmy, boiska,
  • najbliższy punkt z listy (sklep, przystanek, gabinet).

Do tego używa się wzoru (na Ziemi po kuli) – najczęściej Haversine.


2) Gdzie to się przydaje? (konkretne przykłady)

  • „Znajdź najbliższy punkt” (np. najbliższe boisko z listy).
  • „Wpisz adres i zobacz na mapie”.
  • „Policz odległość od szkoły”.
  • „Plan wycieczki – punkty na mapie”.
  • „Aplikacja dla kuriera – zlecenia z koordynatami”.

3) Narzędzia i technologie w projekcie

3.1 Visual Studio (polska wersja)

Tworzymy projekt WPF w Visual Studio (najlepiej .NET 8).

3.2 WebView2

To kontrolka od Microsoftu, która pozwala wstawić „mini przeglądarkę” do aplikacji WPF.

Dzięki temu:

  • wyświetlamy OpenStreetMap,
  • nie kombinujemy z rysowaniem mapy samemu.

3.3 OpenStreetMap

Darmowa mapa świata dostępna w przeglądarce.
My tylko otwieramy odpowiedni link z lat/lon.

3.4 Nominatim (API do geokodowania)

To publiczne API OpenStreetMap do wyszukiwania adresów.
Wysyłamy zapytanie typu:

  • search?q=Warszawa&format=json&limit=1

Odpowiedź jest w JSON i zawiera lat/lon.

Uwaga praktyczna: to API jest publiczne, więc trzeba używać rozsądnie (limit zapytań). Na lekcję i ćwiczenia jest OK.


4) Projekt: MapLocator (WPF)

Co ma robić aplikacja?

  1. Uczeń wpisuje adres w TextBox.
  2. Klik: Szukaj.
  3. Aplikacja pobiera współrzędne z API (Nominatim).
  4. Wyświetla współrzędne na ekranie.
  5. Ładuje mapę OpenStreetMap na tych współrzędnych.
  6. Liczy odległość do stałego punktu (np. szkoły) i wyświetla wynik.

5) Krok po kroku – robimy aplikację

Krok 1: Utwórz projekt

  1. Plik → Nowy → Projekt
  2. Wybierz: Aplikacja WPF (.NET)
  3. Nazwa: MapLocatorWpf
  4. Framework: .NET 8.0 (lub nowszy)

Krok 2: Dodaj WebView2

  1. W Eksploratorze rozwiązań kliknij prawym na projekt
  2. Zarządzaj pakietami NuGet…
  3. Wyszukaj: Microsoft.Web.WebView2
  4. Zainstaluj

Krok 3: Zrób foldery jak w porządnym projekcie

W projekcie dodaj foldery:

  • Modele
  • Logika
  • Walidacja

(Prawy na projekt → Dodaj → Nowy folder)


Krok 4: Model danych (Modele)

Dodaj klasę: Modele/ModeleLokalizacja.cs

namespace MapLocatorWpf.Modele
{
public class ModeleLokalizacja
{
public string? Adres { get; set; }
public double SzerokoscGeo { get; set; } // latitude
public double DlugoscGeo { get; set; } // longitude
}
}

Krok 5: Walidacja (czy uczeń wpisał adres?)

Dodaj klasę: Walidacja/WalidacjaAdresu.cs

namespace MapLocatorWpf.Walidacja
{
    public static class WalidacjaAdresu
    {
        public static bool CzyPoprawny(string? adres)
        {
            // Minimalna walidacja: czy coś wpisano
            return !string.IsNullOrWhiteSpace(adres);
        }
    }
}

Krok 6: Logika – pobieranie współrzędnych z API (Nominatim)

Dodaj klasę: Logika/LogikaGeokodowanie.cs

using System.Globalization;
using System.Net.Http;
using System.Text.Json;namespace MapLocatorWpf.Logika
{
public class LogikaGeokodowanie
{
private readonly HttpClient _httpClient = new HttpClient(); public LogikaGeokodowanie()
{
// Nominatim wymaga sensownego User-Agent (żeby nie być anonimowym botem).
_httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("MapLocatorWpf/1.0 (school project)");
} public async Task<(double lat, double lon)?> PobierzWspolrzedneAsync(string adres)
{
// Uwaga: adres trzeba zakodować do URL
string adresUrl = Uri.EscapeDataString(adres); // limit=1 – bierzemy pierwszy wynik, żeby było prosto
string url = $"https://nominatim.openstreetmap.org/search?q={adresUrl}&format=json&limit=1"; string json = await _httpClient.GetStringAsync(url); // Odpowiedź to tablica JSON. Jak pusta -> nie znaleziono.
using JsonDocument doc = JsonDocument.Parse(json);
var root = doc.RootElement; if (root.GetArrayLength() == 0)
return null; var pierwszy = root[0]; // Nominatim zwraca lat/lon jako stringi (np. "52.2297")
string latStr = pierwszy.GetProperty("lat").GetString() ?? "";
string lonStr = pierwszy.GetProperty("lon").GetString() ?? ""; // Parsujemy po kropce – dlatego InvariantCulture
if (!double.TryParse(latStr, NumberStyles.Float, CultureInfo.InvariantCulture, out double lat))
return null; if (!double.TryParse(lonStr, NumberStyles.Float, CultureInfo.InvariantCulture, out double lon))
return null; return (lat, lon);
}
}
}

Krok 7: Logika – obliczanie odległości (Haversine)

Dodaj klasę: Logika/LogikaOdleglosc.cs

namespace MapLocatorWpf.Logika
{
public static class LogikaOdleglosc
{
// Promień Ziemi w kilometrach
private const double R = 6371.0; public static double ObliczKm(double lat1, double lon1, double lat2, double lon2)
{
// Zamiana stopni na radiany
double dLat = StopnieNaRadiany(lat2 - lat1);
double dLon = StopnieNaRadiany(lon2 - lon1); double a =
Math.Sin(dLat / 2) * Math.Sin(dLat / 2) +
Math.Cos(StopnieNaRadiany(lat1)) * Math.Cos(StopnieNaRadiany(lat2)) *
Math.Sin(dLon / 2) * Math.Sin(dLon / 2); double c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a)); return R * c;
} private static double StopnieNaRadiany(double stopnie)
{
return stopnie * Math.PI / 180.0;
}
}
}

Krok 8: Interfejs – MainWindow.xaml

Otwórz MainWindow.xaml i wklej taki układ.

Uwaga: trzeba dodać namespace do WebView2.

<Window x:Class="MapLocatorWpf.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
        Title="MapLocator - WPF" Height="650" Width="1100">

    <Grid Margin="10">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="380"/>
            <ColumnDefinition Width="10"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>

        <!-- LEWY PANEL -->
        <StackPanel Grid.Column="0">

            <TextBlock FontSize="20"
                       FontWeight="Bold"
                       Text="MapLocator (WPF)"
                       Margin="0,0,0,15"/>

            <!-- Adres -->
            <TextBlock Text="Wpisz adres (np. Działdowo, Grunwaldzka 38):"
                       FontWeight="SemiBold"/>

            <TextBox x:Name="txtAdres"
                     Height="30"
                     Margin="0,5,0,10"/>

            <Button x:Name="btnSzukaj"
                    Content="Szukaj i pokaż na mapie"
                    Height="36"
                    Click="btnSzukaj_Click"/>

            <Separator Margin="0,15,0,15"/>

            <!-- Współrzędne -->
            <TextBlock Text="Współrzędne znalezionego miejsca:"
                       FontWeight="SemiBold"/>

            <TextBlock x:Name="txtWynikLat"
                       Margin="0,6,0,0"/>

            <TextBlock x:Name="txtWynikLon"
                       Margin="0,2,0,10"/>

            <Separator Margin="0,10,0,10"/>

            <!-- Punkt stały -->
            <TextBlock Text="Punkt stały (np. szkoła):"
                       FontWeight="SemiBold"/>

            <TextBlock x:Name="txtPunktStaly"
                       Margin="0,6,0,10"/>

            <Button x:Name="btnOdleglosc"
                    Content="Oblicz odległość do punktu stałego"
                    Height="36"
                    Click="btnOdleglosc_Click"
                    IsEnabled="False"/>

            <TextBlock x:Name="txtOdleglosc"
                       FontSize="16"
                       FontWeight="Bold"
                       Margin="0,10,0,0"/>

            <TextBlock Margin="0,20,0,0"
                       TextWrapping="Wrap"
                       Opacity="0.8">
                W tej aplikacji „GPS” rozumiemy jako współrzędne (lat/lon).
                Adres zamieniamy na współrzędne przez API,
                a mapę wyświetlamy w kontrolce WebView2.
            </TextBlock>

        </StackPanel>

        <!-- PRAWA STRONA – MAPA -->
        <Border Grid.Column="2"
                CornerRadius="8"
                BorderThickness="1"
                BorderBrush="#DDD">

            <wv2:WebView2 x:Name="webMapa"/>

        </Border>

    </Grid>
</Window>

Jeśli w Twoim WPF nie ma PlaceholderText, usuń ten atrybut (nie wszystkie wersje mają).


Krok 9: Kod okna – MainWindow.xaml.cs

Otwórz MainWindow.xaml.cs i wklej:

using System.Globalization;
using System.Windows;
using MapLocatorWpf.Logika;
using MapLocatorWpf.Walidacja;
namespace MapLocatorWpf
{
    public partial class MainWindow : Window
    {
        private readonly LogikaGeokodowanie _geokodowanie = new LogikaGeokodowanie();        // Punkt stały – tu ustawiasz np. szkołę (wstaw swoje realne współrzędne)
        // Przykładowo: Działdowo (warto podmienić na dokładne miejsce szkoły)
        private const double SZKOLA_LAT = 53.2390;
        private const double SZKOLA_LON = 20.1700; private double? _znalezioneLat;
        private double? _znalezioneLon; public MainWindow()
        {
            InitializeComponent();            // Opis punktu stałego w interfejsie
            txtPunktStaly.Text = $"Szkoła: lat={SZKOLA_LAT.ToString("F6", CultureInfo.InvariantCulture)}, " +
                                 $"lon={SZKOLA_LON.ToString("F6", CultureInfo.InvariantCulture)}";            // Startowo ustawiamy mapę na punkt stały (żeby od razu coś było)
            UstawMape(SZKOLA_LAT, SZKOLA_LON, zoom: 15);
        }
        private async void btnSzukaj_Click(object sender, RoutedEventArgs e)
        {
            string adres = txtAdres.Text;            // 1) Walidacja
            if (!WalidacjaAdresu.CzyPoprawny(adres))
            {
                MessageBox.Show("Wpisz adres (minimum kilka znaków).", "Błąd", MessageBoxButton.OK, MessageBoxImage.Warning);
                return;
            }            // 2) Pobranie współrzędnych
            btnSzukaj.IsEnabled = false;
            btnOdleglosc.IsEnabled = false;
            txtOdleglosc.Text = ""; try
            {
                var wynik = await _geokodowanie.PobierzWspolrzedneAsync(adres); if (wynik == null)
                {
                    MessageBox.Show("Nie znalazłem tego adresu. Spróbuj wpisać inaczej (np. miasto + ulica).",
                        "Brak wyników", MessageBoxButton.OK, MessageBoxImage.Information);
                    return;
                }
                _znalezioneLat = wynik.Value.lat;
                _znalezioneLon = wynik.Value.lon;                // 3) Wyświetlenie w UI
                txtWynikLat.Text = $"Latitude (szerokość): {_znalezioneLat.Value.ToString("F6", CultureInfo.InvariantCulture)}";
                txtWynikLon.Text = $"Longitude (długość): {_znalezioneLon.Value.ToString("F6", CultureInfo.InvariantCulture)}";                // 4) Ustaw mapę na znaleziony punkt
                UstawMape(_znalezioneLat.Value, _znalezioneLon.Value, zoom: 16);                // 5) Odblokuj liczenie odległości
                btnOdleglosc.IsEnabled = true;
            }
            catch (Exception ex)
            {
                // Na lekcji warto powiedzieć: „To normalne, że internet czasem nie działa”
                MessageBox.Show("Wystąpił błąd podczas pobierania danych z internetu.\n\n" + ex.Message,
                    "Błąd", MessageBoxButton.OK, MessageBoxImage.Error);
            }
            finally
            {
                btnSzukaj.IsEnabled = true;
            }
        }
        private void btnOdleglosc_Click(object sender, RoutedEventArgs e)
        {
            if (_znalezioneLat == null || _znalezioneLon == null)
            {
                MessageBox.Show("Najpierw wyszukaj adres.", "Info", MessageBoxButton.OK, MessageBoxImage.Information);
                return;
            }
            double km = LogikaOdleglosc.ObliczKm(_znalezioneLat.Value, _znalezioneLon.Value, SZKOLA_LAT, SZKOLA_LON); txtOdleglosc.Text = $"Odległość do szkoły: {km:F2} km";
        }
        private void UstawMape(double lat, double lon, int zoom)
        {
            // OpenStreetMap: ustawiamy widok mapy na danym punkcie
            // Format: https://www.openstreetmap.org/#map=ZOOM/LAT/LON
            string url = $"https://www.openstreetmap.org/#map={zoom}/{lat.ToString(CultureInfo.InvariantCulture)}/{lon.ToString(CultureInfo.InvariantCulture)}";
            webMapa.Source = new Uri(url);
        }
    }
}

6) Co uczeń ma z tego „egzaminacyjnie”?

W tym projekcie uczeń ćwiczy dokładnie to, co często jest sprawdzane w INF.04:

  • Zdarzenia (Click przycisków)
  • Walidacja danych (czy wpisano adres)
  • Komunikacja z API (HttpClient, JSON)
  • Przetwarzanie danych (parsowanie, liczby)
  • Algorytm / obliczenia (odległość – Haversine)
  • UI + logika (podział na foldery, porządek w projekcie)
  • Praca z kontrolką zewnętrzną (WebView2)

7) Rozszerzenia (jak chcesz dopalić temat na kolejną lekcję)

  1. Lista ulubionych miejsc (ListBox + zapis do JSON).
  2. Reverse geocoding (klik na mapie → adres).
  3. „Najbliższy punkt” z listy 10 lokalizacji (pętla + min).
  4. Prosty MVVM (binding do pól zamiast txtWynikLat.Text = ...).

8) Typowe problemy i szybkie rozwiązania

  • WebView2 nie działa / czarny ekran
    Zwykle brakuje runtime WebView2 w systemie. Na większości Windows 10/11 jest, ale jak nie: instalacja „Microsoft Edge WebView2 Runtime”.
  • API zwraca pustą listę
    Adres wpisany zbyt ogólnie. Dopisz miasto, kraj.
  • Zbyt dużo zapytań
    To publiczne API. Na lekcji spoko, ale nie róbmy „odświeżania co 100 ms”.