20200725のJavaScriptに関する記事は27件です。

Google Chartがモーダルで上手く表示できない時

簡単にグラフが作れて人気なGoogle Chart。その中の「GeoChart」は都道府県別の日本地図を作成できます。以下のように都道府県別で色分でき、カーソルを当てるとパラメータを表示してくれます。
map.png

やりたいこと

今回は既にあるオリジナルサービス内に、ユーザー詳細ページにあるリンクをクリックするとモーダルでユーザーのデータが反映されたMAPを表示する機能を実装します。

ということでファイルを作成。
(データを入れる処理は人それぞれなので割愛)

users/_map.html.erb
<div class="modal-dialog modal-lg" role="document">
  <div class="modal-content">
    <div class="modal-header">
      <h4 class="modal-title" id="myModalLabel"><%= "#{@user.name}がイベントで訪れた都道府県" %></h4>
      <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
    </div>
    <div id="regions_div"></div>
    <script src="https://www.gstatic.com/charts/loader.js"></script>
    <script type="text/javascript">
      google.charts.load('current', {'packages':['geochart']});
      google.charts.setOnLoadCallback(drawRegionsMap);
      function drawRegionsMap() {
        var data = google.visualization.arrayToDataTable(gon.map_data);
        var options = { region: 'JP', resolution: 'provinces', width: '100%', height: 500 };
        var chart = new google.visualization.GeoChart(document.getElementById('regions_div'));
        chart.draw(data, options);
      }
    </script>
  </div>
</div>
users/map.js.erb
$("#map-modal").html("<%= escape_javascript(render 'map') %>")
$("#map-modal").modal("show")
users/show.html.erb
--

<%= link_to map_user_path(user), remote: true do %>
  <%= image_tag "/images/japan.png" %>
<% end %>

--

<div class="modal fade" id="map-modal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true"></div>
<div class="modal fade" id="recommend-modal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true"></div>
users_controller.rb
def show
    gon.map_data = @user.prefecture_data
end

prefecture_dataメソッドはmodels/user.rb内に作ったメソッドで、都道府県に行ったデータを取得していると思ってください。

問題

さあ準備ができたので、実際にブラウザでリンクをクリックしてみましょう。

スクリーンショット 2020-07-25 23.41.13.png

あ、あれ!!??(・_・;

上手く表示されていません…
モーダルタイトルの「ゲストユーザーがイベントで訪れた都道府県」は表示されていますが肝心な日本地図は表示されませんでした。

解決

どうやらmodalが完全に表示する前に、地図を描画させてしまっている事が原因のようです。
今回はmap.js.erbにjQueryで以下のように書いてみました。

map.js.erb
$("#map-modal").html("<%= escape_javascript(render 'map') %>")
$("#map-modal").modal("show")

#以下を追記

$("#map-modal").on("shown.bs.modal", function () {
  google.charts.load('current', {'packages':['geochart']});
  google.charts.setOnLoadCallback(drawRegionsMap);
    var data = google.visualization.arrayToDataTable(gon.map_data);
    var options = { region: 'JP', resolution: 'provinces', width: '100%', height: 600 };
    var chart = new google.visualization.GeoChart(document.getElementById('regions_div'));
    chart.draw(data, options);
});

shown.bs.modal(モーダル・ダイアログを開くshowメソッドを呼び出した時のイベント)を使っています。

再度ブラウザからリンクをクリックしてみます!
スクリーンショット 2020-07-25 23.40.53.png

無事成功しました!

参考

Google maps APIを使ったmapをBootstrapのmodal上で表示させる

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

テキストレイヤーの改行をカウントする

テキストレイヤーの中身の改行コードが違った

\nで改行をカウントしようとしたところ全然判定されず、小一時間悩んで調べまくった結果、環境によって改行コードが混在している事が判明。
試しに\rで判定してみたところ無事カウントされたので、参考になれば。
環境:Windows10, AE2019
参考サイト

サンプルコード

var myComp = app.project.activeItem;
var myLayer = myComp.selectedLayers[0];
var myText = myLayer.property("ADBE Text Properties").property("ADBE Text Document").value;
var Text = myText.toString(myText);
var result = Text.match(/\r/g);
if (!(result === null)) {
  alert(result.length);
} else {
  alert("Nothing");
}

AE_Script.png

注意点

スクリプトからエクスプレッションを書き込むなら判定のための改行コードは\\rで記述する。
(バックスラッシュ1回だとエクスプレッション中の改行として認識されるため)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[JavaScript入門]カウントダウンタイマー

はじめに

ECサイトを作成する際、
・期間限定のセールで残り時間を表示したい
ソーシャルゲームを作成する際、
・限定ガチャやボスが逃げるまでの残り時間を表示したい
と思うことあると思います。
今回は、そんな機能についてご紹介します。

方法

今回は、jQueryを使用して作成します。

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>

カウントダウンタイマー機能を作成します。

JavaScript
<script>
  var timerId;
  // カウントダウンの終了日
  var endDateTime = new Date("2021/07/23 20:00:00");

  // カウントダウン機能
  function countDownTimer() {
    var startDateTime = new Date();
    var remaining = endDateTime - startDateTime;

    // カウントダウン後、id="contents"の部分の表示を変更 
    if (remaining < 0) {
      $("#contents").html('東京オリンピック開催中です');
      // 繰り返し動作を停止(setIntervalで設定したタイマーをクリア)
      clearInterval(timerId);
      return 
    }

    // 1日の秒数
    var daySeconds = 24 * 60 * 60 * 1000;

    // 残り日数
    var remainingDays = Math.floor(remaining / daySeconds);
    // 残り時間
    var remainingHrs = Math.floor((remaining % daySeconds) / (60 * 60 * 1000));
    // 残り分数
    var remainingMin = Math.floor((remaining % daySeconds) / (60 * 1000)) % 60;
    // 残り秒数
    var remainingSec = Math.floor((remaining % daySeconds) / 1000) % 60 % 60;

    $("#TimeRemaining").text(remainingDays + '' + remainingHrs + '時間' + remainingMin + '' + remainingSec + '');
  }

  // 一定時間ごとに関数countDownTimer()を呼び出す
  $(function() {
    // 1秒刻みで表示させたいので、1000以下に設定  
    timerId = setInterval('countDownTimer()',1000);
  });
</script>
html
<div id="contents">
  <p>東京オリンピックまで</p>
  <p id="TimeRemaining"></p>
</div>

これで機能としては完成です。
あとは、見栄え良くご自身でデザインしましょう。

参考

東京オリンピック
jQuery
Math.floor
Javascriptのタイマー処理 setIntervalとsetTimeout

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JavaScript入門】カウントダウンタイマー

はじめに

ECサイトを作成する際、
・期間限定のセールで残り時間を表示したい
ソーシャルゲームを作成する際、
・限定ガチャやボスが逃げるまでの残り時間を表示したい
と思うことあると思います。
今回は、そんな機能についてご紹介します。

方法

今回は、jQueryを使用して作成します。

<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>

カウントダウンタイマー機能を作成します。

JavaScript
<script>
  var timerId;
  // カウントダウンの終了日
  var endDateTime = new Date("2021/07/23 20:00:00");

  // カウントダウン機能
  function countDownTimer() {
    var startDateTime = new Date();
    var remaining = endDateTime - startDateTime;

    // カウントダウン後、id="contents"の部分の表示を変更 
    if (remaining < 0) {
      $("#contents").html('東京オリンピック開催中です');
      // 繰り返し動作を停止(setIntervalで設定したタイマーをクリア)
      clearInterval(timerId);
      return 
    }

    // 1日の秒数
    var daySeconds = 24 * 60 * 60 * 1000;

    // 残り日数
    var remainingDays = Math.floor(remaining / daySeconds);
    // 残り時間
    var remainingHrs = Math.floor((remaining % daySeconds) / (60 * 60 * 1000));
    // 残り分数
    var remainingMin = Math.floor((remaining % daySeconds) / (60 * 1000)) % 60;
    // 残り秒数
    var remainingSec = Math.floor((remaining % daySeconds) / 1000) % 60 % 60;

    $("#TimeRemaining").text(remainingDays + '' + remainingHrs + '時間' + remainingMin + '' + remainingSec + '');
  }

  // 一定時間ごとに関数countDownTimer()を呼び出す
  $(function() {
    // 1秒刻みで表示させたいので、1000以下に設定  
    timerId = setInterval('countDownTimer()',1000);
  });
</script>
html
<div id="contents">
  <p>東京オリンピックまで</p>
  <p id="TimeRemaining"></p>
</div>

これで機能としては完成です。
あとは、見栄え良くご自身でデザインしましょう。

参考

東京オリンピック
jQuery
Math.floor
Javascriptのタイマー処理 setIntervalとsetTimeout

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SELECT要素内に画像を表示させたい

画像付き文字を選択させるには?

html の select タグは、一種の選択肢です。
普段は選択済み要素だけが表示されていて、タグをクリックした時だけ、選択肢の内容がプルダウン形式で表示されますね。
この select は文字だけしか表示できませんが、画像も付けてあげた方が分かりやすいと思う場合もあります。例えば、言語を選ぶ時に国旗も表示したい時などがそうですね。
ところが、これがなかなか手強いのです。

(1) 選択済みの時だけ、画像も表示

まずは、選択済みの時だけ画像も表示してみましょう。
やることは以下の通りです。

  • select の background-image を使う事で画像を表示させます。
    background-repeat: no-repeat 指定も必要です。

  • 文字は padding を使って、画像と重ならない位置に移動させます。

  • safari では select には css が効かないそうなので、safari のみ appearance を button 外観にします(?表示が消えますが仕方ありません)。

  • select 選択肢が変更されたら、javascript を使って background-image を変更。これを実現するため、option の value で画像ファイル名を指定します。

こうですね。
(背景色を設定しているのは日本国旗を見えやすくする為なので、国旗でなければ不要です)


See the Pen
SelectWithImg
by Hiroaki Okabe (@phoophiang)
on CodePen.


コツとしては、画像の縦サイズを全て揃えておき、その縦サイズより2px大きい値を height に指定すること。および、font-size を画像縦サイズより2px以上(3pxを推奨)小さく設定することです。font-size は普通なら px 単位での設定をするべきでは無い(remを使うべき)ですが、ここでは画像サイズに合わせることを優先します。
また実際に使う時には、select自体の位置指定とかも必要だと思います。

(2) 選択中も、画像を表示

(1)の方法では、select をクリックした時に出る選択肢には画像が表示されていません。
では同じように、option タグにも background-image を指定してやれば良いだろうと誰もが考えるのですが… 残念ながら、効きません。before を使ってもダメです。
いろいろ試してみた結果、select を使う限りは表示できないという結論に達しました。

select が使えないなら、自分で select モドキを実装するしかありません。以下のようにします。

  • 選択済み(1行だけ)の表示と、プルダウンを開いた時の2つを別々に作ります。
    プルダウンは選択済みの上に重ねて表示します。このようにしないと、プルダウンが開いた時にページのサイズが変わってしまうことがあります。

  • 選択済みとプルダウンの両方を table で作ります。
    これは javascript の一部共通化と、画像とテキストの縦方向を中央に揃えやすくする目的の2つが理由です。

  • 選択済みがクリックされたら選択済みの onclick を消す。プルダウンが閉じられたら onclick を付ける。

  • プルダウン選択中のアイテムが分かりやすいように、hover 設定。

  • プルダウンが開いている時に、外側がクリックされたらプルダウンを閉じる。

こんな感じです。
(ES6未満でも使えるようにしておいたつもりですが、ES6未満では動作未検証です。すみません)

See the Pen imitationSelect by Hiroaki Okabe (@phoophiang) on CodePen.

html 部分では選択済みの table だけ作っておき、この table の id と選択肢の内容を Array で指定して渡して呼びます(Javascriptの最後の部分を参考にしてください)。
CSSでは画像サイズに応じた設定が必要です。プルダウン表示時は、タブレットでも選択しやすいように縦幅を広げてあげると親切です。
画像付きSELECT

スマホとPCで使い分ける(?)

選択肢に画像を表示させたいなら(2)の方法だけで十分なように思いますが、実はスマホでは(1)を使った方が良いと思うのです。
スマホでは select をタップすると、タップした位置では無く、画面下部などで選択肢の横幅が広がって選べるようになってます。iOS(safari)では画面が拡大され、android(Chrome)では選択肢以外が暗くなるなどの工夫も凝らされています。
スマホでのSELECT
この方が広い面積で選べるので、画面が小さいスマホでは select を使ってあげた方が利便性が上がります。
ただ、スマホとPCで html を大きく変更するのは面倒かもしれません。
この場合、(2)の方法を使い、スマホだと画面下部いっぱいにプルダウンを表示してやるみたいな対応も考えられます。ここまでスクリプトが作ってあれば簡単かと思いますので、必要な人は作り込んでみてください(私はそこまで必要じゃ無かったです ^^;)。

最後に

この画像付きSELECTを作るのに一番苦労したのは、実は縦方向のセンタリングでした。
ググれば対応方法はいろいろ見つかりますが、言語選択のように小さな部品では、1ドットの差でも違和感が出てしまいます。
table を使うのがベストだと気付いた後も試行錯誤しました。td の height に 16px を指定しているにも関わらず 24px とかで表示されてしまうのです。td が充分に大きい時には気付かないのですが、小さい時は文字列の line-height の影響を受けると分かるまで時間がかかりました。

この縦方向の試行錯誤のせいで、どうも余計な(無意味な) CSS 指定をしている気もします。気が付かれた方がいましたらご指摘ください。

私は、自分で作ったスマホアプリの公式サイトで、この画像付き選択を使っています。スマホとPCでは別ページになっていて、スマホ用ページでは(1)を、PCでは(2) を使っています。見た目の問題で、国旗の画像もスマホ用の方がPCよりも小さくしています。
サイトの言語選択なので、選択されたら対応ページに飛ぶようにしていますので、ここで紹介したスクリプトとは若干違いますが、ご興味があれば訪問して頂ければ幸いです(宣伝です)。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

5秒でつくるQRコード生成サイト

成果物

https://goofy-leavitt-c6c9f2.netlify.app/

成果物動画

xxx-compressor.gif

ソース

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>QRコード生成サイト</title>

    <!-- bootstrap  -->
    <link
      rel="stylesheet"
      href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
      integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
      crossorigin="anonymous"
    />

    <style>
      body {
        margin: 0;
        background: #f7f7f7;
      }

      /* bootstrapの謎の右の余白を消す */
      #wrap {
        overflow: hidden;
      }

      main {
        max-width: 750px;
        margin: 0 auto;
      }

      #placeHolder img {
        width: 150px;
        height: 150px;
      }
    </style>
  </head>
  <body>
    <div id="wrap">
      <br />
      <main>
        <div class="input-group">
          <input
            type="text"
            id="url"
            class="form-control"
            placeholder="URLを入力"
          />
          <div class="input-group-append">
            <button id="create" class="btn btn-success" type="button">
              作成
            </button>
          </div>
        </div>

        <br />

        <div
          id="placeHolder"
          class="form-group"
          style="text-align: center;"
        ></div>
      </main>
    </div>

    <!-- bootstrap用  -->
    <script
      src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
      integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
      crossorigin="anonymous"
    ></script>
    <script
      src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"
      integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1"
      crossorigin="anonymous"
    ></script>
    <script
      src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"
      integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM"
      crossorigin="anonymous"
    ></script>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/qrcode-generator/1.4.4/qrcode.min.js"></script>

    <script>
      document.getElementById('create').onclick = function () {
        var qr = qrcode(4, 'L');
        qr.addData(document.getElementById('url').value);
        qr.make();
        document.getElementById('placeHolder').innerHTML = qr.createImgTag();
      };
    </script>
  </body>
