Zum Inhalt springen

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“

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:

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.

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:

Terminal window
render_single_tile.py --zxy 17 74577 51762 --stylefile openstreetmap-carto-de/osm-de.xml --outputfile site/rendersinglefile/1.png

Dann mit dem Originalstil, bei dem die nun gerenderte Tile ebenfalls unvollständig war:

Terminal window
render_single_tile.py --zxy 17 74577 51762 --stylefile openstreetmap-carto/mapnik.xml --outputfile site/rendersinglefile/3.png

Nach meinem Verständnis war damit klar, dass der Fehler bereits beim Einlesen der OSM-Daten in die Datenbank auftritt.

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, tags
FROM planet_osm_line
WHERE 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)
end
end

In 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, tags
FROM planet_osm_line
WHERE 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.

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.

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
end
end

Hier 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.

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] = k
end

So 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.

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 attrs
end

Mit

Terminal window
osm2pgsql --create -d gis --slim --output flex -S /srv/tile/openstreetmap-carto-de/openstreetmap-carto-flex-l10n.lua way113.osm

und

Terminal window
osm2pgsql --create -d gis --slim --output flex -S /srv/tile/openstreetmap-carto-de/openstreetmap-carto-flex-l10n.lua way114.osm

kann 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.

Terminal window
key: ref, value: ΕΟ90
key: highway, value: primary
key: maxspeed, value: 40
key: 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: sign
key: source:maxspeed, value: sign

Beim 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.

Terminal window
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: primary
key: maxspeed, value: 40
key: source:maxspeed, value: sign
key: highway, value: primary

Ganz 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:

Terminal window
key: ref, value: ΕΟ90
key: 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: ΕΟ90
key: ref, value: ΕΟ90

Im nächsten Durchlauf wurde highway zweimal bearbeitet.

Terminal window
key: highway, value: primary
key: source:ref, value: Ministerial Decision Γ25871/1963 (ΦΕΚ Β 319/23.07.1963)
key: ref, value: ΕΟ90
key: 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: primary
key: ref, value: ΕΟ90
key: source:ref, value: Ministerial Decision Γ25871/1963 (ΦΕΚ Β 319/23.07.1963)
key: highway, value: primary

Danach ging highway wieder leer aus:

Terminal window
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.

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);
ceil(log2(4)) = 2
2^2 = 4
ElementeHashgrößeFreie Slots
440

Ein weiteres Einfügen löst ein rehash() aus und sortiert die Elemente eventuell neu.

ceil(log2(6)) ≈ ceil(2.58) = 3
2^3 = 8
ElementeHashgrößeFreie Slots
682

Einfügen oder Löschen löst hier kein rehash() aus. Die Reihenfolge der Elemente bleibt stabil.

ceil(log2(8)) = 3
2^3 = 8
ElementeHashgrößeFreie Slots
880

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.

Impressum

Astrid Günther
Dachsenhäuser Str. 46 e
56338 Braubach
Germany
E-Mail: info At astrid-guenther.de

Ich freu mich über Anfragen zu den von mir hier beschriebenen Themen und beantworte diese zeitnah!

Datenschutz

Ich erhebe oder speichere keine personenbezogenen Daten über diese Website. Um den Aufruf dieser Seite zu ermöglichen, speichert der Internet-Provider einige Daten in Server-Log-Files, die ein Browser automatisch weiterleitet: Browsertyp und Browserversion, verwendetes Betriebssystem, Referrer URL, Hostname des zugreifenden Rechners, Uhrzeit der Serveranfrage, IP-Adresse. Die Grundlage für die Datenverarbeitung ist Art. 6 Abs. 1 DSGVO, der die Verarbeitung von Daten zur Erfüllung eines Vertrags oder vorvertraglicher Maßnahmen erlaubt.