PySparkでコントロールブレイク処理

お題は次のエントリです。

gonsuke777.hatenablog.com

上記エントリではいわゆるコントロールブレイク処理(ソート済みのレコードを読み込み、キー項目ごとにグループ分けして行う処理のことでキーブレイク処理と呼ぶことも)を 1 本の SQL でスマートに行っています。これと同じことを PySpark でやってみるという話です。

次のような CSV ファイルを用意しておきます。

sales_date,jan_code,sales_cnt
2014/10/06,AAA,100
2014/10/07,AAA,200
2014/10/08,BBB,100
2014/10/09,BBB,150
2014/10/10,BBB,189
2014/10/11,CCC,120
2014/10/12,CCC,111
2014/10/13,AAA,210
2014/10/14,AAA,545
2014/10/15,AAA,90
2014/10/16,CCC,90

これを Spark DataFrame に読み込みます。

from pyspark.sql.types import StructType, StructField, StringType, IntegerType, DateType

schema = StructType([
  StructField('sales_date', DateType()),
  StructField('jan_code', StringType()),
  StructField('sales_cnt', IntegerType())
])

df = spark.read.csv('<path-to-csv>', schema=schema, header=True, dateFormat='yyyy/MM/dd')
df.show()
# +----------+--------+---------+
# |sales_date|jan_code|sales_cnt|
# +----------+--------+---------+
# |2014-10-06|     AAA|      100|
# |2014-10-07|     AAA|      200|
# |2014-10-08|     BBB|      100|
# |2014-10-09|     BBB|      150|
# |2014-10-10|     BBB|      189|
# |2014-10-11|     CCC|      120|
# |2014-10-12|     CCC|      111|
# |2014-10-13|     AAA|      210|
# |2014-10-14|     AAA|      545|
# |2014-10-15|     AAA|       90|
# |2014-10-16|     CCC|       90|
# +----------+--------+---------+

元の SQL では ROW_NUMBER ウィンドウ関数を使って単純ソートした場合の連続値と jan_code で区切りつつソートした場合の連続値を割り振っていますが、PySpark (Spark SQL) でも pyspark.sql.functions.row_number という同じ関数があります。

from pyspark.sql import Window
from pyspark.sql.functions import row_number

# SQLでの ROW_NUMBER() OVER(ORDER BY SALES_DATE) に相当
df = df.withColumn('simple_sq', row_number().over(Window.orderBy('sales_date')))

# SQLでの ROW_NUMBER() OVER(PARTITION BY JAN_CODE ORDER BY SALES_DATE) に相当
df = df.withColumn('part_jan_sq', row_number().over(Window.partitionBy('jan_code').orderBy('sales_date')))

パーティションとソート順は pyspark.sql.Window クラスのファクトリメソッドを使って生成する pyspark.sql.WindowSpec オブジェクトとして渡します。

あとは distance を計算すれば集約カラムが作られますね。

from pyspark.sql.functions import col

df = df.withColumn('distance', col('simple_sq') - col('part_jan_sq'))
df.orderBy('sales_date').show()

# +----------+--------+---------+---------+-----------+--------+
# |sales_date|jan_code|sales_cnt|simple_sq|part_jan_sq|distance|
# +----------+--------+---------+---------+-----------+--------+
# |2014-10-06|     AAA|      100|        1|          1|       0|
# |2014-10-07|     AAA|      200|        2|          2|       0|
# |2014-10-08|     BBB|      100|        3|          1|       2|
# |2014-10-09|     BBB|      150|        4|          2|       2|
# |2014-10-10|     BBB|      189|        5|          3|       2|
# |2014-10-11|     CCC|      120|        6|          1|       5|
# |2014-10-12|     CCC|      111|        7|          2|       5|
# |2014-10-13|     AAA|      210|        8|          3|       5|
# |2014-10-14|     AAA|      545|        9|          4|       5|
# |2014-10-15|     AAA|       90|       10|          5|       5|
# |2014-10-16|     CCC|       90|       11|          3|       8|
# +----------+--------+---------+---------+-----------+--------+

集約のためのキーができたので、集約を行っておしまい。

grouped_df = df.groupBy(['jan_code', 'distance']) \
  .agg(min('sales_date').alias('sales_date_first'), \
       max('sales_date').alias('sales_date_last'), \
       sum('sales_cnt').alias('cnt_sum'))
grouped_df.orderBy('sales_date_first').show()