</html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

QRコード生成サイトを5秒でつくる

成果物

https://goofy-leavitt-c6c9f2.netlify.app/

成果物動画

xxx-compressor.gif

ソース

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>QRコード生成サイト</title>

    <!-- bootstrap  -->
    <link
      rel="stylesheet"
      href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
      integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
      crossorigin="anonymous"
    />

    <style>
      body {
        margin: 0;
        background: #f7f7f7;
      }

      /* bootstrapの謎の右の余白を消す */
      #wrap {
        overflow: hidden;
      }

      main {
        max-width: 750px;
        margin: 0 auto;
      }

      #placeHolder img {
        width: 150px;
        height: 150px;
      }
    </style>
  </head>
  <body>
    <div id="wrap">
      <br />
      <main>
        <div class="input-group">
          <input
            type="text"
            id="url"
            class="form-control"
            placeholder="URLを入力"
          />
          <div class="input-group-append">
            <button id="create" class="btn btn-success" type="button">
              作成
            </button>
          </div>
        </div>

        <br />

        <div
          id="placeHolder"
          class="form-group"
          style="text-align: center;"
        ></div>
      </main>
    </div>

    <!-- bootstrap用  -->
    <script
      src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
      integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
      crossorigin="anonymous"
    ></script>
    <script
      src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"
      integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1"
      crossorigin="anonymous"
    ></script>
    <script
      src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"
      integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM"
      crossorigin="anonymous"
    ></script>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/qrcode-generator/1.4.4/qrcode.min.js"></script>

    <script>
      document.getElementById('create').onclick = function () {
        var qr = qrcode(4, 'L');
        qr.addData(document.getElementById('url').value);
        qr.make();
        document.getElementById('placeHolder').innerHTML = qr.createImgTag();
      };
    </script>
  </body>
</html>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JS (TS) で CLI を作る要点メモ

概要

Node.js で CLI を作ったので備忘録として作り方の要点をメモします。
npm 等には公開せず、ローカル環境での実行のみを想定しています。

環境

Node.js 12.13.0

要点

package.jsonbin を指定

コマンドラインから実行するコマンドと、コマンドを実行した際に実行されるスクリプトファイルを指定。

package.json
{
  "bin": {
    "my-cli-name": "bin/cli.js"
  }
}

bin で指定したスクリプトを作成

シェバンで node を指定して、実行したい処理を記述します。

bin/cli.js
#!/usr/bin/env node

require('../dist/app.js');

その他作法

  • プロジェクト内の JSON 等静的テキストファイルは require で読み込むようにする。
    • path.resolve を使うとバンドルされた後や bin ディレクトリからの実行時に __dirname の挙動が変わる。(10割勉強不足)
    • 静的ファイルは require でスクリプト内に埋め込んでしまうのが、時間が無い時は学習コスト掛からず手っ取り早い。
    • テンポラリファイル等の置き場所の解決はどうするのが良いのかまだ判断ついてない。

作ったコマンドのインストール

# プロジェクトのディレクトリで
npm link
# または
npm install -g .

振り返り

cli で動くプログラムをまともに作ったのが初めてだったのだけど、
今回シングルプロセス/シングルスレッド + 同期 I/O で作っちゃったので処理速度が結構遅くなりました。
非同期 I/O と Worker Threads を覚えるとよさそう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JavaScript】promise/then を async/await に書き換えるとこうなる。【非同期処理】

あらすじ

世の中はまだまだPromise/then方式のドキュメントが多く、
シンプルにasync/awaitで書きたいときに混乱するので、変換方法をまとめました。

登場人物

function bePromise() { /*今回実行したい非同期処理(=Ajaxなど)*/ } 
function isSuccess() { /*bePromise()が成功か失敗か判断する処理*/ }
function ifSuccess() { /*bePromese()が成功した際に実行したい処理*/ } 
function ifFailure() { /*bePromese()が失敗した際に実行したい処理*/ } 

let responseData; // bePromise()が成功した際の返り値
let errorData; // bePromise()が失敗した際のエラーの内容

Promise / then

function(){
  bePromise()
    .then( (responseData)=>{
      ifSuccess();
    }
    .catch( (errorData)=>{
      ifFailure();
    });
}

async / await

変数定義方式

async function(){
  const responseData = await bePromise();
  if( isSuccess() ){
    ifSuccess();
  }else{
    ifFailure();
  }
}

メソッドチェーン方式

async function(){
  await bePromise()
    .then( (responseData)=>{
      ifSuccess();
    })
    .catch( (errorData)=>{
      ifFailure();
    });
}

try/catch方式

async function(){
  try{
    const responseData = await bePromise();
    ifSuccess();
  }catch(errorData){
    ifFailure();
  }
}

補足

今回のようにbePromise()の後に続く処理が
ひとつの関数にまとめられている場合、

.then( (responseData)=>{
  ifSuccess();
})
.catch( (errorData)=>{
  ifFailure();
});

のようになっている部分を、

.then( ifSuccess )
.catch( ifFailure );

のように、シンプルにまとめることが出来ます。

感想

失敗時の処理をどのくらい細かく扱いたいかによって、
いろんな書き方ができるんだなあと思いました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

もっと簡単に async, await, Promise

以前も async, await, Promise 関係の記事を書いたことがあるのですが、「中で何をやっているかを何となく分かるように」書いた記事だったので、今回は「外から見てどうやって使うかを何となく分かるように」書きたいと思います。

参考「Promise, async, await がやっていること (Promise と async は書き換え可能?) - Qiita

1. promise は整理券のようなもの

非同期処理は、「処理を開始するが、いつ終わるか分からない」ような処理のことです。

promise は、非同期処理の「整理券」と考えると分かりやすいと思います。

例えば、通信してコンテンツを取得する fetch() は promise を返しますが、これは「コンテンツを取得する」という注文を受けて通信を開始するものの、いつ終了するか分からないため、promise という整理券を返します。

await promise をすることで、その整理券の注文の処理が完了するのを待って、コンテンツを受け取ります。

await を使うということは、await が記述されているプログラムそのものもいつ終了するか分からないため、promise にする必要があります。

await が中に記述されている関数 (アロー関数を含む) に async を付けると、promise を返すようになります。

async が付けられた関数を async 関数と呼びます。

やりたいこと
// 
const getJsonSync = url => {
    const response = fetchSync(url);
    const json = response.jsonSync();
    return json;
};

// 
const json = getJsonSync('https://example.com/get/something');

console.log(json.something);
実際の書き方
// 
const getJson = async url => { // getJson() は promise を返す
    const response = await fetch(url); // fetch() が返した promise の処理が完了するのを待つ
    const json = await response.json(); // json() が返した promise の処理が完了するのを待つ
    return json;
};

// 
const json = await getJson('https://example.com/get/something'); // getJson() が返した promise の処理が完了するのを待つ

console.log(json.something);

Deno ではコード全体が非同期になるため、上記の書き方がそのまま使用できます。

ブラウザの JavaScript では実行中のスクリプトがいつ終了するのか分からないと困るため、await を使用しているコード全体を async 関数で囲う必要があります。

実際の書き方 (ブラウザの場合)
(async () => {

    // 
    const getJson = async url => { // getJson() は promise を返す
        const response = await fetch(url); // fetch() が返した promise の処理が完了するのを待つ
        const json = await response.json(); // json() が返した promise の処理が完了するのを待つ
        return json;
    };

    // 
    const json = await getJson('https://example.com/get/something'); // getJson() が返した promise の処理が完了するのを待つ

    console.log(json.something);

})(); // promise を返しているが await していないため、いつ処理が終了するか分からないまま次に進んでいる

2. promise でないものを promise にしたいときに new Promise() する

比較的新しい JavaScript の機能やライブラリでは初めから promise を返すようになっているため、ほぼ async, await だけで書けますが、古い JavaScript の機能や一部のライブラリでは「コールバック関数」を用いて非同期処理をしているため await できません。

そこで new Promise() を使用することで、コールバック関数による非同期処理を promise にすることができます。

new Promise() の書き方
const promise = new Promise((resolve, reject) => {
    // ...
    // 処理が終了したら resolve(返り値) を呼ぶ
    // エラーが発生したら reject(エラー情報) を呼ぶ
});
コールバック関数を用いた画像読み込みと表示
// 画像読み込みを開始しているが、いつ画像が表示されるか分からない
const img = new Image();
img.onload = () => {
    // ... 画像表示処理
};
img.onerror = e => {
    // ... 読み込みエラー時の処理
};
img.src = 'https://example.com/something.png';

// ...
// ソースコードのここの時点で、画像が表示されているかどうかは分からない
new Promise() で promise にする
// 
const promise = new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = () => { resolve(img); };
    img.onerror = e => { reject(e); };
    img.src = 'https://example.com/something.png';
});

