Swingベースのシンプルなスケッチアプリ

概要

JavaFXjdkに同梱されなくなったため,今後,JVM上で動作するGUIアプリの開発は,Swingベースで行うことにしようと思います. そこで,シンプルなスケッチアプリを題材として,SwingベースのGUIアプリのテンプレートを作成しました. 本記事では,まずそのソースコードと動作例を示した上で,以下の点に焦点を当てて解説します.

  • ウィンドウの表示
  • イベントリスナの登録
  • 折れ線の描画

ソースコード

Kotlin

Java

動作例

f:id:Jumpaku:20200526184148p:plain
動作例

解説

プログラムを起動すると以下の流れで処理が実行されます.

  1. mainが実行される
  2. SwingUtilities.invokeLaterが実行される
  3. SwingSketchApprunが実行される

ウィンドウの表示

JFrameはウィンドウを表すクラスです. // ウィンドウの表示の部分ではJFrameインスタンスを生成してウィンドウを表示しています. 具体的には以下の項目を設定した後,setVisibletrueを渡すことでウィンドウが表示されます.

  • ウィンドウのタイトル:JFrameのコンストラクタに渡した文字列はウィンドウのタイトルとなります.
  • 閉じるボタンを押した時の動作:setDefaultCloseOperationJFrame.EXIT_ON_CLOSEを渡すとウィンドウの閉じるボタンを押した時にプログラムを終了します.
  • ウィンドウが持つコンテンツ:addはウィンドウにコンテンツを追加します.
  • ウィンドウのサイズ:packを呼び出すとウィンドウのサイズをコンテンツに応じて調整します.

イベントリスナの登録

アプリケーションにユーザの入力に応じた処理を実行させるためにはイベントリスナを登録する必要があります. イベントとはプログラムの状態の変化をトリガーとして生成されるインスタンスです. 例えば,ユーザがマウスのボタンを押すことでプログラム内のマウス押下状態が変化した時やユーザがマウスドラッグすることでプログラム内のマウス位置が変化した時にはMouseEventインスタンスが生成されます. イベントリスナとはイベントが生成された時にそのイベントを引数にして実行されるメソッドまたはそのようなメソッドを持つクラスです.

// イベントリスナの登録の部分ではイベントリスナとしてMouseInputListenerインスタンスlistenerを生成し,これをpanelに登録しています. これにより,panelの上でユーザがマウスのボタンを押した時やマウスドラッグした時に,そのイベントに応じたlistenerのメソッドが実行されるようになります.

折れ線の描画

JPanelはコンテンツを追加できる汎用的なパネルを表すクラスです. JPanelpaintをオーバーライドすることで図形を描画することができます. ここではシンプルなスケッチを実現するためにユーザのドラッグにより入力された点列を折れ線として描画します. 具体的には// 折れ線の描画の部分で示すように,paintに引数として渡されるGraphics2Dインスタンスに対して,Point2Dのリストとして表された点列をLine2Dとして表される線分で繋いでいます.

Graphics2Dの使い方は以下の記事が参考になると思います. jumpaku.hatenablog.com

Jumpaku Dinner

Jumpaku Dinnerのデモンストレーション画像
Jumpaku Dinnerのデモンストレーション
https://dinner.jumpaku.net

はじめに

夜ご飯決定アプリ「Jumpaku Dinner」を開発・公開し,その成果をLT大会で発表しました. 以下では,

  • Jumpaku Dinnerとは何か
  • どのように使えば良いか
  • どのように動作しているか
  • なぜ制作したか
  • 感想
  • 質疑応答

などを述べます.

Jumpaku Dinnerとは何か

Jumpaku Dinnerは,ある日の夜ご飯として何を作るかを決定するためのWebアプリケーションです. ページ(https://dinner.jumpaku.net)を開くと,ランダムに一つの料理が選択され,その料理の料理名,写真,材料,一言コメントが表示されます.

どのように使えば良いか

「今日の夜ご飯何食べたい?」「なんでも良い」「なんでも良いは困る!」

そんな会話に覚えはありませんか? 夜ご飯で作るものを決められない親,食べられればなんでも良い子供,そういった方がこのアプリに従うことで,喧嘩することなく夜ご飯を決定することができます. 他にも自炊したいけど,何を作ったら良いか分からない一人暮らしの大学生も同様です.

使い方は以下の通りで非常に簡単です.

  1. 今日の夜ご飯はJumpaku Dinnerに従うと心に決める.
  2. Jumpaku Dinnerのページを開く.
  3. Jumpaku Dinnerに提示された夜ご飯を作って食べる.

どのように動作しているか

https://dinner.jumpaku.netで公開しているJumpaku Dinnerの概要を以下に示します.

Jumpaku Dinnerの概要を表す図
Jumpaku Dinnerの概要
Jumpaku Dinnerのフロントエンドは,TypeScriptとReactで記述され,webpackによりビルドされます. Jumpaku Dinnerのバックエンドではhttpdが動作しており,ビルドされたリソースをサーブしています. これらをまとめたDockerイメージはDocker Hubで公開しています.

なぜ制作したか

2020年5月頃より,私は自炊した料理の写真をTwitter(Jumpaku@Jumpaku)にアップロードするようになりました. そして写真は2020年9月頃には50枚程度まで増えていましたが,一方で,作りたい料理は無くなっていきました.

また,その頃,プログラミングに関する活動として競技プログラミングAtCoder)やVPSの管理などしかしておらず,久しくアプリ開発をしていませんでした. そのため,私の開発欲は高まっていました.

そんな中で,私のフォロワのツイートによりあるイベント(【学生限定】秋のLT大会 2020 Online)が開催されること知りました. cist-lt.connpass.com そしてこのLT大会の開催日は,私がそれを知った日の翌日でした.

増えた写真,減りゆく自炊,高まる開発欲,迫る締め切り,VPSの活用,そして発表機会. その時,すべての点が線でつながりました.

「明日までに一人ハッカソンで夜ご飯決定アプリ「Jumpaku Dinner」を制作してVPSで公開し,LT大会で発表しよう!」