# +--------+--------+----------------+---------------+-------+
# |jan_code|distance|sales_date_first|sales_date_last|cnt_sum|
# +--------+--------+----------------+---------------+-------+
# |     AAA|       0|      2014-10-06|     2014-10-07|    300|
# |     BBB|       2|      2014-10-08|     2014-10-10|    439|
# |     CCC|       5|      2014-10-11|     2014-10-12|    231|
# |     AAA|       5|      2014-10-13|     2014-10-15|    845|
# |     CCC|       8|      2014-10-16|     2014-10-16|     90|
# +--------+--------+----------------+---------------+-------+

PySparkでの時刻変換色々

最近はデータエンジニアリングのお仕事がメインで、もっぱら PySpark を触っています。 自分向けの備忘録的も兼ねてちょいちょい blog に tips を書いていきたいと思います。

今回は時刻変換に関するもの。

タイムゾーン付き日付文字列をパースしてtimestamp型に変換

基本は to_timestamp 関数を使います。

from pyspark.sql.functions import col, to_timestamp

df = spark.createDataFrame([('2021-05-16T23:03:49.220Z',)], ['str_datetime'])
df = df.withColumn('datetime', to_timestamp(col('str_datetime'), "yyyy-MM-dd'T'HH:mm:ss.SSSX"))

日時フォーマットのパターン文字列は Java方式 です。Spark は Scala で作られているので、Python でコードを書いていてもこういうところで Java が顔を出してきます。

似たものとしてUNIX時間に変換する unit_timestamp という関数がありますが、こちらはミリ秒以下が切り捨てられることに注意してください。

from pyspark.sql.functions import unix_timestamp
from pyspark.sql.types import TimestampType

df = df.withColumn('time', unix_timestamp(col('str_datetime'), "yyyy-MM-dd'T'HH:mm:ss.SSSX").cast(TimestampType()))

日付文字列をdate型に変換

to_date 関数を使います。

from pyspark.sql.functions import to_date

df = df.withColumn('date_col', to_date(col('str_datetime'), "yyyy-MM-dd'T'HH:mm:ss.SSSX"))

ゾーン情報を使ってローカル時刻にタイムスタンプをずらす

from_utc_timestamp 関数を使います。 現地時刻に変換した文字列を取得したい時やタイムゾーン情報のないデータベースに登録するときとかに使うかも。

from pyspark.sql.functions import from_utc_timestamp

df = df.withColumn('local_time', from_utc_timestamp(col('time'), 'Asia/Tokyo'))

date型、timestamp型のカラムを手動作成する

Pythondatetime.date 型で値を投入すると Spark SQLDateType になります。
同様に datetime.datetime 型で値を投入すると Spark SQLTimestampType になります。

import datetime

a_date = datetime.date(2022, 1, 1)
a_datetime = datetime.datetime(2022, 1, 1, hour=1, minute=10, second=10, microsecond=100000)
df = spark.createDataFrame([(a_date, a_datetime)], ('date_col', 'time_col'))

SoftBank回線でSIMフリースマートフォンに乗り換えたら大変だった話

前置き

これまでスマートフォンとして 3 年前に購入した Pixel 3 XL を使い続けていましたが、さすがにバッテリーがへたってきており、もうすぐサポート期間の終了を迎えることもあって別の機種に乗り換えることになりました。

もうすぐ登場する Pixel 6 を当初は考えていたのですが、非常に高価になるという話で、また端末サイズも今使っている Pixel 3 XL よりさらに大きくなるという話なので、別のものも探してみることにしました。

色々探してみたところ、ASUS からリリースされたばかりの Zenfone8 が良さそうということでこれを購入しました。

www.asus.com

購入判断の根拠は次のようなところです。

  • Qualcomm の最新でハイエンドチップである Snapdragon 888 を搭載していながら税込みで8万円を切るという安さ
    • 今使っている Pixel 3 が Snapdragon800番台だったので、800系以外はあり得ませんでした
    • iPhone 並のパフォーマンスが欲しかったら700番台、800番台は必須だと思います(特にゲームで大きな差が出る)
  • 端末サイズがコンパクトでありながらパンチホールカメラ+ナローベゼルで 5.9 インチの画面サイズを確保している
  • SIM フリーなのでキャリアの余計なアプリとかが入っていない
  • SIM フリー端末にしては珍しく FeliCa を搭載している
    • Suica に全面的に依存していたのでこれは必須

SoftBank回線でSIMフリースマートフォンを使う時の注意点

