LUA
Ein interessantes Problem beim Iterieren mit for ... pairs und der Lua-Funktion next
Abschnitt betitelt „Ein interessantes Problem beim Iterieren mit for ... pairs und der Lua-Funktion next“Ausgangssituation
Abschnitt betitelt „Ausgangssituation“Im Forum meldet ein Nutzer, dass eine Straße auf Kreta nicht vollständig angezeigt wird. Relativ schnell stellt sich heraus, dass dafür der deutsche Stil zum Rendern verwendet wird.
Openstreetmap.de betreibt zwei Tile-Server. Bei beiden sind die Tiles fehlerhaft. Es wäre ein großer Zufall, wenn es sich dabei um ein Hardwareproblem oder ein spezifisches Einleseproblem handeln würde. Daher liegt die Vermutung nahe, dass die Ursache im deutschen Stil zu finden ist.
Der einzige Unterschied bei den beiden Straßensegmenten besteht darin, dass der angezeigte Teil ein zusätzliches Tag enthält, nämlich maxspeed:
Erster Versuch
Abschnitt betitelt „Erster Versuch“Im ersten Versuch habe ich bei dem fehlenden Segment ein Tag, nämlich die Oberfläche surface, eingetragen und auf dem deutschen Server das Neurendern der Tiles erzwungen. Daraufhin erschien der zuvor fehlende Teil der Straße. Nun bin ich eine Information weiter. Eine Lösung ist das aber noch nicht.
Zweiter Versuch
Abschnitt betitelt „Zweiter Versuch“Der deutsche Stil baut auf dem Standardstil openstreetmap-carto auf, der unter openstreetmap.org für die OpenStreetMap-Karten verwendet wird. Er übernimmt also die Grunddarstellung, verändert aber gezielt bestimmte Elemente, um sie für Deutsche besser lesbar zu machen.
Mich interessierte nun, ob die fehlenden Straßensegmente auch im Originalstil fehlen. Deshalb habe ich sie mit diesem gerendert. Hierzu nutzte ich render_single_tile.py, und die notwendigen Informationen habe ich über einen Rechtsklick auf die entsprechende Tile auf https://tile.openstreetmap.de herausgesucht.
Zunächst rendere ich mit dem deutschen Stil, bei dem – wie erwartet – der Straßenabschnitt fehlte:
render_single_tile.py --zxy 17 74577 51762 --stylefile openstreetmap-carto-de/osm-de.xml --outputfile site/rendersinglefile/1.pngDann mit dem Originalstil, bei dem die nun gerenderte Tile ebenfalls unvollständig war:
render_single_tile.py --zxy 17 74577 51762 --stylefile openstreetmap-carto/mapnik.xml --outputfile site/rendersinglefile/3.pngNach meinem Verständnis war damit klar, dass der Fehler bereits beim Einlesen der OSM-Daten in die Datenbank auftritt.
Dritter Versuch mit Trugschluss
Abschnitt betitelt „Dritter Versuch mit Trugschluss“Das Einlesen der Daten erfolgt via osm2pgsql. Auch hier wurde die Version der Lua-Skriptdatei im deutschen Stil, openstreetmap-carto-flex-l10n.lua, auf Grundlage von openstreetmap-carto-flex.lua aus dem Standardstil angepasst.
Zu Beginn hatte ich ja recht schnell festgestellt, dass Straßen angezeigt werden, die zusätzliche Tags enthalten. Beide LUA-Dateien – die Originalversion und die abgewandelte deutsche Version – sind inzwischen so erweitert, dass bestimmte Schlüssel über ignore_keys herausgefiltert werden. Weiter stellte ich fest, dass bei allen Straßensegmenten, die nicht angezeigt werden, die Spalte tags in der Datenbank leer ist. Leider konzentrierte ich mich zu sehr auf die tags-Spalte. Das Feld highway hätte für die osm_id 1340291113 ebenfalls gefüllt sein müssen – dazu später mehr.
SELECT osm_id, name, highway, ref, tagsFROM planet_osm_lineWHERE osm_id IN (1340291113, 1340291114); osm_id | name | highway | ref | tags------------+-----------------------------------------+---------+------+------------------ 1340291113 | Περάματος - Γαζίου - Perámatos - Gazíou | | ΕΟ90 | 1340291114 | Περάματος - Γαζίου - Perámatos - Gazίou | primary | ΕΟ90 | "maxspeed"=>"40"(2 rows)Da ich auf switch2osm.org las, dass die Originalversion openstreetmap-carto-flex.lua noch nicht aktiv genutzt wird, recherchierte ich zunächst in die falsche Richtung.
Ich veränderte das Einleseskript so, dass immer dann, wenn kein Tag vorhanden war, "dummy"=>"true" in die Spalte tags eingetragen wird:
local function add_linear(table_name, attrs, geom) for sgeom in geom:geometries() do attrs.way = sgeom
if next(attrs.tags) == nil then attrs.tags.dummy = "true" end
insert_row(table_name, attrs) endendIn der Test-Datenbank sieht das Ergebnis nun so aus. Dass highway und ref gefüllt wurden, ist Zufall, wie ich später feststellte:
SELECT osm_id, name, highway, ref, tagsFROM planet_osm_lineWHERE osm_id IN (1340291113, 1340291114); osm_id | name | highway | ref | tags------------+-----------------------------------------+---------+------+------------------ 1340291113 | Περάματος - Γαζίου - Perámatos - Gazíou | primary | ΕΟ90 | "dummy"=>"true" 1340291114 | Περάματος - Γαζίου - Perámatos - Gazíou | primary | ΕΟ90 | "maxspeed"=>"40"(2 rows)render_single_tile.py rendert nach diesem Einlesen, sowohl mit dem Original- als auch mit im deutschen Stil alle Straßenteile korrekt.
Mein Workaround wurde jedoch als „bad“ bezeichnet – zu Recht, denn zu diesem Zeitpunkt hatte ich noch nicht verstanden, warum das Problem überhaupt auftrat.
Vierter Versuch
Abschnitt betitelt „Vierter Versuch“Momentan ging ich davon aus, dass die Mapnik Datenbankabfragen, die die PNG-Dateien für die Tiles erzeugen, Probleme haben könnten, wenn die Spalte tags leer ist. Ich hatte bereits erwähnt, dass ich zuvor übersehen hatte, dass auch das Feld highway leer war.
Um es kurz zu machen: Eine Abfrage, die ausschließlich nach Straßenteilen sucht, die Tags enthalten, habe ich nicht gefunden. Allerdings schien die Spalte highway entscheidend zu sein – und diese Erkenntnis führte mich wieder in die richtige Richtung.
Fünfter Versuch
Abschnitt betitelt „Fünfter Versuch“Die Funktion prepare_columns bereitete mir schon länger Sorgen:
for key, value in pairs(object.tags) do if tag_map[key] then if (key == 'name') and (L10NLANG ~= nil) then attrs[key] = gen_l10n_name(object, islinear, iscountry) else attrs[key] = value end found_tag = true elseif ignore_type and key == 'type' then -- luacheck: ignore 542 -- do nothing elseif keep_tag(key) then attrs.tags[key] = value found_tag = true endendHier iterieren wir über object.tags und verändern gleichzeitig die object.tags im l10n-Daemon während der Iteration. Bisher habe ich dies nicht weiter hinterfragt, da lediglich kurzfristig ein Tag hinzugefügt wird. In der eigentlichen for-Schleife bleiben die gleichen Elemente erhalten.
Im Lua Manual steht zwar: “You should not assign any value to a non-existent field in a table during its traversal.” Aber wenn dies tatsächlich problematisch wäre, müssten doch deutlich mehr Straßen fehlerhaft sein.
Ich habe den Lua-Quellcode geprüft: lua-5.4.8/src/ltable.c enthält die Funktion findindex. Die Fehlermeldung "invalid key to 'next'" müsste ausgegeben werden, wenn beim Durchlaufen von object.tags ein Element nicht gefunden wird. Bei mir wird jedoch kein Fehler geloggt.
Sechster Versuch
Abschnitt betitelt „Sechster Versuch“Da ich keine andere Idee hatte, erstelle ich schließlich doch eine Kopie von object.tags und nutze diese als Index in der for-Schleife:
local keys = {}for k in pairs(object.tags) do keys[#keys+1] = kendSo sollte man es laut Lua Manual ohnehin machen. Nun funktioniert alles fehlerfrei. Auch mit wenig Tags. Damit könnte ich es eigentlich belassen: Der Fehler ist verschwunden, und der Code entspricht den Empfehlungen des Lua Manual.
Ich würde jedoch gerne verstehen, warum der Fehler nur bei OSM-Ways mit wenigen Tags auftritt, während solche mit vielen Tags stabil laufen.
Siebter Versuch
Abschnitt betitelt „Siebter Versuch“Ich nutze erneut mein fehlerhaftes Lua-Skript und erstelle zwei OSM-Export-Dateien für schnelle Tests. Eine davon enthält einen problematischen way:
sudo -u tile wget -O ways.osm "https://overpass-api.de/api/interpreter?data=[out:xml];way(id:1340291113);(._;>;);out body;"
<osm version="0.6" generator="Overpass API 0.7.62.8 e802775f"><note>The data included in this document is from www.openstreetmap.org. The data is made available under ODbL.</note><meta osm_base="2025-12-06T16:53:40Z"/>
<node id="313813429" lat="35.3476725" lon="24.8258749"/> ... <node id="8959531924" lat="35.3475664" lon="24.8266017"/> <way id="1340291113"> <nd ref="313813429"/> ... <nd ref="8959531917"/> <tag k="highway" v="primary"/> <tag k="name" v="Περάματος - Γαζίου"/> <tag k="ref" v="ΕΟ90"/> <tag k="source:ref" v="Ministerial Decision Γ25871/1963 (ΦΕΚ Β 319/23.07.1963)"/> </way>
</osm>Und ein benachbartes Segment mit einem way, der unproblematisch ist:
sudo -u tile wget -O ways.osm "https://overpass-api.de/api/interpreter?data=[out:xml];way(id:1340291114);(._;>;);out body;"
<note>The data included in this document is from www.openstreetmap.org. The data is made available under ODbL.</note><meta osm_base="2025-12-06T16:53:40Z"/>
<node id="313813460" lat="35.3465775" lon="24.8360096"/> ... <node id="8959531917" lat="35.3465228" lon="24.8358648"/> <way id="1340291114"> <nd ref="8959531917"/> ... <nd ref="4810699410"/> <tag k="highway" v="primary"/> <tag k="maxspeed" v="40"/> <tag k="name" v="Περάματος - Γαζίου"/> <tag k="ref" v="ΕΟ90"/> <tag k="source:maxspeed" v="sign"/> <tag k="source:ref" v="Ministerial Decision Γ25871/1963 (ΦΕΚ Β 319/23.07.1963)"/> </way>
</osm>Nun füge ich print()-Statements in das Lua-Skript ein. Zu Beginn der Schleife lasse ich mir das aktuell aktive Tag ausgeben. Außerdem lasse ich mir vor und nach dem Aufruf von gen_l10n_name, bei dem object.tags bearbeitet wird, alle object.tags noch einmal zusammen anzeigen.
for key, value in pairs(object.tags) do print(string.format("key: %s, value: %s", tostring(key), tostring(value)))
if tag_map[key] then if key == 'name' and L10NLANG ~= nil then
print(" --> Processing 'name' key with L10NLANG")
for akey, avalue in pairs(object.tags) do print(string.format(" [before] akey: %s, avalue: %s", tostring(akey), tostring(avalue))) end
attrs[key] = gen_l10n_name(object, islinear, iscountry)
for zkey, zvalue in pairs(object.tags) do print(string.format(" [after] zkey: %s, zvalue: %s", tostring(zkey), tostring(zvalue))) end
else attrs[key] = value end found_tag = true elseif ignore_type and key == 'type' then -- luacheck: ignore 542 -- do nothing elseif keep_tag(key) then attrs.tags[key] = value found_tag = true end end
if not found_tag then return nil end
return attrsendMit
osm2pgsql --create -d gis --slim --output flex -S /srv/tile/openstreetmap-carto-de/openstreetmap-carto-flex-l10n.lua way113.osmund
osm2pgsql --create -d gis --slim --output flex -S /srv/tile/openstreetmap-carto-de/openstreetmap-carto-flex-l10n.lua way114.osmkann ich nun recht schnell testen.
Dabei fällt mir auf, dass bei einem Way mit sechs Tags (wie bei osm_id 1340291114) die Reihenfolge der Tags während des Schleifendurchlaufs immer gleich bleibt.
key: ref, value: ΕΟ90key: highway, value: primarykey: maxspeed, value: 40key: source:ref, value: Ministerial Decision Γ25871/1963 (ΦΕΚ Β 319/23.07.1963)key: name, value: Περάματος - Γαζίου --> Processing 'name' key with L10NLANG [before] akey: ref, avalue: ΕΟ90 [before] akey: highway, avalue: primary [before] akey: maxspeed, avalue: 40 [before] akey: source:ref, avalue: Ministerial Decision Γ25871/1963 (ΦΕΚ Β 319/23.07.1963) [before] akey: name, avalue: Περάματος - Γαζίου [before] akey: source:maxspeed, avalue: sign [after] zkey: ref, zvalue: ΕΟ90 [after] zkey: highway, zvalue: primary [after] zkey: maxspeed, zvalue: 40 [after] zkey: source:ref, zvalue: Ministerial Decision Γ25871/1963 (ΦΕΚ Β 319/23.07.1963) [after] zkey: name, zvalue: Περάματος - Γαζίου [after] zkey: source:maxspeed, zvalue: signkey: source:maxspeed, value: signBeim zweiten Aufruf kann die Reihenfolge anders sein. Das steht ja auch so im Lua Manual: “The order in which the indices are enumerated is not specified, even for numeric indices.”
Aber während eines konkreten Schleifendurchlaufs bleibt die Reihenfolge von object.tags immer gleich – also bei meinen print()-Ausgaben für key, akey und zkey.
key: source:ref, value: Ministerial Decision Γ25871/1963 (ΦΕΚ Β 319/23.07.1963)key: name, value: Περάματος - Γαζίου --> Processing 'name' key with L10NLANG [before] akey: source:ref, avalue: Ministerial Decision Γ25871/1963 (ΦΕΚ Β 319/23.07.1963) [before] akey: name, avalue: Περάματος - Γαζίου [before] akey: maxspeed, avalue: 40 [before] akey: source:maxspeed, avalue: sign [before] akey: ref, avalue: ΕΟ90 [before] akey: highway, avalue: primary [after] zkey: ref, zvalue: ΕΟ90 [after] zkey: source:ref, zvalue: Ministerial Decision Γ25871/1963 (ΦΕΚ Β 319/23.07.1963) [after] zkey: name, zvalue: Περάματος - Γαζίου [after] zkey: maxspeed, zvalue: 40 [after] zkey: source:maxspeed, zvalue: sign [after] zkey: highway, zvalue: primarykey: maxspeed, value: 40key: source:maxspeed, value: signkey: highway, value: primaryGanz anders sieht es bei vier Tags aus (wie bei osm_id 1340291113). Hier ist zkey häufig unterschiedlich. Als mein highway fehlte, war die Situation offenbar wie im folgenden Log: Alles startete in der Reihenfolge ref, source:ref, name und highway. Als dann object.tags während des Durchlaufs des name-Tags verändert wurden, änderte sich die Reihenfolge zu source:ref, highway, name und ref. Dies hatte zur Folge, dass highway in diesem Schleifendurchlauf nicht bearbeitet wurde, während ref dafür zweimal verarbeitet wurde:
key: ref, value: ΕΟ90key: source:ref, value: Ministerial Decision Γ25871/1963 (ΦΕΚ Β 319/23.07.1963)key: name, value: Περάματος - Γαζίου --> Processing 'name' key with L10NLANG [before] akey: ref, avalue: ΕΟ90 [before] akey: source:ref, avalue: Ministerial Decision Γ25871/1963 (ΦΕΚ Β 319/23.07.1963) [before] akey: name, avalue: Περάματος - Γαζίου [before] akey: highway, avalue: primary [after] zkey: source:ref, zvalue: Ministerial Decision Γ25871/1963 (ΦΕΚ Β 319/23.07.1963) [after] zkey: highway, zvalue: primary [after] zkey: name, zvalue: Περάματος - Γαζίου [after] zkey: ref, zvalue: ΕΟ90key: ref, value: ΕΟ90Im nächsten Durchlauf wurde highway zweimal bearbeitet.
key: highway, value: primarykey: source:ref, value: Ministerial Decision Γ25871/1963 (ΦΕΚ Β 319/23.07.1963)key: ref, value: ΕΟ90key: name, value: Περάματος - Γαζίου --> Processing 'name' key with L10NLANG [before] akey: highway, avalue: primary [before] akey: source:ref, avalue: Ministerial Decision Γ25871/1963 (ΦΕΚ Β 319/23.07.1963) [before] akey: ref, avalue: ΕΟ90 [before] akey: name, avalue: Περάματος - Γαζίου [after] zkey: name, zvalue: Περάματος - Γαζίου [after] zkey: ref, zvalue: ΕΟ90 [after] zkey: source:ref, zvalue: Ministerial Decision Γ25871/1963 (ΦΕΚ Β 319/23.07.1963) [after] zkey: highway, zvalue: primarykey: ref, value: ΕΟ90key: source:ref, value: Ministerial Decision Γ25871/1963 (ΦΕΚ Β 319/23.07.1963)key: highway, value: primaryDanach ging highway wieder leer aus:
key: source:ref, value: Ministerial Decision Γ25871/1963 (ΦΕΚ Β 319/23.07.1963)key: name, value: Περάματος - Γαζίου --> Processing 'name' key with L10NLANG [before] akey: source:ref, avalue: Ministerial Decision Γ25871/1963 (ΦΕΚ Β 319/23.07.1963) [before] akey: name, avalue: Περάματος - Γαζίου [before] akey: highway, avalue: primary [before] akey: ref, avalue: ΕΟ90 [after] zkey: highway, zvalue: primary [after] zkey: source:ref, zvalue: Ministerial Decision Γ25871/1963 (ΦΕΚ Β 319/23.07.1963) [after] zkey: ref, zvalue: ΕΟ90 [after] zkey: name, zvalue: Περάματος - ΓαζίουBei vier Tags ist es in meinen Tests eher Glückssache, ob alle Tags bearbeitet werden, während bei sechs Tags alles stabil läuft.
Wie ist das zu erklären?
Abschnitt betitelt „Wie ist das zu erklären?“Lua-Tables bestehen aus einem Array-Teil für positive Integer-Keys und einem Hash-Teil für alle anderen Keys. Unsere Tags fallen als Strings in den Hash-Teil. Der relevante Code beim Hinzufügen von Elementen in lua-5.4.8/src/ltable.c sieht meiner Recherche nach folgendermaßen aus:
if (f == NULL) { /* cannot find a free place? */ rehash(L, t, key); /* grow table */ luaH_set(L, t, key, value); return;}Das bedeutet: Wenn kein freier Hash-Slot gefunden wird, wird sofort ein rehash() ausgelöst, wodurch die Elemente unter Umständen neu verteilt werden.
Warum geschieht dies bei manchen Tabellen öfter?
Lua wählt die Hash-Größe immer als (2^n):
lsize = luaO_ceillog2(size);size = twoto(lsize);Beispiel: 4 Elemente
Abschnitt betitelt „Beispiel: 4 Elemente“ceil(log2(4)) = 22^2 = 4| Elemente | Hashgröße | Freie Slots |
|---|---|---|
| 4 | 4 | 0 |
Ein weiteres Einfügen löst ein rehash() aus und sortiert die Elemente eventuell neu.
Beispiel: 6 Elemente
Abschnitt betitelt „Beispiel: 6 Elemente“ceil(log2(6)) ≈ ceil(2.58) = 32^3 = 8| Elemente | Hashgröße | Freie Slots |
|---|---|---|
| 6 | 8 | 2 |
Einfügen oder Löschen löst hier kein rehash() aus. Die Reihenfolge der Elemente bleibt stabil.
Beispiel: 8 Elemente
Abschnitt betitelt „Beispiel: 8 Elemente“ceil(log2(8)) = 32^3 = 8| Elemente | Hashgröße | Freie Slots |
|---|---|---|
| 8 | 8 | 0 |
Ein weiteres Insert löst wieder ein rehash() aus und kann die Elemente neu sortieren. Da die Tabelle nun größer ist, ist die Wahrscheinlichkeit, dass ein wesentlicher Teil der Elemente „verloren geht“ oder falsch importiert wird, jedoch geringer.