import processing.core.*; 
import processing.data.*; 
import processing.event.*; 
import processing.opengl.*; 

import java.util.HashMap; 
import java.util.ArrayList; 
import java.io.File; 
import java.io.BufferedReader; 
import java.io.PrintWriter; 
import java.io.InputStream; 
import java.io.OutputStream; 
import java.io.IOException; 

public class gokuraku_measure extends PApplet {

// 極楽画像計測アプリケーション（Gokuraku_measure）
// Created by Tatsuya Shirai
// National Institute of Technology, Suzuka college
// Mechanical Department
//
// Ver. 1.0 : 2022.06.07
// Ver. 1.1 : 2022.06.08 : filter（ヘルプ表示にバグあり：動作に支障なし）
// Ver. 1.2 : 2022.06.08 : 縮小可能に．軽微なバグの修正（ヘルプ表示）
// Ver. 1.3 : 2022.06.08 : データフォーマットの変更（データファイル１行目のScale:の後にタブを追加）
// Ver. 1.4 : 2022.06.10 : データフォーマットの変更（データファイル１行目をscale, org_weight, org_heigtのタブ区切りに変更），ショートメッセージ表示機能の追加
// Ver. 2.0 : ファイル名（パス無し）を記録する

CurrentImage ci;
ShortMessage shortMessage;

final int Scr_w = 600;
final int Scr_h = 600;

public void setup()
{
  ci           = new CurrentImage();
  shortMessage = new ShortMessage();

  // size(400, 500);
  //サイズ変更を許可する
  surface.setResizable(true);
}

public void draw()
{
  fill(72, 72, 72);

  if (ci.img != null) {
    // 画像が正常にロードされている場合
    int cnt = 0;
    rect(0, 0, width, height);

    // 画像表示
    image(ci.img, 0, 0);
    // フィルターの適用
    ci.apply_filter();
    // プロット表示
    for (cnt = 0; cnt < ci._counter; cnt++) ci.target.plot(ci.measured[cnt].px, ci.measured[cnt].py);
  } else {
    //ヘルプ：画像が選択されていない場合
    int x  = 16;
    int y  = 32;
    int dx = 150;
    int dy = 40;
    noStroke();
    fill(72, 72, 72);
    surface.setSize(Scr_w, Scr_h);
    rect(0, 0, Scr_w - 1, Scr_h - 1);
    fill(Target_color[0][0], Target_color[0][1], Target_color[0][2]);
    textSize(28);
    text("Gokuraku measure (@tatsuva) Ver." + Version, x, y); y += dy;
    textSize(24);
    fill(Target_color[ci.target.t_color][0], Target_color[ci.target.t_color][1], Target_color[ci.target.t_color][2]);
    text("(Left click)",  x, y); text(": measure",                     x + dx, y); y += dy;
    text("Shift+(left)", x, y);  text(": back",                        x + dx, y); y += dy;
    text("(Right clisk)", x, y); text(": end / select file",           x + dx, y); y += dy; 
    text("[ESC]", x, y);         text(": end / exit",                  x + dx, y); y += dy;
    text("[p]", x, y);           text(": change plot type",            x + dx, y); y += dy;
    text("[c]", x, y);           text(": change plot color",           x + dx, y); y += dy;
    text("[+][-][0]", x, y);     text(": scale up, down, reset",       x + dx, y); y += dy;
    text("[g]", x, y);           text(": Gray,Invert",                 x + dx, y); y += dy;
    text("[s]", x, y);           text(": show settings (Console)",     x + dx, y); y += dy;
    text("[d]", x, y);           text(": show data (Console)",         x + dx, y); y += dy;
    text("[f]", x, y);           text(": select folder (folder mode)", x + dx, y); y += dy;
  }
  shortMessage.display_short_message();
}

public void mousePressed()
{
  if (mouseButton == RIGHT) {    // 右クリック時
    ci.flush_data();  // データを出力
    if (ci.folder_mode) {
      // フォルダーモードの場合の
      if (ci.next_image() == null) ci.quit_folder_mode (); // もう画像がフォルダー内に無いならば終了
    } else {
      // 単体画像モードの場合はファイル選択ダイアログボックスを表示
      open_fileselector();
    }
  } else if (mouseButton == LEFT) {  // 左クリック時
    if (ci.img != null) {
      if (keyPressed && keyCode == SHIFT)  ci.back();                   // 計測のキャンセル（バック）
      else                                 ci.measure(mouseX, mouseY);  // 計測
    }
  }
}

public void keyPressed()
{
  switch (keyCode) {
    case ESC : 
      if (ci.img == null) terminate(); else {
        // 画像計測中
        ci.flush_data();    // データを出力
        if (ci.folder_mode) ci.quit_folder_mode(); // フォルダーモードならば終了
        key = 0;            // keyをクリアしないとProcessingが終了してしまう
      }
      // 画像計測時以外は，このままProcessingが終了する
      break;
    case 'p' :
    case 'P' : 
      ci.target.change_shape();
      shortMessage.set_message("Change target shape");
      break;
    case 'c' :
    case 'C' :
      ci.target.change_color();
      shortMessage.set_message("Change target color");
      break;
    case 'g' :
    case 'G' :
      ci.change_filter();
      shortMessage.set_message("Change filter");
      break;
    case 's' : 
    case 'S' : show_settings();            break;
    case 107 :
    case  59 :
    case '+' : ci.rescale(Scale_rate);     break;
    case 109 :
    case '-' : ci.rescale(1.0f/Scale_rate); break;
    case  96 :
    case '0' : ci.rescale( 0.0f);           break;
    case 'd' :
    case 'D' : show_alldata();             break;
    case 'f' :
    case 'F' :
      File fp;
      fp = new File(ci.folder_mode_folder);
      selectFolder("Select a folder to process:", "folderSelected", fp);  
      break;
  }
}

//// ファイル・フォルダー選択ダイアログボックス関係の関数
// ファイル選択ダイアログボックスの呼び出し
public void open_fileselector()
{
  selectInput("Select a file to process:", "fileSelected");
}
// ファイル選択後の処理
public void fileSelected(File selection) 
{
  ci.flush_data();
  if (selection == null) ci.img = null;
  else ci.load(selection.getAbsolutePath());
}

// フォルダーモード
public void folderSelected(File selection) {
  File[] files;

  if (selection == null) return;  // キャンセル時
  files = selection.listFiles();
  ci.flush_data();                // 計測済みデータの書き出し
  ci.folder_mode_folder = selection.getAbsolutePath();
  shortMessage.set_message("User select folder = " + ci.folder_mode_folder);
  ci.open_folder(selection.listFiles());
}

//// それ以外のユーザー関数
// 終了時の処理
public void terminate()
{
  if (ci.img != null) ci.export_data();  // 計測済みならば出力
  println("Exit program.");
  exit();
}

// 設定を表示
public void show_settings()
{
  println("[Settings]");
  println("Max_measure = " + Max_measure);
  println("Target_size = " + Target_size);
  println("Target_color = " + Target_color[ci.target.t_color][0] + ", " + Target_color[ci.target.t_color][1] + ", " + Target_color[ci.target.t_color][2]);
  println("Scale = " + ci.scale);
}

// 計測済みのデータを表示
public void show_alldata()
{
  if (ci.img == null) return;
  if (ci._counter <= 0) println("no data");
  int n;
  println("[Data]");
  for (n = 0; n < ci._counter; n++) println(n + " : " + ci.measured[n].px + " , " + ci.measured[n].py); 
}
// Readme:
// ファイルを一つずつ選択する単一画像モードと，フォルダー単位で選択するフォルダーモードがあります．
// 各画像の計測点の数が決まっているのであれば，「定数」の Max_measure の数を指定して下さい．
// もし決まっていないのであれば，おおよそ予想される最大値を指定して下さい．
// Max_measure で指定した数の計測が完了したら，自動的に次の画像ファイルを選択するダイアログボックスが表示されます．
// 単一画像モード，フォルダーモード共に，現在の画像の計測を終了したい場合は（右クリック）して下さい．単一画像モードの場合は次のファイルを開くダイアログボックスが表示されます．
// フォルダーモードの場合の途中でフォルダーモードを終了したい場合も「ESC」キーを押して下さい．
// メニュー画面が表示された状態で「ESC」キーを押すとプログラムは終了します．
// 出力されるデータの書式は，一行目にスケール，二行目以下は，”測定点の番号，Ｘ座標，Ｙ座標，赤，緑，青”です．色はそのピクセルの色を各色に分解したものです．
// （注意）出力されるデータファイルは強制的に上書きされます．１ヵ所でも計測されていた場合は，計測を中断しても上書きされます．

// 定数
final int    Max_measure      = 32;               // ここで指定した数のポイントを計測し終えたら自動的に次の画像の選択用ダイアログボックスが表示される
                                                  // フォルダーモードの場合はフォルダー内の次の画像が自動的に開かれる

final int    Target_size      = 5;                // ターゲットマーカーのサイズ
final int    Target_color[][] = {{255,  80,  80}, // ターゲットマーカーの色 : 赤，白，黒 
                                 {255, 255, 255},
                                 {0, 0, 0}};  
final double Initial_scale    = 1.0f;              // 画像のスケール（倍率）初期値
final double Scale_rate       = 1.5f;              // 倍率の比率
final String Export_ext       = ".txt";           // 出力するデータファイルの拡張子
final int    filter_type[]    = {GRAY, INVERT};   // 表示する画像に掛けるフィルター Either THRESHOLD, GRAY, OPAQUE, INVERT, POSTERIZE, BLUR, ERODE, or DILATE

final int    Delay_message    = 2000;          // ショートメッセージの表示時間（ミリ秒）

final String Version          = "2.0";
// 計測する画像に関するクラス
class CurrentImage {
  PImage  img;
  String  fullpathname;            // 開いている画像ファイルのフルパス名
  double   scale;                  // 画像の拡大率 ：注意　計測中にも変更できるが，計測精度が落ちる恐れがある．読み込み前に変更するか，再読み込みする
  int      org_width, org_height;  // 画像のオリジナルサイズ
  boolean folder_mode;             // フォルダーモード時，true;
  File[]  files;                   // フォルダーモード：ファイルリスト
  int     f_no;                    // フォルダーモード時の読みだし位置
  int     filter_no;               // -1 : なし，0 : GRAY, 1 : INVERT
  String  folder_mode_folder;      // フォルダーモード時のフォルダー
  