前置きが長くなりました。ということで家電量販店で上記端末を購入、そのまま家に持ち帰って家で Pixel に指していた SIM を指してセットアップ、その日は問題なく使え、新端末の快適なパフォーマンスを楽しんでいました。

が、次の日に異変に気付きます。外出時に使ってみるとネットワークに一切つながらないのです。幸い電話は使うことができたのですが、外出中は久しぶりに Wi-Fi 乞食になってしまいました。 *1

もしかしてやっぱり契約変更とかが必要なのかな?と思って家から近い SoftBank ショップで見てもらったのですが、「SIM はそのまま指して使えるはず」「SIM カードや設定には特に問題がない」「端末に問題がありそうだから購入したお店で端末を見てもらって」との回答で解決に至りませんでした…。

仕方がないので翌日端末を購入した家電量販店で見てもらうことにしたのですが、そこで実は SoftBank の SIM カードは大きく分けて次の 3 種類があることを教えてもらいました。 *2

  1. iPhone 用の SIM
  2. SoftBank から販売している Android 端末用の SIM
  3. SIM フリースマートフォン向けの SIM

自分が今まで使っていたのは 2 番の SIM カードだったので、3 番への交換が必要だったのでした。家電量販店にはキャリアショップもあったので、そちらに案内してもらい、様々な契約変更を行って、無事にネットワークにつながる SIM が手に入りました(応対した人は別店舗での不手際について申し訳ないとかなり恐縮してました)。

こんな感じでちゃんと使えるようになるまでに色々バタバタしてしまいました。この辺りのガイドラインは Web サイト等できちんと案内して欲しいところです。まあできればキャリアから販売している端末を使ってもらいたいからなんでしょうけど。同じようなことをしようとする人がもしいたら参考になるかと思い、今回の顛末をまとめることにしました。

お陰様で新端末ではこれまで使えなかった5G回線やVoLTEも使えるようになりました。

Zenfone 8について

最後におまけで Zenfone 8 の感想を。

  • さすが Snapdragon 888 だけあってパフォーマンスは速い、アズレンも爆速で動く
    • 最近のアズレン弾幕の描画が派手になったり、戦闘開始時のスキル発動が多く重なるようになったのでかなり負荷が大きく、Pixel 3だと力不足になる場面が時々ありました
    • ゲームやベンチマークツール等高負荷を要求するアプリが起動すると自動的にクロック数を上げる仕組みがあるようです
      • そのためか高負荷処理を行っているとかなり発熱します
      • ベンチマークツールでブースト機能を起動するのはちょっとずるくないかという気もしますがw
  • 端末スピーカーが非常に良い
    • Pixelもステレオスピーカーでしたが、音の広がりが全然違いました
  • スクリーンでの指紋認証は便利
  • 端末サイズがコンパクトなので片手操作が容易、それでいてスクリーンサイズが犠牲になっていない
    • 画面についてはリフレッシュレートが 120MHz で滑らかに動くことも特長ですが、自分の目ではあんまり分かりませんでしたw
  • Qi 充電が無いのはちょっと残念
  • デュアル SIM なのでもし海外に行く機会があったらそのまま現地の SIM を追加で刺して使えるので便利そう

*1:この時に気付いたのですが、オリンピックのお陰でか東京の地下鉄は車内 Wi-Fi が通るようになっていたのですね

*2:こんな面倒なことしているのSoftBankくらいらしいですが…

IntelliJ IDEA 2020.2でJavaFXのランタイムが同梱されなくなりました

IntelliJ IDEA の最新版 2020.2 がリリースされましたね。日本語でも新機能について案内する記事がアップされています。

www.jetbrains.com

この記事の最後のように次のような個人的に気になる記述がありました。

  • パフォーマンスと表示の問題を回避するため、新たに JCEF との統合機能を提供して IntelliJ Platform のプラグインを実行できるようにしました。

まず多くの人は「JCEFって何?」ってなると思います。これはアプリケーションに Chromium を組み込めるようにするためのフレームワーク CEF (Chromium Embedded Framework) の Java ラッパーのことです。

IntelliJIDE 内部で使う Web ブラウザコンポーネント (Markdown エディタの HTML プレビューなどで使っています) として今後は JCEF を使うようになったということです。これまでは IDE 内部で使う Web ブラウザコンポーネントとしては JavaFX の WebView を使っていましたが、2020.2 でこれをやめることになります。本件について JetBrains の Blog 記事で説明がありました。