// 
const img = await promise;
// ... 画像表示処理
// ソースコードのここの時点で、画像は表示されている

「なぜコールバック関数による非同期処理よりも promise による非同期処理が良いのか」はもうひとつの記事で説明していますので、ここでは「とにかく promise の方が良い」と思ってください。

参考「Promise, async, await がやっていること (Promise と async は書き換え可能?) - Qiita」(再掲)

3. new Promise() は処理したいときにその都度 new する

既に await して完了した promise を再度 await しても、その promise に割り当てられている処理は再実行されないため、promise は使いまわすことができません。

async 関数は呼ぶたびに新たな promsie が生成されますが、new Promise() の場合は毎回 new してください。

上手くいかない例
// 
const sleepPromise = new Promise(resolve => {
    setTimeout(resolve, 1000);
});

// 
console.log('a');

await sleepPromise; // 1 秒待機

console.log('b');

await sleepPromise; // 1 秒待機してくれない

console.log('c');
上手くいく例
// 
const sleep = () => new Promise(resolve => {
    setTimeout(resolve, 1000);
});

// 
console.log('a');

await sleep(1000); // 1 秒待機

console.log('b');

await sleep(1000); // 1 秒待機

console.log('c');

4. promise.then() は使わない

promise には then() というメソッドがありますが、これを使いやすくしたものが await なので、使わないでください。

「Promise チェーン」はもはや古い概念です。

JavaScript が今でも prototype を内部的に使用しているにもかかわらずコードを書くときは class 構文を使用するように、then() もあるけど使わないでください。

5. promise.catch() は場合によっては使う

then() と同様に catch() というメソッドもあるのですが、こちらはコードのテクニックとして使用したほうが便利な場合があります。

非同期処理で発生したエラーをスルーして無視したい場合に使用すると良いです。

エラーを無視する
//
const getJson = async url => {
    const response = await fetch(url);
    const json = await response.json();
    return json;
};

//
const json = await getJson('https://example.com/get/something').catch(() => ({ something: '(default)' }));

console.log(json.something);

少し応用的な使い方として、Promise.all() でまとめて promise を並列実行するとき、一部の promise だけエラーを無視することも可能です。

Promise.all() で一部の promsie だけエラーを無視する
const jsons = await Promise.all([
    getJson('https://example.com/get/required1'),
    getJson('https://example.com/get/required2'),
    getJson('https://example.com/get/required3'),
    getJson('https://example.com/get/optional1').catch(() => ({})),
    getJson('https://example.com/get/optional2').catch(() => ({}))
]);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】基礎編 ⑶

新しく学んだことをアウトプットしています。

React.Fragment

JSXは複数の要素を持つことができないため、
<div>タグで複数の要素を格納するのが一般的でした。

ですが、HTMLで描画した時に、
本来は<div>タグが必要ない所にまで要素が付与されてしまうので、意外と困る時がありますよね。

そんな時に、使用するのが、React.Fragmentなのです。

React.Fragmentは<div>タグと同じ役割を担っており、
HTMLでは要素としてカウントされないので、使い勝手が良いです。

App.jsx
import React, { Component } from 'react';

class App extends Component {
  render() {
    return(
      // divの替り
      <React.Fragment>
        <User name={"Taro"} age={8}/>
      </React.Fragment>
    );
  }
}

export default App;

関数コンポーネント

コンポーネントには、
クラスコンポーネントと関数コンポーネントの2つがあります。

Reactでは、VirtualDomというDomが存在していて、その中でどのDomが変更になったのか管理していて、その変更点のみを実際のDomに反映していく機構があります。
それぞれのDomにkeyを与えてあげて、必要最低限なDomの範囲をReactで管理しています。

App.jsx
// クラスコンポーネント

import React, { Component } from 'react';

class App extends Component {

  const profiles = [
    { name: "Taro", age: 8 },
    { name: "Hanako", age: 5 },
  ]

  render() {
    return(
      <React.Fragment>
        {
          profiles.map((profile, index) => {
            return(
              // keyをindexで与えてあげる
              <User name={profile.name} age={profile.age} key={index} />
            );
          })
        }
      </React.Fragment>
    );
  }
}

//  関数コンポーネント

const User = (props) => { // 引数にpropsを持たせる
  return(
    <div>
      <p> I am {props.name} and {props.age} years old!</p>
    </div>
  )
}

export default App;

defaultProps

ReactのpropsにはdefaultPropsという機構があります。
propsを受け取るComponentにdefaultPropsを設定してあげる。

以下のように設定してあげると、
Noname さんの age がデフォルトで 1 と出力されます。

App.jsx
import React, { Component } from 'react';

class App extends Component {

  const profiles = [
    { name: "Taro", age: 8 },
    { name: "Hanako", age: 5 },
    { name: "Noname" }
  ]

  render() {
    return(
      <React.Fragment>
        {
          profiles.map((profile, index) => {
            return(
              <User name={profile.name} age={profile.age} key={index} />
            );
          })
        }
      </React.Fragment>
    );
  }
}

const User = (props) => { 
  return(
    <div>
      <p> I am {props.name} and {props.age} years old!</p>
    </div>
  )
}
// propsを受け取るComponentでdefaultPropsを設定
User.defaultProps = {
  age: 1
}

export default App;

propTypes

先ほどのdefaultPorpsと同様に、
ReactのpropsにはpropTypesという機構があります。
propsを受け取るComponentにpropsTypesを設定してあげる。

以下のように設定してあげると、
name や age に型を設定することができ、 validation をかけることができます。
型を設定しておくことで、 name に数値が入れられたり、age に文字列が入るのを防止してくれます。
また、 isRquired とすることで、 age が空の場合はエラーが発生するようになります。

App.jsx
imoprt React from 'react';
// prop-Typesをimport
imoprt PropTypes from 'prop-types';
// 省略


// 
User.propTypes = {
  name: PropTypes.string,
  age: PropTypes.number.isRequired,
}

以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Salesforce ファイルアップロード使用事例

WEB側にファイルアップロードして、Salesforceオブジェクトに登録する実装例。

  • Visualforce側Pageの実装(関連部分の実装のみ表現)
Sample.v
<div class="file_drop_area">
    <div class="file_drop">
        <input type="file" class="file_drop" id="upload" accept="image/*, application/pdf" data-action-name="uploadFileAction" data-drop-area-class="file_drop_area" />
    </div>
    <apex:actionFunction action="{!pageUploadFile}" name="uploadFileAction" oncomplete="completeMsg();" rerender="refreshArea">
        <apex:param name="fileBody" value="" assignTo="{!fileBody}"/>
        <apex:param name="fname" value="" assignTo="{!fileName}" />
        <apex:param name="fileClass" value="" assignTo="{!fileClass}"/>
        <apex:param name="fileSize" value="" assignTo="{!fileSize}" />
    </apex:actionFunction>
</div>

  • JSの実装
Sample.js
$(function(){
  // ファイルドロップ時の設定
  $(document).on('dragenter', ".file_drop", function (e) {
      e.stopPropagation();
      e.preventDefault();
  });
  $(document).on('dragover', ".file_drop", function (e) {
      e.stopPropagation();
      e.preventDefault();
      e.originalEvent.dataTransfer.dropEffect = 'copy'; 
  });
  $(document).on('drop', ".file_drop", function (e) {
    e.stopPropagation();
    e.preventDefault();
    uploadFile(e.originalEvent.dataTransfer.files.item(0), $(this).data("action-name"), $(this).data("drop-area-class"));
  });

  // ファイル選択ダイアログの設定
  $(document).on('change', ".upload_file", function () {
    uploadFile($(this).prop('files')[0], $(this).data("action-name"), $(this).data("drop-area-class"));
    // ファイル選択状態を解除
    $(this).val("");
  });
});

/**
 * ファイルアップロード処理を行う大本。
 * ファイルアップロード時はこちらをまず呼び出す。
 * file:アップロードするファイル
 * actionName: アップロード時に実行する関数。apex:actionFunctionにて指定。
 * className: ファイルアップロードエリア(ファイルをドラッグするエリア)のクラス名。
 */
function uploadFile(file, actionName, className) {
  if (!validateUploadFile(file, className)) { // validateUploadFileの処理内容はここで割愛
    // バリデーションエラー時の処理
    return;
  }
  // 画像ファイルとPDFは処理が異なる
  if (file.type != "application/pdf") {
    uploadImageFile(actionName, file, className, file.type, file.size);
  } else {
    // 画像を読み込み
    var reader = new FileReader();
    reader.readAsDataURL(file);
    reader.onload = event => {
      submitFile(actionName, event.target.result, file.name, className, file.type, file.size);
    };
  }
}

/**
 * 画像ファイル用アップロード処理。
 * 画像ファイルはアップロード時にファイルサイズを80%に落とす。
 * actionName: アップロード時に実行する関数。apex:actionFunctionにて指定。
 * file:アップロードするファイル
 * className: ファイルアップロードエリア(ファイルをドラッグするエリア)のクラス名。
 */
function uploadImageFile(actionName, file, className) {
  var reader = new FileReader();

  reader.readAsDataURL(file);
  reader.onload = event => {
    var img = new Image();
    img.src = event.target.result;
    img.onload = () => {

      var width = img.width;
      var height = img.height;

      var canvas = document.createElement('canvas');
      canvas.width = width;
      canvas.height = height;
      var ctx = canvas.getContext('2d');

      // img.width and img.height will contain the original dimensions
      // 画像を再描する
      ctx.drawImage(img, 0, 0, width, height);
      submitFile(actionName, canvas.toDataURL(file.type, 0.8), file.name, className, file.type, file.size);
    },
    reader.onerror = error => console.log(error);
  };
}

/**
 * ファイルアップロード処理。
 * apex:actionFunctionで設定したcontrollerの処理を実行する関数を呼び出す。
 * actionName: アップロード時に実行する関数。apex:actionFunctionにて指定。
 * base64File:アップロードするファイルを変換したbase64文字列。
 * fileName: ユーザーがアップロードするファイルのファイル名。
 * className: ファイルアップロードエリア(ファイルをドラッグするエリア)のクラス名。
 */
function submitFile(actionName, base64File, fileName, className, fileType, fileSize) {
  //サーバ処理pageUploadFileを実行する
  Function('return this')()[actionName](base64File, fileName, fileType, fileSize);
  //ファイルドロップエリアの画像差し替え
  // 処理内容割愛(アップロードされた画像を画面にプレビュー)
  $("." + className).show();
}

  • Apex側の実装
    ガバナ制限を超えないように、ファイル内容を格納するプロパティをtransientで定義
Sample.cls
// ※ここが重要※ ガバナ制限を超えないように、ファイル内容を格納するプロパティをtransientで定義
public transient String fileBody {get; set;}// 添付ファイルデータ
public String fileName {get; set;}// 添付ファイル名  
public String fileClass {get; set;}// ファイルタイプ
public String fileSize {get; set;}// ファイルサイズ
/**
 * 画面からアップロード処理を行う.
 * @return PageReference
 */
