ViewerFunctions - Ein (sehr) einfacher Funktionsplotter

Ein weiteres kleines Projektchen: ViewerFunctions

Noch eine Anwendung für den Viewer: Man kann damit recht einfach ein Panel erstellen, in dem eine DoubleFunction geplottet wird. Natürlich mit Zoomen und Verschieben (aber (per default) ohne Rotieren ;)), und hübschen Koordinatenachsen und einer Legende.

Natürlich sind die Konfigurationsmöglichkeiten, die man sich dafür wünschen könnte, endlos. Aber der Fokus war (für die konkreten Fälle, wo ich den bisher verwendet habe) darauf, schnell und einfach ein Panel irgendwo einfügen zu können, in dem eine oder mehrere Funktionen geplottet werden. Und das geht jetzt, mit wenigen Zeilen code:

Wie üblich, auch bald unter


<dependency>
  <groupId>de.javagl</groupId>
  <artifactId>viewer-functions</artifactId>
  <version>0.0.1</version>
</dependency>

Habe mal den SimpleViewerFunctionsTest ausprobiert. Funktioniert soweit ganz gut.

Was ich ein wenig unintuitiv fand ist das ziehen mit der rechten Maustaste. Gerade von Kartenanwendungen bin ich hier schon die linke Maustaste gewöhnt. Rechte Maustaste ist immer Kontext-Menu.

Zwei Zeilen, die ich anders gemacht hätte im SimpleViewerFunctionsTest sind

DoubleFunction<Double> function0 = Math::sin; // statt x -> Math.sin(x)```

Und dann habe ich auch noch ganz, ganz tieeeef reingezoomt. 
Und da war das Verhalten etwas Merkwürdig. Zuerst fing es an die Scala Mittig zu schreiben, anstatt an der Coordinatenachse, die bereits ausserhalb des Sichtfelds lag. Einmal Horizontal, ein anderes mal Vertikal.

Und wenn ich dann noch viel weiter gezoomt habe, dann bekam ich einmal noch folgende Meldungen:

[SPOILER]

Exception in thread “AWT-EventQueue-0” java.lang.ArrayIndexOutOfBoundsException: 1
at de.javagl.viewer.functions.Axes.computeWorldTicks(Axes.java:86)
at de.javagl.viewer.functions.AxesPainter.updateY(AxesPainter.java:313)
at de.javagl.viewer.functions.AxesPainter.validateAxes(AxesPainter.java:262)
at de.javagl.viewer.functions.AxesPainter.paint(AxesPainter.java:327)
at de.javagl.viewer.Viewer.paintComponent(Viewer.java:477)
at javax.swing.JComponent.paint(JComponent.java:1056)
at javax.swing.JComponent.paintToOffscreen(JComponent.java:5219)
at javax.swing.BufferStrategyPaintManager.paint(BufferStrategyPaintManager.java:290)
at javax.swing.RepaintManager.paint(RepaintManager.java:1265)
at javax.swing.JComponent._paintImmediately(JComponent.java:5167)
at javax.swing.JComponent.paintImmediately(JComponent.java:4978)
at javax.swing.RepaintManager$4.run(RepaintManager.java:824)
at javax.swing.RepaintManager$4.run(RepaintManager.java:807)
at java.security.AccessController.doPrivileged(Native Method)
at java.security.ProtectionDomain$1.doIntersectionPrivilege(ProtectionDomain.java:75)
at javax.swing.RepaintManager.paintDirtyRegions(RepaintManager.java:807)
at javax.swing.RepaintManager.paintDirtyRegions(RepaintManager.java:782)
at javax.swing.RepaintManager.prePaintDirtyRegions(RepaintManager.java:731)
at javax.swing.RepaintManager.access$1300(RepaintManager.java:64)
at javax.swing.RepaintManager$ProcessingRunnable.run(RepaintManager.java:1720)
at java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:311)
at java.awt.EventQueue.dispatchEventImpl(EventQueue.java:756)
at java.awt.EventQueue.access$500(EventQueue.java:97)
at java.awt.EventQueue$3.run(EventQueue.java:709)
at java.awt.EventQueue$3.run(EventQueue.java:703)
at java.security.AccessController.doPrivileged(Native Method)
at java.security.ProtectionDomain$1.doIntersectionPrivilege(ProtectionDomain.java:75)
at java.awt.EventQueue.dispatchEvent(EventQueue.java:726)
at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:201)
at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:116)
at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:105)
at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:93)
at java.awt.EventDispatchThread.run(EventDispatchThread.java:82)

[/SPOILER]

Aber ansonsten sehr schön.