blog.jetbrains.com

ご存じの通り IntelliJ は Swing をベースに作られています。従って JavaFX の WebView は JFXPanel を通して使うことになりますが、そのために性能やレンダリングの問題を解決できなかったとのことです。

実際、JavaFX と Swing はレンダリングスレッドもタイミングも異なり、Swing アプリの上で JavaFX ノードを描画するときは JavaFXレンダリングエンジンである Prism エンジンではなく Java2D を使って描画するため、JavaFX のパフォーマンスを 100% 発揮することができないのも確かです。この不整合を解決しようとする計画も過去にはありましたが、 Oracle の JavaFX へのやる気が無くなった ので...。

というわけで上記の記事では IntelliJプラグイン開発者に対して今後は JavaFX への依存をやめ、Web ブラウザコンポーネントを使いたいときは JCEF を使うことを促しています。

ですが、JCEF は現在絶賛開発中のステータスで、ユーザーとして利用できるような段階にありません。ビルド方法のガイドはありますが利用方法のガイドはなく、API ドキュメントもありません。ラップ対象の CEF については既に利用可能なステータスにあり (C++ のライブラリです) 、ある程度ドキュメントもあるので、こちらを参照して利用方法を推測することになります。

さすがにこの状態で使うのは辛いので、JetBrains 側でラップした APIプラグイン開発者向けに用意したようです。以下のドキュメントで解説されています。

jetbrains.org

というわけで IntelliJ IDEA 2020.2 からは JavaFX のランタイムは同梱されなくなります。この点について個人的に1点気になることがありました。

実は IntelliJJava IDE の中で唯一 JavaFX Scene Builder を内部に組み込んでいた IDE でした。JavaFX のランタイムが同梱されなくなる 2020.2 ではどうなってしまうのでしょうか?

というわけで早速 FXML ファイルを開いてみました。すると...

f:id:aoe-tk:20200729200101p:plain

「Scene Builder Kit をダウンロードしてくれ」というリンクが表示されました。このリンクをクリックすると次に JavaFX Runtime のダウンロードが求められ、それも行うと次のように Scene Builder が表示されました。

f:id:aoe-tk:20200729200142p:plain

というわけで別途ランタイムをダウンロードする必要があるものの、Scene Builder が使えなくなったわけではないのでご安心ください。

以上、誰も気付かないであろう IDEA 2020.2 の変更点についてのお話でした。

横須賀軍港めぐりに乗ってきました

これまで横須賀には 2 回ほど遊びに行ったことがありましたが、今回初めて横須賀軍港めぐりのフェリーに乗ってきました。

www.tryangle-web.com

文字通り横須賀の軍港をぐるっと一周するクルージングで、ヴェルニー公園からは見えないところ (特に米軍基地のところ) を見ることができます。

写真を色々撮ったのでここにまとめてアップしました。

最初に見えるのは米軍の場所に停泊させてもらっている海自のおやしお型潜水艦。これはヴェルニー公園からも見えますね。 f:id:aoe-tk:20190428130333j:plain

添乗員からの説明が無かったのですが、米軍側のところに停泊していた古びた船が目に付きました。これは宿泊艦だそうで、横須賀の地元の人にはお馴染みの風景だとか。 f:id:aoe-tk:20190428130537j:plain

ご存じ、海自のヘリ空母いずも。やっぱりでかかった。 f:id:aoe-tk:20190428211959p:plain

こちらは米海軍が利用しているところ。アーレイ・バーク駆逐艦達がずらっと並んでいます。 f:id:aoe-tk:20190428212246p:plain

空母ロナルド・レーガンも停泊していました。そしてその手前を隠すかのようにタイコンデロガ級巡洋艦達がずらっと並んでいました。添乗員の説明によると米空母は機密の塊なので余り近くには寄らせてくれないそうです。 f:id:aoe-tk:20190428130927j:plain

空母ロナルド・レーガン。やや遠目ですが、それでもでかい! f:id:aoe-tk:20190428131032j:plain

何やら四角いブロックが並んでいたのですが、これは船の消磁のためのものだそうです。日本では横須賀にだけあるのだとか。遠目には大きなコンテナ船が見えますね。 f:id:aoe-tk:20190428131357j:plain

掃海母艦うらが。この船も大きかったです。 f:id:aoe-tk:20190428132257j:plain

最近就役した潜水救難艦のちよだです。確か墜落した F-35 の捜索に加わっていたはずですがもう帰ってきてたんですね。 f:id:aoe-tk:20190428132607j:plain

