Openstreetmap-Karten in eigener Anwendung einbauen und weiterverarbeiten

Kurze Vorgeschichte
Zur Nachbereitung wollte ich unsere Wandertouren (wir erstellen oft Tracks mit einem Outdoor-Navigationsgerät) auf einer Landkarte darstellen. Zudem wollte ich die Joggingtouren - Sportuhr mit Trackfunktion und Herzrhythmusmessung - ebenfalls mit einer Karte auswerten: Je nach Herzfrequenz sollte die Route in unterschiedlicher Farbe eingezeichnet werden.
Als Programmiersprache verwende ich C#. Die ProgrammCode-Teile in dieser Ausarbeitung wurden mit Visual Studio / C# erstellt.

Idee:
Ich lade einen Kartenausschnitt, wie man ihn oft in Internetseiten trifft, herunter und zeichne darin die Punkte / Linien ein.
Im Netz findet man viele Beispiele dazu. Hier z.B. eine Adressangabe (URL-Text) für ein Gebiet um den Nördlinger Rieskrater in Bayern:
https://www.openstreetmap.org/export/embed.html?bbox=10.0,48.7,10.9,49.03&layer=mapnik

1. Problem:
Der Webbrowser bei Windows-Forms-Apps unterstützt zwar die Einrichtung von Graphics, aber ich(!) konnte damit keine Graphikobjekte direkt im Webbrowser erstellen. Wenn jemand dazu einen Tipp hat ... schickt mir gerne eine mail dazu!
Nun kopiere ich einfach die Karte in eine Picturebox und dort werden die Tracks eingezeichnet. Das klappt alles - über den kleinen Umweg - problemlos.

2. Problem:
Leider besitzt im Allgemeinen z.B. die Kartenecke links unten nicht die nördliche Breite und die östliche Länge, die ich beim Aufruf verwendet habe (10°;48,7°). Ich dachte die Grenzen der Karte werden durch die Daten im URL-Text beschrieben. Das ist leider nicht der Fall!
Mit diesem Problem werde ich mich hier ausführlich beschäftigen.
Es gibt verschiedene Wege die Karten zu analysieren. Ich habe mich hier für den Weg entschieden, den OSM_Aufruf so zu gestalten, dass die Kartendimension zur Webbrowsergröße passt, indem ich die Längen und eine Breitenangabe vorgebe und eine Breitenangabe entspr. bestimme.


Bild
Dieses Bild erhält man bei einem Browser der Breite 900 Pixel und der Höhe 500 Pixel als Ergebnis bei der obigen URL (...=10.0,48.7,10.9,49.03). Natürlich ohne die roten Eintragungen.
Die Karte ist viel "zu groß"; der "angeforderte" Bereich von 10,0° bis 10,9° östlicher Länge und von 48,7° bis 49,03° nördlicher Breite ist hier mittig rot eingezeichnet. Je nach Längen- und Breitenangaben variiert die Breite des Randes auch noch ... ein Weiterverarbeiten wird schwierig.

           

Aufbau dieser Seite

Einleitung (siehe oben)

A: Das "2. Problem" genauer betrachtet
A1: Angabe der Längen und der unteren nördl. Breite: Wie erhält man die passende obere nördl. Breite?
A2: Wie bestimmt man die Breite der Randbereiche in einer Karte?
A3: Wie erhält man aus den Pixelwerten (im Browser; in der Picturebox) die Länge- und Breitenangabe?

B: Programmierung im Visual Studio / mit C# in einer Windows-Foms-App
B1: Erstellung einer Testumgebung
B2: Bestimmung der oberen nördlichen Breite (zu A1)
B3: Bestimmung der Randbreiten / Randhöhen (zu A2)
B4: Kopie in eine Picturebox; Beispiel der Weiterverarbeitung (zu A3)

 ---------------------------------------------------------------------------------------------------------------

A: Das "2. Problem" genauer betrachtet
Zuerst machen wir uns klar, wie man die Koordinatenangaben wählen muss, damit sie zur Größe des Browsers bzw. der Picturebox passen.
Bild Wir geben die Längenangaben und auch die untere nördliche Breitenangabe vor. Das Verhältnis der horizontalen wirklichen Entfernung (aus der Längendifferenz) zur vertikalen wirklichen Entfernung (aus der Breitendifferenz) sollte 900 zu 500 betragen. (siehe A1)
Damit haben wir auf jeden Fall eine Karte die im Verhältnis zur Webbrowsergröße passt.
Jetzt gilt es herauszufinden, wie groß die Ränder an den Seiten sind. Diese werden bei dem Download mit zusätzlichem Kartenmaterial gefüllt. (siehe A2)