public PageReference pageUploadFile(){
    try {
        // 添付書類を保存する(カスタムオブジェクトです。Contactとアップロード画像との紐付く関係を持っているためのオブジェクト)
        AttachedFile__c attFile = new AttachedFile__c(
                                          Test_Column__c = 'testColumn',
                                          File_Type__c = 'testType',
                                          Contact_Id__c = contact.Id);
        insert attachedFile;

        // 画像データ保存のため、取得したデータを処理する
        String fileData = fileBody.substring(fileBody.indexOf(',') + 1, fileBody.length());

        // コンテンツバージョンを保存する
        ContentVersion conVer = new ContentVersion();
        conVer.Title = fileName;// タイトル
        conVer.PathOnClient = fileName;// クライアントでのパス
        conVer.VersionData = EncodingUtil.Base64Decode(fileData);// バージョンデータ
        conVer.IsMajorVersion = true;// メジャーバージョン
        insert conVer;

        // ContentDocumentIdを取得する
        String recordId = conVerData.Id;
        String query = 'SELECT ContentDocumentId FROM ContentVersion  WHERE Id IN :recordId';
        ContentVersion conVerNew = Database.query(query);

        // コンテンツドキュメントリンクを保存する
        ContentDocumentLink conDocLink = new ContentDocumentLink();
        conDocLink.ContentDocumentId = conVerNew.ContentDocumentId;
        conDocLink.LinkedEntityId = attFileData.Id;
        conDocLink.ShareType = 'I'; // 必須 V、C、I(詳細の意味はAPI参照)
        conDocLink.Visibility = 'AllUsers'; // (他の設定値はAPI参照)
        insert conDocLink;

        // ContentDocumentオブジェクトの登録処理もありますが、その登録が自動で登録されるので、意識しなくて大丈夫です。
        // Contactデータを保存する(処理を省略)
        return null;
    } catch (Exception e) {
        // エラー処理
    }

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue.js】propsのプロパティ名にキャメルケースを使う場合の注意点

propsのプロパティ名

Vue.jsにおいて、propsのプロパティ名にキャメルケースを使う場合に1つだけ注意点があります。それは、HTMLとJavaScriptでは文字の処理方法が異なるということです。

HTMLでは、属性名の大文字と小文字を区別せず、すべての大文字を小文字として認識します。つまり、HTMLのテンプレート内では、キャメルケースのプロパティ名はケバブケースで書く必要があります。具体的には下記のとおりです。

JavaScript
Vue.component('comp-child', {
  template: '<p>{{ compText }}</p>',
  props: {
    compText: String
  },
})
HTML
<comp-child comp-text="HTMLではケバブケースで書きましょう"></comp-child>

JavaScriptではキャメルケースを使用したcompTextというプロパティ名を、HTMLではケバブケースを使用したcomp-textとすることで正常に動作します。

まとめ

Vue.jsのpropsのプロパティ名においてキャメルケースを使うことはよくあるかと思いますが、HTMLとJavaScriptでの文字の処理方法の違いを押さえ、HTMLではケバブケースを使う必要があることを覚えておきましょう。

Twitterアカウント

Twitterも更新していますので、よかったらフォローお願いします!
Twitterアカウントはこちら

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

web MIDI apiでテストアプリ作成

概要・目的

昔、仕事の関係で、AUDIO,MIDI関連の仕事をしている影響で、以下のような記事を見つけました。

https://html5experts.jp/ryoyakawai/16787/

ここで使われているweb MIDI APIを使えば、楽にaudioやmidi用のテストアプリを作れたため、それに関してここに残しておこうかと思います。

最終的にraspberry pi+USBキーボード+小型モニタでシンセサイザーみたいなものを作れたらなと思っています(後々は演奏情報をAWSにあげて演奏ログをクラウドに残す・・・なんてこともできるかと画策しています)

web MIDI apiとは

まず、web MIDI apiとは、webブラウザを介して、MIDIデバイス(USBキーボード・オーディオインターフェイス・電子楽器等)と通信を行うためのAPIです。
要は、webブラウザを操作することでMIDIデバイスを操作したり、MIDIデバイスを操作することでwebブラウザを介して音を鳴らしたりすることを可能にします。

そもそもMIDIとは

簡単に言ってしまえば、MIDIとは演奏情報のことになります。
wavファイルやmp3ファイルなどには曲そのものが入っているのに対し、midiファイルにはその曲の演奏情報が入っています。ですので、midファイルは再生させる機械によって顕著に音が変わったりします。
ちなみに、MIDIデータを使う理由として、wavデータやmp3データよりもmidiデータの方が格段にサイズが小さいことと、システムエクスクルーシブメッセージというのを利用することで演奏情報以外のデータのやり取りも可能になることがあるかと思います(他にも利点はあるかと思います)。

環境作成

必要なソフトウェアは以下の二つです。リンク先からインストールしてください
(google chrome以外のブラウザでも使用可能かとは思いますが、確認していません)
- vs code
- google chrome

また、以下のリンク先から、web MIDI api用のソースコードを入手します。
https://github.com/ryoyakawai/x-webmidi/archive/0.10.23.zip

実装

(javascriptの勉強不足にて、後々に更新予定。)

起動方法

まず、vscodeを開き、「拡張機能」、もしくは「extension」(下図の青で囲まれたアイコン)をクリックし、「Live Server」(下図の赤で囲まれたコンテンツ)ををインストールしてください
スクリーンショット 2020-07-25 14.49.03.png

次に、入手した「midiPlayground」フォルダをvscodeで開き。「src」フォルダにある「keyboard.html」を右クリックします。
そうすると、下図のようにウインドウが出るので「Open with Live Server」をクリックします。
これで、google chromeが開き、アプリケーションが開かれます。
スクリーンショット 2020-07-25 14.56.12.png

実際の起動画面

実際の起動画面は以下になります。
キーボードの箇所をクリックすることで音がなるかと思います。また、「Program」の赤色のバーを動かすことで、キーボードを押した時の音が変わります(MIDIでいうプログラムチェンジメッセージを出します)。

スクリーンショット 2020-07-19 17.15.08.png

成果物

今回作成したものを、以下のリンク先においています。ご自由にお使いください。
https://github.com/tomsWalker/webAPP/tree/master/midiPlayground

参考文献

https://html5experts.jp/ryoyakawai/16787/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue3(beta)+ TypeScript + vue-router + Vuex 環境構築メモ

Vue CLI をインストール

yarn global add @vue/cli

プロジェクトを作成

Typescript, Router, Vuexを入れておきます。

vue create try-vue-next

? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, TS, Router, Vuex, Linter
? Use class-style component syntax? No
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling
JSX)? No
? Use history mode for router? (Requires proper server setup for index fallback in production) No

? Pick a linter / formatter config: Prettier
? Pick additional lint features: Lint on save
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files

Vue 3 CLI プラグインを追加

cd try-vue-next
vue add vue-next

package.json の更新

最新バージョンの Vue beta を使用するようにします。

  ...
  "dependencies": {
    "vue": "^3.0.0-beta.24" 
  },
  "devDependencies": {
    ...
    "@vue/compiler-sfc": "^3.0.0-beta.24",
    ...

エラーの修正

このままだと動かないのでいくつか修正します。
参考までに変更前のコードはコメントアウトしました。

  • shims-tsx.d.ts

    削除します

  • shims-vue.d.ts

shims-vue.d.ts
declare module "*.vue" {declare module "*.vue" {
  // import Vue from "vue";
  // export default Vue;
  import { defineComponent } from 'vue'
  export default defineComponent
}

  • component/HelloWorld.vue
HelloWorld.vue
...
<script lang="ts">
// import Vue from "vue";
import { defineComponent } from 'vue'

// export default Vue.extend({
export default defineComponent({
  name: "HelloWorld",
  props: {
    msg: String
  }
});
</script>
...
  • router/index.ts
index.ts
// import { RouteConfig, createRouter, createWebHashHistory } from "vue-router";
import { RouteRecordRaw, createRouter, createWebHashHistory } from "vue-router";
import Home from "../views/Home.vue";

// const routes: Array<RouteConfig> = [
const routes: Array<RouteRecordRaw> = [
  {
    path: "/",
    name: "Home",
    component: Home
  },
...
  • store/index.ts
index.ts
// import Vuex from "vuex";
import { createStore } from "vuex";

// export default Vuex.createStore({
export default createStore({
  state: {},
  mutations: {},
  actions: {},
  modules: {}
});

動作確認

yarn serve

いつものページが表示されました。

コメント 2020-07-25 122107.png

参考

Vue 3.0.0-betaのお試し環境をVue CLIで作ってみた

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

javascriptを使って簡単な計算機を作るpart5 入門者向け

計算機を作る

今回作る機能

・クリアボタンと小数点ボタンの追加
・クリアボタンでテキストボックスの値をリセットする機能
・入力できる文字列や文字数などの制限

これらの機能実装するにあたり、スタイルの変更を行った。

ボタンの追加とスタイルの変更

(スクリーンショット)
スクリーンショット 2020-07-21 12.24.07.png

CLEARボタンで、テキストボックス内の値をリセットすることができる。

RECORDボタンは、今後追加するであろう計算結果をリストに追加するためのボタンを前もって配置しておいた。

入力できる文字列や文字数などの制限

意図しない文字列

(制限済み)・最初に記号ボタンが押せてしまう
(+ - * / . =)

(制限済み)・記号ボタンを連続で押せてしまう

(制限済み)・0を連続で押せてしまう
(00.1など)
100とは打てるようにしたいので工夫が必要

(制限済み)・小数点は一つまで打てるようにしたい
(0.1.0などは計算機としては変なので)

(実装済み)・記号ボタンも一つまで
記号ボタンの括りを
(+ - * / . =)

(+ - * /)(.)(=)に変更する必要がありそう。

文字数に関して

(制限済み)・テキストボックスの表示範囲外に文字を打てないようにしたい
(文字数をテキストボックス範囲内に収まるように)

(制限済み)・「1÷3」などの計算で、小数点以下の桁が長くなるとテキストボックスの表示範囲外に飛び出してしまう。

サンプルコード

HTML
caluculate.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="css/caluculate.css">
  <title>計算機</title>
</head>
<body>
  <div class="caluculate">
    <div class="wrapper">
      <div id="number-text">0</div>
    </div>
    <div class="other-btns">
      <button id="clear-btn">CLEAR</button>
      <button id="record-btn" onclick="record()">RECORD</button>
    </div>
    <div id="btns">
      <div id="num-btns">
        <button onclick="number(7)">7</button>
        <button onclick="number(8)">8</button>
        <button onclick="number(9)">9</button>
        <button onclick="number(4)">4</button>
        <button onclick="number(5)">5</button>
        <button onclick="number(6)">6</button>
        <button onclick="number(1)">1</button>
        <button onclick="number(2)">2</button>
        <button onclick="number(3)">3</button>
        <button id="zero" onclick="zero(0)">0</button>
        <button name="point" onclick="point('.')">.</button>
        <button name="equal" onclick="calc('=')">=</button>
      </div>

      <div id="symbol-btns">
        <button name="symbol" onclick="calc('/')">/</button>
        <button name="symbol" onclick="calc('*')">*</button>
        <button name="symbol" onclick="calc('-')">-</button>
        <button name="symbol" onclick="calc('+')">+</button>
      </div>
    </div>
  </div>

  <script src="js/caluculate.js"></script>
</body>
</html>

CSS
caluculate.css
* {
  margin: 0;
  padding: 0;
}

.caluculate {
  margin: 100px auto;
}

.wrapper {
  width: 300px;
  margin: 0 auto;
}

.wrapper > #number-text {
  width: 285px;
  height: 54px;
  line-height: 54px;
  margin-left: 5px;
  margin-bottom: 5px;
  /* padding: 4px; */
  font-size: 48px;
  border: 1px solid black;
  /* 右から左へ入力するためのスタイル */
  text-align: right;
}

.other-btns {
  margin: 0 auto;
  width: 300px;
}

#clear-btn {
  width: 135px;
  margin-left: 5px;
  font-size: 24px;
  /* 上下のズレを直すためのスタイル */
  vertical-align: middle;
}

#record-btn {
  width: 135px;
  font-size: 24px;
  margin-left: 13px;
  /* 上下のズレを直すためのスタイル */
  vertical-align: middle;
}