手前は海洋観測艦のしょうなんです。そして奥にいる艦番号が消された船は退役した旧ちよだです。まさか新旧ちよだを同時に見られるとは思わなかったのでちょっと感激しました。 *1 f:id:aoe-tk:20190428213425p:plain

ずらっと並んだ汎用護衛艦達。左からいかづち、あまぎり、ゆうぎりです。むらさめ型のいかづちが一回り大きいのが分かりますね。 f:id:aoe-tk:20190428132848j:plain

新鋭のそうりゅう型潜水艦も見ることができました。舵がX型になっているのが特徴的です。 f:id:aoe-tk:20190428132400j:plain

こんなのも見られました。日産の工場と、そこで生産された自動車を運ぶためのコンテナです。 f:id:aoe-tk:20190428214224p:plain

手前からてるづき、むらさめ、たかなみ、補給艦のときわ、そしてイージス艦のきりしまです。 f:id:aoe-tk:20190428133606j:plain

きりしまは錨に塗装をしている珍しい場面に遭遇しました。 f:id:aoe-tk:20190428214845p:plain

こんな感じで陸側からは見ることができない様々な風景を見ることができました。軍艦好きなにはお勧めのクルージングなので、横須賀に寄った際にはぜひ。

*1:細かいところですが、旧ちよだには喫水線部に黒帯の水線塗装がされていましたが、新しいちよだにはありませんでした。なんでだろう?

とにかく簡単にEmacsのフォントを設定する方法

最近仕事場のマシンを新しくし、Mac をやめて Windows にしました。でもエディタはどの OS でも Emacs を使う人間なので、Windows にも Emacs をインストールし、その際に久々に Emacs の設定を色々いじったのですが、フォントについてもプログラミング向け等幅フォントを使うように設定したりしました。

Emacs の設定で良く鬼門とされるのがフォント設定です。Emacs文字集合別にきめ細かくフォントを設定できるようになっているので、よく知らない人にはすごく難しいように思われています。でも、実は最近の Emacs ではとりあえず Ascii と日本語の文字集合に対してサクッとフォントを設定すればいいのならば実に簡単に設定できるようになっています。Emacs のフォント設定をぐぐっても小難しい方法ばかり出てきて、一番簡単な方法にたどり着かない人が多いのではないかと思い、メモっておくことにしました。

メニューから [Options] - [Set Default Font...] を選択します。 f:id:aoe-tk:20190224235803p:plain

すると、次のようなフォント選択ダイアログが出てきます。ここで使いたいフォントを選択します。日本語と Ascii 文字で同じフォントが使われるようにするため、[欧文] に対して選択します。この例ではプログラミング向けフォントとして割と定番の Takaoゴシック を選んでいます。 f:id:aoe-tk:20190224235819p:plain

選択すると Emacs のフォント設定が変わります。ここで再びメニューから [Options] - [Save Options] をクリックして設定を保存します。 f:id:aoe-tk:20190224235836p:plain

以上です。

え、 .emacs ファイルに S 式をごちゃごちゃ書くんじゃないの? と思われた方がいるかもしれません。実は Emacs はこのように GUI で結構設定変更ができるように進化しているのです。「Emacs って小難しい設定ファイルをいじくり回さないといけない古くさいエディタなんでしょ?」なんてと思っている方は認識を改めてください!

とは言え、この設定結果は結局 .emacs に書き込まれます。non ascii な文字が入っているフォントファミリーを選ぶ場合、.emacs には予めマジックコメントを入れてファイルエンコーディングを明記しておいた方がいいかもしれません (やっぱり .emacs 覗かないといけないじゃねーか、って突っ込まれそうですがw) 。

;;; -*- coding: utf-8 -*-

なお、「自分はあくまで .emacs をいじる派だ!」という方は次のように default-frame-alist 変数にフォントファミリーと文字の大きさをハイフンでつなげて設定すれば OK です。ざっくり設定ならばこれで十分です。

