簡単Stimulus

この記事は ケーシーエスキャロット Advent Calendar 2023 の記事です。

先日お仕事でStimulusを扱う機会があり、 最初こそとっつきにくいと感じましたが、割とスムーズに理解することができました。 ちょうど良い機会なので、Stimulusの使い方をまとめてみようと思います。

と言っても基本的な使い方は Stimulus Handbook にまとまっているので、 この記事では実際に実装しながら使い方を簡単に振り返ってみようと思います。

注意

  • この記事ではStimulusの導入方法は扱いません

作るもの

更新ボタンを押すと、APIから現在時刻を取得し、画面に表示する機能をStimulusで実装します。
イメージは以下です。

今回は段階を追って進めていきます。

準備

view と stimulus controller を用意します。

view

こんな感じのviewを用意します。

<div>
  <button>更新</button>
  <div>
    <%= Time.current %>
  </div>
</div>

Stimulus controller

Stimulus の controller は bin/rails g stimulus コマンドを使うと、Stimulus の controller ファイルの作成と、読込設定まで追加してくれます!(便利)
今回は update-button という名前の controller を作成するので、コマンドは以下になります。

bin/rails g stimulus update_button

※update-button という命名はあまり適していないような気がしますが、スルーしてください。

作成された app/javascript/controller/update_button_controller.js はこんな感じになっていると思います。

import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="update-button"
export default class extends Controller {
  connect() {
  }
}

1: 更新ボタンにクリックイベントを設定する

いきなり全てを実装するのも難しいので、まずは更新ボタンにクリックイベントを設定するところから。
更新ボタンをクリックすると alert ダイアログを表示するようにしてみます。

view と Stimulus controller を以下のように書き換えます。

<div data-controller="update-button">
  <button data-action="click->update-button#update">更新</button>
  <div>
    <%= Time.current %>
  </div>
</div>
export default class extends Controller {
  update() {
    alert("update!!!")
  }
}

view の この部分。

<div data-controller="update-button">
  <button data-action="click->update-button#update">更新</button>

一番上の div の data-controller で適応させる Stimulus の controller を指定します。
※controller のファイル名は _ だが、controller 名としては - を使うので注意

また、button には data-action でイベントと、イベント発火時に実行する処理を設定しています。
この場合は、クリックイベント発火時に、 update-button controller の update というメソッドを実行する、という設定になります。

2: 更新ボタンクリック時に時刻表示を書き換えるようにする

次は実際に画面の表示を書き換えられるようにします。
更新ボタンをクリックすると、日時表示の要素の innerHTML を「更新した!」という文字で更新します。

書き換え先の要素は getElementByID などを使って取得することもできますが、 Stimulus には要素を参照するための target という属性があるのでこれを使います。

view と Stimulus controller を以下のように書き換えます。

<div data-controller="update-button">
  <button data-action="click->update-button#update">更新</button>

  <!-- ここを書き換える -->
  <div data-update-button-target="area">
    <%= Time.current %>
  </div>
</div>
export default class extends Controller {
  // area という target を定義
  static targets = ["area"]

  update() {
    this.areaTarget.innerHTML = "更新した!"
  }
}

書き換え先の要素を area という名前の target で指定させるようにしました。 この area はどこを参照するかというのは view で、data-controller名-target という属性で指定します。

  <div data-update-button-target="area">
    <%= Time.current %>
  </div>

area target は controller 側で this.[target名]Target とすると参照することができます。
この場合 this.areaTarget で、areatargetに指定した要素が参照可能です。

3: APIから取得した値で時刻表示を書き換えるようにする

次は実際APIを呼び出して、取得した値で時刻表示を書き換えるようにします。

使用するAPIは、 /api/time/now で呼び出すと、現在時刻をテキストで返してくれるものとします。
実装はこんな感じ。

class Api::TimeController < ApplicationController
  def now
    render plain: Time.current
  end
end

API の URL は Stimulus controller 内に直接定義することもできますが、 またまた Stimulus には value という、値を受け渡すのに便利な属性があるので、 こちらを利用します。

view と Stimulus controller を書き換えます。

<div data-controller="update-button" data-update-button-url-value="/api/time/now">
  <button data-action="click->update-button#update">更新</button>
  <div data-update-button-target="area">
    <%= Time.current %>
  </div>
</div>
export default class extends Controller {
  static targets = ["area"]
  // url という value を定義
  static values = { url: String }

  update() {
    fetch(this.urlValue, { method: "GET" })
      .then((response) => response.text())
      .then((time) => this.areaTarget.innerHTML = time)
  }
}