そう思い至った私はすぐに参加登録をし,環境構築から開発をスタートしたのでした.

以上の経緯により,Jumpaku Dinnerを制作しました.

感想

  • 自分で撮った写真を活用したアプリを制作できて良かった.
  • 自分にとって役に立つアプリを制作できて良かった.
  • VPSで動作させるサービスが増えて良かった.
  • 締め切りに追われながらの一人ハッカソンは,スリリングで楽しかった.
  • 完成した成果を発表でき,さらにTwitter等で反応を頂けて嬉しかった.

質疑応答

LT大会で発表した際に,「短期間で完成させるためにはどうしたら良いか?」という質問をいただきました. これに対し,「短期間で完成させられる程度まで要求を低くしていけば良い.」と答えました. このことは,開発時に意識してやっていたわけではないのですが,質問されたことにより,何かを完成させるために大事なことの一つとして自覚することができました. こういったことも含めて発表して良かったと思います.

まとめと展望

  • 夜ご飯決定アプリ「Jumpaku Dinner」を制作しました.
  • 自分のVPSでJumpaku Dinnerを公開しました.
  • 開発成果をLT大会で発表しました.

今後の展望として料理に関するデータをデータベースで管理するようにしたいと考えています.

リンク

C++におけるabs関数のオーバーロードについて調べた

目的

https://ja.cppreference.com/w/cpp/header によると,cmathcstdlibなどのC互換ヘッダはstd名前空間で宣言した関数をグローバル名前空間でも宣言するかもしれない. 実際,以下のソースコードコンパイルしてみると,グローバル名前空間abs関数とstd名前空間abs関数の両方を参照できることが分かる.

#include <cmath>
#include <cstdlib>
#include <iostream>

using ll = long long int;

int main() {
  ll x = 1e9;
  std::cout << ::abs(x * x) << std::endl;
  std::cout << std::abs(x * x) << std::endl;
}
g++ -std=c++17 -O2 -Wall -Wextra -Wno-comment  -o main main.cpp

しかし,以下のように生成された実行ファイルを実行してみると,グローバル名前空間abs関数とstd名前空間abs関数が異なる結果を出力していることが分かる.

  • 実行コマンド
./main
  • 実行結果
1486618624
1000000000000000000

このプログラムは(109) * (109)の絶対値を求めようするものであるが,グローバル名前空間abs関数を使うと正しい値を求められない.

本記事では,グローバル名前空間abs関数とstd名前空間abs関数がどのように解決されるかを確かめ,この問題の対策を考える.

検証

https://cpprefjp.github.io/reference/cstdlib.html によると,cstdlibint型の絶対値を求めるint abs(int)を宣言している. また,https://cpprefjp.github.io/reference/cmath/abs.html によると,cmathint abs(int)に加えて,long long int型の絶対値を求めるlong long int abs(long long int)を宣言している.

そこで,グローバル名前空間abs関数とstd名前空間abs関数に対して,上のプログラムのようにlong long int型の引数を渡した時,これらの関数がそれぞれint abs(int)long long int abs(long long int)のどちらに解決されるのかを確かめるために,以下のソースコードコンパイルしてみた.

ソースコード

#include <cmath>
#include <cstdio>
#include <cstdlib>

#include <type_traits>
using std::is_same_v, std::declval;
using ll = long long int;

int main() {
  static_assert(is_same_v<decltype(::abs(declval<ll>())), int>);
  static_assert(is_same_v<decltype(std::abs(declval<ll>())), ll>);
}

コンパイル

検証は以下の環境で行った.

以下のコマンドによりコンパイルを行い,実行ファイルmainを得た.

g++ -std=c++17 -O2 -Wall -Wextra -Wno-comment  -o main main.cpp

結果

上のソースコードコンパイルに成功したことから,以下のことが分かる.

  • グローバル名前空間abs関数にlong long int型の引数を渡すとint abs(int)に解決される.
  • std名前空間abs関数にlong long int型の引数を渡すとlong long int abs(long long int)に解決される.

考察

検証結果から最初に示したプログラムで,グローバル名前空間abs関数を使った場合に正しい値を求められなかった原因は次のようなものだと考えられる. グローバル名前空間abslong long int型の引数を渡すと,absint abs(int)に解決されるため,引数がint型に変換される. この時,(109) * (109) == 1018はint型で表現するには大きすぎるためオーバーフローが発生し,正しくない値がabsに渡ってしまう.

対策

グローバル名前空間int abs(int)関数にlong long int型の引数を渡してしまう誤りはコンパイルエラーとして検出できない. また,long long int型の値の絶対値を求めるときはllabsを使うように意識するという方法や,abs関数の前に常に「std::」を付けるように意識するという方法は,ミスの無い完璧な人間は存在しないということを考えれば,対策として十分とは言えない. したがって,一般的な対策はとても難しいと考えられる.

しかし,対象をプログラミングコンテストなどに限れば以下のような対策が実施可能である. 予めテンプレートとなるソースコードを用意しておくことが可能なAtCoderhttps://atcoder.jp)のようなプログラミングコンテストでは,そのテンプレートに次の記述を追加しておくことで,グローバル名前空間int abs(int)関数にlong long int型の引数を渡してしまう誤りをなくすことができる.

#include<cmath>
using std::abs;

このusing宣言により,グローバル名前空間std名前空間abs関数が導入されるため,long long int型をグローバル名前空間abs関数の引数にしようとした場合でもlong long int abs(long long int)が参照されるようになる. 実際,最初のソースコードにこれを追加して実行すると正しい結果が得られる.

#include <cmath>
#include <cstdlib>
#include <iostream>

using ll = long long int;

using std::abs;

int main() {
  ll x = 1e9;
  std::cout << ::abs(x * x) << std::endl;
  std::cout << std::abs(x * x) << std::endl;
}
  • 実行結果
1000000000000000000
1000000000000000000

JavaFXのウィンドウやSVG画像への図の描画

本記事はMuroran Institute of Technology Advent Calendar 2019 12/14の記事です.

