GL Constants Translator

Weil ich mich zu oft darüber geärgert habe, irgendwelche hin- und her-mappings durch absurde Web- und Volltextsuchen in irgendwelchen Headern machen zu müssen, dachte ich mir, dass ich das mal als einen Software-As-A-Cloud-Microservice in eine hochprofessionell gestaltete, anspruchsvolle, moderne Webseite mit schickem, responsivem Design packen könnte:

https://javagl.github.io/GLConstantsTranslator/GLConstantsTranslator.html

Zahlen oder Konstanten eintippen. Konstanten oder Zahlen kommen raus. :rolleyes: Vielleicht findet’s ja noch jemand praktisch.

Ok, hab mir das ganze mal angeschaut. Die Idee ist ja super. Aber die Umsetzung.

[OT]Switch-Case mit über 1000 Fällen? :o) gefrühstückt?[/OT]

Aber ich will ja nicht nur ranten und hab das mal versucht in schöner.
index.html

[SPOILER]

<!DOCTYPE html>
<html>
  <head>
    <title>GL Constants Translator</title>
    <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
    <script type="text/javascript" src="GLConstantsTranslator.js"></script>
    <script type="text/javascript">
      function update() {
		  	document.getElementById("output").innerHTML = renderResult(translate(this.value));
		  }
		  function init() {
			  document.getElementsByName("Input")[0].addEventListener('input', update);
			  document.getElementById("output").innerHTML = renderResult(translate(""));
			  document.getElementsByName("Input")[0].focus();
		  }
      function renderResult(result) {
				if(result && result.length > 0) {
        return "<table id='data'><th class='long'>Constant Name</th><th>Decimal</th><th>Hex</th>"
          + result
				    .map(renderRow)
				  	.join("
")
			   	+ "</table>";
				} else {
					return "<p align='center'><b>No matching results ;(</b></p>"
				}
		  }
		  function renderRow(item, idx) {
				var rowclass = (idx % 2 === 0)? "even":"odd";
		    return "<TR class='" + rowclass + "'><TD>" + item.name + "</TD><TD>" + item.decimalvalue + "</TD><TD>" + item.hexvalue + "</TD></TR>";
      }
    </script>
    <style>
	    table#data {

		  	width: 100%;
		  }
      th td{
		    width: 15%;
		  }
		  th.long {
		    width: 60%;
	    }
			tr.even {
				background-color: #ddd;
			}
    </style>
  </head>
  <body onload="init()">
    <p align="center">
	    Type GL constant names or values into the input field, to see the corresponding values or possible names in the output field
	  </p>
	  <p align="center">
	  	Constant names can be in upper- or lower case, and the leading <b><code>"GL_"</code></b> is optional
	  </p>
	  <p align="center">
		  Values can be in decimal or hexadecimal. Hexadecimal values have to start with <b><code>"0x"</code></b>
	  </p>
    <table align="center">
	    <tr>
        <td>Filter: </td>
        <td><input style="font-family:'Courier'; font-size:16px" size="60" type="text" name="Input" /></td>
      </tr>
    </table>
    <div id="output">The output</div>
	</body>
</html>

[/SPOILER]

GLConstantsTranslator.js
[SPOILER]


var data = [{ "name" : "GL_DEPTH_BUFFER_BIT" , "decimalvalue" : "256", "hexvalue" : "0x100" },
            { "name" : "GL_STENCIL_BUFFER_BIT" , "decimalvalue" : "1024", "hexvalue" : "0x400" },
            { "name" : "GL_COLOR_BUFFER_BIT" , "decimalvalue" : "16384", "hexvalue" : "0x4000" },
            { "name" : "GL_FALSE" , "decimalvalue" : "0", "hexvalue" : "0x0" },
            { "name" : "GL_TRUE" , "decimalvalue" : "1", "hexvalue" : "0x1" },
            { "name" : "GL_POINTS" , "decimalvalue" : "0", "hexvalue" : "0x0" },
            { "name" : "GL_LINES" , "decimalvalue" : "1", "hexvalue" : "0x1" }];

// alles zu Posten wird das Forum nicht mitmachen, daher mal gekürzt.

var translate = function(input) {
  var theFilter = function(item) {true};
  var searchString = input.toUpperCase();
  theFilter = function(item) {
      return item.name.indexOf(searchString) !== -1;
  };
  if (input.startsWith("0x") || input.startsWith("0X")) {
        value = parseInt(input, 16);
        var searchString = input.toLowerCase();
        theFilter = function(item) {
          return item.hexvalue.startsWith(searchString);
        };
  } else {
    if (!isNaN(parseInt(input, 10))) {
      theFilter = function(item) {
        return item.decimalvalue.startsWith(input);
      };
    }
  }
  return data.filter(theFilter);
}

[/SPOILER]

Eingabe ist ein Filter.
Initial wird alles angezeigt. Der Client muss eh alles laden, dann kann er das auch rendern.
Hat zumindest den Vorteil, dass man nun auch sieht, was es an Möglichkeiten gibt. So Codecompletion und so.
Es wird im gesamten Konstantennamen gesucht und nicht nur am Anfang.

Bei den Zahlen könnte man sich noch überlegen ob man das ganze sortiert.
Exakte Treffer ganz oben. Danach Konstanten die genauso beginnen und darunter dann Zahlen, die die Ziffern beinhalten.

Pfft. Geh’ mal in den Baumarkt und kauf’ dir da einen Hammer. Und dann frag’ den Verkäufer: „Gibt’s den auch in blau?“ :kiss:

Na, mal im Ernst:

Ich hatte zwar schon überlegt, ob es geschickter wäre, das in Objekte zu packen (aber ggf. nur „value“ - das ist ja dann nur eine Formatierungsfrage), aber dann dachte ich mir: Egal. Performance spielt keine Rolle. Wartbar sein muss es auch nicht sein*. (Und dass irgendein HTML-Nazi über die geinlineten Styles flennt, war mir eh schon klar :stuck_out_tongue_winking_eye: )