A1: Angabe der Längen und der unteren nördl. Breite: Wie erhält man die passende obere nördl. Breite?
Bild Der schwarze Längenkreis besitzt den Radius R=6370km (Erdradius). Ein Grad auf dem Längenkreis entspricht also der Streckenlänge
Ey = 2πR/360

Der Breitenkreis (untere nördliche Breite α) mit dem Radius r = R·cos(α) besitzt somit den Umfang 2πr und ein Grad darauf entspricht also der Entfernung Ex  = 2πr/360

Bild
Für das Stück von 10° bis 10,9° auf dem Breitenkreis mit α = 48,7° erhält man somit:
        x = Ex · (10,9 - 10,0);

Der Kartenausschnitt soll zu den Browserdaten passen: y:x = 500:900 ==>
        y = x · (500/900)
Das wiederum entspricht auf dem Längenkreis
y/Ey Grad

y/Ey  = x · (500/900) / (2πR/360) =
Ex · (10,9 - 10,0) · (500/900) / (2πR/360)  =  
2πr/360 · (10,9 - 10,0) · (500/900) / (2πR/360)  =
cos(α) · (10,9 - 10,0) · (500/900)




Damit erhält man für den oberen Breitengradwert:    

         β = α + cos(α)·(10,9 - 10,0)·(500/900)



A2: Wie bestimmt man die Breite der Randbereiche in einer Karte?

Dazu muss man wissen, dass OSM-Karten NUR in festgelegten Maßstäben geladen werden können!
siehe https://wiki.openstreetmap.org/wiki/DE:Zoom_levels

Man verwendet zur Beschreibung der Maßstäbe sogenannte Zoomstufen: Sie reichen von 0 bis 19

ZoomstufeLängengraddifferenz
auf einer Karte
mit 256 Pixel Breite
0360
1180
290
345
422,5
511,25
65,625
72,8125
81,40625
90,703125
100,351563
110,175781
120,087891
130,043945
140,021973
150,010986
160,005493
170,002747
180,001373
190,000687
Bei den rechten Werten wird fortlaufend durch 2 geteilt!

Wir berechnen nun für unser obiges Beispiel die optimale Zoomstufe:

1. Versuch: mit der Zoomstufe 13
Das bedeutet 0,043945° Längengraddifferenz auf einer Karte mit 256 Pixel Breite.    0,043945 = 360 / 213
Auf unserer 900-Pixel-Karte könnten wir also 0,043945 · 900/256 Grad ≈ 0,1545 Grad unterbringen:
Das ist zu wenig! Wir benötigen 0,9 Grad.

2. Versuch: mit der Zoomstufe 12
Das bedeutet 0,087891° Längengraddifferenz auf einer Karte mit 256 Pixel Breite.    0,087891 = 360 / 212
Auf unserer 900-Pixel-Karte könnten wir also 0,087891 · 900/256 Grad ≈ 0,309 Grad unterbringen:
Das ist zu wenig! Wir benötigen 0,9 Grad.

3. Versuch: mit der Zoomstufe 10
Das bedeutet 0,351563° Längengraddifferenz auf einer Karte mit 256 Pixel Breite.   0,351563  = 360 / 210
Auf unserer 900-Pixel-Karte könnten wir also 0,351563 · 900/256 Grad ≈ 1,236 Grad unterbringen:
Das ist ok! Unsere 0,9 Grad können untergebracht werden!
Bei dem Download wird nun genau dieser Zoomlevel verwendet und der noch übrige Platz wird um die Karte herum einfach mit weiterem Kartenmaterial gefüllt. Die Karte mit den gewünschten Koordinaten wird zental eingebaut. (siehe oben; erstes Bild)

Es geht auch ohne Probieren:  
    Bedingung     [ 360 / 2OptimaleZoomStufe]  · (900/256)  >  Längengraddifferenz

   ==>   2OptimaleZoomStufe < 360 · (900/256) / Längengraddifferenz  
   ==>   OptimaleZoomStufe · log(2) < log( 360 · (900/256) / Längengraddifferenz )
   
   ==>  OptimaleZoomStufe  <   log( 360 · (900/256) / Längengraddifferenz )  /   log(2)