#btns {
  width: 300px;
  display: flex;
  margin: auto;
}

button {
  width: 65px;
  height: 57px;
}

#num-btns {
  margin: 5px;
}

#num-btns > button {
  margin-bottom: 5px;
  font-size: 24px;
}

/* #num-btns > button:last-child {
  width: 136px;
} */

#symbol-btns {
  height: 228px;
  display: flex;
  flex-direction: column;
  display: inline-block;
  margin-top: 5px;
}

#symbol-btns > button {
  margin-bottom: 5px;
  height: 57px;
  font-size: 24px;
  text-align: center;
}

Javascript
caluculate.js
var show = document.getElementById('number-text');
var total = ''; //現在の式
var operator = ''; //演算子
var currentValue = ''; //現在の値
var flag = 0; //ボタンが押されたあとに効かなくする※flag(フラグ)を立てる。最初は0を代入。

//数字入力の関数
var number = data => { //押されたボタンの値 data()をアロー関数にしている
  if (currentValue.length <= 8) {
    flag = 0;
    currentValue += data;
    show.textContent = currentValue;
    console.log(show.textContent); //確認用
  }
};

//0についての関数
var zero = data => {
  if(currentValue === '0') {
    return; //最初の文字が0のとき0を押せなくする
  } else if (currentValue.length <= 8){
    flag = 0;
    currentValue += data;
    show.textContent = currentValue;
    console.log(show.textContent);
  }
}

//小数点(.)の関数
var point = data => {
  if (currentValue === '') {
    return; //最初に小数点を押せなくする
  } else if (!currentValue.includes('.')) {
    currentValue += data;
    show.textContent = currentValue;
  }
}

//計算の関数(内部的に計算をするも含む)
var calc = data => {
  if (currentValue === '') {
    return; //最初に演算子を押せなくする
  }else if (flag === 0 && data !== "=") {
    flag = 1;

    var formula = total + operator + currentValue;
    total = eval(formula);

    operator = data;
    currentValue = '';
    show.textContent = total;
  } else if (flag === 1 && data === "=") {
    var formula = total + operator + total;
    total = limitNum(eval(formula));

    currentValue = "";
    show.textContent = total;
  } else if (data === "=") {
    flag = 1;

    var formula = total + operator + currentValue;
    total = limitNum(eval(formula));

    currentValue = "";
    show.textContent = total;
  } else {
    operator = data;
  }
  console.log(show.textContent);
};

//小数点以下の桁数を揃える関数
function limitNum(num) {
  return Math.round(num*10000000)/10000000;
}

//CLEARボタンの関数
function reset() {
  operator = '';
  total = '';
  currentValue = '';
  flag = 0;
  show.textContent = '0';
};

var clear = document.getElementById('clear-btn')
clear.addEventListener('click', () => {
  reset();
});

参考サイト

計算機の作り方など

JavaScriptで簡単な電卓アプリを作ってみた

Javascriptで電卓を作ろう 第3回 連続で演算記号や小数点が押された場合の処理

CSS関連

cssで要素を横並びにする方法まとめ

ボタンやフォームの上下位置がずれたとき 他

制限機能に関して

JavaScript アロー関数を説明するよ

【JavaScript】アロー関数式を学ぶついでにthisも復習する話

jQueryのdata()で属性を取得・設定・変更する方法まとめ!

MDN:Array.prototype.includes()

MDN:eval()

JavaScriptでeval関数の使い方とリスクとは【初心者向け】

JavaScript初歩知識3 アラート確認&flag(flagを立ててボタンを制御するの部分)

Javascriptで電卓を作ろう 第7回「4 ÷ 3 = 1.33333333333333333」の表示桁数を10桁にしたい

MDN:Math.round()

【JavaScript】桁指定して四捨五入・切り上げ・切り捨て

残りの今後の構想

・記号入力は、ボタン切り替え機能を使って計算したい。要は、iphoneの電卓を想定

テキストボックスには記号は入力せず、内部的に入力する。
記号に文字数を使うと、長い桁になったときに足りなくなるし、見辛そうなので。
(テキストボックス表示例:「1」→(+ボタンを押す)→「1」→(=ボタンを押す)→「2」)

7/25追記:機能自体は実装できたので、どの演算子ボタンを押したか分かりやすくするためのCSSを書く。

・計算結果をリストに追加していく機能
(複数の計算があったとき用のメモ代わり)
RECORDボタン押してリストに追加する。

・計算結果リストの編集ができる機能
(作れそうなら作ってみる)

感想など

今回の機能を実装するにあたって、参考サイトのコードを最初は全然読めなかったけど、一つ一つのコードを、単語の意味やどんな意図で使われているかを調べることで少しずつ前に進めることができた。

コードをそのままコピペとはやらずに、コードの意味を一つ一つ理解してから次に進むってやっているので、かなり時間をかけてしまっているのは問題点。

参考にしたコードを組み合わせることで思い通りの結果を得られることも学んだ。

手を動かすことで学べるを初めて実感したかもしれない。
行動する前から諦めては何もできないことも実感した。

今後の構想の内容を実装しながら、次何作るか考えておかねば。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptのみでAzure Face APIを実行する

TL;DR

文末のソースコードを見てね!

やりたいこと

JavaScriptを使いローカルの画像ファイルをブラウザに読み込み、サーバにアップロードせずAzure Face APIにかける。

解説

ほぼほぼ最小構成にするため、画面のレイアウトとかは考えていません。

index.htmlの一部
<body>
  <div id="output"></div>
  <div id="img"></div>
</body>

ファイルをドラッグ&ドロップでブラウザに読み込みます。formを使ってもよいのですが、ドラッグ&ドロップでやりたい気分だったので。

詳しい解説はソースコードのコメントを参照。バリバリコメントを書いておきます。

javascript ドラッグ&ドロップ部分
/**
 * ドロップイベント発火時に動く関数
 * 画像ファイルがドロップされたら1枚だけブラウザに読み込む
 * @param {event} e - イベントで取得した内容
 */
function handleDropAction(e) {
  // ブラウザデフォルトのイベント処理を停止
  e.preventDefault();
  e.stopPropagation();

  // ドラッグ&ドロップでブラウザに渡した画像の取得
  const droppedFiles = e.dataTransfer.files;

  // ファイルが1つ以上あれば処理開始
  if (droppedFiles.length > 0) {
    const ajaxData = new FormData();
    const file = droppedFiles[0];    // 1つ目のファイルだけ使う。あとは無視

    // 指定のファイルタイプ(画像)以外の場合はアラートを出して処理を中断
    const permitType = ["image/jpeg", "image/png", "image/gif"];
    if (file && permitType.indexOf(file.type) === -1) {
        alert("この形式のファイルはアップロードできません");
        return;
    }

    /*
     * ファイルのアップロード
     */
    ajaxData.append("file", file);  // アップロードデータを準備

    // 今開いているページにドラッグ&ドロップしたファイルを POST する 
    fetch("index.html", {
      "method": "POST",
      "body": ajaxData
    })
    .then(result => {
      // POST されたファイルを読み込み
      const fr = new FileReader();
      fr.onload = () => {
        // 読み込みが完了したとき

        // 読み込んだ画像をimgタグにしてブラウザのid=imgのdivタグ内に追加
        const image = new Image();
        image.src = fr.result;
        document.getElementById("img").append(image);

        // 顔認識 API 呼び出し
        // 読み込んだファイルのDataURLを渡している
        processImage(fr.result);
      };
      // DataURL形式でドラッグ&ドロップファイルされたファイルを読み込む
      fr.readAsDataURL(file);
    })
    .catch(err => {
      // ファイルの読み込みに失敗したとき
      console.log(err);  // エラー内容をコンソールに出力
    });
  }
}

// id=imgのdevタグにドロップイベント発生時に呼び出される関数をセット
const dropArea = document.getElementById("img");
dropArea.addEventListener("drop", handleDropAction);
javascript API呼び出し部分
/**
 * 顔認識APIにドラッグ&ドロップした画像を投げる
 * @param {string} dataUrl - ドラッグ&ドロップしたファイルを表す DataURL
 */
function processImage(dataUrl) {
  // APIキーとエンドポイントはAzureポータルで自分で発行したものを使ってね
  const subscriptionKey = "<Subscription Key>";
  const uriBase = "https://<My Endpoint String>.com/face/v1.0/detect";

  // APIの引数をGETパラメータで渡す
  // とりあえずアリアリで
  const params = 
      "returnFaceId=true" +
      "&returnFaceLandmarks=false"+
      "&returnFaceAttributes=" +
          "age,gender,headPose,smile,facialHair,glasses,emotion," +
          "hair,makeup,occlusion,accessories,blur,exposure,noise";

  // DataURL形式の画像データをバイナリを扱えるデータ形式blobに変換する
  // この辺はコピペなので詳しくは調べてね
  const type = dataUrl.slice(5,dataUrl.indexOf(";"));  // DataURLからファイルタイプを取得
  const bin = atob(dataUrl.split(',')[1]);
  const buffer = new Uint8Array(bin.length);
  for (var i = 0; i < bin.length; i++) {
      buffer[i] = bin.charCodeAt(i);
  }
  const blob = new Blob([buffer.buffer], { type: type });

  /*
   * ドラッグ&ドロップした画像とAPIパラメータでFace APIを呼び出す
   */
  // バイナリデータなのでoctet-streamで。
  // APIキーは添えるだけ
  fetch(uriBase + "?" + params, {
    headers: {
      "Content-Type": "application/octet-stream",
      "Ocp-Apim-Subscription-Key": subscriptionKey
    },
    method: "POST",
    body: blob
  })
  .then(response => {
    // なにがしかの反応がAPIからあったらjsonだと思って読んでみる
    return response.json();
  })
  .then(jso => {
    // json()関数が成功したらJavaScript Objectが返ってくる罠があるのでjsonにデコードする
    // id=outputタグの内容を帰ってきたjsonで上書き
    document.getElementById("output").innerText = JSON.stringify(jso);
  })
  .catch(err => {
    // APIの呼び出しに失敗したとき
    console.log(err);  // エラー内容をコンソールに出力
  });
};

肖像権の問題があるのでスクリーンショットはありません。(面倒くさいだけ)

ハマりポイント

FileReaderで読ませた画像ファイルがそのままではどうしてもFace APIに渡せず、四苦八苦しました。

以下の記事が大変参考になりました。こちらはブラウザ上からWebカメラを起動し、ボタンを押したタイミングでキャプチャした画像を直接 Face API に渡すという上のレベルのことをしています。コピペするだけで動いて面白いのでこちらもおススメ!

JavaScriptで、画像をFaceAPIにアップロードして表情認識したい|teratail

注意

外部ファイルの読み込みになるので、file://で表されるURLではオリジン間リソース共有(CORS)扱いになってエラーが出て、うまく処理されません。

ローカルで試す場合は、PHPやPython、Rubyなどで簡易的にHTTPサーバを立てるとよいでしょう。

shell
# PHPでHTTPサーバを立てる例
cd /path/to/src/ # index.html のあるディレクトリに移動しておく
php -S localhost:8080  # http://localhost:8080/ にアクセスするとうまく動く

注意2

API キーが丸見えになるので実用性はないぞ!

ソースコード

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Face detection</title>
  <script src="main.js"></script>
  <style>
    html, body { margin: 0; padding: 0; width: 100%; height: 100%; }
    div#img { border: solid 1px black; width: 30%; height: 30%; }
  </style>