Aber gerade dieser Punkt der „Codecompletion“ war etwas, was schon als potentiell wichtiges „TODO“ auf meiner hirninternen Liste stand. Mein primärer use-case ist zwar „Doppelklick auf Wert, StrgC, StrgV, Doppelklick auf Ausgabe, StrgC, StrgV wo es hinsoll“. Aber es schadet nicht, macht mehr her, kann bei anderen use-cases praktisch sein, und … ist so responsive :smiley: (War gerade erst noch etwas skeptisch, ob er bei der kompletten Liste vielleicht in die Knie geht, wenn man „GL“ oder „0x“ eintippt, aber das scheint noch zu gehen).

Von daher: Danke. In echter JavaScript-Manier werd’ ich das wohl mal ungefragt klauen :smiley:

(*) Der eigentliche Inhalt wird (sehr pragmatisch) automatisch generiert: Er lutscht https://www.opengl.org/registry/api/GL/glcorearb.h und eine gl.h aus einem hartverdrahteten Pfad von mir, sucht dann nach Zeilen der Form
#define [irgendwas aus Großbuchstaben und _] [irgendwas, was in eine Zahl geparst werden kann]
und speichert das als Map<String,Value>. Scheint aber so weit hinzukommen.

EDIT: Das war mir dann jetzt doch tatsächlich mal mein erstes, echtes „Danke“ hier im Forum wert :smiley:

*** Edit ***

Hmpf. Wollte es gerade mal selbst verwenden, und gerade die, für die ich es brauche, sind natürlich nicht dabei: „GL_FLOAT_VEC3=35665“… Muss wohl noch glslang/gl_types.h at master · KhronosGroup/glslang · GitHub dazunehmen…

Wenn man überhaupt keine GL Konstanten kennt, dann ist die Variante etwas ungeschickt. Mit so einem Filter und alles anzeigen, das lädt dann schon mal zum stöbern ein.

1000 Werte sind heute auch nicht mehr die Welt. JavaScript ist da auf aktueller Hardware ausreichend schnell.

Mit so einem switch kettet man sich halt einen extremen Klotz ans Bein, indem man eben nur ein exaktes Match bekommt.
Und bei zwei Varianten nach denen gesucht werden kann, eben zwei switch…
Da steigt dann schonmal die Datenmenge die man vom Server initial holen muss.

Wenn man wie hier die kompletten Daten mitliefert, dann ist auch das Filtern kein Problem. Wenn man einen Roundtrip zum Server einplanen muss, dann sieht die Rechnung wieder anders aus. Kriegt man heute aber auch noch hin.

Der einzige Spieler der etwas zu tun bekommt ist der Browser, der die neue Tabelle neu layouten darf. Wenn das zu aufwendig wird, dann kann man so Ansätze verfolgen wie Facebook mit React.
Alte Tabelle gegen neue diffen und daraus ein minimales Update bauen, falls nötig, und dieses dem Browser übergeben. Aber Tabellen können diese an sich schon relativ gut und schnell.

[OT]Dann kann ich ja in meiner nächsten Bewerbung getrost angeben, dass JS-Kenntnisse vorhanden sind[/OT]

Naa, da hätt’ ich dann schon kein Switch mehr verwendet. Das kam halt schon so aus dem Codegenerator, der für die Java-Variante halt eine Klasse mit Konstanten und Methoden „stringFor“ und „forString“ erzeugt. (Ja, auch da könnte man Klassen verwenden, aber da kümmert sich Eclipse um die Autocompletion :D).

Ich hatte zwar auch in Betracht gezogen, die ganzen Konstanten von JavaScript aus der besagten https://www.opengl.org/registry/api/GL/glcorearb.h ziehen zu lassen (dann würde sich die Frage nach einen Update für GL > 4.4 erübrigen), aber erstens ist die deutlich größer als nur die Liste der Konstanten, zweitens ist das Parsen da dann vieeel kritischer, und drittens reicht diese eine Header-Datei ja nicht. Die Informationen sind ja über mehrere Header verstreut. Das ist ja eins der Hauptprobleme bei der Suche: Kagge, wo ist jetzt GL_FLOAT_VEC3 definiert? Mal google anwerfen… ah, da scheint’s zu stehen (Klick, Scroll, Strg+F…) aha… hex-Wert 0x4711 … ich brauch’ das aber dezimal … mal den Taschenrechner anwerfen… Ein Krampf :rolleyes:

[QUOTE=ionutbaiu;137718]
[OT]Dann kann ich ja in meiner nächsten Bewerbung getrost angeben, dass JS-Kenntnisse vorhanden sind[/OT][/QUOTE]

ICH AUCH :cool:

Vielen Dank! Sehr hilfreich.
Hatte mir vor 1/2 Jahren auch so einen Translator geschrieben damit meine Programme automatisch einen sinnvolleren Debug/Warn/Error-Log ausgeben können und man sich nicht immer im Internet tot suchen muss - was dank deiner Webseite jetzt der Vergangenheit angehören dürfte. Pöse wie ich bin aber nie veröffentlicht.
Legte sowieso mehr Wert auf Kontext, da die Konstanten häufig doppelt verwendet werden und nicht auf Vollständigkeit.

Die Konstanten werden eigentlich nicht doppelt verwendet, mit zwei Ausnahmen:

  • Bit flags (mit der endung „_BIT“)
  • Spezialwerte, wie GL_ZERO==GL_FALSE usw

Jedenfalls ist die neue Version jetzt auf GL Constants Translator - und dank ionutbaiu deutlich hübscher :slight_smile:

Die fehlenden Konstanten sind jetzt auch dabei. Ja, ich stehe auch zu blöden Fehlern:

Fixed:

#define [irgendwas aus Großbuchstaben oder Ziffern und _] [irgendwas, was in eine Zahl geparst werden kann]

:rolleyes:

*** Edit ***

(Ein „Problem“ ist jetzt noch, was bei der Eingabe von z.B. „GL_R“ eine seeehr lange Liste erscheint, und die tatsächlich existierende Konstante „GL_R“ nur ganz unten. Aber das jetzt noch irgendwie gewichten und sortieren würde wohl zu weit führen…)