für eine Längengraddifferenz von 0,9° ergibt dies hier den Wert etwa 10,458;   also ist 10 der beste Wert!!

Unsere Karte besitzt bei Zoomstufe 10 von links nach rechts 1,236° Längengraddifferenz.
Für die "angeforderten" 0,9° braucht es also (900 / 1,236) · 0,9 Pixel ≈ 655 Pixel.
Für die beiden Ränder links und rechts bleiben so noch (900-655) Pixel = 245 Pixel  übrig. Pro Rand links und rechts etwa 122 Pixel.
Bei den Rändern oben und unten sind die Verhältnisse gleich:   RandbreiteOU = Randbreite Oben/Unten
RandbreiteOU : 122 = 500 : 900 ==> RandbreiteOU = 122 · 500 / 900 ≈ 68
Für die "angeforderten" 0,33° bei der Breitendifferenz bleiben also 500 - 2·68 Pixel = 364 Pixel übrig!

Verhältnisse mit Pixelangaben in schwarz
Bild



A3: Wie erhält man aus den Pixelwerten (im - nicht zugänglichen - Browser; in der Picturebox) die Länge- und Breitenangabe?

Man beginnt mit der Pixelnummerierung links oben bei 0|0.
Unsere Position sei x | y
Die zugehörigen Koordinatenangaben errechnen sich nun wie folgt:
Beispiele:
x = 122 ==> 10,0°;       x = 122 + 655 = 777 ==> 10,9°
x = 0 ==> 10° + 0,9° · (-122)/655 ≈ 9,832°
x = 899 ==> 10° + 0,9° · 777/655 ≈ 11,068°

Länge = 0,9° · (x-122) / 655 + 10,0°
Breite = 49,03 - 0,33° · (y-68) / 364 oder
          48,7 + 0,33° · (364+68-y) / 364




B: Programmierung im Visual Studio / mit C# in einer Windows-Foms-App

B1: Erstellung einer Testumgebung
Wir erstellen ein neues Projekt als "Windows-Forms-App" mit dem Namen "OSM_Karte" im Verzeichnis "V" (z.B. C:\temp\OSM). Die Form habe die Breite 1300 Pixel und die Höhe 700 Pixel.


I. Wir erstellen darin ein TabControl-Element (die ganze Form ausfüllend: Dock auf Fill stellen). Darin sollten zwei tabPage-Elemente vorhanden sein: tabPage1 mit dem Reitertext "Browser"; tabPage2 mit dem Text "Bild"
II.Zudem benötigen wir einen WebBrowser bestimmter Größe (Dock: None / Size: 900;500)
III.Eine TextBox mit dem Text der obigen URL
IV.Zwei Buttons: Button1 (Text "URL") und Button2 (Text: "Kopie")

Bild

Hier der ganze Inhalt von Form1.cs


using System;
using System.Drawing;
using System.Windows.Forms;

namespace OSM_Karte
{
   public partial class Form1 : Form
   {
       public Form1()
       {
           InitializeComponent();
       }

       private void button1_Click(object sender, EventArgs e)
       {
           string url = textBox1.Text;
           webBrowser1.Navigate(url);
       }

       private void button2_Click(object sender, EventArgs e)
       {
           // dazu später mehr
       }
   }
}


Nach dem Start (F5) drückt man auf den URL-Button und es erscheint die entsprechende Landkarte im Browser. (man erhält obige Karte ohne die roten Zusatzinformationen)

B2: Bestimmung der oberen nördlichen Breite (zu A1)


Dazu erweitern wir unsere Browser-Seite um drei Eingabefelder (textBox2 bis textBox4) für die beiden Längen- und eine Breitenangabe; zudem sorgen wir für eine Anzeige des Ergebnisses (richTextBox1) bzw. evtl. sonstiger Rechenwerte. Bild Ein weiterer Button (button3; "Bestimme obere Breite") startet die Berechnung. button4 ("Analyse") analysiert die Lage der Landkarte.