  int     _counter;     // 計測済みデータ数

  PointData []  measured = new PointData[Max_measure];
  TargetMarker  target;
  
  // コンストラクター
  CurrentImage() {
    img          = null;
    fullpathname = null;
    _counter     = 0;
    scale        = Initial_scale;
    org_width = org_height = -1;
    // フォルダーモード関係
    folder_mode        = false;
    files              = null;
    f_no               = 0;
    filter_no          = -1;
    folder_mode_folder = "";
    
    // 計測データ領域の初期化
    for (_counter = 0; _counter < Max_measure; _counter++) measured[_counter] = new PointData();
    // ターゲットマーカーの初期化
    target = new TargetMarker();
  }
  // 画像ファイルの読み込み
  public PImage load(String path) {
    boolean reload = false;
    PImage img0;
    if (path == null) reload = true;
    if (! reload) fullpathname = path;
    if ((img0 = loadImage(fullpathname)) == null) return null;  // 読み込み失敗時

    // 読み込み完了
    img = img0;
    println("Loaded image: " + fullpathname);
    // 元画像のサイズを保存
    org_width  = img.width;
    org_height = img.height;
    // 画像を拡大する場合
    if (scale != 1.0f) {
      img.resize((int)(img.width * scale), (int)(img.height * scale));
    }
    surface.setSize(img.width, img.height + 40);
    
    if (! reload) _counter  = 0;
    return img;
  }
  // 再読み込み
  public PImage reload() {
    if (fullpathname == null) return null;  // 異常な状態
    return load(null);
  }
  