Mag sein das das Sortieren zu weit führt. Hab es aber trotzdem mal gemacht.

//Returns a comparator that compares by a property.
var compareBy = function(property) {
  return function(a, b) {
    var aProperty = a[property];
    var bProperty = b[property];
    if (aProperty < bProperty) {
      return -1;
    } else if (aProperty > bProperty) {
      return 1;
    } else { 
      return 0;
    }
  }
};

var translate = function(input) {
	var searchString = input.toUpperCase();
	var comparator = function(a, b) {
		var matchpos = a.name.indexOf(searchString) - b.name.indexOf(searchString);
		if (matchpos === 0) {
			return compareBy("name")(a, b);
		} else {
			return matchpos;
		}
	};
	var theFilter = function(item) {
		true;
	};
	theFilter = function(item) {
		return item.name.indexOf(searchString) !== -1;
	};
	if (input.startsWith("0x") || input.startsWith("0X")) {
		value = parseInt(input, 16);
		var searchString = input.toLowerCase();
		theFilter = function(item) {
			return item.hex.startsWith(searchString);
		};
		comparator = compareBy("hex");
	} else {
		if (!isNaN(parseInt(input, 10))) {
			theFilter = function(item) {
				return item.dec.startsWith(input);
			};
			comparator = compareBy("dec");
		}
	}
	var result = data.filter(theFilter);
	if (comparator) {
		result.sort(comparator);
	}

	return result;
};


Funktion CompareBy ist nur eine kleine Hilfe, um einfacher nach den verschiedenen Properties sortieren zu können und liefert einen Comparator zurück.

Den standard Comparator, den ich nutze sortiert zuerst nach der Position des Matches.
Je früher der Substring, desto höher das Ranking.
Bei Gleichheit, z.B. “R”, wird nachrangig alphabetisch sortiert.

Bei hexadezimaler Suche, nach dem Hex-Wert, alphabetisch.
Bei Decimaler Suche, nach dem Decimal-Wert, alphabetisch.

Und wieder 1:1 übernommen :smiley:

Wenn du irgendein „acknowledgement“ wünschst, bescheid sagen: Mention im Code oder auf der README? Oder was auch immer…

Das update liegt wieder unter GL Constants Translator

(Jetzt mit zynisch-ignorantem „Fork mich doch…“-Banner :smiley: )

Ich meinte nicht vom Wert her sondern im Kontext.
Mein Liebling 1282, sagt leider überhaupt nichts aus, wird praktisch immer und überall verwendet. Also muss man das wiederum in der Doku nachschlagen was das wiederum eigentlich bedeutet in dem Moment. :wink:

Wäre es möglich das ganze jetzt auf Wunsch abzuschalten?
Die erste Version lief ganz gut auf dem Smartphone aber jetzt stottert das ganze vor sich hin. Man kann kaum den Wert eingeben.
Vielleicht ein kleines Delay nach dem letzten eingegebenen Buchstaben bevor aktualisiert wird?

Und gerade bei GL_INVALID_OPERATION hilft einem die Doku dann auch nur sehr bedingt weiter…

Zum Performance-Problem: Notfalls vielleicht eine Checkbox „[x] Realtime“, die per default aus ist!? Für etwas ausgefeilteres würde ich da jetzt irgendwas mit setTimeout rumprobieren, oder vielleicht damit, die Eingabe schrittweise zu filtern ODER die Ausgabe (d.h. die Tabelle) inkrementell aufzubauen? Oder vielleicht hat @ionutbaiu da ja einen Ansatz? :smiley:

Hab es mir mal angeschaut.

Als erstes mal gemessen, was da überhaupt wie lange braucht.

Translate GL_: 10.34ms
Render GL_: 3.43ms
Update UI GL_: 40.9ms
Filter for: GL_: 58.72ms
  1. gefiltert und sortiertes Array zurückliefern.
  2. String bauen
  3. String via innerHTML an DOM übergeben.
  4. Gesamt

Dann hab ich das mal so umgestellt, dass nur was gemacht wird, wenn das Textfeld via Enter submittet wird.

Zum Vergleich mit “depth_buffer_bit”, das ganze zweimal hintereinander

Translate depth_buffer_bit: 1.56ms
Render depth_buffer_bit: 0.8ms
Update UI depth_buffer_bit: 17.53ms
Filter for: depth_buffer_bit: 23.25ms

Zweites mal

Translate depth_buffer_bit: 1.79ms
Render depth_buffer_bit: 1.31ms
Update UI depth_buffer_bit: 1.56ms
Filter for: depth_buffer_bit: 9.82ms

Wenn sich an der Tabelle nichts ändert, dann ist das ganze relativ smooth. Ändert sich viel dann dauert es länger. Auch wenn nur wenig dargestellt wird.

Aber es wird schonmal klar, dass hier viel am UI liegt. Desktop geht, Mobilgerät ist noch etwas träger.

Testweise hab ich mal die Tabelle weggelassen und das Ergebnis in einer Textarea ausgegeben, was das Updaten der UI wesentlich beschleunigt.

Zusammengefaßt:
Updates bei Submit
Ergebnis in eine Textarea ausgeben
=> Gute Performance

Auf meinem Smartphone: lumia 630, hab ich mal beides probiert mit Submit und mit EventListener.
Beides läuft ziemlich zügig, wenn das Ergebnis in einer Textarea dargestellt wird.

Von daher, würde ich empfehlen eine Textarea zu nehmen.

Oder was meint ihr?

Die Tabelle ist IMHO schon schick, und ich denke, wenn man das aktualisieren erst bei “Enter” macht, wäre das schon OK. Auch in einer TextArea müßte man sich ja um eine Tabellarische Formatierung Gedanken machen.

Ich hab jetzt mal ein Delay eingebaut von 500ms.

Hat sich der Wert nach 500ms nicht geändert, springt das ganze an.
Zusätzlich sind noch ein paar log-Einträge vorhanden um die Zeit ein wenig beobachten zu können und welche Zwischenschritte übersprungen wurden.

<!DOCTYPE html>
<html>