adventar.org

概要

本記事では

  1. JavaFXウィンドウへの図の描画,
  2. SVG画像への図の描画,
  3. JavaFXウィンドウとSVG画像への同じ図の描画

について具体的なGradleプロジェクトの例を用いて説明します.

JavaFXJavaGUIライブラリの一つです. JavaFXはJava8で標準ライブラリに追加されてJDKの一部となりましたが,Java11からはJDKに同梱されなくなり,代わりにOpenJFXを利用することができるようになりました. 1.では,JavaFXCanvasクラスに図の描画を行うためにorg.jfreeのFXGraphics2DというライブラリのFXGraphics2Dクラスを利用します. FXGraphics2Dクラスは標準ライブラリのGraphics2Dクラスを継承しているため,Graphics2Dクラスと同じように扱うことができます.

SVG画像はベクタ画像(品質の劣化なくサイズを変更可能な形式の画像)の一つです. SVG画像の形式はXML形式で,仕様はW3Cによって勧告されています. 2.では,SVG画像に図の描画を行うためにorg.jfreeのJFreeSVGというライブラリのSVGGraphics2Dクラスを利用します. SVGGraphics2Dクラスも標準ライブラリのGraphics2Dクラスを継承しており,Graphics2Dクラスと同じように扱うことができます.

3.では,FXGraphics2DクラスとSVGGraphics2DクラスがGraphics2Dクラスを継承していることを生かたポリモーフィックなプログラムによって,JavaFXウィンドウとSVG画像への同じ図の描画を行います.

1.,2.,3.はOpenJFX,FXGraphics2D,JFreeSVGといった標準のJDKに含まれないライブラリを利用しますが,これらのライブラリの管理を含めたプロジェクトのビルドを行うために,本記事ではGradleというビルドツールを使用します. Gradleはbuild.gradleという設定ファイルに従って,コンパイル,ライブラリの用意,実行といった操作を自動的に行うソフトウェアです.

以下では,まず,本記事の実行環境について説明し,Graphics2Dクラスの基本的な使い方を示します. その後1.,2.,3.についてそれぞれ,Gradleプロジェクトの例を示します. 本記事ではJavaの環境としてJava13を利用しました.

実行環境

本記事のプログラムは,次のようにGitHubリポジトリからクローンして実行することができます.

git clone https://github.com/Jumpaku/AdventCalendar_2019-12-14.git
cd AdventCalendar_2019-12-14

# JavaFXウィンドウへの図の描画
cd example1
./gradlew run
# ./gradlew.bat run (Windowsの場合) 

# SVG画像への図の描画
cd ../example2
./gradlew run
# ./gradlew.bat run (Windowsの場合) 

# JavaFXウィンドウとSVG画像への同じ図の描画
cd ../example3
./gradlew run
# ./gradlew.bat run (Windowsの場合) 

ただし,Java13とGitがインストールされていることが前提となります.

このリポジトリは以下のように3つのプロジェクトexample1,example2,example3で構成されています. f:id:Jumpaku:20191213230441p:plain 例えば,example1ディレクトリで./gradlew runを実行すると,Gradleがexample1/build.gradleに書かれたプロジェクトの設定に従ってプロジェクトをビルドし,起動します. プログラムが起動されるとexample1/src/main/java/example1/Main.javamainメソッドが呼ばれます. このmainメソッドはFXGraphics2Dクラスを利用します. FXGraphics2Dクラスはorg.jfreeのFXGraphics2Dというライブラリに含まれています. 必要となるライブラリをbuild.gradleに書いておくと,ビルドする時にGradleによってダウンロードされてライブラリを利用できるようになります.

Graphics2Dの基本的な使い方

Graphics2Dの基本的な使い方として

  • 基本図形のストローク描画とフィル描画
  • パス操作
  • 領域操作
  • アフィン変換

を示します.

基本図形のストローク描画とフィル描画

    public static void exampleDrawAndFill(Graphics2D g) {
        // 楕円を表現するEllipse2Dオブジェクトを生成する
        // 基本図形は他にLine2D(線分),Rectangle2D(長方形),Arc2D(弧)などもある
        Ellipse2D ellipse = new Ellipse2D.Double(0.0, 25.0, 100.0, 50.0);
        // Ellipseオブジェクトの内部の塗りつぶし色を設定する
        g.setColor(new Color(255, 75, 0));
        // Ellipseオブジェクトの内部を塗りつぶす
        g.fill(ellipse);
        // Ellipseオブジェクトの輪郭線の色を黒に設定する
        g.setColor(Color.BLACK);
        // Ellipseオブジェクトの輪郭線を太さ1の線に設定する
        g.setStroke(new BasicStroke(1f));
        g.draw(ellipse);
    }