</head>
<body>
  <div id="output"></div>
  <div id="img"></div>
</body>
</html>
main.js
function $(id) {
  return document.getElementById(id);
}

/**
 * ドロップイベント発火時に動く関数
 * 画像ファイルがドロップされたら1枚だけブラウザに読み込む
 * @param {event} e - イベントで取得した内容
 */
function handleDropAction(e) {
  // ブラウザデフォルトのイベント処理を停止
  e.preventDefault();
  e.stopPropagation();

  // ドラッグ&ドロップでブラウザに渡した画像の取得
  const droppedFiles = e.dataTransfer.files;

  // ファイルが1つ以上あれば処理開始
  if (droppedFiles.length > 0) {
    const ajaxData = new FormData();
    const file = droppedFiles[0];    // 1つ目のファイルだけ使う。あとは無視

    // 指定のファイルタイプ(画像)以外の場合はアラートを出して処理を中断
    const permitType = ["image/jpeg", "image/png", "image/gif"];
    if (file && permitType.indexOf(file.type) === -1) {
        alert("この形式のファイルはアップロードできません");
        return;
    }

    /*
     * ファイルのアップロード
     */
    ajaxData.append("file", file);  // アップロードデータを準備

    // 今開いているページにドラッグ&ドロップしたファイルを POST する 
    fetch("index.html", {
      "method": "POST",
      "body": ajaxData
    })
    .then(result => {
      // POST されたファイルを読み込み
      const fr = new FileReader();
      fr.onload = () => {
        // 読み込みが完了したとき

        // 読み込んだ画像をimgタグにしてブラウザのid=imgのdivタグ内に追加
        const image = new Image();
        image.src = fr.result;
        $("img").append(image);

        // 顔認識 API 呼び出し
        // 読み込んだファイルのDataURLを渡している
        processImage(fr.result);
      };
      // DataURL形式でドラッグ&ドロップファイルされたファイルを読み込む
      fr.readAsDataURL(file);
    })
    .catch(err => {
      // ファイルの読み込みに失敗したとき
      console.log(err);  // エラー内容をコンソールに出力
    });
  }
}

/**
 * 顔認識APIにドラッグ&ドロップした画像を投げる
 * @param {string} dataUrl - ドラッグ&ドロップしたファイルを表す DataURL
 */
function processImage(dataUrl) {
  // APIキーとエンドポイントはAzureポータルで自分で発行したものを使ってね
  const subscriptionKey = "<Subscription Key>";
  const uriBase = "https://<My Endpoint String>.com/face/v1.0/detect";

  // APIの引数をGETパラメータで渡す
  // とりあえずアリアリで
  const params = 
      "returnFaceId=true" +
      "&returnFaceLandmarks=false"+
      "&returnFaceAttributes=" +
          "age,gender,headPose,smile,facialHair,glasses,emotion," +
          "hair,makeup,occlusion,accessories,blur,exposure,noise";

  // DataURL形式の画像データをバイナリを扱えるデータ形式blobに変換する
  // この辺はコピペなので詳しくは調べてね
  const type = dataUrl.slice(5,dataUrl.indexOf(";"));  // DataURLからファイルタイプを取得
  const bin = atob(dataUrl.split(',')[1]);
  const buffer = new Uint8Array(bin.length);
  for (var i = 0; i < bin.length; i++) {
      buffer[i] = bin.charCodeAt(i);
  }
  const blob = new Blob([buffer.buffer], { type: type });

  /*
   * ドラッグ&ドロップした画像とAPIパラメータでFace APIを呼び出す
   */
  // バイナリデータなのでoctet-streamで。
  // APIキーは添えるだけ
  fetch(uriBase + "?" + params, {
    headers: {
      "Content-Type": "application/octet-stream",
      "Ocp-Apim-Subscription-Key": subscriptionKey
    },
    method: "POST",
    body: blob
  })
  .then(response => {
    // なにがしかの反応がAPIからあったらjsonだと思って読んでみる
    return response.json();
  })
  .then(jso => {
    // json()関数が成功したらJavaScript Objectが返ってくる罠があるのでjsonにデコードする
    // id=outputタグの内容を帰ってきたjsonで上書き
    $("output").innerText = JSON.stringify(jso);
  })
  .catch(err => {
    // APIの呼び出しに失敗したとき
    console.log(err);  // エラー内容をコンソールに出力
  });
};

/**
 * DOM読み込み完了時の動作
 * @param {function} loaded - DOM読み込み完了時に実行するコールバック関数
 */
function ready(loaded) {
  if (["interactive", "complete"].includes(document.readyState)) {
    loaded();
  } else {
    document.addEventListener("DOMContentLoaded", loaded);
  }
}

// DOMが読み込み完了したあとでDOMにイベントハンドラをセットする
ready(() => {
  const dropArea = $("img");

  // ドロップ領域のブラウザデフォルトのイベント処理を停止
  ["drag", "dragstart", "dragend", "dragover", "dragenter", "dragleave"].forEach(event => {
    dropArea.addEventListener(event, (e) => {
      e.preventDefault();
      e.stopPropagation();
    });
  });

  // ドロップ領域のドロップイベント発生時に呼び出される関数を設定
  dropArea.addEventListener("drop", handleDropAction);
});

関連記事

【Javascript】フレームワークを使わずにドラッグアンドドロップでjsonを読み込む - Qiita

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

スマホもPCの一員としてファイル同期したいじゃない?

前回の続きです。

前回はGo言語のお力を借りてWindows、Linuxが載ってるデバイス間は同期できたねー
でも僕らはたいてい他のコンピューターも抱えているハズ。

そう、スマホ!

こいつも前回のエコシステムに乗っけられればもっと家庭内ネットワークはいいものになるハズ。
PC達とスマホを同期する機会なんて無いって?そうでしょうか??僕は

  • NASに溜め込んだMP3をランダムで引っこ抜く
  • PC側でゲインとか音質とかバッチ処理で変換処理して標準化
  • スマホに転送する

って事をよくやるので必要で、こんな使い方が今回のツールを作る出発点だったりします

でけた!

andoukie.gif

このアプリはこんな機能があります。

  • アクセス先IPアドレスをQRコードで読みます。なので手入力の手間が要りません
  • ゲージとかスイッチで直感的に同期をコントロールできます
  • PC版みたく、クライアントしかないファイルを消して完全同期する機能があります

詳しくはリポジトリ見てくださいな。releaseの.apkをもってくればインスコできます。

つかいたい!

  • PC版を起動したら、EnterかSpaceを押すとQRコードが表示されます

1.png

  • それをスマホ版アプリで読み取れば同期が開始されます

UIわからん!

2.png

①は同期した時にサーバーに無いのに、クライアントにあるファイルを消すモードです。完全同期しておきたい時に使います。

②は同期ゲージです。この数字が何秒毎に同期するかになります。同期間隔が狭いとデカいファイルが転送しきれないのでその点はご注意を

③は同期状況とか動作ステータスが色々でます

あとがき

アプリの開発にApache cordovaを使いましたが、

・プラグインが継続保守されてなくて大量に開発停止してて動かないものが多い
・エラーを教えてくれない、のは良いがあってもスルーして動く(無い関数があっても平気で動く)
・デバッグしてデプロイしてもアプリを再インスコしないと修正できない

という僕的にうーんな点があって苦労した・・
これ流石に修正してコミットするレベル越えすぎなので逃げてすいませんが。。

追伸

Syncthing知らないで作ってしまったので参考に持ってよくできないか考え中っす!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.Js マウスオーバー(mouseover)で画像をゆっくり表示

VueJs初学者のちょっとした倉庫です。簡単だけどシンプルだけど使えそうな表現を目指しています。

マウスオーバーで画像がゆっくり表示される

←気が向いたらLGTMボタンぽちっとお願いします。

カーソルが要素に重なると拡大され、要素の値を取得します。

VueJsmouseover.gif

ソース

shuffle.html
<!doctype html>
<html lang="ja">
<head>

    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>PickUp</title>

    <style scoped>
        .fade-enter-active, fade-leave-active {
            transition: opacity 3s;
        }
        .fade-enter, .fade-leave-to {
            opacity: 0
        }

        div {
            display: inline-block;
        }

        body {
            font-family: "Montserrat", "游ゴシック", YuGothic, "ヒラギノ角ゴ ProN W3", "Hiragino Kaku Gothic ProN", "メイリオ", Meiryo, sans-serif;
        }

        ul {
            margin-left: 00px;
            list-style: none;
        }

        div#mainlist {
            color: #8EF1FF;
            font-size: 25px;
        }

        div#mainlist ul li:hover {
            list-style-type: ">   ";
            color: #7B3CFF;
            font-size: 75px;
        }


        .SubTitle {
            margin-left: 600px;
            position: relative;
            display: inline-block;
            font-weight: bold;
            padding: 0.25em 0;
            text-decoration: none;
            color: #67c5ff;
        }

        .SubTitle:before {
            position: absolute;
            content: '';
            width: 100%;
            height: 4px;
            top: 100%;
            left: 0;
            border-radius: 3px;
            background: #67c5ff;
            transition: .2s;
        }

        .SubTitle:hover:before {
            top: -webkit-calc(100% - 3px);
            top: calc(100% - 3px);
        }

    </style>
</head>
<body>
<div id="app">
    <div>
        <h1>
            <div id="mainlist">
                <ul>
                    <div>
                        <span class="SubTitle">あなたの好きな犬種は??</span>
                        <li v-for="dog in dogs" :key="dog.name" @mouseover="activeDog=dog.id">
                            {{dog.name}}
                        </li>
                    </div>
                    <div>
                        <transition name="fade">
                            <img v-show="this.activeDog==1" src="tiwawa.png"></img>
                        </transition>
                    </div>
                </ul>
            </div>
        </h1>
    </div>
    <p v-show="activeDog">selected:{{activeDog}}</p>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.19/lodash.min.js"></script>

<script>
    var app = new Vue({
        el: "#app",
        data: {
            dogs: [
                {id: 1, name: 'チワワ'},
                {id: 2, name: 'トイプードル'},
                {id: 3, name: 'ポメラニアン'},
                {id: 4, name: '柴犬'},
                {id: 5, name: 'ダックスフンド'},
                {id: 6, name: 'パグ'},
                {id: 7, name: 'ブルドック'},
                {id: 8, name: 'コーギー'},
                {id: 9, name: 'パピヨン'},
                {id: 10, name: 'マルチーズ'},
                {id: 11, name: 'シェパード'},
                {id: 12, name: 'ラブラドール'},
                {id: 13, name: 'ビーグル'},
                {id: 14, name: 'ゴールデンレトリバー'},
                {id: 15, name: '秋田犬'},
            ],
            activeDog: 0,
        },
        methods: {}
    });
</script>
</body>
</html>

最後に

以上です。気が向いたらLGTMぽちっとお願いします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

固定資産税の償却資産仮計算サイトを作った

償却資産仮計算サイトを作成しました

償却資産とは

簡単にいうと事業用に使用している10万円以上の物を申告する必要があります。
大体、その年の1月末までに申告する必要があります。
国税に減価償却で申告しているものを市町村に申告する必要があると思っていただければ大丈夫です。

毎年1月1日現在、事業の用に供することができる資産のうち、次の(1)(2)の要件を満たすものです。

(1)土地及び家屋以外の有形の固定資産で、所得税法又は法人税法の所得の計算上、減価償却の対象となる資産(土地及び家屋の用語の意義は、地方税法第341条の規定によります。)
(2)耐用年数が1年以上で取得価額(1個又は1組あたり)が10万円以上の資産。ただし、取得価額が10万円未満の資産であっても個別に償却しているものは申告対象となります。

税額の計算方法はややこしいので省略します。

来年度は減免の可能性があります!