<head>
        <title>GL Constants Translator</title>
        <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">

        <script type="text/javascript" src="scripts/GLConstantsData.js"></script>
        <script type="text/javascript" src="scripts/GLConstantsTranslator.js"></script>
        <script type="text/javascript">
                function update() {
                        var textOnChange = this.value;
                        setTimeout(function() {
                                if (textOnChange === document.getElementsByName("Input")[0].value) {
                                        doUpdate(textOnChange);
                                } else {
                                        console.log("Discarding: " + textOnChange);
                                }

                        }, 500);

                }

                function doUpdate(text) {

                        console.time("Filter for " + text);

                        console.time("Translate " + text);
                        var result = translate(text);
                        console.timeEnd("Translate " + text);

                        console.time("Render " + text);
                        var renderedResult = renderResult(result);
                        console.timeEnd("Render " + text);

                        console.time("Update UI " + text);
                        document.getElementById("output").innerHTML = renderedResult;
                        console.timeEnd("Update UI " + text);

                        console.timeEnd("Filter for " + text);
                }

                function init() {
                        initConstants();

                        document.getElementsByName("Input")[0].addEventListener('input', update);
                        document.getElementById("output").innerHTML = renderResult(translate(""));
                        document.getElementsByName("Input")[0].focus();
                }

                function renderResult(result) {
                        if (result && result.length > 0) {
                                return "<table id='data'><th class='long'>Constant Name</th><th>Decimal</th><th>Hexadecimal</th>" +
                                        result.map(renderRow).join("
") + "</table>";
                        } else {
                                return "<p align='center'><b>No matching results</b></p>";
                        }
                }

                function renderRow(item, idx) {
                        var rowclass = (idx % 2 === 0) ? "even" : "odd";
                        return `<TR class="${rowclass}"><TD>${item.name}</TD><TD>${item.dec}</TD><TD>${item.hex}</TD></TR>`;
                }
        </script>
        <style>
                html {
                        overflow-y: scroll;
                }
                
                table#data {
                        width: 90%;
                        margin-left: auto;
                        margin-right: auto;
                        font-family: 'Courier';
                        font-size: 16px;
                }
                
                th td {
                        width: 15%;
                }
                
                th.long {
                        width: 60%;
                }
                
                tr.even {
                        background-color: #eeeeee;
                }
        </style>
</head>

<body onload="init()">
        <p align="center">
                Type GL constant names or values into the input field to filter the table.
        </p>
        <p align="center">
                Constant names may be in upper- or lowercase.
        </p>
        <p align="center">
                Inputs starting with <code><b>"0x"</b></code> will be interpreted as hexadecimal constant values.
        </p>
        <p align="center">
                Other numerical inputs will be interpreted as decimal constant values.
        </p>
        <table align="center">
                <tr>
                        <td>Filter: </td>
                        <td>
                                <input style="font-family:'Courier'; font-size:16px" size="60" type="text" name="Input" />
                        </td>
                </tr>
        </table>
        <div id="output">
                The output
        </div>
</body>

</html>

Zusätzlich rufe ich noch diese Funktion auf, um die Daten schon einmal nach Namen vorsortiert zu haben. Der Erfolg ist dabei allerdings im homöopatischen Bereich. Da geht die Zeit nicht flöten.

function initConstants() {
				data = data.sort(compareBy("name"));
}

Hmja, das mit dem Delay ware eine Option. Ich dachte auch gerade noch an „pragmatische“ Dinge, wie prüfen, ob jemand nur „G“, „L“ oder „_“ (oder vielleicht „0x“) eingetippt hat - wissend, dass dann eigentlich nichts zu tun ist.

Aber ein bißchen rumspielen wollte ich dann doch: Eine Tabelle mit 1800 Einträgen macht natürlich nur bedingt Sinn. Ich dachte, dass man sie vielleicht inkrementell aufbauen könnte, um „responsive“ ( :smiley: ) zu bleiben.

Das, was ich da implementiert habe, funktioniert NICHT - aber vielleicht kann jemand (sagen, warum nicht :smiley: bzw.) einschätzen, ob das Sinn machen könnte, oder in ähnlicher Form funktionieren könnte.

Der Gedanke war, dass man

  • Wenn etwas eingetippt wird, ein „update“ schedult.
  • Dieses Update hängt z.B. 100 neue Zeilen in die Tabelle (über den DOM)
  • Am Ende des updates wird mit setTimeout automatisch das update für die nächsten 100 Zeilen eingereiht
  • Wenn man in der Zwischenzeit was tippt, wird das update „interrupted“, und ein neues Update gestartet

Es funktioniert … naja, so halb :rolleyes: Das eigentliche Einfügen geht (man sieht den Scrollbar-Button schrumpfen :D), und das interrupten geht meistens auch, aber nicht immer. Dieser Versuch, das „aktuelle Update“ in einer globalen Variablen namens „currentUpdateCall“ zu speichern, und der dann ggf. so ein „interrupted“ unterzujubeln, damit sie sich selbst (und ihre „rekursiven Aufrufe“) beendet, ist wohl etwas gewagt… (eigentlich dachte ich, dass das recht einfach und klar sein müßte … Multithreading und Race-Conditions sollten ja nicht das Problem sein … :wink: … aber um die Frage, wo dieses „currentUpdateCall“ denn liegt, wenn man es mit setTimeout startet, muss ich wohl noch meinen Kopf wickeln…)

Aber vielleicht will ja schonmal jemand seine Gedanken dazu äußern?