;; Initial frame settings
(setq default-frame-alist
      (append (list
              '(font . "Takaoゴシック-10"))
              default-frame-alist))

以上、Emacs のフォント設定を行う一番簡単な方法についてでした。

JavaFX 11に追加されたRobotクラスの紹介

このエントリは じゃばえふえっくす Advent Calendar 2018 の 3 日目のエントリとしてぶっ込みました。もうクリスマスは終わってますが、まだ空いていますし、 1 日目のエントリ で次のようなことを書いていたのをまだ放置していたので、それを拾うエントリとなります。

上記の Maven リポジトリをご覧になると分かりますが、Java FX 11 もリリースされました。リリースノートは次の通りです。

https://github.com/javafxports/openjdk-jfx/blob/jfx-11/doc-files/release-notes-11.md

まあ新機能は少ないのですが、 Spinner コントロールに改善が加えられていたり (詳しくは Yucchi_jp さんのこのブログエントリ を) 、新たに Robot というクラスが追加されています。この Robot クラスについては別のエントリで触れる予定です。

というわけで、JavaFX 11 になって新たに追加された Robot クラスについて触れたいと思います。

Robot クラスはプログラムから画面操作を行うために必要なメソッドが実装されたクラスです。あたかもロボットが画面操作を人の代わりに行うようなことを実現するというわけです。 AWT にも同様のクラス があります。一番のユースケースGUI の自動テストを実装することで、それを実現するために次のような操作を実行するためのメソッドが実装されています。

  • マウスの各種操作 (移動、クリック、スクロールホイール) を行う
    • マウスの現在位置を取得するためのメソッドもあります
  • キーボードをタイプする
    • タイプとは別にプレスとリリースにもメソッドが用意されています
  • 画面のスクリーンショットを撮る
  • 指定した座標の色を取得する

モジュールは javafx.graphics に、パッケージは javafx.scene.robot に入っています。

画面を操作するのに必要なものは一通りそろっているので、これで定形作業を肩代わりするプログラムを作るのに使えますね。ゲームの周回操作とかにもいいかも。

実はこのクラス、昔からあったのですが internal なクラスとなっていました。このクラスを使って自動テストフレームワークを作っていたライブラリが多く、Java のモジュール化に伴い「外に出してほしい」というリクエストが非常に多かったのですが、11 になって晴れて表に出てきてくれました。

ところで以前、自分の書いた次のエントリで、JavaFX で xeyes のクローンを作った話をしました。

aoe-tk.hatenablog.com

そこでこんなことを書いていました。

  • 以前は Mac 上で Swing の EDT を実行すると HeadlessException が飛ぶ問題があり、仕方なく Swing を土台として作っていたのですが、現在はこの問題は解消されているので、JavaFX を土台としたものに変更しています。
    • 唯一、グローバルなマウスカーソルのアドレスを取得するのに AWT に頼っていますが、JavaFX 11 で待望の Robot クラスが追加されるので、11 になれば "Pure JavaFX" なアプリケーションにできる見込みです。

てなわけで、早速この Robot を使って "Pure JavaFX" なアプリにしちゃいましょう。

まずは 10 以前のマウスポジションを取得するコード。

private void updateMousePosition() {
    SwingUtilities.invokeLater(() -> {
        final Point pointerLocation = MouseInfo.getPointerInfo().getLocation();
        Platform.runLater(() -> updateEye(pointerLocation.x, pointerLocation.y));
    });
}

Swing の EDT と JavaFX のアプリケーションスレッドを行ったり来たりして見通しが悪いですね。これを 11 の Robot クラスを使うように変更します。使い方は簡単で、普通にインスタンスを取得すればいいです。Controller の初期化処理で取得しておきます。

private Robot robot;

@Override
public void initialize(URL location, ResourceBundle resources) {
    robot = new Robot();
    // (以下略)

後はこのインスタンスを使ってマウスカーソルのグローバルポジションを取得するだけです。 updateMousePosition() メソッドの内容が次のように変わりました。

private void updateMousePosition() {
    updateEye(robot.getMouseX(), robot.getMouseY());
}

はい、これで複数スレッドを行き来する必要がなくなりました。しかも java.desktop モジュールへの依存も無くなるので、配布 JRE のイメージサイズも小さくすることができますね。 *1

少し付け加えると、 Robot から取得するマウス座標の値は AWT と違って double 型になります。なので updateEye() メソッドの引数の型を変える必要がありました。

というわけで JavaFX 11 の新機能である Robot クラスの紹介でした。さすがに今年の Advent Calendar への投稿はこれで終わりかな。皆様良いお年を。

*1:AWT、Java2D、Swing を全部雑に java.desktop モジュールにぶち込んでるんですよね...。お陰でサーバーサイドアプリケーションでも JavaBeans 関連の API を使うだけなのにこのモジュールへの依存が必要がだったり。