  // 計測データの格納
  public int measure(int x, int y) {
    if (_counter >= Max_measure) return _counter;  // 異常な状態
    measured[_counter].px = x;
    measured[_counter].py = y;
    measured[_counter].get_color(img);
    shortMessage.set_message((_counter + 1) + " : (" + x + ", " + y + ")");  
    if (++_counter >= Max_measure) {  // 最大計測数に達した場合
      ci.export_data();
      open_fileselector();
    }
    return _counter;
  }
  // 計測取り消し
  public int back() {
    if (--_counter < 0) _counter = 0;
    shortMessage.set_message("Back to " + _counter);
    return _counter;
  }
  // ファイルへ出力
  public void export_data() {
    int     cnt = 0;
    String  dest_filename;
    PrintWriter output;
    dest_filename = fullpathname + Export_ext;  // 拡張子の分解は面倒くさいので，単純に拡張子を追加する
    output = createWriter(dest_filename);
    if (output != null) {
      File fp = new File(fullpathname);
      output.println(scale + "\t" + org_width +"\t" + org_height + "\t" + fp.getName());
      for (cnt = 0; cnt < ci._counter; cnt++) {
        output.print(cnt + "\t" + ci.measured[cnt].px + "\t" + ci.measured[cnt].py);
        output.println("\t" + (int)red(ci.measured[cnt].c) + "\t" + (int)green(ci.measured[cnt].c) + "\t" + (int)blue(ci.measured[cnt].c));
      }
      output.flush();
      output.close();
      shortMessage.set_message("Completed exporting all data to " + dest_filename);
    } else println("Error: Can't open file =" + dest_filename);
  }
  // 計測済みデータの出力
  public void flush_data() {
    if (ci.img != null && ci._counter > 0) export_data();  // 中断して次のファイルを選択した際に，計測済みのデータを出力する
    ci.img = null;
  }
  // フォルダーモードの開始
  public void open_folder(File [] f) {
    files = f;
    f_no  = 0;
    folder_mode = true;
    shortMessage.set_message("[Folder mode]:Start");
    if (next_image(true) == null) {
      // 次の画像ファイルが存在しなかった場合
      quit_folder_mode();
    }
  }
  // 次の画像を表示（フォルダーモード）
  public PImage next_image() {
    return next_image(false);
  }
  public PImage next_image(boolean first) {
    PImage ret = null;
    if (! first) f_no++;  // 初回以外は最初にカウントアップする：次の画像だから
    for (; f_no < files.length; f_no++) {
      if (files[f_no].getPath().endsWith(Export_ext)) continue;  // データファイルは読み飛ばす（ワーニングが少しは減る）
      if (files[f_no].isDirectory())                  continue;  // フォルダーは読み飛ばす
      if ((ret = loadImage(files[f_no].getAbsolutePath())) != null) break;
    }
    if (ret == null) return null;  // 最後のファイルに到達（存在しない）
    ret = load(files[f_no].getAbsolutePath());
    return ret;
  }
  public void quit_folder_mode() {
    folder_mode = false;
    img         = null;
    shortMessage.set_message("[Folder mode]:Quit");
  }

