home    zurück    Inhaltsverzeichnis
erste Version am 19.04.2017
letzte Änderung am 22.04.2017

OpenStreetMap in einem wx.Window - Seite 3


Hier nun also erstmal die Überlegungen dazu, wie OSMsrc2 umzubauen ist, um asynchron einen weiteren Kartenausschnitt berechnen zu können. Vorzugsweise sollte nicht nur ein zweiter Kartenausschnitt, sondern beliebig viele davon, verarbeitet werden können.
Zum einen müssten alle Funktionen thread-safe werden, zum anderen müssten diverse Klassen-globale Variablen mehrfach vorgehalten werden. Pro Variable, die bisher mit dem Prefix self. beginnt, wäre zu entscheiden, ob sie einmal oder n-mal existieren muss. Also quasi die Aufteilung in Instanz- und Klassen-Variablen.
So muss die aktuelle Basis-Koordinate garantiert n-mal existieren und die Rohform der geladenen Track-Liste (also in Lat/Lon) garantiert nur einmal. Obwohl .... auch dafür wäre es denkbar, dass man eine Karte einmal mit und einmal ohne (oder einer anderen) Track-Liste verwalten können möchte.
Am einfachsten wäre es wohl, die komplette Klasse mit allen ihren Daten n-mal zu instanziieren. Dann müsste sie aber auch tatsächlich n-mal in voller Größe im Speicher gehalten werden.

Mal schauen...
Ein cat /proc/<proc#>/status  | grep VmSize liefert für das frisch gestartete OSMwin2.py
- VmSize:     2840724 kB
mit Anzeige aller AccessPoint-Koordinaten
- VmSize:     2899844 kB
und mit zusätzlicher Anzeige meines Meta-Tracks
- VmSize:     2923764 kB
Also ein Delta von 2923764-2840724=83.040 kB=81 MB.

Instanziiere ich in OSMwin2.py ein zweites OSMscr2(), erhalte ich (ohne Zusatzdaten):
- VmSize:     3127908 kB
Somit 3127908-2840724=287184 kB=280MB für die leere Instanz.

Für andere Python-Scripte mit wx-GUI werden folgende Werte geliefert:
- VmSize:      859620 kB  (bwm2.py)
- VmSize:      760772 kB  (mpled.py)
- VmSize:      900476 kB  (CallMonClient.py)
- VmSize:      772884 kB  (RollladenMonitor.py)

Ohne wx-GUI ist Python nicht so hungrig:
- VmSize:       37232 kB  (pinger2.py)
- VmSize:       59760 kB  (clipsrv.py)
- VmSize:     356364 kB  (CallMonServer.py)

Fazit: wxPython langt schon ordentlich zu und OSMwin2.py belegt dreimal soviel Speicher wie etwa bwm2.py - aber die 280 MB für OSMscr2() und das Delta von 81 MB für die Daten ist verhältnismäßig wenig.
Es könnte also reichen, in der Funktion zum Nachfahren eines Tracks eine DeepCopy von self.osm zu erstellen und diese dann in einem Thread parallel das Bitmap für den nächsten Track-Point berechnen zu lassen.
Oder auch nicht. Das langt nicht. Es muss pro scrollMapToPoint()-Lauf zwischen den Instanzen gewechselt werden. Nur so kann das Folge-Bitmap für trackpoint[n+1] schon berechnet werden, während in der anderen Instanz noch die Zwischenschritte von trackpoint[n] bis trackpoint[n+1] per scrollMapToPoint() abgefahren werden. Nach Umschaltung wird dann trackpoint[n+1] bis trackpoint[n+2] abgefahren, während parallel das Bitmap für trackpoint[n+2] berechnet wird.
Beide Instanzen müssten (abwechselnd) asynchron rechnen können. Das hört sich irgendwie unschön an.

Am komfortabelsten wäre es, wenn es in OSMscr2 einfach eine weitere Funktion prepareNewCenter(posLL) gäbe, die asynchron das Bitmap für eine neue Koordinate im Fenster-Mittelpunkt berechnet. Und natürlich noch eine switchToNewCenter() zum umschalten.
Auch denkbar wäre, prepareNewCenter() ein Handle liefern zu lassen. Umschalten dann mit switchToNewCenter(handle). So wären mehrere  vorberechnete Bitmaps möglich. Über das Handle würde ein Satz Variablen innerhalb von OSMscr2 referenziert werden.
Innerhalb der Klasse würden die in einem Dictionary abgelegt werden. Etwa als {0:vars1, 1:vars2}.

Nun stellt sich also wieder die Frage: welche Variablen.
- Track-Listen und Punkt-Listen gibts pro Instanz nur einmal. Fertig. Aus.
- Fenstergröße, Tile-Größe, Rahmen-Tile-Anzahl und Zoom-Level auch nur einmal.