Routine für Button3: (zugehörige Mathematik bei A1; siehe oben)


      // globale Variable:
       double Lli = 10.0;
       double Lre = 10.9;
       double Bun = 48.7;
       double Bob = 49.03;
       int horizRand, vertRand, Kartenbreite, Kartenhoehe;
       // Ende der globalen Variablen ------------------------------------------

       private void button3_Click(object sender, EventArgs e)
       {
           // global definiert: Lli, Lre, Bun, Bob
           Lli = Convert.ToDouble(textBox2.Text.Replace('.',','));
           Lre = Convert.ToDouble(textBox3.Text.Replace('.', ','));
           Bun = Convert.ToDouble(textBox4.Text.Replace('.', ','));

           Bob = Bun + Math.Cos(2 * Math.PI * Bun / 360.0) * (Lre - Lli) *
               (webBrowser1.Height*1.0 / webBrowser1.Width);

           richTextBox1.Clear();
           richTextBox1.AppendText("nördl. Breite oben: " + Bob.ToString("#0.00000")+"\n\n");
           textBox1.Text = "https://www.openstreetmap.org/export/embed.html?bbox=" +
               Lli.ToString("#0.00000").Replace(',', '.') + "," +
               Bun.ToString("#0.00000").Replace(',', '.') + "," +
               Lre.ToString("#0.00000").Replace(',', '.') + "," +
               Bob.ToString("#0.00000").Replace(',', '.') + "&layer=mapnik";
           
           // URL-Button erzeugt dann das Kartenbild im Browser
       }





B3: Bestimmung der Randbreiten / Randhöhen (zu A2)

Für die Analyse mit Button4 ... (zugehörige Mathematik bei A2; siehe oben)


      private void button4_Click(object sender, EventArgs e)
       {
           double hilfsWert = Math.Log(360*(webBrowser1.Width*1.0/256)/(Lre-Lli)) / Math.Log(2);
           int OptimalerZoomWert = (int)Math.Floor(hilfsWert);
           richTextBox1.AppendText("opt. Zoomwert: " + OptimalerZoomWert.ToString() + "\n");
           double GradAuf256horizPixel = 360 / (Math.Pow(2, OptimalerZoomWert));
           richTextBox1.AppendText("horizontal pro 256 Pixel: " +
               GradAuf256horizPixel.ToString("0.00000") + " Grad\n\n");

           double horizPixelFuerKarte = 256 * (Lre - Lli) / GradAuf256horizPixel;
           Kartenbreite = (int)Math.Floor(horizPixelFuerKarte);
           richTextBox1.AppendText("horizontal für die Karte: " +
               Kartenbreite.ToString() + " Pixel\n");
           // horizRand global definiert:
           horizRand = (webBrowser1.Width - (int)Math.Floor(horizPixelFuerKarte)) / 2;
           richTextBox1.AppendText("Rand links/rechts je: " + horizRand.ToString() + " Pixel\n\n");

           double vertPixelFuerKarte = webBrowser1.Height*(horizPixelFuerKarte/ webBrowser1.Width);
           Kartenhoehe = (int)Math.Floor(vertPixelFuerKarte);
           richTextBox1.AppendText("vertikal für die Karte: " +
               Kartenhoehe.ToString() + " Pixel\n");
           // vertRand global definiert:
           vertRand = (webBrowser1.Height - (int)Math.Floor(vertPixelFuerKarte)) / 2;
           richTextBox1.AppendText("Rand oben/unten je: " + vertRand.ToString() + " Pixel\n");
       }



B4: Kopie in eine Picturebox; Beispiel der Weiterverarbeitung (zu A3)

Ein Klick auf den Button "Kopie -->"  erzeugt eine Browserkopie in einer PictureBox ... aus der 2. Seite


       private void button2_Click(object sender, EventArgs e)
       {
           Bitmap bitmap = new Bitmap(webBrowser1.Width, webBrowser1.Height);
           Rectangle bounds = new Rectangle(new Point(0, 0), webBrowser1.Size);
           webBrowser1.DrawToBitmap(bitmap, bounds);
           pictureBox1.Image = bitmap;
           
           tabControl1.SelectTab(tabPage2);
       }


Bild

Bewegt man die Maus über dem Bild so wird rechts fortlaufend die entspr. Länge und Breite ausgegeben.


       private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
       {
           int x = e.X, y = e.Y;

           textBox9.Text = x.ToString();
           double laenge = Lli + (x - horizRand) / (1.0 * Kartenbreite) * (Lre - Lli);
           textBox7.Text = laenge.ToString("#0.00000");

           textBox10.Text = y.ToString();
           double breite = Bob - (y - vertRand) / (1.0 * Kartenhoehe) * (Bob - Bun);
           textBox8.Text = breite.ToString("#0.00000");
       }



In dieser Picturebox könnte man jetzt auch GPX-Files (Tracks beim Wandern, Joggen, Radfahren etc.) einzeichnen.