  // 画像の拡大率変更
  public void rescale(double rate) {
    double pre_scale = scale;
    if (rate == 0.0f) {
      if (scale == Initial_scale) return;                 // 初期値で変更なしの場合
      else                        scale = Initial_scale;  // 初期化
    } else {
      scale *= rate;
//    if (scale < Initial_scale) scale = Initial_scale;
    }
    // 既に計測済みの場合はスケールを変換する
    if (_counter > 0) {
      int n;
      for (n = 0; n < _counter; n++) {
        measured[n].px = (int)(measured[n].px / pre_scale * scale);
        measured[n].py = (int)(measured[n].py / pre_scale * scale);
      }
    }
    shortMessage.set_message("Change scale " + pre_scale + " -> " + scale);
    reload();  // 画像の再読み込み
  }
  
  // フィルターの変更
  public void change_filter() {
    if (++filter_no > 1) filter_no = -1;
  }
  public void apply_filter() {
    if (filter_no >= 0) filter(filter_type[filter_no]);
  }
}

// 計測するデータに関するクラス
class PointData {
  int   px, py;  // X, Y座標
  int c;       // 色

  PointData() {
    px = py = -1;    // 特に意味はない
  }
  public void get_color(PImage img) {
    c = img.pixels[px + py * img.width];
  }
}

// ターゲットマーカーに関するクラス
class TargetMarker {
  char mode;   // 0: none, 1: cross,  2: circle, 3: poiont
  int  size;
  int  t_color;  // 0, 1, 2
  TargetMarker() {
    mode = 1;
    size = Target_size;
  }
  // ターゲットマーカーの形状を変更
  public void change_shape() {
    if (++mode > 3) mode = 0;
  }
  // ターゲットマーカーの色を変更
  public void change_color() {
    if (++t_color > Target_color.length - 1) t_color = 0; 
  }
  public void plot(int x, int y) {
    stroke(Target_color[t_color][0], Target_color[t_color][1], Target_color[t_color][2]);
    noFill();
    switch (mode) {
      case 0 : break;
      case 1 : line(x - size, y, x + size, y); line(x, y - size, x, y + size); break;
      case 2 : ellipse(x, y, size * 2, size * 2); break;
      case 3 : rectMode(CENTER); rect(x, y, 3, 3); break;
      default : mode = 0;
    }
  }
}

// ウインドウとコンソールエリアに短いメッセージを一時的に表示するためのクラス
class ShortMessage {
private  int    start;
private  String message;
  ShortMessage() {
    start   = millis();
    message = "";
  }
  public void set_message(String str) {
    message = str;
    println(str);
    start   = millis();
//  delay(500);          // 連続表示に対応するために．ただし動作が遅くなるため無効にしておくのでご自由に．
  }
  public void display_short_message() {
    if (millis() - start < Delay_message) {
      fill(255, 255, 255);
      textSize(16);
      text(message, 8, height - 24);
    }
  }
}
  static public void main(String[] passedArgs) {
    String[] appletArgs = new String[] { "gokuraku_measure" };
    if (passedArgs != null) {
      PApplet.main(concat(appletArgs, passedArgs));
    } else {
      PApplet.main(appletArgs);
    }
  }
}