Pro Handle bräuchte es:
- self.pos0LL - die Geo-Koordinaten der (0, 0)-Position im Fenster
- self.needs_refresh - die Kennung, dass im Bitmap noch Leer-Tiles enthalten sind
- self.shiftXY - der aktuelle Anzeige-Versatz von aktuellen Bitmap bezogen auf die (0, 0)-Position im Fenster
- self.shift_tempXY - eine [mittlerweile] vollkommen überflüssige Variable -> kann weg
- self.shift_statXY - eine [mittlerweile] vollkommen überflüssige Variable -> kann weg
- self.bmpbuf - das aktuelle Bitmap
- self.mdc - der aktuelle wx.MemoryDC
- self.old_params - das Variablen-Tupel zur Änderungserkennung
- self.border_fixXY - der Pixel-Versatz zwischen Fenster(0, 0) und der linken oberen Ecke von Tile(0, 0)

Die zu Pixel-Koordinaten konvertierten Listen für Track- und Point-Listen habe ich ja schlauerweise :-) eh schon als relativ ausgelegt. Sie müssen nur bei Änderung des Zoom-Levels neu berechnet werden.

Statt switchToNewCenter(handle) könnte einigen Funktionen auch einfach zusätzlich das Handle übergeben werden, auf dem sie arbeiten sollen.
Als da wären:
- getLatLonForPixel(posXY)
- getPixelForLatLon(posLL)
- getVisibleRectangle()
- getZoomAndCenterForAllPointsVisible(listLL) - kann zu __getZoomAndCenterForAllPointsVisible(listLL) werden
- drawMap()
- doMoveMap(distXY)
- endMoveMap()
- getBitmap()
- forceRefresh()
- getDataForPoint(posXY)

Die Funktionen sollten dann z.B. deklariert werden als:
    def getLatLonForPixel(self, posXY, handle=0):
Damit müsste man sich nicht um das Handle kümmern, wenn man nur einen Satz Variablen nutzen will.
Wenn eine Funktion andere Funktionen aufruft, muss sie dabei das Handle jeweils mit übergeben.

Nun stellt sich die Frage, welcher Ansatz der bessere ist. Umschalten oder jede Funktion mit Handle aufrufen.
Eigentlich sollte auch beides gehen. Wenn man handle==0 als Meta-Handle betrachten würde, stünde das für "das Handle, das zuletzt mittels switchToNewCenter(handle) eingestellt wurde". Alle anderen Handles würden Direktzugriff bedeuten.
Aber prepareNewCenter(posLL) sollte dann eher forkMap(posLL) oder noch besser newMap(posLL) heißen (weil fork eine Kopie impliziert, es aber nur ein Thread sein wird). Und entsprechend switchToMap(handle).


Gut. Damit sollte der Zugriff auf die unterschiedlichen Variablen-Sätze durch sein. Nun fehlt noch das Threading.


Die Funktion newMap(posLL) legt ein neues Handle mit einem Satz Variablen an, stellt darin posLL ein, startet drawMap(handle) in einem Thread und liefert das Handle an den Aufrufer.
Der Aufrufer fragt irgendwann später per getBitmap(handle) nach dem Bitmap.
Wenn es bereits fertig berechnet ist, ist alles gut und getBitmap() kann sofort liefern. Um das entscheiden zu können, braucht es noch eine Variable bitmap_valid zum Handle.
Wenn das Bitmap hingegen noch nicht fertig ist, muss nun zwangsläufig doch gewartet werden. Dafür gibt es threading.join(). Damit der weiß, auf was er warten soll, muss die den Thread repräsentierende Variable ebenfalls noch zum Handle gespeichert werden. Außerdem kann die Rückgabe von needs_refresh nicht länger von drawMap() kommen. Sie muss jetzt von getBitmap() kommen.
Nebenbei bemerkt....ein dropMap(handle) sollte es wohl auch geben.
Damit hätte ich nun ein Bild. Aber der Thread wäre beendet.
Nun könnte ich mittels doMoveMap(distXY) synchron darin rumscrollen. Aber wie lasse ich dann asynchron das Bitmap für den nächsten Track-Point berechnen? Ich will ja nicht per newMap(posLL) noch ein [drittes...und viertes...] Handle dafür bauen.

Mal überlegen, wie die geänderte Funktion autoFollow() aussehen sollte...

über alle Tracks
  h0=0 # das Default-Handle für synchrone Berechnung
  setCenter(track[0]) # ersten Punkt vom aktuellen Track in der Fenstermitte synchron berechnen und darstellen
  h1=newMap(track[1]) # zweiten Punkt vom aktuellen Track in Fenstermitte asychron berechnen
  über alle Trackpunkte des aktuellen Tracks
    scrollMapToPoint(n, n+1) # vom aktuellen zum nächsten Trackpunkt scrollen
    switchToMap(h1) # getBitmap() auf die n+1-Karte umschalten
    h0, h1=(h1, h0) # Handles tauschen
    Refresh() # onPaint() aufrufen