パス操作

    public static void examplePath(Graphics2D g) {
        // 複雑なパス図形を表現できるPath2Dオブジェクトを生成する
        Path2D curve = new Path2D.Double();
        // Path2Dオブジェクトの開始点を設定する
        curve.moveTo(125.0, 25.0);
        // Path2Dオブジェクトの開始点に,3次Bezier曲線を接続する
        curve.curveTo(200.0, 25.0, 100.0, 75.0, 175.0, 75.0);
        // Path2Dオブジェクトの輪郭線を描画する
        g.setColor(new Color(3, 175, 122));
        g.setStroke(new BasicStroke(1f));
        g.draw(curve);
        // Path2Dオブジェクトを生成する
        Path2D polyline = new Path2D.Double();
        // Path2Dオブジェクトの開始点を設定する
        polyline.moveTo(125.0, 25.0);
        // Path2Dオブジェクトの開始点に,線分を接続する
        polyline.lineTo(200.0, 25.0);
        // Path2Dオブジェクトに,さらに線分を接続する
        polyline.lineTo(100.0, 75.0);
        // Path2Dオブジェクトに,さらに線分を接続する
        polyline.lineTo(175.0, 75.0);
        // Path2Dオブジェクトの輪郭線を黒い太さ1線に設定し,描画する
        // BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUNDは線分の端点や接続部分を丸くするための設定
        g.setColor(Color.BLACK);
        g.setStroke(new BasicStroke(1f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
        g.draw(polyline);
    }

領域操作

    public static void exampleArea(Graphics2D g) {
        // 3個のEllipse2Dオブジェクトを生成する
        Ellipse2D ellipse0 = new Ellipse2D.Double(0.0, 100.0, 100.0, 50.0);
        Ellipse2D ellipse1 = new Ellipse2D.Double(0.0, 100.0, 50.0, 100.0);
        Ellipse2D ellipse2 = new Ellipse2D.Double(0.0, 100.0, 50.0, 50.0);
        // 3個のEllipse2Dオブジェクトの和集合領域を表現するAreaオブジェクトを生成する
        Area union = new Area(ellipse0);
        union.add(new Area(ellipse1));
        union.add(new Area(ellipse2));
        // 3個のEllipse2Dオブジェクトの共通集合領域を表現するAreaオブジェクトを生成する
        Area intersection = new Area(ellipse0);
        intersection.intersect(new Area(ellipse1));
        intersection.intersect(new Area(ellipse2));
        // 和集合領域と共通集合領域を塗りつぶす
        g.setColor(new Color(77, 196, 255));
        g.fill(union);
        g.setColor(new Color(0, 90, 255));
        g.fill(intersection);
        // 和集合領域と共通集合領域の輪郭線を描画する
        g.setColor(Color.BLACK);
        g.setStroke(new BasicStroke(1f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
        g.draw(union);
        g.draw(intersection);
    }

アフィン変換

    public static void exampleTransform(Graphics2D g) {
        // 弧を表現するArc2Dオブジェクトを生成する
        Arc2D arc = new Arc2D.Double(-1.0, -1.0, 2.0, 2.0, -45.0, 270.0, Arc2D.PIE);
        // アフィン変換を表現するAffineTransformオブジェクトを生成する
        AffineTransform t = new AffineTransform();
        t.translate(150.0, 150.0); // 平行移動
        t.rotate(Math.PI / 2); // 回転
        t.scale(50.0, 50.0); // 拡大
        // Arc2Dオブジェクトにアフィン変換した図形オブジェクトを生成する
        Shape transformed = t.createTransformedShape(arc);
        // アフィン変換した図形オブジェクトを塗りつぶす
        g.setColor(new Color(255, 241, 0));
        g.fill(transformed);
        // アフィン変換した図形オブジェクトの輪郭線を描く
        g.setColor(Color.BLACK);
        g.setStroke(new BasicStroke(1f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
        g.draw(transformed);
    }

JavaFXウィンドウへの図の描画

JavaFXウィンドウへの図の描画を行うプロジェクトの設定ファイル,メインクラス,実行結果を以下に示します.

build.gradle

plugins {
    id 'java'
    id 'application'
    // JavaFXを利用するためにOpenJFXのプラグインを追加する
    id 'org.openjfx.javafxplugin' version '0.0.8'
}
group 'jumpaku'
version 'SNAPSHOT'
// 使用するライブラリのダウンロード元としてMavenセントラルリポジトリを指定する
repositories {
    mavenCentral()
}
// JavaFXに関する設定をする
javafx {
    // 使用するJavaFXのバージョンを指定する
    version = "13.0.1"
    // JavaFXで使用するモジュールを指定する(モジュールはJava9の機能)
    modules = ['javafx.controls', 'javafx.swing']
}
// example2.Mainクラスをメインクラスとして指定する
mainClassName = 'example1.Main'
// FXGraphics2Dライブラリを追加する
dependencies {
    implementation group: 'org.jfree', name: 'fxgraphics2d', version: '1.8'
}

Main.java

package example1;
/*import...*/
public class Main {
    /**
     * JavaFXウィンドウに図を描画する
     * さらに描画した図をpng画像としてファイルに書き出す
     * @param args
     */
    public static void main(String[] args) {
        // Appクラスを指定してアプリケーションを起動する
        Application.launch(App.class, args);
    }
    public static class App extends Application {
        /**
         * アプリケーション起動時に呼び出される
         * @param primaryStage
         */
        @Override
        public void start(Stage primaryStage) throws IOException {
            // Canvasオブジェクトを用意する
            int width = 320;
            int height = 240;
            Canvas canvas = new Canvas(width, height);
            // CanvasオブジェクトのGraphicsContext2DオブジェクトをFXGraphics2Dクラスのコンストラクタに渡す
            GraphicsContext ctx = canvas.getGraphicsContext2D();
            Graphics2D g = new FXGraphics2D(ctx);
            // Graphics2Dクラスを通してCanvasオブジェクトに図を描画する
            exampleDrawAndFill(g);
            examplePath(g);
            exampleArea(g);
            exampleTransform(g);
            // Canvasオブジェクトをシーングラフに追加し,ウィンドウを表示する
            primaryStage.setScene(new Scene(new Pane(canvas)));
            primaryStage.show();
            // CanvasオブジェクトのPNG画像を書き出す
            SnapshotParameters params = new SnapshotParameters();
            WritableImage img = canvas.snapshot(params, new WritableImage(width, height));
            File file = new File("example.png");
            ImageIO.write(SwingFXUtils.fromFXImage(img, null), "png", file);
        }
    }
    public static void exampleDrawAndFill(Graphics2D g) {/*...*/}
    public static void examplePath(Graphics2D g) {/*...*/}
    public static void exampleArea(Graphics2D g) {/*...*/}
    public static void exampleTransform(Graphics2D g) {/*...*/}
}

実行結果

example1ディレクトリにおいて,./gradlew runを実行して表示されたウィンドウを以下に示します.

f:id:Jumpaku:20191212024459p:plain

また,書き出されたPNG画像(example1/example.png)を以下に示します.

f:id:Jumpaku:20191212024023p:plain

SVG画像への図の描画

SVG画像への図の描画を行うプロジェクトの設定ファイル,メインクラス,実行結果を以下に示します.

build.gradle

plugins {
    id 'java'
    id 'application'
}
group 'jumpaku'
version 'SNAPSHOT'
// 使用するライブラリのダウンロード元としてMavenセントラルリポジトリを指定する
repositories {
    mavenCentral()
}
// example2.Mainクラスをメインクラスとして指定する
mainClassName = 'example2.Main'
// JFreeSVGライブラリを追加する
dependencies {
    implementation group: 'org.jfree', name: 'jfreesvg', version: '3.4'
}

Main.java

package example2;
/*import...*/
public class Main {
    /**
     * SVG画像に図を描画しファイルに書き出す
     * @param args
     */
    public static void main(String[] args) {
        // SVGGraphics2Dオブジェクトを用意する
        SVGGraphics2D g = new SVGGraphics2D(320, 240);
        // SVGGraphics2Dオブジェクトに図を描画する
        exampleDrawAndFill(g);
        examplePath(g);
        exampleArea(g);
        exampleTransform(g);
        // SVGGraphics2DオブジェクトからSVG画像の文字列を取得しファイルに書き出す
        String svg = g.getSVGDocument();
        try (PrintWriter writer = new PrintWriter(new File("./example.svg"))) {
            writer.print(svg);
        } catch (FileNotFoundException exp) {
            exp.printStackTrace();
        }
    }
    public static void exampleDrawAndFill(Graphics2D g) {/*...*/}
    public static void examplePath(Graphics2D g) {/*...*/}
    public static void exampleArea(Graphics2D g) {/*...*/}
    public static void exampleTransform(Graphics2D g) {/*...*/}
}

実行結果

example2ディレクトリにおいて,./gradlew runを実行して書き出されたSVG画像(example2/example.svg)を以下に示します.

JavaFXウィンドウとSVG画像への同じ図の描画

JavaFXウィンドウとSVG画像への同じ図の描画を行うプロジェクトの設定ファイル,メインクラス,実行結果を以下に示します.

build.gradle

plugins {
    id 'java'
    id 'application'
    // JavaFXを利用するためにOpenJFXのプラグインを追加する
    id 'org.openjfx.javafxplugin' version '0.0.8'
}
group 'jumpaku'
version 'SNAPSHOT'
// 使用するライブラリのダウンロード元としてMavenセントラルリポジトリを指定する
repositories {
    mavenCentral()
}
// JavaFXに関する設定をする
javafx {
    // 使用するJavaFXのバージョンを指定する
    version = "13.0.1"
    // JavaFXで使用するモジュールを指定する
    modules = ['javafx.controls', 'javafx.swing']
}
// example3.Mainクラスをメインクラスとして指定する
mainClassName = 'example3.Main'
// FXGraphics2DライブラリとJFreeSVGライブラリを追加する
dependencies {
    implementation group: 'org.jfree', name: 'fxgraphics2d', version: '1.8'
    implementation group: 'org.jfree', name: 'jfreesvg', version: '3.4'
}

Main.java

package example3;
/*import...*/
public class Main {
    /**
     * JavaFXウィンドウ上にドラッグ軌跡を描画するPNG画像に書き出す
     * JavaFXウィンドウに描画されたものと同じドラッグ軌跡をSVG画像にも書き出す
     */
    public static void main(String[] args) {
        // Appクラスを指定してアプリケーションを起動する
        Application.launch(App.class, args);
    }
    public static class App extends Application {
        /** 直前のドラッグ位置 */
        Point2D previousPoint;
        /**
         * アプリケーション起動時に呼び出される
         * @param primaryStage
         */
        @Override
        public void start(Stage primaryStage) {
            int width = 640;
            int height = 480;
            // Canvasオブジェクトを生成する
            Canvas canvas = new Canvas(width, height);
            // FXGraphics2Dオブジェクトを生成する
            FXGraphics2D fxGraphics2D = new FXGraphics2D(canvas.getGraphicsContext2D());
            // SVGGraphics2Dオブジェクトを生成する
            SVGGraphics2D svgGraphics2D = new SVGGraphics2D(width, height);
            // FXGraphics2DオブジェクトとSVGGraphics2Dオブジェクトをまとめたリストを生成する
            List<Graphics2D> graphics2DList = Arrays.asList(fxGraphics2D, svgGraphics2D);
            // Canvasオブジェクトにマウスプレス時のイベントハンドラを設定する
            canvas.setOnMousePressed(e -> {
                // マウスプレス時には図をクリアして現在のカーソル位置を保存する
                graphics2DList.forEach(g -> {
                    g.setBackground(Color.WHITE);
                    g.clearRect(0, 0, width, height);
                });
                previousPoint = new Point2D.Double(e.getX(), e.getY());
            });
            // Canvasオブジェクトにマウスドラッグ時のイベントハンドラを設定する
            canvas.setOnMouseDragged(e -> {
                // マウスドラッグ時には直前のカーソル位置と現在のカーソル位置の間に線分を描画して現在のカーソル位置を保存する
                Point2D.Double currentPoint = new Point2D.Double(e.getX(), e.getY());
                graphics2DList.forEach(g -> g.draw(new Line2D.Double(previousPoint, currentPoint)));
                previousPoint = currentPoint;
            });
            // Canvasオブジェクトにマウスリリース時のイベントハンドラを設定する
            canvas.setOnMouseReleased(e -> {
                // マウスリリース時にはSVG画像とPNG画像の書き出しを行う
                // PNG画像を書き出す
                SnapshotParameters params = new SnapshotParameters();
                WritableImage img = canvas.snapshot(params, new WritableImage(width, height));
                try {
                    File file = new File("example.png");
                    ImageIO.write(SwingFXUtils.fromFXImage(img, null), "png", file);
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
                // SVG画像を書き出す
                String svg = svgGraphics2D.getSVGDocument();
                try (PrintWriter writer = new PrintWriter(new File("./example.svg"))) {
                    writer.print(svg);
                } catch (FileNotFoundException exp) {
                    exp.printStackTrace();
                }
            });
            // Canvasオブジェクトをシーングラフに追加し,ウィンドウを表示する
            primaryStage.setScene(new Scene(new Pane(canvas)));
            primaryStage.show();
        }
    }
}

実行結果

example3ディレクトリにおいて,./gradlew runを実行しました. 表示されたウィンドウにドラッグ軌跡が描画される様子を以下に示します.

書き出された(example3/example.png)を以下に示します. f:id:Jumpaku:20191213233934p:plain 書き出されたSVG画像(example3/example.svg)を以下に示します.

まとめ

  • Graphic2Dクラスの基本的な使い方として,基本図形のストローク描画とフィル描画,パス操作,領域操作,アフィン変換を示した.
  • JavaFXのウィンドウやSVG画像に図を描画する例を示した.
  • FXGraphics2DやSVGGraphics2Dを利用しGraphic2Dクラスを通して統一的なプログラムで図の描画を行う例を示した.
  • Java13で,JavaFX,FXGraphics2D,JFreeSVGといったライブラリを利用するGradleプロジェクトの設定例を示した.
  • JavaFXJDKに同梱されなくなって,準備が面倒だと思った.

ICPC 2019 Asia Yokohama Regionalに参加

本記事はMuroran Institute of Technology Advent Calendar 2019 12/13の記事です.

adventar.org

はじめに

私はこれまで,2016年,2017年,2018年のICPC国内予選に選手として参加しています. 今回も選手として参加したかったのですが,今回は選手としての参加資格がありませんでした. 他の参加選手からコーチになるように頼まれたので,引き受けることにしました. 今回,私の大学から5チームが国内予選に参加し,そのうち1チームが横浜のアジア地区予選に出場することとなり,コーチとして一緒に行ってきました. 以下ではICPC 2019 Asia Yokohama Regionalに参加した感想を時系列順に述べます.

ICPC 2019 Yokohama Regional 国内予選

準備

コーチの最も重要な任務はチーム登録と実行委員会からのメールの伝達です. 特にチーム登録は意外と大変でした. 選手はアカウントを作成し,これをコーチに伝えます. コーチは選手を検索し,作成したチームに選手を追加します. その後,選手たちは登録情報の補充をします. あるチームでは,アカウント登録の完了がなかなか終わらずチーム登録を完了できません. また,あるチームでは,チーム登録まで完了しても,登録情報の補充が完了しない選手がいます. 催促をしてもなかなか反応がないこともありました. 登録期限が迫る中,私は選手登録とチーム登録が完了できるか心配でした. 最終的には全員参加できたので良かったです. 私が,過去にICPC国内予選に参加した時のコーチも同じような思いをしていたと思うと,改めて感謝の念を抱きました.

他にも,選手たちと一緒に監督を引き受けてくれる先生を探し,引き受けてくれるようにお願いしました. 監督の仕事は,試験監督のような感じで,国内予選当日に問題のコピーを配り,選手が不正しないようにを監視することです. また,監督は必要に応じて審判団との連絡も行います. 私の指導教員が引き受けてくれてありがたかったです.

本番

ICPC国内予選当日,コーチの仕事はありませんでした. 室蘭工業大学からはSataniChoが横浜のアジア地区予選に出場することとなりました.

問題を解いた

選手たちと同じモチベーションで打ち上げに臨むためには,同じ問題を解いて同じ熱を纏う必要があります. 研究室でICPCに参加していないメンバ2人を誘って問題を解く会を開催しました. 私たちは正式な参加ではないため,パソコンは1人1台使用可,インターネット接続可,複数ディスプレイ可で解きました. 他の2人がA,Bを担当し,私はCを担当しました. 最初C++で解こうと思いましたが,組み合わせを全て列挙すれば良いことに気がついてPythonのitertoolsを使うことにしました. 他の2人がA,Bを解き終わってからは3人で協力して解きました. ほぼ3時間以内に解き終わることができました. Pythonが遅すぎたため,最後はPyPyで実行しました. 協力しながら解きごたえのある問題を解くことができて楽しかったです.

ICPC 2019 Asia Yokohama Regional

横浜で開かれるアジア地区予選に出場するSataniChoの選手たちには旅費が支給されます. なんと,コーチにも旅費が支給され,一緒に行くことができました. 日程は3日間で,1日目はリハーサル,2日目は本番と立食パーティ,3日目は会社見学でした.

1日目

5時半くらいに起床し,荷物の準備をしました. 6:15に待ち合わせをして,選手の車に乗せていただき,室蘭工業大学から新千歳空港まで行きました. 自転車では半日かかる約70kmの道のりですが,車だったため9:30出発の飛行機に余裕を持って到着しました. 昼過ぎに羽田空港に到着し蒙古タンメンを食べに行きました. f:id:Jumpaku:20191213033148j:plain 客が並んでいたため,リハーサルの集合時刻に間に合うか際どいところでしたが,結果的には間に合ったと思います. 会場の横浜産貿ホール マリネリアに到着すると英語で質問されました. 大学名をきかれたのだと解釈し,"Muroran Institute of Technology"と回答しましたが,ピンとこなかったようだったため,"From Hokkaido"と言い直したら,受付へ通されました. あとで考え直すと,もしかしたら北海道大学と勘違いされていたかもしれません. アジア地区予選の公用語は英語なのかと察しました. 受付では,スタッフにStudent IDを見せてくれと英語で言われましたが,よく聞き取れずにいるともう1人のスタッフが"Gakusei shou"と分かりやすい英語で教えてくれました.

リハーサル会場にはチームごとにブースが設けられており,水,弁当,しおり,Tシャツ,参加証明書,ネームタグが支給されました. Tシャツとネームタグは大会の間はずっと付けなければいけないようでした. 今まで選手登録の度に,なぜTシャツのサイズが要求されるのか疑問でしたが,会場で支給されるからだと知ることができました. また,朝時間がなくて3日間の着替えの準備が不十分だったのですが,Tシャツが支給されたことによりこの問題が解決しました.

リハーサルでは,まず諸注意があり,デモンストレーションがあり,その後,選手たちが実際にリハーサル問題を解きます. 国内予選を勝ち抜いた選手たちからは,多くのことを学びました. C++ラムダ式は変数に代入した上で,引数で自分自身を受け取ることで再帰することができることを知った時は衝撃を受けました. また,アジア地区予選ではジャッジシステムを用いるため,実行時間に何時間も使って強引に解くことはできないことも知りました. そして,ACする度にスタッフが風船を持ってやってきて,ブースの机の横に引っ掛けていきます. これはとてもにぎやかな感じがしました. しかし,コーチがその場でするべきことは特にありません. 自分はなぜここにいるのか,という当初から存在していた答えの無い問いと向き合う決心をしました. 起床が早朝だったため,気がつくとウトウトしていました.

リハーサル後,私たちは会場の近くの中華街へ夕食を食べにいきました. まず,インターネットで調べて人気そうだった中華料理屋に並びました. 行列に並ぶのが嫌になったため,その向かいの中華料理屋へいくことになりました. その店は2階建てで,2階のテーブルに案内されました. そこにはスタッフが1人いました. 2階には客用のテーブルが並んでいましたが,厨房などは無いようでした. いったい料理はどこから来るのか,食器はどこへ帰っていくのかと思いましたが,料理用エレベータで上ってきた料理を客に出し,片付けた食器を料理用エレベータで下ろしていたのでした. 私は色々な料理を食べることのできるコース料理を注文しようかとも思いましたが,高かったため,回鍋肉定食を注文しました. とても美味しく,また,お腹も満たされました. f:id:Jumpaku:20191213033435j:plain

ホテルに着いて21時になるとAtCoder Beginner Contest 145が始まりました. そのE問題のTLEを修正することができないまま終わってしまいました. atcoder.jp 提出したプログラムはソートとDPを組み合わせた解法で,計算量も解法も想定解法と同じはずでしたがTLEとなってしまいました. 原因の究明をしたいとは思いましたが,あまり寝るのが遅くなって寝坊したら選手たちに申し訳ないため,すぐに寝ました.

2日目

本番では,コーチは会場である横浜産貿ホール マリネリアに入ることができず,会場横の神奈川県民ホールで待機することになっていました. ここで,遂にコーチが活躍する機会が訪れました. 選手たちは時計やスマートフォンなどを会場に持ち込めないため,コーチが預かることになったのです. 選手の役に立つことができました.

神奈川県民ホールに着いたのは8:50くらいでしたが,開館時刻が9:00と書いてあったため待つこととしました. 入り口前で待っているとスタッフが1人やってきました. 公用語が英語だと察した私は,挨拶は"Good morning"か"Hello"どちらが良いかと迷いましたが,「おはようございます」言われました. そこは日本語なのか,と混乱した私から1呼吸遅れて出てきたのはカタコトの"Ohayou gozaimasu"でした. そのスタッフが言うには,神奈川県民ホールは開館が9:00であるため,そこから準備が始まるらしく,大変なんだなと思いました.

開館後どうして良いか分からなかった私は周りの人についていくことにしました. みんなの向かう先には,ステージのある大きなホールがあり,多くの椅子が並んでいました. 座っていると着々と準備が進んでいき,ステージ上で大会の中継が始まりました. しばらくすると問題用紙も配布されました. コーチはこのホールで大会が終わるまで中継を見たり,問題を読んだりしながら待機するのかと思いかけたところで,周りを見渡すと,あまりにも人が少ないことに気がつきました. 少し飽きた私はお手洗いのついでに,ホールの外のロビーの様子を見に行くことにしました.

ロビーには軽食が並べられており,問題用紙を広げながら楽しそうに議論をしている他のコーチたちが,何人かいました. ロビーの椅子の方が座りやすため,そこで軽食をいただきながら昨日のAtCoderのE問題を解き直そうと思いました. しかし,AtCoder後に充電するのを忘れていたため,私のMacBookProの充電は風前の灯火でした. 使用可能な電源がないようだったため,パソコンの使用は諦めることにしました. 大会終了までまだまだ時間があります. やることが無くて困った私はSataniChoのその時点での順位の確認をしながら,気がつくと鞄を抱き締めたままウトウトしていました. 周りが賑やかになってきたことに気がついて周囲を見ると選手たちがいて,大会が終了したことを知りました.

その後ホールに移動し,解説と最終順位の発表,協賛企業からのお話などを聞きました. f:id:Jumpaku:20191213033021p:plain そして,みんなで横浜産貿ホール マリネリアへ向かい,そこで表彰式と立食パーティがありました. SataniChoも何かの賞を受賞していました.

立食パーティでは,まず,ご飯を食べた後,各企業の紹介ブースを回りました. 初めて見る会社も多くあり視野が広がりました. また,鞄に入り切らないほど多くの景品をいただきました. 他にも,タピオカミルクティを獲得するために数独を解いたり,笑顔度を測定したりしました.

3日目

最終日は会社見学でした. 私たちのグループはHauwei,NECしながわ水族館の順に見学をしました.

Hauwei

8:15分にホテルをチェックアウトし,Hauweiへ向かいました. そこでは,Hauweiの日本の研究所の会社説明を受け,エンジニアたちからそれぞれの仕事内容を聞き,エンジニアと昼食をいただきました. f:id:Jumpaku:20191213033048j:plain 私たちと一緒に昼食を食べたエンジニアは,写真が好きで,カメラに関する仕事をしているということで,写真やカメラに対する熱い想いを感じました.

また,しながわ水族館に先に行った他のグループは昼食をどうしているのだろうか,といった会話をSataniChoのチームメイトとした時に,私が「魚料理でも食べているんじゃないか」とジョークを言うと向かいで座っていた他チームの方が笑ってくれて良い人だなと思いました.

NEC

Hauweiの次にNECへ行きました. 移動は個人行動ということになっていたと思いますが,他の選手たちについて行きました. その選手たちも途中で道が分からなくなり,NECと書かれた大きなビルをに向かって歩きました. 一応,選手の監視役のようなICPCスタッフもいましたが,選手の引率役ではないようでした.

NECではAIの話とセキュリティの話を聞いた後で,顔認証技術を利用したシステムのデモンストレーションを見ました.

AIの話では「あの頃は CHOCOLATE」というお菓子の開発について聞きました. これは各時代の新聞から取り出した単語と単語から連想される味を組み合わせたデータを学習して,ある時代のムードをチョコレートの味で再現するというもののようです. その時に流された動画もとてもお洒落でした. しかし,AIが再現した味が本当にその時代のムードを表現できているのかどうかは,確かめられないのでは? と思い,質問しました. これに対しNECの社員は,待ってましたと言わんばかりに回答を始め,私は一本取られたなと思いました.

また,セキュリティの話ではCTFの紹介があり,興味を持ちました.

顔認証技術を利用したシステムのデモンストレーションでは,まるでPSYCHO-PASS攻殻機動隊のような近未来感のあるシステムが紹介され,とてもワクワクしました.

しながわ水族館

NECの後はしながわ水族館へ向かいました. その時には,私は監視役について行けば,しながわ水族館にたどり着けると気がついていました. 他の選手たちもそのことに気づいたようでした. 私は降りる駅もわからないまま監視役について行きました. まるで,ハンター試験のようだと思いました. しかし,一部の選手たちが途中で違う電車に乗ったり,別の駅で降りたりしようとした際には,監視役は選手たちが間違わないように指示を出し,最後には先頭を歩いてしながわ水族館まで選手たちを導きました. 実は優しい人だったのかと思いました.

また,移動途中の電車内においては,Hauweiで私のジョークに笑ってくれた選手が,僕たちはいつまでこのネームタグをつけていなければいけないのか,という話の中で,「ACした時の風船をつけるよりマシだ」とジョークを言っていて面白いと思いました.

しながわ水族館では,自由行動ということだったので,閉館まで見て回りました. まず,アシカかセイウチのショーを見ました. 水族館スタッフがアシカとセイウチを見分けるには耳を見れば良いと言っていた気がしますが,どっちがどっちかは,もう忘れてしまいました. 他にもクラゲ,イカ,エイ,ペンギン,サメ,ドクターフィッシュなどが印象的でした.

f:id:Jumpaku:20191213033121j:plainf:id:Jumpaku:20191213033543j:plainf:id:Jumpaku:20191213033823j:plainf:id:Jumpaku:20191213033750j:plainf:id:Jumpaku:20191213033217j:plain

特にドクターフィッシュのコーナーでは水槽に手を入れると,ドクターフィッシュが群がってきて意外と高い周波数で手をつつきます. 痛くもなく,痒くもなく,くすぐったくもなく,とても不思議な感覚でした. ちょうどそこで,昼食で私のジョークに笑ってくれて,電車でAC風船のジョークを言っていた方と再会しました. 彼は,ドクターフィッシュの感覚が気に入ったようで,「家でドクターフィッシュを飼って顔を入れたい」とユーモアのあることを言っていました. 彼と話したのそれが最後ですが,TwitterのIDを聞くなど,もっと仲良くなりたかったなと少し後悔しています.

しながわ水族館の会社見学では,ショーの準備をしたり,展示物を工夫したりといった,客を楽ませるための心遣いを感じて,尊敬しました. また,水族館の仕事は展示の他にも環境保全や生態調査もあることを知りました.

帰路

しながわ水族館を出た後は,研究室の在室表のマグネットとして使用できるマグネット付きチンアナゴを記念に買い,その後夕食としてラーメンを食べました.

f:id:Jumpaku:20191213034729j:plainf:id:Jumpaku:20191213033519j:plain

余裕を持って遅い出発時刻の飛行機を予約していたため,時間が余りましたが,早めに羽田空港へ行って,お土産を買ったり,プログラムを書いたりすることにしました.

空港ではさっさと保安検査場を通過した後,お土産としてすいーとぽてたまごを買い,それから搭乗時刻まで一昨日のAtCoderでTLEのまま放置してあったE問題の解き直しました. アルゴリズム自体は想定解法通りであったにも関わらず,実行時間がかかってしまうボトルネックは,DPのメモ化に用いているstd::unordered_mapだろうと考えました. 実際,std::unordered_mapの代わりに2次元にしたstd::vectorを用いるとTLEしませんでした. よく調べてみるとキーの衝突が頻発いていることが分かりました. 私はどうしてもDPのメモ化にハッシュマップを用いたかったため,次の三つを試してみました.

  1. std::unordered_map::reserveを呼んでメモリ確保を最初の一回だけにする.
  2. std::arraystd::mapを組み合わせてシンプルな自前のハッシュマップを実装する.
  3. std::tupleに対して実装しているハッシュ関数を変更する.

まず,1.のみの場合は特に効果を感じませんでした. atcoder.jp 次に,2.のみの場合は,結果はTLEとなりましたが,TLEとなるテストケースが2個に減り,効果があることが分かりました. atcoder.jp さらに,2.と3.を同時に行うと,無事ACできました. atcoder.jp ハッシュ関数は元々Effective Javaを参考に実装していたのですが,よりハッシュ値が分散するようにチューニングしました. 最後に,標準ライブラリがある場合には出来るだけそれを使うべきだと考えて,3.のみの場合についても試してみました. atcoder.jp 結果はTLEでしたが,先ほどの2.のみの場合に残ったTLEのテストケースのうち一つがAC,もう一つがTLEであり,ACした方は実行時間が1986 msだったため,もう一度提出してみると今度は1999 msでACすることができました. atcoder.jp

新千歳空港から室蘭へは再び選手の車に乗せていただきました.

まとめ

  • ICPCアジア地区予選への参加が初めてで分からないことも多かったが,とても楽しく貴重な経験ができた.
  • 今までコーチや監督を引き受けていただいた方達に改めて感謝する.
  • コーチの分の旅費まで出し,大会を運営していただいたICPCに感謝する.
  • コーチを引き受けさせていただいた選手たちに感謝する.
  • std::unordered_mapは意外と遅いことがある.
  • ハッシュマップは意外と簡単に実装できる.
  • ハッシュマップにおいて,ハッシュ関数の設計はとても重要である.