<!DOCTYPE html>
<html>
    <head>
        <title>GL Constants Translator</title>
        <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
        <script type="text/javascript" src="scripts/GLConstantsData.js"></script>
        <script type="text/javascript" src="scripts/GLConstantsTranslator.js"></script>
        <script type="text/javascript">
        
            var currentUpdateCall;
            
            function update() {
                if (typeof currentUpdateCall !== "undefined")
                {
                    console.log("Interrupting");
                    currentUpdateCall.interrupted = true;
                }
                else
                {
                    console.log("currentUpdateCall is undefined, doing update");
                }
                doUpdate(this.value);
            }
            
            function updateTable(result)
            {
                interrupted = false;
                
                var output = document.getElementById("output");
                output.innerHTML = '';
                
                var table = document.createElement("table");
                table.id = "data";
                
                var th0 = document.createElement("th");
                th0.className = "long";
                th0.appendChild(document.createTextNode("Constant Name"));
                table.appendChild(th0);

                var th1 = document.createElement("th");
                th1.appendChild(document.createTextNode("Decimal"));
                table.appendChild(th1);
                
                var th2 = document.createElement("th");
                th2.appendChild(document.createTextNode("Hexadecimal"));
                table.appendChild(th2);

                updateChunk(0, 100, table, result);
                output.appendChild(table);
            }
            
            function updateChunk(min, size, table, result)
            {
                var max = min + size;
                var finished = false;
                if (max > result.length)
                {
                    max = result.length;
                    finished = true;
                }
                for (var i=min; i<max; i++)
                {
                    var tr = document.createElement("tr");
                    if (i % 2 === 0)
                    {
                        tr.className = "even";
                    }
                    
                    var td0 = document.createElement("td");
                    var txt0 = document.createTextNode(result**.name);
                    td0.appendChild(txt0);
                    tr.appendChild(td0);

                    var td1 = document.createElement("td");
                    var txt1 = document.createTextNode(result**.dec);
                    td1.appendChild(txt1);
                    tr.appendChild(td1);

                    var td2 = document.createElement("td");
                    var txt2 = document.createTextNode(result**.hex);
                    td2.appendChild(txt2);
                    tr.appendChild(td2);
                    
                    table.appendChild(tr);
                }
                if (!finished)
                {
                    currentUpdateCall = function() 
                    {
                        console.log("Appending "+min+" size "+size+" of "+result.length);
                        if (!interrupted)
                        {
                            updateChunk(min+size, size, table, result);
                        }
                        else
                        {
                            currentUpdateCall = undefined;
                        }
                    };
                    console.log("Schedule  "+min+" size "+size+" of "+result.length);
                    setTimeout(currentUpdateCall, 250);
                }
                else
                {
                    currentUpdateCall = undefined;
                }
                
            }
    
            function doUpdate(text) {
    
    
                console.time("Filter for " + text);
    
                console.time("Translate " + text);
                var result = translate(text);
                console.timeEnd("Translate " + text);
    
                updateTable(result);
                //console.time("Render " + text);
                //var renderedResult = renderResult(result);
                //console.timeEnd("Render " + text);
    
                //console.time("Update UI " + text);
                //document.getElementById("output").innerHTML = renderedResult;
                //console.timeEnd("Update UI " + text);
    
                console.timeEnd("Filter for " + text);
            }
                
            function init() {
                document.getElementsByName("Input")[0].addEventListener('input', update);
                document.getElementById("output").innerHTML = renderResult(translate(""));
                document.getElementsByName("Input")[0].focus();
            }

            function renderResult(result) {
                if (result && result.length > 0) {
                    return "<table id='data'><th class='long'>Constant Name</th><th>Decimal</th><th>Hexadecimal</th>" + 
                        result.map(renderRow).join("
") + "</table>";
                } else {
                    return "<p align='center'><b>No matching results</b></p>";
                }
            }

            function renderRow(item, idx) {
                var rowclass = (idx % 2 === 0) ? "even" : "odd";
                return "<TR class='" + rowclass + "'>" +
                  "<TD>" + item.name + "</TD>" +
                  "<TD>" + item.dec + "</TD>" + 
                  "<TD>" + item.hex + "</TD></TR>";
            }
        </script>
        <style>
            html {
                overflow-y: scroll;
            }        
            table#data {
                width: 90%;
                margin-left:auto; 
                margin-right:auto;
                font-family:'Courier'; 
                font-size:16px;
            }
            th td {
                width: 15%;
            }
            th.long {
                width: 60%;
            }
            tr.even {
                background-color: #eeeeee;
            }
        </style>
    </head>
    <body onload="init()">
        <p align="center">
            Type GL constant names or values into the input field to filter the table.
        </p>
        <p align="center">
            Constant names may be in upper- or lowercase. 
        </p>
        <p align="center">
            Inputs starting with <code><b>"0x"</b></code> will be interpreted as hexadecimal constant values. 
        </p>
        <p align="center">
            Other numerical inputs will be interpreted as decimal constant values. 
        </p>
        <table align="center">
            <tr>
                <td>Filter: </td>
                <td>
                <input style="font-family:'Courier'; font-size:16px" size="60" type="text" name="Input" />
                </td>
            </tr>
        </table>
        <div id="output">
            The output
        </div>

        <a href="https://github.com/javagl/javagl.github.io"><img style="position: absolute; top: 0; right: 0; border: 0;" src="https://forum.byte-welt.net/images/ForkMeOrDont.png" alt="Fork me on GitHub"></a>
    </body>
</html>

Strg+F “interrupted”

in update() ist es ein property von currentUpdateCall

in updateTable() ohne var wird es im Global-Scope angelegt mit false.

in updateChunk() wird das aus dem Global-Scope festgelegte interrupted überprüft, was false ist somit auch nichts unterbrochen werden kann da !interrupted.

Es würde mich also sehr wundern, wenn überhaupt was abgebrochen wird.

— Edit —

Was funktionieren kann ist ein Array anzulegen. Ich habe meines mal calls genannt.
In der update(), den aktuellen Aufruf in calls zu legen. calls.push(this.value);

Und diesen text, (this.value), dann durch alle Methoden durchzuschleifen, so dass dieser als parameter auch in updateChunks vorhanden ist.
Anstelle auf interrupted zu überprüfen, dann einfach auf text === calls[calls.length-1] prüfen um zu sehen, ob der aktuelle Durchgang noch aktuell genug ist.

Ohja, erst was es komplett global, aber dann ist mir gedämmert, dass es wohl schon in der currentUpdateCall-instanz liegen muss.