Das Ziehen mit der rechten Maustaste ist ein „Seiteneffekt“ davon, dass das ja auch nur ein „Viewer“ ist (siehe verlinkten Thread). Und dort ist z.B. die linke Maustaste mit „Rotieren“ belegt. Der Problematik bzw. der Fragestellung bin ich mir mehr als bewußt. Im Moment wird das recht pragmatisch gelöst, durch die MouseControls Klasse, die eben ein MouseControl erstellen kann, das die Default-Interaktionen kapselt.

Darüber hinaus gehend ist die Frage ja dann: Wie gibt man dem Benutzer (d.h. in diesem Fall dem Entwickler, der diese Library verwendet) die Kontrollmöglichkeiten, die er haben will?

Erste Andeutungen von Ansätzen zu dieser Verallgemeinerung und Konfigurierbarkeit sieht man schon in DefaultMouseControl: Es gibt dort Predicate<MouseEvent>-Instanzen, die überprüfen, ob ein Event eine bestimmte Aktion auslösen soll. Der Gedanke ist, „sowas“ dann irgendwie zu exponieren, damit der Entwickler den Fall, dass man mit der Linken Maustaste verschieben kann, GROB (!) mit sowas lösen kann wie

viewer.setMouseControl(
    MouseControls.createTranslating(
        InputEventPredicates.buttonDown(1)));

Aber wie auch schon an anderer Stelle angedeutet: Das reicht nicht. Was ist, wenn der Benutzer sagen will: „Ich will dass bei gedrückter mittlerer Maustaste die Ansicht rotiert wird, und wenn zusätzlich SHIFT gedrückt ist, soll doppelt so schnell rotiert werden!“ ? Ab einem bestimmten Punkt (d.h. ab einem gewissen Bedarf an Konfigurierbarkeit) ist man wohl genötigt, die etwas patzig wirkende Antwort zu geben: „Setz’ das MouseControl auf null, und häng’ dir deinen eigenen MouseMotionListener dran, der macht, was du willst“.

Wie weit ich (VOR dieser Antwort) die Konfigurationsmöglichkeiten noch aufbohren werde, muss ich noch überlegen. Allgemeiner kann man ja sagen, dass „Inputs“ mit „Aktionen“ verbunden werden sollen - eigentlich ganz analog zu Key Bindings, aber … eben auch für MouseEvents. Genaugenommen tauchte diese Frage für mich schon in „stärkerer“ Form bei Swogl auf - ein paar Gedanken dazu gibt’s in diesem Thread. Eine Lösung hatte ich in der letzten Version von Swogl (die jetzt schon wieder Jahre alt ist :frowning: ) auch eingebaut. Die hatte Ähnlichkeit zu dem Predicate<MouseEvent>-Ansatz. Ich hoff(t?)e, das mal irgendwann „generisch“ lösen zu können, so dass vielleicht dieses „Maus-Auf-Aktion-Mapping-Konfigurier-Ding“ als eigenständige Lib existieren könnte. Aber da muss noch mehr Hirnschmalz reingesteckt werden.

Danke für den Hinweis mit dem „gaaaaanz tief reinzoomen“. Tatsächlich steht in meiner TODO-Liste auch schon
Viewer: Limit scaling to prevent rendering errors
Ab einem bestimmten Punkt ist der Abstand zwischen zwei „Achsen-Ticks“ einfach kleiner als das, was durch double noch aufgelöst werden kann. Dann gibt es erstmal „nur“ Renderartefakte, aber dass es später dann noch mit einer Exception abkachelt, war mir nicht bewußt - das schiebt das „TODO“ ein ganzes Stück weiter nach oben :wink: Mal schauen, wie ich da ein sinnvolles „Limit“ einbauen kann.

EDIT2: Was mich (als ich das „TODO“ geschrieben habe) davon abgehalten hatte, das gleich einzubauen: Es ist nicht so trivial. Um den Punkt x=0 kann man seeehr weit reinzoomen, so dass man z.B. nur noch das Intervall 0 - 0.000000000001 auf der x-Achse hat. Aber um den Punkt x=10000000000 geht das nicht - da kann man froh sein, wenn man das Intervall 10000000000 - 10000000000.1 noch aufgelöst bekommt. Tricky, tricky…

EDIT: Ach ja, die Method References… Obwohl die eigentlich total cool sind, gehen sie mir noch nicht so „flüssig“ von der Hand, und ich ertappe mich immer wieder, wie ich „x → foo(x)“ verwende, oder schlicht alte Gewohnheiten (wie das SwingUtilites.invokeLater) beibehalte…

Die ViewerFunctions sind nun Teil des https://github.com/javagl/Viewer - Projektes, das in Viewer - ein JPanel zum Zoomen, Verschieben und Rotieren diskutiert wird.