API の URL を url というvalueで渡せるようにしました。
value が参照する値の指定も同様に view 側で行います。
値の指定は data-controller名-value名-value という属性で行います。

<div data-controller="update-button" data-update-button-url-value="/api/time/now">

data-controller と同じ要素に指定する必要があるので注意

url value は controller 側で this.[value名]Value とすると参照することができます。
この場合 this.urlValue で、指定した API の URL が参照できます。

これで完成です!

まとめ

  • data-controller で適応させる controller を指定
  • data-action で要素にイベントを設定できる
  • 他の参照したい要素は target 、値を参照した場合は value が使える

WMS の地図画像を Google Map 上に表示する

この記事は ケーシーエスキャロット Advent Calendar の記事です。

WMS とは、地図画像配信方式の一つです。 表示したい地図範囲を指定すると、その範囲を切り取った画像が返ってくるという方式でデータの配信を行います。

地理院地図|地理院タイルについて

これを Google Map 上に表示してみよう、というのが今回の内容です。

WMSAPI 仕様

今回利用しようと思っているWMSAPIは、大体以下のような仕様とします。

  • 範囲指定: 左下(南西)と右上(北東)の緯度経度を指定(世界測地系
  • サイズ指定: px単位

Google Map 上に画像を表示する方法

まず Google Map でどのように地図データの画像を表示するかです。

Google Map では 256 × 256 に画像を分割し表示させる、タイル方式という方法で地図画像の表示を行っています。 タイルがどうなっているかは、こちらのサイトを見るとイメージしやすいかと思います。

タイル座標確認ページ

実際に Google Maps API でタイルを表示する場合、 overlayMapTypes を用いて、以下のようなコードになります。

コードからなんとなくわかる通り Google Maps API では、 必要なタイル座標とzoomレベルを getTileUrl に渡し、返ってきたURL(API)から画像を取得して表示するという動きをします。

// オーバーレイオブジェクト作成用
class CoordMapType {
  constructor(tileSize) {
    this.tileSize = tileSize;
  }

  MapOption() {
    let layer = new google.maps.ImageMapType({
      tileSize: this.tileSize,
      isPng: true,
      getTileUrl: function(point, zoom) {
        // タイル取得のためのAPIを設定
        url = "https://XXXXXXXXXX/{x}/{y}/{z}"
                .replace('{z}', zoom)
                .replace('{x}', point.x)
                .replace('{y}', point.y)
        return url;
      },
      opacity: 0.7
    });
    return layer;
  }
}

// Google Map オブジェクト作成
var map = new google.maps.Map(document.getElementById("map"), {
  zoom: 10,
  center: { lat: 35.68, lng: 139.76 },
});

// オーバーレイオブジェクト作成、Google Map に追加
opts = new CoordMapType(new google.maps.Size(256, 256));
map.overlayMapTypes.insertAt(0, opts.MapOption());

つまり WMSGoogle Map 上に表示するには?

getTileUrl で該当タイルに当てはまるWMSAPI を返すようにしてあげれば、表示することができそうです。

が、ここで問題があります。

getTileUrl に引数で渡されるのはタイル座標(X, Y)と zoom レベルなので、 緯度経度の情報を取得するには、タイル座標を緯度経度に変換する必要があるのです。

タイル座標から緯度経度への変換

タイル座標を緯度経度に変換する方法は、以下のサイトで紹介されていました。

TrailNote : 座標の変換(世界座標、ピクセル座標、タイル座標、緯度・経度)

つまり、

  1. タイル座標をピクセル座標に変換
  2. ピクセル座標と zoom レベルを使って、緯度経度に変換

というステップでタイル座標→緯度経度の変換ができそうです。

これをこんなかんじでコードにおこしてみます。

// タイル座標を緯度経度に変換する
function tilePointToLatLon(x, y, zoom) {
  // 左下: タイル座標->ピクセル座標
  let ldX = x * 256;
  let ldY = y * 256 + 255;

  // 左下: ピクセル座標->緯度経度
  dl = pixPointToLatLon(ldX, ldY, zoom);
 
  // 右上: タイル座標-> ピクセル座標
  let urX = x * 256 + 255;
  let urY = y * 256;

  // 右上: ピクセル座標->緯度経度
  ur = pixPointToLatLon(urX, urY, zoom);

  return { down_left: dl, up_right: ur };
}

// ピクセル座標を緯度経度に変換する
function pixPointToLatLon(x, y, zoom) {
  // 表示可能上限
  // L = 180 / PI * asin(tanh(PI)) = 85.05112878
  const L = 85.05112878;

  // 経度
  let lon = 180 * ((pixX / Math.pow(2, zoom + 7)) - 1);

  // 緯度
  let lat = 180/Math.PI * (Math.asin(Math.tanh(-Math.PI/Math.pow(2, zoom+7) * pixY + Math.atanh(Math.sin(Math.PI/180 * L)))));

  return { lat: lat, lon: lon }
}

タイル座標→ピクセル座標の箇所で、左下の y座標と右上のx座標に 255 を足しているのは、 単純にタイル座標→ピクセル座標変換すると左上のピクセル座標になってしまうためです。 f:id:aoiro6:20201218001739p:plain

下の図のように、左上を (0, 0) とすると、タイルの1辺は 256 px なので、左下と右上はそれぞれ (0, 255) と (255, 0) になるので、それぞれ255を足しています。 f:id:aoiro6:20201218001750p:plain

これでタイル座標からWMS指定に必要な緯度経度が取得できました。 後は、 getTileUrl にこの変換ロジックを使ってWMSをタイルっぽく取得できるようにしてあげれば表示ができます。

落とし穴…

しかしこれ、実はIEだと動きません。

なぜかというと、緯度経度変換処理の中で使用している Math.tanh()Math.atanh()IEではサポートされていないからです。(なんてこった)

そのため、IEでも表示させたい場合はそれぞれ代替の計算式を使って、以下のように書き換えてあげると良いと思います。

// ピクセル座標を緯度経度に変換する
function pixPointToLatLon(x, y, zoom) {
  // 表示可能上限
  // L = 180 / PI * asin(tanh(PI)) = 85.05112878
  const L = 85.05112878;

  // 経度
  let lon = 180 * ((pixX / Math.pow(2, zoom + 7)) - 1);

  // 緯度
  let lat = 180/Math.PI * (Math.asin(tanh(-Math.PI/Math.pow(2, zoom+7) * pixY + atanh(Math.sin(Math.PI/180 * L)))))

  return { lat: lat, lon: lon }
}

function tanh(x) {
  if (Math.tanh) {
    return Math.tanh(x);
  } else {
    var a = Math.exp(+x), b = Math.exp(-x);
    return a == Infinity ? 1 : b == Infinity ? -1 : (a - b) / (a + b);
  }
}

function atanh(x) {
  if (Math.atanh) {
    return Math.atanh(x);

  } else {
    // |x| < 1 以外の場合は考慮していない
    if (Math.abs(x) < 1) {
      return Math.log((1+x)/(1-x)) / 2;
    }
    return Nan;
  }
}

感想

これでWMSGoogle Map 上に表示させるという目的はクリアできましたが、 実際表示させてみるとめちゃくちゃ表示に時間がかかります。(正直実用的ではなさそう)

とはいえ、久々に数式見たり、頭を悩ませてコードを書いたりして良い経験になりました。

御覧いただきありがとうございました。

自作キーボードを作りました

この記事は ケーシーエスキャロット Advent Calendar 2019 - Qiita の 6日目の記事です。

昨日の記事は ifbun さんの「社会人13年目の夏季集中講座」でした。 学習した内容&感想がいい感じにまとまっていて、すごく良い記事ですね!

さて、実は今年自作キーボードデビュー(といって良いのか)を果たしました。 せっかくなので、この機会にきっかけとか雑感とか振り返ってみようと思います。

きっかけ

もともとセパレートタイプで程よい高さのキーのものはないかと思って探していた*1ときに、ふと、 自作キーボードなら、自分の好みのキーボード見つかるかも?くらいの軽い気持ちで、 色々調べ始めたのがきっかけです。*2

また、自作キーボード専門店がOPENしたというのも影響しています。

yushakobo.jp

購入したもの

Mint60 というキーボードの基本セット(基盤などの基本的なものは入っているが、キースイッチ・キーキャップ・ケーブルなどは入っていない)の マットクリアにしました。

eucalyn.shop

※ なぜこのキーボードにしたかというと、単純に数字列があって、セパレート形式で、row staggered でもともと使っていたノートPCの配列に近いから、という理由です。

あとはリンク先にも書いてあるとおり、

  • キースイッチ
  • キーキャップ
  • ケーブル類

を揃えました。

ちなみにキースイッチは Gateron の青軸*3、キーキャップは DSA*4 という規格のものを購入しました。

制作

eucalyn.hatenadiary.jp

基本的にはこちらの記事の通りに組み立てを勧めました。 隣で見ていた emorima さんからの熱い(?)サポートも入りつつ、なんとか組み立てることができました。

ハンダを使うのは中学の授業以来だったので、うまくできるか不安でしたが、結構いい感じにできたと思います!

組み立て途中と完成品の写真がこちら f:id:aoiro6:20191207000453j:plain f:id:aoiro6:20191207000448j:plain f:id:aoiro6:20191207000443j:plain

組み立て後の悲劇

実は組み立て後しばらくはいい感じに使えていたんですが、TRSコネクタ(右と左を繋ぐケーブルを指す部分)の調子が悪くなり、 右側だけ反応しないという状態になりました。 これ、付け替えようとすると、ハンダ付けしたキーをすべて取り除かないといけないと行けないんですね。 なので、泣く泣くキーをすべてはずして組み立て直しました。はんだシュッ太郎は偉大です… www.amazon.co.jp

使い心地

とても気に入っています。 写真からはわからないと思いますが、実は左下の緑のキーが esc キーになっていて、 vimmer の自分的には遠くにあった esc キーが押しやすい位置に来てくれていて嬉しいです。

まとめ

今回作ってみて、たしかに時間もお金もかかるし、組み立ても苦労したけど、 やっと自分好みのキーボードに出会えて良かったなと思います。 キーを打つのがとても快適になりました。

今は Mint60 で満足しているので、別のキーボードのことは考えていませんが、 また魅力的なキーボードが出てきたら作ってみたいですね。

*1:当時売っていたやつは殆どがキーの高さが高すぎて、手の小さい自分には打ちにくいものが多かった

*2:emorima さんの布教の成果でもある

*3:押したときに音が結構するけど、押し心地と押したときの重さが好き

*4:ちょい薄めの作りで、これまた高さが好み

2018年振り返り

この記事は ケーシーエスキャロット Advent Calendar 2018 の最終日の記事になります。

Advent Calendar もついに最終日となりました。

2018年も、あと少しで終わりですね。今年は個人的に、色々な変化があったと感じる年でした。 ということで、2018年を振り返ってみました。

まず、今年は RubyKaigi を始め、Ruby World ConferenceVimConf と、いろいろなカンファレンスに参加しました。

Ruby 関連のカンファレンスへの参加が主ですが、今年は前から気になっていたテキストエディタ vim のカンファレンス、VimConf にも参加してみました。 実は vimrc がまっさら&ノープラグインの状態で使っていたので、プラグインのお話されると途端に置いてけぼりをくらうなどしていましたが、 Ruby 系のカンファレンスに参加することが多い身としては、Ruby がマイノリティとなっている環境はとても新鮮でした。

以前もカンファレンスなどは参加したことがありますが、 今までは誰かに言われて参加する、という流れだったのが、今年は自主的に参加を決めたりと、自分的には大きな一歩を踏み出せた感があります。

また、カンファレンス参加にあたり、持ち運びができるPCを新しく購入しました。 (もともと家で使っているノートPCは持ち運びを想定しないで購入したので、15.6型のとても重く、持ち運ぶのも一苦労で・・・)

というのも、カンファレンスとか参加したときに、手ぶらだとすごく手持ち無沙汰! 今聞いた内容をすぐに試して見たい、調べたい!!

ということで、買っちゃいました。Thinkpad の X1Carbon。勢いです。 最初は Mac でも良いかなと思っていたのですが、ちょうどPC買おうと思った時期にセールで安くなっていたのもあり、 X1Carbon の方を購入しました。 使ってみてしばらく経ちますが本体も動作も軽くて快適です。 ちなみに、ubunts と windows10 のデュアルブートで使っています。

今年カンファレンスにたくさん参加しようと思ったのは、このPCを手に入れたという要因も大きいです。

という感じで、今年は外部のお話を聞く機会が多かった年でした。 さきほども書きましたが、外に出ようと思ったのは自分に取ってとても大きな変化でした。

また来年も引き続き RubyKaigi やその他気になるカンファレンスに参加できたらと思います。

Rakeのススメ

この記事は、 株式会社ケーシーエスキャロット アドベントカレンダー の11日目の記事です。

いい機会なので、以前 Rake タスク作成を行ったときに、試行錯誤の結果得たものをまとめてみようと思います。 少しでも 参考になれば幸いです。

Rake タスクとは

Rake タスクとは、Ruby で書く Make のようなものです。 Make だと Makefile に処理を定義していきますが、 Rake も同様に Rakefile というファイルに処理を定義します。

例えば、 Rakefile に以下のように記載すると、 sample と言うタスクが作成されます。 作成したタスクは rake -T を実行すると一覧表示されます。

desc 'sample task'
task :sample do
  puts "do sample"
end
$ rake -T
rake sample  # sample task

実行する場合は rake タスク名 で実行します。

$ rake sample
do sample

namespace の活用

タスクは namespace をつけることもできるので、ある程度タスクのまとまりが大きくなる場合などは namespace でまとめておくと良いと思います。

require 'fileutils'

namespace :workdir do
  desc 'ワークディレクトリの作成'
  task :make do
    if File.exist? 'work'
      puts "workdir already exist."
    else
      FileUtils.mkdir 'work'
    end
  end
end
$ rake -T
rake workdir:make  # ワークディレクトリの作成

引数の使い方

タスクに引数を渡すには、タスクごとに受け渡す方法と、環境変数として受け渡す方法があり、 それぞれ実装方法、実行方法が異なります。

desk '引数をタスクで定義する方法'
task :sample_1, [:str] do |task, args|
  puts "input str => #{args[:str]}"
end

desc '引数を環境変数で定義する方法'
task :sample_2 do
  puts "input str => #{ENV['INPUT_STR']}"
end
$ rake -T
rake sample_1[str]  # 引数をタスクで定義する方法
rake sample_2       # 引数を環境変数で定義する方法

$ rake sample_1['---test---']
input str => ---test---

$ rake sample_2 INPUT_STR='---test---'
input str => ---test---

タスクごとに受け渡す場合、受け取った引数は、ブロックの第2引数(上の例だと args )から取得することができます。 タスク一覧で、引数の情報を確認することができるのが便利です。

環境変数として受け渡す方法は、引数の情報を環境変数 ENV として定義します。受け渡すと言うよりは、設定すると言う感覚でしょうか。 どのタスクからでも参照可能なので、例えば一回で複数のタスクを実行させる場合には都合がいいです。

こちらは、タスクごとに定義する方法と違い、タスク一覧では引数の情報を確認することができませんので、説明文などに記載しておくと親切です。

個人的には、環境などタスク全体に関わる引数に関しては、環境変数として定義させ、それ以外はタスクごとに定義させると言うように使い分けしています。(できるだけ)

invoke と execute

他のタスクを呼び出す場合は Rake::Task#invokeRake::Task#execute は他のタスクを呼び出すときに使います。 使い方は以下になります。

desc 'invoke を使ってタスク呼び出し'
task :sample_invoke, [:str] do |task, args|
  Rake::Task['output'].invoke(args[:str])
end

desc 'execute を使ってタスク呼び出し'
task :sample_execute, [:str] do |task, args|
  Rake::Task['output'].execute(args)
end

# desc のないタスクはタスク一覧に表示されない
task :output, [:str] do |task, args|
  puts "input => #{args[:str]}"
end
$ rake sample_execute['aaa']
input => aaa

$ rake sample_execute['aaa']
input => aaa

invokeexecute も、実行時に引数を渡すことができますが、渡し方がそれぞれ異なります。 引数以外にも、以下にまとめたように挙動が異なるため、使用するケースによってどちらを使うか判断する必要があります。

invoke execute
引数 文字列 or 配列形式で渡して、渡した順番で各引数の項目と値を解釈する Rake::TaskArguments をそのまま渡す or ハッシュ形式で引数の項目と値を明示的に指定する
実行回数 1回のみ実行可能 何回でも実行可能
依存タスク 実行する 実行しない

依存タスク

依存タスクは、そのタスク実行時に必ず実行するタスクの定義です。 他のタスクを呼び出す場合はタスク実行時の好きなタイミングで呼び出せますが、依存タスクは必ずタスク実行前に呼び出されますので、タスクの前準備の処理を定義するのに向いています。

依存タスクは以下のように定義します。

desc '依存タスクサンプル'
task :sample_izon, [:str] => :header do |task, args|
  puts "input => #{args[:str]}"
end

# 依存タスク
task :header do
  puts "##########"
end
$ rake sample_izon['aaa']
##########
input => aaa

Rails で Rake タスク作ったことある人は、依存タスクの定義、見たことあるかもしれません。 そう、DB操作系のタスクを作る場合に定義する必要のある environment です。

task :hoge => :environment do
・・・
end

実はこの environment も、DB接続の準備をするタスクで、依存タスクとして定義させることで、DB操作が可能になるというわけです。

まとめ

ざざざっとまとめてみました。 Rake タスク便利なので、皆さん作って、 Ruby ライフをエンジョイしてください。