Das mit dem Array erschließt sich mir nicht ganz. Es sollte ja immer höchstens EIN “currentUpdateCall” aktiv sein. Und welcher auch immer gerade aktiv ist: Den soll man bei einer Eingabeänderung unterbrechen, d.h. dafür sorgen, dass er sein “updateChunk” nicht mehr macht, und vor allem nicht seinen “Nachfolger” in die Queue legt.

Hab’ mal versucht, das ganze “Objektorientierter” zu machen und aufzuräumen: Er baut eine “Verkettete Liste von ChunkUpdate-Knoten” auf, und arbeitet die ab. Es scheint im wesentlichen zu funktionieren. Lediglich wenn man wie blöd schnell “g-backspace-g-backspace…” hämmert, scheint er manchmal updates zu machen, die eigentlich abgebrochen sein sollten…


<!DOCTYPE html>
<html>
    <head>
        <title>GL Constants Translator</title>
        <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
        <script type="text/javascript" src="scripts/GLConstantsData.js"></script>
        <script type="text/javascript" src="scripts/GLConstantsTranslator.js"></script>
        <script type="text/javascript">
        
            var currentChunkUpdate;
            
            var ChunkUpdate = function (min, size, table, result, text) {
                this.min = min;
                this.table = table;
                this.result = result;
                this.text = text;
                this.interrupted = false;
                

                if (min + size >= result.length)
                {
                    this.max = result.length;
                    console.log("ChunkUpdate instantiated "+this.info());
                    this.successor = undefined;
                }
                else
                {
                    this.max = min + size;
                    console.log("ChunkUpdate instantiated "+this.info());
                    this.successor = new ChunkUpdate(this.max, size, table, result, text); 
                }
            };            
            
            ChunkUpdate.prototype.info = function() {
                return this.min+" to "+this.max+" for "+this.text+", status "+this.interrupted;
            };
            
            ChunkUpdate.prototype.execute = function() {
                
                var that = this;
                if (!this.interrupted)
                {
                    console.log("Executing  "+this.info());
                    updateChunk(this.min, this.max, this.table, this.result);

                    if (typeof this.successor !== "undefined")
                    {
                        currentChunkUpdate = this.successor;

                        console.log("Schedule   "+currentChunkUpdate.info());
                        setTimeout(function()
                        {
                            that.successor.execute();
                        }, 250);
                    }
                }
                else
                {
                    console.log("Executing  "+this.info()+" was interrupted");
                    currentChunkUpdate = undefined;
                }
            };            
            
            
            
            function update() {

                if (typeof currentChunkUpdate !== "undefined")
                {
                    console.log("Interrupt  "+currentChunkUpdate.info());
                    currentChunkUpdate.interrupted = true;
                }
                else
                {
                    console.log("currentChunkUpdate is undefined, doing update");
                }
                doUpdate(this.value);
            }
            
            function updateTable(result, text)
            {
                var output = document.getElementById("output");
                output.innerHTML = '';
                
                var table = document.createElement("table");
                table.id = "data";
                
                var th0 = document.createElement("th");
                th0.className = "long";
                th0.appendChild(document.createTextNode("Constant Name"));
                table.appendChild(th0);

                var th1 = document.createElement("th");
                th1.appendChild(document.createTextNode("Decimal"));
                table.appendChild(th1);
                
                var th2 = document.createElement("th");
                th2.appendChild(document.createTextNode("Hexadecimal"));
                table.appendChild(th2);

                output.appendChild(table);
                
                currentChunkUpdate = new ChunkUpdate(0, 100, table, result, text);
                currentChunkUpdate.execute();
            }
            
            function updateChunk(min, max, table, result)
            {
                for (var i=min; i<max; i++)
                {
                    var tr = document.createElement("tr");
                    if (i % 2 === 0)
                    {
                        tr.className = "even";
                    }
                    
                    var td0 = document.createElement("td");
                    var txt0 = document.createTextNode(result**.name);
                    td0.appendChild(txt0);
                    tr.appendChild(td0);

                    var td1 = document.createElement("td");
                    var txt1 = document.createTextNode(result**.dec);
                    td1.appendChild(txt1);
                    tr.appendChild(td1);

                    var td2 = document.createElement("td");
                    var txt2 = document.createTextNode(result**.hex);
                    td2.appendChild(txt2);
                    tr.appendChild(td2);
                    
                    table.appendChild(tr);
                }
            }
    
            function doUpdate(text) {
    
    
                console.time("Filter for " + text);
    
                console.time("Translate " + text);
                var result = translate(text);
                console.timeEnd("Translate " + text);
    
                updateTable(result, text);
                //console.time("Render " + text);
                //var renderedResult = renderResult(result);
                //console.timeEnd("Render " + text);
    
                //console.time("Update UI " + text);
                //document.getElementById("output").innerHTML = renderedResult;
                //console.timeEnd("Update UI " + text);
    
                console.timeEnd("Filter for " + text);
            }
                
            function init() {
                document.getElementsByName("Input")[0].addEventListener('input', update);
                document.getElementById("output").innerHTML = renderResult(translate(""));
                document.getElementsByName("Input")[0].focus();
            }

            function renderResult(result) {
                if (result && result.length > 0) {
                    return "<table id='data'><th class='long'>Constant Name</th><th>Decimal</th><th>Hexadecimal</th>" + 
                        result.map(renderRow).join("
") + "</table>";
                } else {
                    return "<p align='center'><b>No matching results</b></p>";
                }
            }

            function renderRow(item, idx) {
                var rowclass = (idx % 2 === 0) ? "even" : "odd";
                return "<TR class='" + rowclass + "'>" +
                  "<TD>" + item.name + "</TD>" +
                  "<TD>" + item.dec + "</TD>" + 
                  "<TD>" + item.hex + "</TD></TR>";
            }
        </script>
        <style>
            html {
                overflow-y: scroll;
            }        
            table#data {
                width: 90%;
                margin-left:auto; 
                margin-right:auto;
                font-family:'Courier'; 
                font-size:16px;
            }
            th td {
                width: 15%;
            }
            th.long {
                width: 60%;
            }
            tr.even {
                background-color: #eeeeee;
            }
        </style>
    </head>
    <body onload="init()">
        <p align="center">
            Type GL constant names or values into the input field to filter the table.
        </p>
        <p align="center">
            Constant names may be in upper- or lowercase. 
        </p>
        <p align="center">
            Inputs starting with <code><b>"0x"</b></code> will be interpreted as hexadecimal constant values. 
        </p>
        <p align="center">
            Other numerical inputs will be interpreted as decimal constant values. 
        </p>
        <table align="center">
            <tr>
                <td>Filter: </td>
                <td>
                <input style="font-family:'Courier'; font-size:16px" size="60" type="text" name="Input" />
                </td>
            </tr>
        </table>
        <div id="output">
            The output
        </div>

        <a href="https://github.com/javagl/javagl.github.io"><img style="position: absolute; top: 0; right: 0; border: 0;" src="images/ForkMeOrDont.png" alt="Fork me on GitHub"></a>
    </body>