dropMap(h1)

Der onPaint() sähe mit den neusten Änderungen so aus:
  self.osm.drawMap()
  self.dc=wx.PaintDC(self)
  imgbuf, (sx, sy), self.refresh_needed=self.osm.getBitmap()
  self.dc.DrawBitmap(imgbuf, sx, sy)
Der drawMap() berechnet das Bild synchron, prüft aber vorher, ob er bereits die gültige Version verfügbar hat.
Ist vorher auf den richtigen Variablen-Satz umgeschaltet worden, würde er erkennen, dass das Bitmap bereits fertig ist.
Wenn es aber noch nicht fertig ist, würde drawMap() nochmal synchron loslegen, das Bild zu berechnen.
Offenbar ist es keine gute Idee, wenn der newMap() einfach drawMap() als Thread startet.

Vielleicht sollte die drawMap-Berechnung grundsätzlich asynchron ablaufen.
Und in getBitmap() wird grundsätzlich per join() auf Fertigstellung der Bitmap gewartet.

Nun habe ich erste Tests gemacht.
Der drawMap() startet immer einen Thread, der getBitmap() wartet immer per join().
Klappt auch alles perfekt, solange kein zweites Handle eröffnet wird.
Der newMap() ruft drawMap(handle), der drawMap() startet den Thread als daemon.
Aber er verreckt scheinbar in:
  def __newBitmap(self, handle=0):
    w, h=self.__getBitmapSize()
    print "__newBitmap 1-%d\n"%(handle,),
    self.v[handle].bmpbuf=wx.EmptyBitmapRGBA(w, h, 204, 204, 204, 1)
    print "__newBitmap 2-%d\n"%(handle,),
    self.v[handle].mdc=wx.MemoryDC()
    print "__newBitmap 3-%d\n"%(handle,),
    self.v[handle].mdc.SelectObject(self.v[handle].bmpbuf)
    print "__newBitmap 4-%d\n"%(handle,),

Der "__newBitmap 3-1" kommt noch, danach ist dann Feierabend:
[xcb] Unknown request in queue while dequeuing
[xcb] Most likely this is a multi-threaded client and XInitThreads has not been called
[xcb] Aborting, sorry about that.
python: xcb_io.c:165: dequeue_pending_request: Zusicherung »!xcb_xlib_unknown_req_in_deq« nicht erfüllt.
Abgebrochen


Tatsächlich hatte ich schon recht frühzeitig die Befürchtung, dass sich einige der Funktionen von wxPython unerfreulich verhalten könnten, wenn sie innerhalb eines Threads aufgerufen werden.
Aber vielleicht liegt es gar nicht am Thread.
Die Doku zu wx.MemoryDC sagt:
Note: Note that the memory DC must be deleted (or the bitmap selected out of it) before a bitmap can be reselected into another memory DC.
So ein Scheiß! Prinzip Highlander ... oder was!?

Notfalls könnte ich aber ggf. dafür sorgen, dass immer nur ein SelectObject() aktiv ist.
Wenn das Bitmap erstmal erzeugt ist, gibt es innerhalb von OSMscr2 keine Schreiboperationen mehr darauf.
Löschen ließe sich das Ding wohl mit der Zuweisung von wx.NullBitmap.


Nach diversen Tests und Umbauten bin ich jetzt eher der Ansicht, dass das Problem darin liegt, das sowas wie etwa DrawBitmap() nicht damit klarkommt, wenn parallel dazu in einem weiteren DeviceContext ebenfalls DrawXXX-Befehle aufgerufen werden.
Mittlerweile habe ich einen wx.PaintDC() in OSMwin2 und genau einen wx.MemoryDC() in OSMscr2. Die Schreibzugriffe auf den wx.MemoryDC() werden per threading.Lock.acquire() und threading.Lock.release() serialisiert.
Trotzdem verreckt das Programm in dem Moment, wenn der Thread aktiv läuft und dann parallel in OSMwin2 die onPaint()-Funktion betreten wird.
Eben habe ich eine entsprechende Frage auf stackoverflow gestellt. Mal schauen, ob von da was Erhellendes kommt.

Bisher kam leider nichts wirklich brauchbares.... die Alpha-Version von wxPython 4.0.0 ist m.E. keine Option.
Ich lege mir mal die auf asynchrone Verarbeitung umgebauten Scripte in einen Ordner v2.0 und mache mit der v1.3 auf der nächsten Seite weiter.