令和2年2月~10月までの任意の3ヶ月間の事業収入が、前年の同期間と比べて、30%以上減少している場合は令和3年度の償却資産分の固定資産税が一部または全て減免の可能性があります。
詳しくはお近くの資産税係までお問い合わせを!

計算サイト

償却資産仮計算
https://calc-property-tax.herokuapp.com/

スクリーンショット 2020-07-11 22.40.25.png

基本的にフロントはHTMLとjavascript、サーバーサイドはDjangoとpythonで書いています。
htmlとcssとjsさえあればどんな環境でも動作するよう、計算機能自体はスタンドアロンで動くようにしてます。Excelが介在するので種類別明細書は不可能でしたが。。。

内部で仮計算するシステムを前提に作ったので、エラー処理とか甘いです。
丁寧に扱ってください。
種類別明細書は20個までしか出力されません・・・・

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

正規表現の使用事例

プロジェクト中使っていた正規表現を整理する。

  • メールアドレスのチェック
メールアドレスのチェック.cls
'^[a-zA-Z0-9!#%&*+\\-\\./=?_`{|}]*@[a-zA-Z0-9\\-\\.]*$'
  • 半角文字チェック
半角文字チェック.cls
'[ -~。-゚]'
  • 半角英数記号
半角英数記号.cls
'[a-zA-Z0-9!-/:-@ -/:-@-~]'
  • 半角カタカナ
半角カタカナ.cls
'[ヲ-゚]'
'[\\uff61-\\uff9f]'
  • 全角ひらがな
全角ひらがな.cls
'[ぁ-んー]'
  • 全角カタカナ
全角カタカナ.cls
'[ァ-ンヴー]'
  • 郵便番号形式チェック
郵便番号形式チェック.cls
'[\\d{3}-\\d{4}]'
  • パスワードチェック 半角英数字、特殊記号12桁
パスワードチェック.cls
'^(?=.*[A-Z])(?=.*[0-9])(?=.*[a-z])(?=.*[!"#$%&'()\-^@[;:],./\|`{+*}<>?_])[a-zA-Z0-9!"#$%&'()\-^@[;:],./\|`{+*}<>?_]{12}$'
  • 第1、第2水準以外の文字チェック[亜-黑]
第1、第2水準以外の文字チェック.cls
String value = 'テスト文字列';
// 指定文字コードに変換
String encode = EncodingUtil.urlEncode(value, 'ISO-2022-JP');
String decode = EncodingUtil.urlDecode(encode, 'ISO-2022-JP');
// 変換前後で比較
if (value != decode) {
    //変換失敗
    return false;
}
  • すべて○○ですかの表現
すべて○○ですか.cls
'^[正規表現式]+$'
  • ○○が含まれているか
○○が含まれているか.cls
'.*[正規表現式]+.*'
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ScrollHintの使い方

公式サイト

ScrollHint

サンプルソース

index.html
<!DOCTYPE html>
<html lang="jp">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ScrollHint</title>
    <link rel="stylesheet" href="https://unpkg.com/scroll-hint@latest/css/scroll-hint.css">
    <style>
        .js-scrollable {
            width: 100%;
            overflow-x: scroll;
            width: 500px;
        }
        table {
            width: 800px;
        }
        th {
            font-size: 16px;
            font-weight: bold;
            padding: 20px;
        }
        td {
            font-size: 14px;
            padding: 20px;
            line-height: 1.4;
        }
    </style>
</head>
<body>
    <div class="js-scrollable">
        <table>
            <thead>
                <tr>
                    <th>列1</th>
                    <th>列2</th>
                    <th>列3</th>
                </tr>
            </thead>
            <tbody>
                <tr>
                    <td>サンプルテキストサンプルテキスト</td>
                    <td>サンプルテキスト</td>
                    <td>サンプルテキストサンプルテキストサンプルテキスト</td>
                </tr>
                <tr>
                    <td>サンプルテキストサンプルテキスト</td>
                    <td>サンプルテキスト</td>
                    <td>サンプルテキストサンプルテキストサンプルテキスト</td>
                </tr>
                <tr>
                    <td>サンプルテキストサンプルテキスト</td>
                    <td>サンプルテキスト</td>
                    <td>サンプルテキストサンプルテキストサンプルテキスト</td>
                </tr>
            </tbody>
        </table>
    </div>
    <script src="https://unpkg.com/scroll-hint@latest/js/scroll-hint.min.js"></script>
    <script>
        new ScrollHint('.js-scrollable');
    </script>
</body>
</html>

解説

ScrollHintのcssを読み込む

<link rel="stylesheet" href="https://unpkg.com/scroll-hint@latest/css/scroll-hint.css">

ScrollHintのjsを読み込む

<script src="https://unpkg.com/scroll-hint@latest/js/scroll-hint.min.js"></script>

対象のclassを指定する

<script>
    new ScrollHint('.js-scrollable');
</script>

オプション

テキストを変更する

<script>
    new ScrollHint('.js-scrollable-', {
        i18n: {
          scrollable: 'スクロールできます'
        }
    });
</script>

シャドウを追加する

<script>
    new ScrollHint('.js-scrollable-03', {
        suggestiveShadow: true
    });
</script>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Deno で画像ファイルの RGBA を読み書きしてモザイクをかける

Deno で画像ファイルを読み込み、RGBA の情報を書き換えて保存する方法になります。

以前、ImageMagick でモザイクをかけようと思ったのですが、自分の求める条件を完全には満たさなかったため、Deno で画像ファイルにモザイクをかけるプログラムを自作してみました。

参考「ImageMagick でモザイクをもっと綺麗にかける - Qiita

1. はじめに

1.1. ここでのモザイクの条件

  • 色を平均した正方形が並ぶようなモザイク (ピクセル化 / Pixelize)
  • 指定した大きさごとに正方形にして、画像サイズが割り切れない場合も上手くする

1.2. Deno で画像ファイルを読み書きする手段

Deno で画像ファイル関係のライブラリは主に以下の 2 つがありますが、いずれも未完成版のようです。

そのため今回は Deno.run() を用いて、ImageMagick のコマンドを実行して画像の読み書きをすることにしました。

ここでは ImageMagick は画像の読み書きのために使用し、画像データの加工はしません。

※ ImageMagick を使用したり、コマンドを直接実行することから、外部に公開してるサーバー上で動かすのはお勧めできません。
※サーバー上で使用したい場合にはセキリティ対策をする必要があります。

2. ソースコード

Ubuntu で動作確認しています。

pixelize.js
// 
const decoder = new TextDecoder();

// 
const readImageInfo = async path => {

    const p = Deno.run({
        cmd: ['convert', path, '-format', '{ "width": %[fx:w], "height": %[fx:h] }\\n', 'info:-'],
        stdout: 'piped'
    });

    const infoUint8Array = await p.output();

    const { success } = await p.status();
    if ( ! success ) return;

    p.close();

    const infoText = decoder.decode(infoUint8Array);
    const info = JSON.parse(infoText);

    return info;

}

const readImageRgba = async path => {

    const p = Deno.run({
        cmd: ['convert', path, 'rgba:-'],
        stdout: 'piped'
    });

    const rgba = await p.output();

    const { success } = await p.status();
    if ( ! success ) return;

    p.close();

    return rgba;

}

const writeImageRgba = async (path, rgba, width, height) => {

    const p = Deno.run({
        cmd: ['convert', '-size', '' + width + 'x' + height, '-depth', '8', 'rgba:-', path],
        stdin: 'piped'
    });

    await Deno.writeAll(p.stdin, rgba);
    p.stdin.close();

    const { success } = await p.status();
    if ( ! success ) return;

    p.close();

};

// 
const forRange = (begin, end, step = 1) => callback => {
    for (let i = begin; i < end; i += step) {
        callback(i);
    }
};

// 
const [imageInputPath, imageOutputPath, blockSizeStr] = Deno.args;

const blockSize = parseInt(blockSizeStr);

// 
const info = await readImageInfo(imageInputPath);
const rgba = await readImageRgba(imageInputPath);

// 
const pixelSize = 4;

const size = info.width * info.height * pixelSize;
const blockRowSize = info.width * blockSize * pixelSize;
const widthSize = info.width * pixelSize;
const blockWidthSize = blockSize * pixelSize;

const ibm = size;
const ibs = blockRowSize;
forRange(0, ibm, ibs)(ib => {

    const jbm = ib + widthSize;
    const jbs = blockWidthSize;
    forRange(ib, jbm, jbs)(jb => {

        const cm = jb + pixelSize;
        forRange(jb, cm)(c => {

            let pixelCount = 0;
            let pixelColor = 0;

            const jm = Math.min(c + blockWidthSize, ib + widthSize);
            const js = pixelSize;
            const forRangeBlock = callback => forRange(c, jm, js)(j => {

                const im = Math.min(j + blockRowSize, ibm);
                const is = widthSize;
                forRange(j, im, is)(i => {
                    callback(i);
                });

            });

            forRangeBlock(i => {
                pixelColor += rgba[i];
                pixelCount++;
            });

            pixelColor /= pixelCount;

            forRangeBlock(i => {
                rgba[i] = pixelColor;
            });

        });

    });

});

await writeImageRgba(imageOutputPath, rgba, info.width, info.height);
使用例
deno run --allow-run pixelize.js image.jpg image_pixelized.jpg 48
image.jpg image_pixelized.jpg
image.jpg image_pixelized.jpg

3. 説明

3.1. Deno.run() による convert コマンドの実行

Deno.run() を用いて ImageMagick の convert コマンドを実行します。

標準入出力をパイプして情報をやり取りします。

画像の幅と高さを JSON 形式で標準出力
convert <path> -format '{ "width": %[fx:w], "height": %[fx:h] }\n' info:-
画像を生の RGBA データに変換して標準出力
convert <path> rgba:-
生の RGBA データを標準入力、画像ファイルに変換して保存
convert -size <width>x<height> -depth 8 rgba:- <path>

参考「function Deno.run() - deno doc
参考「class Deno.Process - deno doc
参考「Input Filename - Command-line Processing - ImageMagick

3.2. モザイクの計算

色を平均するだけですが、なるべく速く、あまりメモリを消費せずに計算する方法を探しました (有名なアルゴリズム等は特にない?) 。

まず、色を平均したいブロックの位置を計算して、ブロックごとに平均の色で上書きすることで、計算用のメモリ使用量を小さくできます。

そして、RGBA のデータから目的のピクセルのデータの位置を計算する際に、ピクセルのX, Y 座標をから RGBA データのインデンクスを毎回計算 (index = width * y + x) するよりも、端から順に足し算を用いたほうが計算量を減らせます。

test01.png

結論からいうと、

  1. ブロックの Y 座標 (データ位置 ib)
  2. ブロックの X 座標 (データ位置 jb)
  3. ブロック内の X 座標 (データ位置 j)
  4. ブロック内の Y 座標 (データ位置 i)

の順に座標を計算すると、幅や高さの最大値を考慮しながら足し算の繰り返しで上手くピクセルを走査することができます。

他の順番では上手く走査することができません (座標の最大値の計算が上手くてきない) 。

4. ImageMagick コマンドのみでモザイクをかける場合との比較

前回の記事で、ImageMagick コマンドのみでモザイクをかけた場合との比較です。

ここではマスクの処理はしません。

参考「ImageMagick でモザイクをもっと綺麗にかける - Qiita」(再掲)

4.1. ブロック単位で割り切れない領域の色の問題の解決

前回の記事の方法では、ブロック単位で割り切れない領域の色を求めるとき、画面端の色を引き延ばすことで疑似的に求めていましたが、周辺の色が近い色でないと本来の色から離れてしまう問題がありました。

今回の方法ではその問題を解決できています。

画像サイズ 1600px、ブロックサイズ 90px の例:

元画像
(画像の下 1px と右 1px だけが黒)
前回の方法
ピクセル化画像
今回の方法
ピクセル化画像
test2.png test2_pixelized.png test2_pixelized2.png

4.2. 実行時間

実行時間は変換画像やブロックサイズにより変動するため、しっかり比較することはむずかしいですが、おおよそ実行時間のオーダーは同じようです。

time コマンドによる実行時間の計測結果です (しっかり平均したりしていませんが、参考程度に) 。

画像サイズ 1600px、ブロックサイズ 90px の例:

前回の方法 今回の方法
real 0m0.945s 0m0.939s
user 0m0.750s 0m0.672s
sys 0m0.219s 0m0.281s

前回の方法では、場合により real 実行時間が半減することがあるようです (ImageMagick か複数の CPU を使用して処理している?) 。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Vue.js】ElementUIでテーブルを作るときにデータを加工したくなった

はじめに

VueのUIライブラリ「ElementUI」でテーブルを作成するときに、
データを加工しようとしたら少しハマったので今回記事にさせて頂きました。

開発環境

  • MacOS
  • vue 2.6.11
  • vuecli 4.4.6
  • element-ui 2.13.2
  • vue-moment 4.1.0
  • moment-timezone 0.5.31

テーブルを作る

とりあえずこんな感じのテーブルを作ります。
コンポーネントについては、公式ドキュメントが参考になります。
スクリーンショット 2020-07-25 0.00.53.png

<template>
    <div class="result-area">
        <el-table
            :data="tableData">
            <el-table-column
                prop="id"
                label="ID"
                width="100">
            </el-table-column>
            <el-table-column
                prop="name"
                label="ニックネーム"
                width="200">
            </el-table-column>
            <el-table-column
                prop="created_at"
                label="作成日"
                width="200">
            </el-table-column>
        </el-table>
    </div>
</template>
<script>
import CommonHelper from '../helper/CommonHelper';

export default {
    mixins: [CommonHelper],
    data() {
        return {
            tableData: [{
                id: 1,
                name: '山田太郎',
                created_at: '2020-07-01 10:00:00'
            }, {
                id: 2,
                name: '鈴木一郎',
                created_at: '2020-07-02 11:00:00'
            }, {
                id: 3,
                name: '菅田将暉',
                created_at: '2020-07-03 12:00:00'
            }]
        };
    }
}
</script>

データを加工して表示

このテーブルの作成日を日本語に直して表示したい。
スクリーンショット 2020-07-25 0.20.23.png

<template>
    <div class="result-area">
        <el-table
            :data="tableData">
            <el-table-column
                prop="id"
                label="ID"
                width="100">
            </el-table-column>
            <el-table-column
                prop="name"
                label="ニックネーム"
                width="200">
            </el-table-column>
            <el-table-column
                prop="created_at"
                label="作成日"
                width="200">
                <template slot-scope="scope">
                    {{date_format(scope.row.created_at)}}
                </template>
            </el-table-column>
        </el-table>
    </div>
</template>

scopeを定義してあげて、date_formatという関数を使用して変換しています。
scope.row.created_at」の「row」を忘れていて、少しハマってしまいました。
一応関数も書いておきます。

export default {
    methods: {
        date_format(date) {
            let format_date = this.$moment(date);
            format_date.locale('ja');
            return format_date.format('LL');
        }
    }
}

おまけ

この書き方を利用して、こんな感じのもできます。
スクリーンショット 2020-07-25 0.36.34.png

<template>
    <div class="result-area">
        <el-table
            :data="tableData">
            <el-table-column
                prop="id"
                label="ID"
                width="100">
            </el-table-column>
            <el-table-column
                prop="name"
                label="ニックネーム"
                width="200">
            </el-table-column>
            <el-table-column
                prop="id">
                <template slot-scope="scope">
                    <el-button
                        size="mini"
                        @click="pushDetail(scope.row.id)">
                        <i class="el-icon-edit-outline"></i>
                    </el-button>
                </template>
            </el-table-column>
        </el-table>
    </div>
</template>
<script>
import CommonHelper from '../helper/CommonHelper';

export default {
    mixins: [CommonHelper],
    data() {
        return {
            tableData: [{
                id: 1,
                name: '山田太郎',
                created_at: '2020-07-01 10:00:00'
            }, {
                id: 2,
                name: '鈴木一郎',
                created_at: '2020-07-02 11:00:00'
            }, {
                id: 3,
                name: '菅田将暉',
                created_at: '2020-07-03 12:00:00'
            }]
        };
    },
    methods: {
        pushDetail(id) {
            console.log(id);
        }
    }
}
</script>

アイコンを押すと、methodsにidを渡すことができます。

おわりに

ElementUIはコンポーネントも豊富で使いやすいのですが、
日本語の記事が少なめなので、もっと記事が増やしていきたいと思います。

よろしければ、フォローお願いします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JS初心者がKintoneカスタマイズ行うために その2

お疲れ様です〜「りょうちん」です。

やーKintoneのカスタマイズ難しいです!

というのも「Kintone JavaScript API」と「Kintone REST API」という関数群が存在するのですがごっちゃになってました。

kintone rest apiとjavascript apiに関して
kintone カスタマイズでできること
kintone 連携カスタマイズでできること

同じく迷ってる人はこの辺り参考にしてみるといいかも知れません。

まだ完全に把握出来ていませんが、
Kintone JavaScript APIはKintoneの動作や見た目の変更できるAPI。
Kintone REST APIは主にkintoneアプリのレコード操作等が行えるAPI。という認識です。

kintone rest apiとjavascript apiに関して

両者に関係があるとすれば、kintone JavaScript APIで準備されているkintone.api() という関数を使うと、JavaScriptでkintoneのREST APIの操作が多少容易に行えるというくらいです。

上記の引用の通りですが、両者は切り離して考える方が理解しやすくなると思います。

では、また次回!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

validate.jsを使ってクライアントバリデーションを設定してHTMLで躓いた話

結論:input type=numberは電話番号や郵便番号に使ってはいけないよという事。

やりたい事

  • テキストボックスに全角文字、半角英字、「0」で始まる数値を入力できないようにする
  • 「0」始まる数値が入力された時はvalidate.jsを使ってフォームを送信できないようにする

参考

input type=”number”の落とし穴 – “number”を使うと先頭の0が認識されない
https://plustrick.com/input_type_number/
validate.js
https://validatejs.org/
jQuery Validation 独自のルールを追加する
https://symfoware.blog.fc2.com/blog-entry-2354.html

HTML

input type=textで全半角英字・全角数字を入力できない関数をJsで作ってみたけど、テンキーで数字が入力できなかったり、tabキーを使えないなど、様々考慮する事があったので、ここは手っ取り早くinput type=numberを使う事にした。

<form id="form">
  <input type=number id="number" name= "number">
  <!-- validate.jsでエラーにかかったらエラーメッセージを表示する -->
  <p class="number-err-msg"></p>
</form>

スピンボタン(右のボタン)を消したければ以下をcssに追加しましょう

<!-- ChromeSafari -->
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button {
    -webkit-appearance: none;
    margin: 0;
}
<!-- FirefoxIE -->
input[type="number"] {
    -moz-appearance:textfield;
}

Js

サーバーバリデーションもかけていますが、クライアントバリデーションもかけます。validate.jsの使い方は割愛します

$.validator.addMethod(onlyHalfnumber, function(value, element) {
    if ( this.optional( element ) ) {
        return true;
    }
        <!-- 先頭が0にマッチする場合 -->
        if (value == /^0/) {
            return false;
        }
        return true;
    }, '先頭が0はダメですっ!!');
}

    $("#form").validate({
        rules: {
            number: {
                onlyHalfNumber: true
            }
        }
    });

これでバリデーション設定完了のはずが...

フォームが送信されて登録されてしまいました。原因が冒頭で書いたinput type=nemberですね。数字の先頭が「0」で始まっていると、「0」の部分は「無し」という認識に変わってしまうようです。

結論

input type=numberを使えば、整数以外の入力はできないので便利なのですが、先頭に「0」を付ける場合(電話番号など)type=textを大人しく使うかtype=telを使うのが良さそうですね。HTMLは悪意があればいくらでも書き換えられるのでサーバーバリデーションを設定するのをお忘れなく

終わり
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RailsでGoogleMapsAPIを表示・ピン表示まで

はじめに

過去に自分のポートフォリオでGoogleMapsAPIを利用していましたが、流れを忘れていたのでリマインドです。

環境

rails 5.2.3
ruby 2.6.3

今回すること

・現在地情報から経度緯度を取得しマーカーを表示する。
・取得した一覧情報をjson形式に変え、マーカーとして地図上に配置。
・地図の表示

表示させるコントローラー作成、今回アクション名はsearchで。

spaces_controller.rb
  def search
    @spaces = Space.all
  end

モデルの作成、住所を入力後緯度経度を自動取得させる。

space.rb
class Space < ApplicationRecord
  geocoded_by :address
  after_validation :geocode, if: :address_changed?
end
schema.rb
create_table "spaces", force: :cascade do |t|
    t.string "address"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.float "latitude"
    t.float "longitude"
end

MAPを表示させるscript

javascript
  function initMap() 
    //マップを表示させる箇所
    var target = document.getElementById('gmap');
    //マップ表示
    var map = new google.maps.Map(document.getElementById('gmap'), {
      center: {lat: 35.681167, lng: 139.767052},//中心点設定
      zoom: 8//mapの拡大率設定
    });

    //現在地マーカーの作成
    //現在地取得できない場合
    if(!navigator.geolocation){
      infoWindow.setPosition(map.getCenter());
      infoWindow.setContent('Geolocation に対応していません。');
      infoWindow.open(map);
    }

    navigator.geolocation.getCurrentPosition(function(position) {
      //現在地の軽度緯度取得
      var pos = { lat: position.coords.latitude, lng: position.coords.longitude };
      var mark = new google.maps.Marker({
        //ドロップダウンアニメーションの設定
        animation: google.maps.Animation.DROP,
        position: pos,
        map: map,
        // 現在地ピンのアイコン作成。今回デフォルトのデザインは複数のマーカーに使用するので変更。
        icon: {
          fillColor: "rgb(48, 255, 176)",
          fillOpacity: 1.0,
          path: google.maps.SymbolPath.BACKWARD_CLOSED_ARROW,
          scale: 4,
          strokeColor: "green",
          strokeWeight: 1.0
        }
      });
    }, function() {
      infoWindow.setPosition(map.getCenter());
      infoWindow.setContent('Error: Geolocation が無効です。');
      infoWindow.open(map);
    });
    //複数マーカー
    //一覧のデータをjson形式に
    var spaces = #{raw @spaces.to_json};
    var marker = [];
    var info;
    for(var i = 0; i < spaces.length; ++i) {
      //一覧から一つずつ経度緯度の情報を取り出してマーカー作成
      spot = new google.maps.LatLng({lat: spaces[i].latitude, lng: spaces[i].longitude});
      marker[i] = new google.maps.Marker({
        position: spot,
        map: map,
        animation: google.maps.Animation.DROP
      });
      markerEvent(i);
    }
    //複数マーカーのhover時イベント
    function markerEvent(i) {
      marker[i].addListener('mouseover', function() {
        var target = $('#' + (i + 1));
        $(".highlight").removeClass("highlight");
        target.addClass("highlight");
        var position = target.offset().top - 280;
        $('body,html').animate({scrollTop:position}, 400, 'swing');
      });
    }
    //-------------------------------------------------------------
  }


script src="https://maps.googleapis.com/maps/api/js?key=#{ENV[""]}&callback=initMap" async defer

今回はテンプレートエンジンをslimにしてその中に直接書いているのでデータの変数格納のあたりが通常と違っているので注意。
gemのgeocoderを使用すると名前から経度と緯度を取得するのが非常に楽ですが、精度が悪く、番地単位まで表示してくれないので今回はGoogle Geocodingを使用しています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む