</html>

So bin auch nochmal dem weissen Hasen gefolgt.

Das ist was dabei rauskam.

[SPOILER]

<!doctype html>
<html lang="en">

<head>
        <meta charset="utf-8">
        <title>GL Constants Translator</title>
        <script type="text/javascript" src="scripts/GLConstantsData.js"></script>
        <script type="text/javascript" src="scripts/GLConstantsTranslator.js"></script>
        <script>
                var filteredData = data;

                var filter = "";

                var chunkSize = 100;

                function elementInViewport2(el) {
                        var top = el.offsetTop;
                        var left = el.offsetLeft;
                        var width = el.offsetWidth;
                        var height = el.offsetHeight;

                        while (el.offsetParent) {
                                el = el.offsetParent;
                                top += el.offsetTop;
                                left += el.offsetLeft;
                        }

                        return (
                                top < (window.pageYOffset + window.innerHeight) &&
                                left < (window.pageXOffset + window.innerWidth) &&
                                (top + height) > window.pageYOffset &&
                                (left + width) > window.pageXOffset
                        );
                }

                function renderRow(idx) {
                        var rowclass = (idx % 2 === 0) ? "even" : "odd";
                        var item = filteredData[idx];
                        var tr = document.createElement("tr");
                        tr.setAttribute("class", rowclass);

                        var td = document.createElement("td");
                        td.textContent = (idx + 1);
                        tr.appendChild(td);

                        var td1 = document.createElement("td");
                        td1.textContent = item.name;
                        tr.appendChild(td1);
                        var td2 = document.createElement("td");
                        td2.textContent = item.dec;
                        tr.appendChild(td2);
                        var td3 = document.createElement("td");
                        td3.textContent = item.hex;
                        tr.appendChild(td3);

                        return tr;
                }

                function addElements(start) {
                        console.log("adding " + start);
                        var frag = document.createDocumentFragment();
                        for (var i = 0; i < chunkSize; i++) {
                                if ((start + i) < filteredData.length) {
                                        var row = renderRow(start + i);
                                        frag.appendChild(row);
                                }
                        }

                        document.getElementsByClassName("tbody")[0].appendChild(frag);
                        if ((start + chunkSize - 1) < filteredData.length) {
                                var loader = document.createElement("div");
                                loader.setAttribute("class", "loader");
                                document.getElementById("output").appendChild(loader);
                                var interval = setInterval(function() {
                                        console.log("checking loader");
                                        if (!document.contains(loader)) {
                                                console.log("loader disappeared");
                                                clearInterval(interval);
                                                return;
                                        }
                                        if (elementInViewport2(loader)) {
                                                addElements(start + chunkSize);
                                                loader.parentNode.removeChild(loader);
                                                clearInterval(interval);
                                        }
                                }, 250);


                        }
                }

                function reset() {
                        function remove(elements) {
                                if (elements) {
                                        while (elements.length > 0) {
                                                elements[0].parentNode.removeChild(elements[0]);
                                        }
                                }
                        }
                        remove(document.getElementsByClassName("loader"));
                        remove(document.getElementsByClassName("tbody"));
                        var tb = document.createElement("tbody");
                        tb.setAttribute("class", "tbody");
                        document.getElementById("data").appendChild(tb);

                }

                function update() {
                        reset();
                        filterNew = document.getElementsByName("Input")[0].value;

                        if (filterNew.startsWith(filter)) {
                                filter = filterNew;
                                filteredData = translate(filteredData, filter);
                        } else {
                                filter = filterNew;
                                filteredData = translate(data, filter);
                        }

                        addElements(0);
                }

                function init() {
                        console.log("init");
                        document.getElementsByName("Input")[0].addEventListener('input', update);
                        document.getElementsByName("Input")[0].focus();
                        update();
                }
        </script>

        <link rel="stylesheet" type="text/css" href="style.css">
</head>

<body onload="init()">
        <p align="center">
                Type GL constant names or values into the input field to filter the table.
        </p>
        <p align="center">
                Constant names may be in upper- or lowercase.
        </p>
        <p align="center">
                Inputs starting with <code><b>"0x"</b></code> will be interpreted as hexadecimal constant values.
        </p>
        <p align="center">
                Other numerical inputs will be interpreted as decimal constant values.
        </p>
        <table align="center">
                <tr>
                        <td>Filter: </td>
                        <td>
                                <input style="font-family:'Courier'; font-size:16px" size="60" type="text" name="Input" />
                        </td>
                </tr>
        </table>
        <div id="output">
                <table id='data'>
                        <th>No</th>
                        <th class='long'>Constant Name</th>
                        <th>Decimal</th>
                        <th>Hexadecimal</th>
                        <tbody class="tbody"></tbody>
                        </tbody>
                </table>
        </div>

        <a href="https://github.com/javagl/javagl.github.io"><img style="position: absolute; top: 0; right: 0; border: 0;" src="https://forum.byte-welt.net/images/ForkMeOrDont.png" alt="Fork me on GitHub"></a>
</body>

</html>

[/SPOILER]

[SPOILER]

var compareBy = function(property) {
	return function(a, b) {
		var aProperty = a[property];
		var bProperty = b[property];
		if (aProperty < bProperty) {
			return -1;
		} else if (aProperty > bProperty) {
			return 1;
		} else {
			return 0;
		}
	}
};


var translate = function(filteredData, input) {
  console.log("Searching on "+filteredData.length);
  var searchString = input.toUpperCase();
	var comparator = function(a, b) {
		var matchpos = a.name.indexOf(searchString) - b.name.indexOf(searchString);
		if (matchpos === 0) {
			return compareBy("name")(a, b);
		} else {
			return matchpos;
		}
	};
	var theFilter = function(item) {
		true;
	};
	theFilter = function(item) {
		return item.name.indexOf(searchString) !== -1;
	};
	if (input.startsWith("0x") || input.startsWith("0X")) {
		value = parseInt(input, 16);
		var searchString = input.toLowerCase();
		theFilter = function(item) {
			return item.hex.startsWith(searchString);
		};
		comparator = compareBy("hex");
	} else {
		if (!isNaN(parseInt(input, 10))) {
			theFilter = function(item) {
				return item.dec.startsWith(input);
			};
			comparator = compareBy("dec");
		}
	}
	var result = filteredData.filter(theFilter);
	if (comparator) {
		result.sort(comparator);
	}

	return result;
};

[/SPOILER]

[SPOILER]

 html {
         overflow-y: scroll;
 }
 
 table#data {
         width: 90%;
         margin-left: auto;
         margin-right: auto;
         font-family: 'Courier';
         font-size: 16px;
 }
 
 th td {
         width: 15%;
 }
 
 th.long {
         width: 60%;
 }
 
 tr.even {
         background-color: #eeeeee;
 }

[/SPOILER]

Externes Stylesheet.
translate()-Funktion mit einem parameter erweitert, so dass man die Daten auf denen gesucht wird übergeben kann. Wenn nach a gesucht wurde und danach direkt nach ab sucht, dann wird das Ergebnis eine Untermenge des Ergebnisses von a sein, wodurch man nicht mehr alles durchsuchen muss.

Auf der eigentlichen Seite, wird der vorhergehende Suchbegriff und das Ergebnis gespeichert.
Das hilft einerseits, den Suchraum klein zu halten wenn die Suche verfeinert wird, andererseits wird dies für die Update-Strategie benötigt.

Bei der Eingabe wird gesucht, das Ergebnis gespeichert und die ersten 100 Einträge angezeigt.
Unter die Einträge wird ein Token (div.loader) gehängt.
Dieses Token wird 4 mal pro Sekunde gepollt und überprüft, ob es im Sichtbaren bereich ist, (also ob ganz nach unten gescrollt wurde) wurde ganz nach unten gescrollt, entfernt sich dieses Token, beendet den Timer und startet eine Funktion, die wiederum die nächsten 100 Einträge der Tabelle hinzufügt. (Load-More-On-Scroll-To-Bottom)

Neuer Suchbegriff entfernt das Token und beendet das pollen und die Geschichte geht von vorne los.

Fragen?

(Dass das ganze noch “aufgeräumt” werden muss, ist wohl klar: CSS raus, und insbesondere die Scripte nicht direkt ins HTML (wenn nicht unbedingt nötig) - ich find’ das zumindest häßlich, wenn’s im HTML steht)

Das mit dem Nachladen, wenn man ganz nach unten gescrollt hat, finde ich irgendwie nicht so cool. Ich hasse das z.B. auch bei der Google-Bildersuche. Ja, es ist “in”, aber es gefällt mir von der Handhabung und der “user experience” her nicht: Man tippt irgendwas ein, und bekommt dann ein Ergebnis. “Ah, fertig”. Und sieht den Scrollbalken: “Ah, ein bißchen mehr als eine Seite”. Dann scrollt man ein Stück, und der Scollbar-Knopf rutscht einem durch das Nachladen unter dem Mauszeiger weg. Und wie viel da tatsächlich noch kommt, weiß man nie.

Das mit dem inkrementellen Filtern der Eingabe

"g" -> result = filter(all, "g")
"gl" -> result = filter(result, "gl") // result statt "all"
"gl_" -> result = filter(result, "gl_") // result statt "all"

macht zwar technisch Sinn und kann nicht schaden (wenn man es mit vertretbarem Aufwand einbauen kann - das scheint ja der Fall zu sein, aber ich muss es noch auseinanderpflücken). Aber offenbar ist ja das teuerste bzw. das Hauptproblem, das echte HTML im Browser anzuzeigen - also weder das Filtern noch das Erstellen des HTMLs, sondern wirklich nur das “.innerHTML = result”.

Ich fände das mit dem inkrementellen Aufbauen der Tabelle im Hintergrund schöner. Es scheint ja grundsätztlich zu funktionieren, und falls da nichts offensichtlich “falsch” oder “schlecht” ist, würde ich das eher so machen. Vielleicht noch kombiniert mit dem inkrementellen Filtern, das scheint ja recht einfach zu sein.

Gut über das Nachladen beim scrollen kann man sich streiten. Ich finds cool.

Es gibt allerdings noch einen Punkt, der aus dem inkrementellen Filtern folgt.

Wenn das result global abgelegt wird, dann kann man auch schauen ob das Ergebnis nach dem filtern gleich bleibt.

Und wenn es das tut, dann braucht man gar nichts updaten. Wenn das updaten des Chunks läuft auch nicht unterbrechen, sondern einfach weiterlaufen lassen.

Das würde ich hier in der Update-Methode aus deinem vorletzten Post unterbringen.

function update() {
                var previousResult = result;
                result = translate(result, this.value);
                if (previousResult.equals(result)) {
                    console.log("Nothing Changed");
                } else {

                    if (typeof currentChunkUpdate !== "undefined")
                    {
                        console.log("Interrupt  "+currentChunkUpdate.info());
                        currentChunkUpdate.interrupted = true;
                    }
                    else
                    {
                        console.log("currentChunkUpdate is undefined, doing update");
                    }
                    doUpdate(this.value);
                }
            }

Dazu eben noch eine equals Methode auf Array definieren.

Wenn die Seite geladen ist, dann ändert sich bei “g”, “gl”,“gl_”, überhaupt nix auf dem UI.