20200808のJavaScriptに関する記事は17件です。

【JS】JSを根本から再復習、関数とかオブジェクトとか

関数

JSの世界では、関数は実行可能なオブジェクトです。
具体的には、関数を引数として渡すことができたりします・。

コールバック関数

function hello() {
    console.log('hello');
}

// 引数のcallbackに hello が渡ってくる
// この引数のことを callback 関数と呼びます
function sample(callback) {
    callback("yo");
}

sample(hello);
// 無名関数だと下記のような書き方になる
sample(function(name) {
    console.log('hello ' + name);
});

this

実行コンテキストによって this の参照先は変わることになります。

呼び出し元の参照を保持し、
オブジェクト → 呼び出し元のオブジェクト
関数 → グローバルオブジェクトを指します。

callbackも関数に含まれます。

bind

this や引数の参照先を変更し、使用時点で実行しません。

束縛するとは?

thisを固定するで理解しています。
JavaScript の超便利なメソッド bind で this を制御する – Foreignkey, Inc.
関数バインディング

call, apply

thisや引数のの参照先を変更し、使用時点で実行します。

アロー関数

無名関数を記述しやすくしたもので、() => {};のように書くことができます。

無名関数とアロー関数の違い

アロー関数は、thisarguments の値を取りません。
ではどうやって this の値を取ってきてるかというと、外側の this を探しにいくことで値を取得することができるようになっています。
なので、アロー関数で定義した関数は bindを使って this を束縛することはできないことに注意します。

prototype

  • オブジェクトに存在する特別なプロパティ
  • prototypeの参照が、__proto__ に コピーされる
  • prototypeを使用する理由は、メモリの効率化
  • ES6になってからは、裏側でおごいているのがこいつになる

プロトタイプチェーン

プロトタイプの多重形成のことを指します。

下記のように __proto__が層上になっている状態をプロトタイプチェーンと言います、
image.png

優先順位

基本は階層が浅いものから呼び出されることになります。

  1. 自分自身のプロパティ
  2. コンストラクターが持っているメソッド
  3. 他にprototypeを持っているか確認していく
  4. Objectに格納されているプロパティ(ここにもなければ、undefined)

クラス

ES6からクラス表記でかけるようになっています。

class Person {
    // コンストラクタ
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    // Person.prototype.hello = function() { ... と同じことを表す
    hello() {
        console.log('hello ' + this.name);
    }
}

// インスタンス化
const bob = new Person('Bob', 23);
console.log(bob);

コンストラクタ

新しくオブジェクトを作成するための土台となるものを指します。

インスタンス化

newを使用し、コンストラクタ関数からオブジェクトを作成することを指します。

クラス継承と super

super - JavaScript | MDN

class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    hello() {
        console.log('hello ' + this.name);
    }
}

// 継承には extends を使用します
class Japanese extends Person {
    constructor(name, age, gender) {
        // 継承元(Person)のコンストラクターを呼び出すことができる
        // コンストラクターを呼び出しているため、name, ageを引数として渡している
        super(name, age);
        this.gender = gender;
    }
    hello() {
        console.log('Konnichiwa ' + this.name);
    }
}

オブジェクトリテラル
{}を使った初期化のことを指します。

チェーンメソッド

言葉の通り、メソッドを繋いでいくことを指します。

class Calculator {
    constructor() {
        this.val = null;
        // アンダーバーはこのクラス内でしか使用しないという意味です。
        this._operator;
    }

    set(val) {
        if(this.val === null) {
            this.val = val;
        } else {
            this._operator(this.val, val);
        }
        // チェーンを実装するためには、Calculatorのオブジェクトを返す必要がある
        return this
    }
    plus() {
        this._operator = function(val1, val2) {
            const result = val1 + val2
            this._equal(result)
        }
        return this
    }
    minus() {
        this._operator = function(val1, val2) {
            const result = val1 - val2
            this._equal(result);
        }
        return this
    }
    _equal(result) {
        this.val = result
        console.log(result)
        return this
    }
}

// インスタンス化
const calc = new Calculator();

// チェーンメソッド
calc.set(10)
    .plus()
    .set(3)
    .minus()
    .set(4)

ビルトインオブジェクト

  • String, Date, Numberとか
  • Javascriptエンジンによって生成される
  • JSの場合は、 String()のように宣言するのではなく、 変数に string を入れてあげることで実行される

ラッパーオブジェクト

  • プリミティブ値を内包するオブジェクト
  • new しなくても暗黙的に

シンボル

プロパティの重複を避けるために必ず一意の値を返す関数のことを指します。

スプレッド演算子

ES6だと、可変長パラメータはスプレッド演算子で表現します。
...arryのような書き方
スプレッド構文 - JavaScript | MDN

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

【JS】デフォルトで用意されているダイアログ

警告のメッセージを表示する alert()

alert("Hello");

Google chromeで、JSONデータをアラートで表示したい時

JSONデータ(data)だけを引数にしたalertメソッドだと、「Object」としか表示されない

alert(JSON.stringify(data));

こうすることで、JSONデータをアラート表示できるようになる

クローズドクエスチョンを表示する confirm()

はい、いいえかで答えられるメッセージを表示させます。

confirm(メッセージ);

実際によく使うのは、メッセージ対する返答によって処理を変えることです。

let result = confirm("このメッセージを送信しますか?");

if(result){
  //「はい」を選んだ時の処理
}else{
  //「いいえ」を選んだ時の処理
}

オープンクエスチョンを表示する prompt()

具体的な返答が求められるメッセージを表示させます。

let result = prompt(入力ダイアログに表示する文字列, 入力欄の初期値)

confirmメソッドと同様、実際によく使うのは、メッセージ対する返答によって処理を変えることです。

let name = prompt("あなたの名前は?", '太郎')

if(name){
  //入力された時の処理
}else{
  //キャンセルが押された時の処理
}

計算の答えによって、アラートを変える

let answer = prompt('1 + 1は?')

if (answer === 2){
  alert'正解です!'
} else {

}
if(answer === ''){
  alert('空欄で回答しないでください')
}else if(answer === null){
  alert('キャンセルしないでください')
}else if(answer != 2){
  alert('不正解です')
}else{
  alert('正解です!')
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

bottlejs hello ?

動機

自作のSPAをスクラップ&ビルドするので今度はDIを使ってみようと思ったけどinverify はなんだか面倒くさそうなので bottlejs を使う。

前準備

Webpackとnpm install --save bottlejs とかやっておいた。

実装

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
   <meta charset="UTF-8">
   <title>Kamishibai framework</title>
</head>
<body>
    <!-- IE11 promise -->
    <script src="https://www.promisejs.org/polyfills/promise-7.0.4.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/2.0.4/fetch.min.js"></script>
    <script src="/js/bundle.js"></script>
</body>
</html>
app.js
import Bottle from 'bottlejs'
window.kms = window.kms||{};
window.kms.Unko=function(){
    this.color = "brown";
}
window.kms.rootBottle = new Bottle();
window.kms.rootBottle.service("?",window.kms.Unko);
window.kms.unko = window.kms.rootBottle.container["?"];
alert("?の色は"+window.kms.unko.color);

image.png

結論

見事にボトルに?を詰めておくことができた。
これで何ができるかってことだけど、処理の流れとオブジェクトの作られ方が分離できるってことかなと思う。
例えば、その?に?がついていたとしても、ロジック側でその設計を考える必要がなくなるというか。あらかじめ?付きの?をボトルに詰めときゃいいじゃんっていう。

その用途だったらかなり古いけど bottlejs で十分機能するかなと思って実験中。

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

JavaScript ストップウォッチを作成

JavaScriptで簡単なストップウォッチを作ってみました。
機能的には基本のスタート/ストップのほか、ラップタイム/スプリットタイムも付けました。

01.jpg
02.jpg
実働サンプル

CSS、JavaScriptともに必要最小限の記述しかしていませんので処理も追いやすいかと思います。

index.html
<!doctype html>
<html lang='ja'>
<head>
<meta charset='utf-8'>
<meta name='viewport' content='user-scalable=no,width=device-width,initial-scale=1'>
<title>stopwatch</title>
<style>
    html {
        touch-action: manipulation;
        -webkit-touch-callout: none;
        -webkit-user-select: none;
    }
    div.button {
        box-sizing: border-box;
        position: relative;
        width: 100%;
        text-align: center;
    }
    input[type='button'] {
        width: 48%;
        height: 50px;
        max-width: 180px;
        font-size: 25px;
        border-radius: 8px;
        background-color: #fff;
        -webkit-appearance: none;
        -webkit-user-select: none;
    }
    div#disp {
        position: relative;
        text-align: center;
        font-size: 20px;
        margin-bottom: 20px;
    }
    div#lap {
        position: relative;
        text-align: left;
        font-size: 18px;
        height: 300px;
        overflow-y: scroll;
        width: 340px;
        min-height: 200px;
        margin-top: 60px;
        margin-bottom: 10px;
        left: 38px;
    }
    div#mask {
        position: relative;
        width: 320px;
        overflow: hidden;
        margin: auto;
    }
    div#lap>div {
        height: 30px;
    }
    #container {
        width: 100%;
        text-align: center;
    }
</style>
</head>
<body>
    <div id='container'>
        <div id='mask'>
            <div id='lap'></div>
        </div>
        <div id='disp'>0:00:00.000 / 0:00:00.000</div>
        <div class='button'>
        <input type='button' id='start' value='START'>
        <input type='button' id='reset' value='RESET'>
        </div>
    </div>
    <script src='./stopwatch.js'></script>
</body>
</html>
stopwatch.js
"use strict";

const lapCount  = 50; // ラップ保持数

const storage = getStorage();

let state       = storage.state       ?? 0, // 動作状態 0:停止中 1:動作中
    startTime   = storage.startTime   ?? 0, // スタートタイム保持
    stopTime    = storage.stopTime    ?? 0, // ストップタイム保持
    lapTime     = storage.lapTime     ?? 0, // ラップスタートタイム保持
    lapStopTime = storage.lapStopTime ?? 0, // ラップストップタイム保持
    id; // setInterval ID

onload = function() {
    // localStorageに動作状態が保存されていた場合はパラメータ復元
    if(state === 1) {
        if(id = setInterval(printTime, 1)) {
            document.querySelector('#start').value = 'STOP';
            document.querySelector('#reset').value = 'LAP';
        }
        setStorage();
    }
}

const eventHandlerType =
    document.querySelector('#start').ontouchstart !== undefined ? 'touchstart' : 'mousedown';

// START押下時イベント
document.querySelector('#start').addEventListener(eventHandlerType, function() {
    // 停止中
    if(state === 0) {
        // カウント開始
        if(id = startCount()) {
            // ボタンのラベル変更
            document.querySelector('#start').value = 'STOP';
            document.querySelector('#reset').value = 'LAP';
            // 動作状態を変更
            state = 1;
            setStorage();
        }
    }
    // 動作中
    else {
        // カウントインターバルが動作中
        if(id) {
            // インターバル停止
            clearInterval(id);
            // カウント停止
            stopCount();
            // ボタンのラベルを戻す
            document.querySelector('#start').value = 'START';
            document.querySelector('#reset').value = 'RESET';
            // 動作状態を変更
            state = 0;
            deleteStorage();
        }
    }
}, false);

// RESET押下時イベント
document.querySelector('#reset').addEventListener(eventHandlerType, function() {
    // 停止中ならリセット
    if(state === 0) {
        stopTime    = 0;
        lapStopTime = 0;
        // 表示初期化
        document.querySelector('#lap').innerHTML = '';
        document.querySelector('#disp').textContent = '0:00:00.000 / 0:00:00.000';
    }
    // 動作中ならLAP動作
    else {
        // #lap内の最後にdiv要素追加
        document.querySelector('#lap').appendChild(document.createElement("div"));

        // 追加した要素に経過時間表示
        document.querySelector('#lap>div:last-of-type').textContent = getTimeString();

        lapTime = Date.now();

        // lap保持数を超えたら先頭の子要素を削除
        if(document.querySelector('#lap').childElementCount > lapCount)
            document.querySelector('#lap').removeChild(document.querySelector('#lap').childNodes[0]);

        // スクロール位置を最下部に
        document.querySelector('#lap').scrollTop = document.querySelector('#lap').scrollHeight;

        setStorage();
    }
}, false);

// カウント開始
function startCount() {
    const now = Date.now();
    startTime = now - stopTime;
    lapTime   = now - lapStopTime;
    return setInterval(printTime, 1);
}

// カウント停止
function stopCount() {
    const now   = Date.now()
    stopTime    = now - startTime;
    lapStopTime = now - lapTime;
}

// 経過時間表示
function printTime() {
    document.querySelector('#disp').textContent = getTimeString();
}

function getTimeString() {
    const
        now       = Date.now(),
        time      = now - startTime,
        splitTime = now - lapTime,

        main =
            Math.floor(time / 3600000) + ':' +
            String(Math.floor(time / 60000) % 60).padStart(2, '0') + ':' +
            String(Math.floor(time / 1000) % 60).padStart(2, '0') + '.' +
            String(time % 1000).padStart(3, '0'),

        split =
            Math.floor(splitTime / 3600000) + ':' +
            String(Math.floor(splitTime / 60000) % 60).padStart(2, '0') + ':' +
            String(Math.floor(splitTime / 1000) % 60).padStart(2, '0') + '.' +
            String(splitTime % 1000).padStart(3, '0');

    return main + ' / ' + split;
}

// localStorage保存
function setStorage() {
    localStorage.setItem('stopwatch_params', JSON.stringify({
        state: state,
        startTime: startTime,
        stopTime: stopTime,
        lapTime: lapTime,
        lapStopTime: lapStopTime,
    }));
}

// localStorage削除
function deleteStorage() {
    localStorage.removeItem('stopwatch_params');
}

// localStorage取得
function getStorage() {
    const params = localStorage.getItem('stopwatch_params');
    return params ? JSON.parse(params) : {};
}

追記

計測開始時に必要なパラメータをCookieに保存し、動作中にブラウザを閉じたりリロードしても復帰できるようにしました。
(ラップタイムの履歴は保存しません)
保存されたCookieは、計測停止時、または2日経過した時点で削除されます。

追記2

動作時パラメータの保存先をCookieからlocalStorageに変更しました。

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

GASで毎朝その日の予定なんかを通知するLINE Bot - 1

この記事について

Qiita初投稿のうえ、プログラミング初心者です。至らぬ点がたくさんあるとは思いますが暖かく見守ってください…
Google Apps Scriptを用いて毎朝LINEでその日のいろいろを教えてくれるBotを作ったので解説も兼ねてご紹介します。

この記事で取り扱う内容

  • Google Apps Scriptの基本
  • JavaScriptの基本~発展
  • LINE Messaging API
  • Flex Message
  • Webスクレイピング(天気取得)
  • Googleカレンダー(祝日取得)
  • TimeTree API(予定取得)

実際どんな感じ?

以下の画像のようなLINE Botになります

表示するもの

  • 日付
  • 祝日
  • 収集があるごみ
  • 天気(最高気温)
  • 予定(TimeTreeから)

GASプロジェクトを作成

こちらから新規のGoogle Apps Script プロジェクトを作成します。

アドレスバーにscript.newで新規プロジェクトを作成できます。
.new ドメインを利用したショートカット一覧(英語)

注意すること

V8 ランタイムを搭載したプロジェクトでのみ機能します(新規プロジェクトは最初から搭載しています)。
既存のプロジェクトを使う場合は、それがV8 ランタイムを搭載したプロジェクトであることを確認してください。
プロジェクトを開いた際にこのプロジェクトは Chrome V8 を搭載した新しい Apps Script ランタイムで実行しています。という表示があれば問題ありません(詳細)。

ええい、これが完成したコードだ!

(自分の拙いコードを公開するのは結構勇気が要りますね)

コード.js
function notify() {

  const garbage = [0, '資源ごみ', '可燃ごみ', 'カン・ビン', '不燃ごみ', '可燃ごみ', 'プラスチックごみ'];
  const area_code = 317;

  const d = new Date();
  const today = (d.getMonth() + 1) + '' + d.getDate() + '日(' + '日月火水木金土'[d.getDay()] + '';


  const [event_holiday] = CalendarApp.getCalendarById('ja.japanese#holiday@group.v.calendar.google.com').getEventsForDay(d);
  const holiday = event_holiday ? event_holiday.getTitle() : '';


  let weather = "不明";
  let temp = "?℃"
  const content = UrlFetchApp.fetch('https://www.jma.go.jp/jp/yoho/' + area_code + '.html').getContentText();

  let pos = content.indexOf('<th class="weather">', content.indexOf('<th class="weather">') + 1);
  if (~pos) weather = content.substring(pos + 20, content.length);
  pos = weather.indexOf('"><br>\n</th>');
  if (~pos) weather = weather.substring(0, pos);
  pos = weather.indexOf('alt=');
  if (~pos) weather = weather.substring(pos + 5, weather.length);

  const words = {
    '時々': '|',
    '一時': '|',
    '': '»',
    '晴れ': '',
    '曇り': '',
    '': '',
    '': ''
  };
  for(let key in words) {
    weather = weather.replace(key, words[key]);
  }

  pos = content.indexOf('<td class="max">');
  if (~pos) {
    temp = content.substring(pos + 16, content.length);
    temp = temp.substring(0, temp.indexOf('')) + '';
  }


  let flex = {
    'type': 'bubble',
    'size': 'giga',
    'body': {
      'type': 'box',
      'layout': 'vertical',
      'contents': [
        {
          'type': 'text',
          'text': today,
          'weight': 'bold',
          'size': 'xxl',
          'flex': 0
        }, {
          'type': 'box',
          'layout': 'horizontal',
          'contents': [
            {
              'type': 'filler'
            }, {
              'type': 'text',
              'text': weather,
              'size': 'lg',
              'color': '#444444',
              'flex': 0,
              'gravity': 'center'
            }
          ]
        }, {
          'type': 'box',
          'layout': 'horizontal',
          'contents': [
            {
              'type': 'filler'
            }, {
              'type': 'text',
              'text': temp,
              'size': 'lg',
              'color': '#f44336',
              'margin': 'xs',
              'flex': 0,
              'gravity': 'center'
            }
          ]
        }, {
          'type': 'separator',
          'margin': 'xl',
          'color': '#808080'
        }, {
          'type': 'box',
          'layout': 'vertical',
          'contents': [
            //EVENTS
          ],
          'margin': 'xl'
        }
      ]
    }
  };


  if (holiday != '') {
    flex.body.contents[1].contents.splice(0, 0, {
      'type': 'text',
      'text': holiday,
      'size': 'md',
      'color': '#808080',
      'flex': 0,
      'gravity': 'center'
    });
  }


  if (garbage[d.getDay()]) {
    let line = holiday == '' ? 1 : 2;
    flex.body.contents[line].contents.splice(0, 0, {
      'type': 'text',
      'text': garbage[d.getDay()],
      'size': 'md',
      'color': '#808080',
      'flex': 0,
      'gravity': 'center'
    });
  }


  const props = PropertiesService.getScriptProperties();

  const TIMETREE_TOKEN = props.getProperty('TIMETREE_TOKEN');
  const opt = {
    'headers': {
      'Authorization': 'Bearer ' + TIMETREE_TOKEN
    },
    'method': 'get'
  };
  const cals = JSON.parse(UrlFetchApp.fetch('https://timetreeapis.com/calendars', opt).getContentText()).data;
  const z = (t) => ('0' + t).slice(-2);
  let ev_exists = 0;

  for (let key in cals) {
    let cal = JSON.parse(UrlFetchApp
                         .fetch('https://timetreeapis.com/calendars/' + cals[key].id + '/upcoming_events?timezone=Asia/Tokyo&days=1', opt)
                         .getContentText()).data;
    for (let k in cal) {
      let {title, start_at, end_at, all_day} = cal[k].attributes;
      let start = new Date(start_at);
      let end = new Date(end_at);
      let time = all_day ? '終日' : z(start.getHours()) + ':' + z(start.getMinutes()) + '-' + 
        z(end.getHours()) + ':' + z(end.getMinutes());
      let schedule = {
        'type': 'box',
        'layout': 'horizontal',
        'contents': [
          {
            'type': 'text',
            'text': time,
            'flex': 0,
            'color': '#808080',
            'gravity': 'center',
            'size': 'md'
          }, {
            'type': 'text',
            'text': title,
            'size': 'lg',
            'weight': 'bold',
            'color': '#606060',
            'flex': 0,
            'gravity': 'center',
            'margin': 'lg'
          }
        ],
        'margin': 'sm'
      };
      flex.body.contents[4].contents.push(schedule);
      ev_exists |= 1;
    }
  }

  if (!ev_exists) {
    flex.body.contents.splice(3, 2);
  }

  const LINE_TOKEN = props.getProperty('LINE_TOKEN');
  const payload = {
    'messages': [
      {
        'type': 'flex',
        'altText': today,
        'contents': flex
      }
    ]
  };

  const opt_line = {
    'headers': {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': 'Bearer ' + LINE_TOKEN
    },
    'method': 'post',
    'payload': JSON.stringify(payload)
  };

  UrlFetchApp.fetch('https://api.line.me/v2/bot/message/broadcast', opt_line);
}

もちろん、これだけでは機能しません。

アクセストークンを取得し設定する必要があるのですが、長くなりましたので、手順は次の記事でご紹介します。

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

three.js オブジェクトがカメラに収まるように奥行きを自動調整させる

概要

three.jsに限らずですが、何らかの3Dオブジェクトをカメラにちょうど収めたいという場面があると思います。固定アスペクト比なら手動で調節しても良いですが、アスペクト比が変動する場合、物体がはみ出したり小さすぎたりしてしっくりこない場合があります。
なので、いい感じにオブジェクトが収まるようにカメラの奥行きを自動調節するものをthree.jsで実装してみました。

デモ

以下が今回作ったデモです。
画面をドラッグすると物体が回転しますが、ちょうど直方体全体がカメラに収まるように調整されているのがわかると思います。

https://arihide.github.io/demos/fit_camera/
camera-fit.gif

ソースコードはこちら
https://github.com/Arihide/demos/tree/master/fit_camera

解説

camera-fit.png
やりたいことを改めて説明すると、上の画像のように現在のカメラ位置ではスクリーンに収まらない場合に、カメラの奥行きを調節することで収まるようにします。

これを2つの手順で実現します。
1. スクリーンの中心から一番遠い点を求める
2. スクリーンから一番遠い点が画面内に収まるように奥行きを調整する

1. スクリーンの中心から一番遠い点を求める

物体すべてをカメラに収めるとは、言い換えると「その物体の中で一番スクリーンから遠い頂点を画面に収める」のと等しいです。
なので、ここではその頂点を求めたいワケですが、今回は泥臭く全ての頂点を1つ1つ射影して、どの点が最も遠いか求めています。

        { // 1. スクリーン座標の中心からみて最も遠い点を求める
            let max = -Infinity, maxIndex = 0
            let point = new THREE.Vector3()

            for (let i = 0; i < 8; i++) {

                point.set(
                    i & 0b100 ? this._targetBox.max.x : this._targetBox.min.x,
                    i & 0b010 ? this._targetBox.max.y : this._targetBox.min.y,
                    i & 0b001 ? this._targetBox.max.z : this._targetBox.min.z,
                ).sub(this._targetCenter).project(this.camera)

                let pointMax = Math.max(point.x, point.y, -point.x, -point.y)

                if (pointMax >= max) {
                    max = pointMax
                    maxIndex = i
                }
            }

            farthestPoint.set(
                maxIndex & 0b100 ? this._targetBox.max.x : this._targetBox.min.x,
                maxIndex & 0b010 ? this._targetBox.max.y : this._targetBox.min.y,
                maxIndex & 0b001 ? this._targetBox.max.z : this._targetBox.min.z,
            )
        }

2.スクリーンから一番遠い点が画面内に収まるように奥行きを調整する

次に、先ほど求めた頂点をカメラに収める処理について説明しますが、説明のため視錐台をy軸方向から見た画像を載せます。
camera-fit-explain.png
ここで、
- 座標軸原点は現在のカメラ位置
- $\theta$ はカメラの画角
- $(z, x)$ は先ほど求めたスクリーンから一番遠い点
です。
図からもわかるように$(z, x)$は描画領域に収まっていない(=スクリーンの外にある)です。
なので、ちょうど収まるようなカメラと点の距離 $z_{target}$ を求めます。

正接の定義から以下が成り立ちます。

\tan (\theta / 2) = \frac{x}{z_{target}}  

これを次のように式変形します。

z_{target} = z \cdot \frac{x}{z \tan(\theta / 2)} 

ここで、$\frac{x}{z \tan(\theta / 2)} $は実はxをパースペクティブ射影変換した物と等価です。
(詳しくは完全ホワイトボックスなパースペクティブ射影変換行列を参照)
そこで、この射影変換をprojectと書くとすると、$z_{target}$ の導出式は次のように書けます。

z_{target} = z \cdot project(x)

最後に
1. スクリーンの中心から一番遠い点を求める
2. スクリーンから一番遠い点が画面内に収まるように奥行きを調整する
の処理を実際に行っているソースコードを掲載します。
(CameraFitter.jsのfitCamera()関数部分)

        const targetToCamera = new THREE.Vector3()
        const farthestPoint = new THREE.Vector3() // スクリーン座標中心から最も遠い点

        this.camera.lookAt(this._targetCenter)

        { // 1. スクリーン座標の中心からみて最も遠い点を求める
            let max = -Infinity, maxIndex = 0
            let point = new THREE.Vector3()

            for (let i = 0; i < 8; i++) {

                point.set(
                    i & 0b100 ? this._targetBox.max.x : this._targetBox.min.x,
                    i & 0b010 ? this._targetBox.max.y : this._targetBox.min.y,
                    i & 0b001 ? this._targetBox.max.z : this._targetBox.min.z,
                ).sub(this._targetCenter).project(this.camera)

                let pointMax = Math.max(point.x, point.y, -point.x, -point.y)

                if (pointMax >= max) {
                    max = pointMax
                    maxIndex = i
                }
            }

            farthestPoint.set(
                maxIndex & 0b100 ? this._targetBox.max.x : this._targetBox.min.x,
                maxIndex & 0b010 ? this._targetBox.max.y : this._targetBox.min.y,
                maxIndex & 0b001 ? this._targetBox.max.z : this._targetBox.min.z,
            )
        }

        // 2.スクリーンから一番遠い点が画面内に収まるように奥行きを調整する
        // 直方体からカメラに向かう単位ベクトル
        targetToCamera.subVectors(this.camera.position, this._targetCenter).normalize()

        // 直方体→カメラベクトルに射影
        const farthestPointProjected = new THREE.Vector3()
        farthestPointProjected.copy(farthestPoint).projectOnVector(targetToCamera)

        farthestPoint
            .sub(farthestPointProjected) // 原点を通るスクリーンに並行な面に移動する
            .project(this.camera)

        const scale = Math.max(farthestPoint.x, farthestPoint.y, -farthestPoint.x, -farthestPoint.y)

        // 結果を格納
        this.spherical.setFromCartesianCoords(
            this.camera.position.x - this._targetCenter.x,
            this.camera.position.y - this._targetCenter.y,
            this.camera.position.z - this._targetCenter.z,
        )
        this.spherical.radius = (this.spherical.radius * scale) + farthestPointProjected.length()

        // 球座標から元に戻す
        this.camera.position
            .setFromSpherical(this.spherical)
            .add(this._targetCenter)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactで作ったアプリをHerokuにデプロイする時にやること!!

こんにちは!
私は、作り終わったアプリをいつもHerokuにデプロイするのですが、大抵どっかでつまづいてしまうんですよね。
っていうことで、今日は、Reactでアプリを作った時のHerokuでのデプロイについて、私自身の実体験をもとに備忘録も兼ねてまとめてみます!

1.Heroku(ヘロク)に登録

まずは、以下のサイトに行ってアカウント登録をします。
https://jp.heroku.com/

今回は、登録の仕方の詳細は、端折らせて頂きます。

2.Create New Appする

それでは、引き続きHerokuで作業していきましょう。
下の画像のような、Newというボタンがあるので、クリックして、Create New Appします。
スクリーンショット 2020-08-08 15.47.38.png
すると、App nameの入力を求められます。お好きな名前を付けてください。(ただし、大文字などいくつか使えない文字や記号もあります。)
もし、この部分を空欄にしてしまっても、Herokuがテキトーに名前を付けてくれるので大丈夫です!
Choose a regionはUnited StatesのままでOKです。
フォームの最後にあるCreate appボタンを押せば、これでアプリケーションをデプロイするための最初の準備ができました!

3.アプリを作成する

ここでは、Reactを使って作りたいアプリを1つ作ってください。
むしろHello Worldでレンダリングするだけのページでも構いません。
作り方の解説は端折ります。

4.さぁ、デプロイしてみましょう!

おかえりなさい!いよいよデプロイです!
ターミナルを開いて、以下のコマンドを入力してください。

 heroku login 

q以外で好きなキーを押すように指示が表示される。好きなキーを押します。
すると、ブラウザが開いて、Log inと書かれたボタンが中央にあるのでクリック。
これで、ターミナルからHerokuへのログインができました。
それでは、作ったアプリを保存してあるフォルダに移動してください。

 cd 作ったアプリを保存してあるフォルダ

そして以下のコマンドを入力してください。

git init
/* Reinitialized existing Git repository inと出てきます。
in以降は、GitHubのリポジトリ名とかになってます。*/

heroku git:remote -a Herokuで登録したまたは与えられたApp name
/* set git remote heroku to っていうのが出てきます。
文字に色ついてたりしますが問題ないです。*/

git add . // addとピリオドの間に半角スペースを忘れない!

git commit -am "make it better"
/*  1 file changed, 2 insertions(+), 1 deletion(-)のような感じで、
変更したファイルの数が出てきます。何も変更していなければ、nothing to commit
って出てきます。*/

git push heroku master

少し時間がかかりますが、下にズラーと文字とか並びますが気長に待ちましょう。

5.ここから注意!エラー発生!?

Reactで作ったアプリをデプロイする時、git push heroku masterした後にエラーが起こりがちです。

! [remote rejected] master -> master (pre-receive hook declined)
error: failed to push some refs to 'https://git.heroku.com/xxx.git'

上記のエラーが出てくることがあります。(xxxはみなさんのHerokuでのApp nameです。)
ブラウザのHerokuに行って、Overviewページに移動します。そこでBuild faildと赤字で書かれているものが、ページ右半分の一番上に表記されているはずです。その項目の、View build logというリンクをクリックして見てみます。すると、buildで何が起こったのかを調べることができる「ログ」が書かれています。
その「ログ」の最後の方までスクロールしてください。以下のような文章が見つかると思います。

Some possible problems:

       - Node version not specified in package.json
         https://devcenter.heroku.com/articles/nodejs-support#specifying-a-node-js-version

       - A module may be missing from 'dependencies' in package.json
         https://devcenter.heroku.com/articles/troubleshooting-node-deploys#ensure-you-aren-t-relying-on-untracked-dependencies

/* 2つ目の、A moduleの文章は出てこない人もいると思います。
パッケージやライブラリの使用状況による!*/

6.A module may be missing from 'dependencies' in package.jsonに対する解決策

エディタで、package.jsonを開き、上から4番目くらいにある"dependencies"という項目を見てください。そこできちんと使っているライブラリやパッケージの名前とバージョンの漏れがないか確認しましょう。このエラーが出ている人は、漏れがある可能性があるので、使っているのに書いていないパッケージなどがあれば、漏れなく追加しましょう。

7.Node version not specified in package.jsonに対する解決策

「ログ」の上の方を見てみると、lock fileをアップデートしてくださいって出てくるので、以下のコマンドを実行します。

npm install

忘れないうちに、ブラウザ上のHerokuのSettingsページにある、Buildpacksに次のBuildpackを追加しておきます。

  • heroku/nodejs
  • https://github.com/mars/create-react-app-buildpack

Add buildpackというボタンをクリックして上記の2つを追加します。
heroku/nodejsというBuildpackは選択肢にあります。
2番目のBuildpackは、このようにURL表記で入力欄に入力してください。

上記の工程を終了したら、もう一度、git add .の部分からデプロイに挑戦してみてください。

それでもダメな場合、package.jsonの"scripts"の"start"部分を次のように変更してください。

"scripts": {
    "start": "node index.js", // 左のように、node index.jsにする!
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },

以上です!!
長くなりましたが、お読み頂きありがとうございました!
不備等あれば、コメントでお知らせください!

参考にさせて頂いた投稿

Reactアプリケーションをherokuにデプロイする方法

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

Day.jsでよく使う機能の覚書

JavaScriptの日付操作ライブラリであるDay.jsでよく利用する使い方について書き残しておきます。

公式ドキュメントが充実しているので、網羅的に知りたい方はそちらを見ていただいた方が早いでしょう。

基本

インストール

npm install dayjs --save

// or 

yarn add dayjs

import

import dayjs from 'dayjs';

const now  = dayjs() // 現在の日付情報を取得

日付情報の取得

例として現在の日時から各種情報を取得します。

月が0から始まる値で取得されることに注意しましょう。

dayjs().year() // 年
dayjs().month() // 月 (0~11)
dayjs().date() // 日
dayjs().day() // 曜日 0(日曜日)~6(土曜日)
dayjs().format('YYYY/MM/DD') // フォーマットして表示

日付情報の設定

dayjs().year(2020) // 日付情報の年を2020に更新
dayjs().month(8) // 日付情報の月を9月に更新
dayjs().date(12) // 日付情報の日を12日に更新
dayjs('2020-08-12') // 文字列をパースして更新

その他

日付情報の加算

dayjs().add(3, 'year') // 年に3を加算
dayjs().add(3, 'month') // 月に3を加算
dayjs().add(3, 'week') // 週に3を加算

日付情報の減算

dayjs().subtract(3, 'year') // 年に3を減算
dayjs().subtract(3, 'month') // 月に3を減算
dayjs().subtract(3, 'week') // 週に3を減算

unixtimeで出力

dayjs().unix()
dayjs().valueOf() // ミリ秒unixtime

unixtimeで日付情報の更新

dayjs.unix(1596836857)
dayjs(1596869215397)

Dateからの変換

const d = new Date(2020,8,12)
const day = dayjs(d)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TypeScript 型エイリアス vs インターフェース

基本の違い

型エイリアス ・・・ クラスやオブジェクトの規格を定義
インターフェース ・・・ 型や型の組み合わせに別名を付ける

型エイリアス
type Enemy = {
  hp: number
  mp: number
}

上記型エイリアスをインターフェースにすると、下記のようになります。
インターフェース

インターフェース
interface Enemy = {
   hp: number
   mp: number
}

クラスやオブジェクトの規格はどちらでも定義可能ですが、
両者の違いは一体なんなのでしょうか?

【違いその1】 型エイリアスは任意の型を指定できる!

型エイリアスは型の式( |&)を指定することができます。
インターフェースでは下記の記述はできません。

型エイリアス
type A = number
type B = A | string

【違いその2】 拡張の記述法が異なる

正確に言うと、extendsによる拡張はinterfaceのみで使用でき、型エイリアスは【違いその1】の型の式を使用することでextendsと同じ機能を表現可能です。

型エイリアス
type Animal = {
   name: string
}
type Dog = Animal & {
   bark: string
}
インターフェース
interface Animal = {
  name: string
}

interface Dog extends Animal {
   bark: string
}

【違いその3】 拡張の割り当てチェックの違い

【違いその2】に付随するものになりますが、 インターフェースの拡張は拡張元のインターフェースが、拡張先のインターフェースに割り当て可能かを確認します。

インターフェース
interface Base {
  ok: number | string
  ng: number
}
interface Ex extends Base {
  ok: number
  ng: string  // 拡張元に割り当て不可能なのでここでエラーになる
}

型エイリアスでの交差&では、拡張元の型と拡張先の型を結合してくれます。

型エイリアス
type Base = {
  ok: number
}
type Ex = Base & {
  ok: string | number
}

let Test: Ex = {
  ok: 333 // 結合され、 okは numberなので333が代入できる
}

【違いその4】 同名を複数宣言した場合にインターフェースはマージする

同じスコープ内で同名でinterfaceを宣言するとマージされます。
これは宣言のマージと言う機能で、型エイリアスにはない機能です。

宣言のマージ
interface Dog {
  name: string
}

interface Dog {
  bark: string
}

let dog: Dog = {
  name: 'ポチ',
  bark: 'ワンヌ'
}

参考

TypeScriptのInterfaceとTypeの比較

・Boris Cherny著『プログラミングTypeScript』(オライリー・ジャパン発行」

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

海を含めて日本を4色で塗り分ける

ソースコード

main.js
const colors = ["red", "blue", "green", "yellow"];

const prefs = {
    0: {name: "", color: null, adjacentPrefCodes: [1, 2, 3, 4, 5, 6, 7, 8, 12, 13, 14, 15, 16, 17, 18, 22, 23, 24, 26, 27, 28, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47]},
    1: {name: "北海道", color: null, adjacentPrefCodes: [0]},
    2: {name: "青森県", color: null, adjacentPrefCodes: [0, 3, 5]},
    3: {name: "岩手県", color: null, adjacentPrefCodes: [0, 2, 4, 5]},
    4: {name: "宮城県", color: null, adjacentPrefCodes: [0, 3, 5, 6, 7]},
    5: {name: "秋田県", color: null, adjacentPrefCodes: [0, 2, 3, 4, 6]},
    6: {name: "山形県", color: null, adjacentPrefCodes: [0, 4, 5, 7, 15]},
    7: {name: "福島県", color: null, adjacentPrefCodes: [0, 4, 6, 8, 9, 10, 15]},
    8: {name: "茨城県", color: null, adjacentPrefCodes: [0, 7, 9, 11, 12]},
    9: {name: "栃木県", color: null, adjacentPrefCodes: [7, 8, 10, 11]},
    10: {name: "群馬県", color: null, adjacentPrefCodes: [7, 9, 11, 15, 20]},
    11: {name: "埼玉県", color: null, adjacentPrefCodes: [8, 9, 10, 12, 13, 19, 20]},
    12: {name: "千葉県", color: null, adjacentPrefCodes: [0, 8, 11, 13]},
    13: {name: "東京都", color: null, adjacentPrefCodes: [0, 11, 12, 14, 19]},
    14: {name: "神奈川県", color: null, adjacentPrefCodes: [0, 13, 19, 22]},
    15: {name: "新潟県", color: null, adjacentPrefCodes: [0, 6, 7, 10, 16, 20]},
    16: {name: "富山県", color: null, adjacentPrefCodes: [0, 15, 17, 20, 21]},
    17: {name: "石川県", color: null, adjacentPrefCodes: [0, 16, 18, 21]},
    18: {name: "福井県", color: null, adjacentPrefCodes: [0, 17, 21, 25, 26]},
    19: {name: "山梨県", color: null, adjacentPrefCodes: [11, 13, 14, 20, 22]},
    20: {name: "長野県", color: null, adjacentPrefCodes: [10, 11, 15, 16, 19, 21, 22, 23]},
    21: {name: "岐阜県", color: null, adjacentPrefCodes: [16, 17, 18, 20, 23, 24, 25]},
    22: {name: "静岡県", color: null, adjacentPrefCodes: [0, 14, 19, 20, 23]},
    23: {name: "愛知県", color: null, adjacentPrefCodes: [0, 20, 21, 22, 24]},
    24: {name: "三重県", color: null, adjacentPrefCodes: [0, 21, 23, 25, 26, 29, 30]},
    25: {name: "滋賀県", color: null, adjacentPrefCodes: [18, 21, 24, 26]},
    26: {name: "京都府", color: null, adjacentPrefCodes: [0, 18, 24, 25, 27, 28, 29]},
    27: {name: "大阪府", color: null, adjacentPrefCodes: [0, 26, 28, 29, 30]},
    28: {name: "兵庫県", color: null, adjacentPrefCodes: [0, 26, 27, 31, 33]},
    29: {name: "奈良県", color: null, adjacentPrefCodes: [24, 26, 27, 30]},
    30: {name: "和歌山県", color: null, adjacentPrefCodes: [0, 24, 27, 29]},
    31: {name: "鳥取県", color: null, adjacentPrefCodes: [0, 28, 32, 33, 34]},
    32: {name: "島根県", color: null, adjacentPrefCodes: [0, 31, 34, 35]},
    33: {name: "岡山県", color: null, adjacentPrefCodes: [0, 28, 31, 34]},
    34: {name: "広島県", color: null, adjacentPrefCodes: [0, 31, 32, 33, 35]},
    35: {name: "山口県", color: null, adjacentPrefCodes: [0, 32, 34]},
    36: {name: "徳島県", color: null, adjacentPrefCodes: [0, 37, 38, 39]},
    37: {name: "香川県", color: null, adjacentPrefCodes: [0, 36, 38]},
    38: {name: "愛媛県", color: null, adjacentPrefCodes: [0, 36, 37, 39]},
    39: {name: "高知県", color: null, adjacentPrefCodes: [0, 36, 38]},
    40: {name: "福岡県", color: null, adjacentPrefCodes: [0, 41, 43, 44]},
    41: {name: "佐賀県", color: null, adjacentPrefCodes: [0, 40, 42]},
    42: {name: "長崎県", color: null, adjacentPrefCodes: [0, 41]},
    43: {name: "熊本県", color: null, adjacentPrefCodes: [0, 40, 44, 45, 46]},
    44: {name: "大分県", color: null, adjacentPrefCodes: [0, 40, 43, 45]},
    45: {name: "宮崎県", color: null, adjacentPrefCodes: [0, 43, 44, 46]},
    46: {name: "鹿児島県", color: null, adjacentPrefCodes: [0, 43, 45]},
    47: {name: "沖縄県", color: null, adjacentPrefCodes: [0]}
};

function paintColor(pref) {
    // 既に自分に色が塗られている場合、処理を行わずにtrueを返す
    if (pref.color != null) return true

    for (let i = 0; i < colors.length; i++) {
        let color = colors[i];
        pref.color = color;

        // 隣接する県と色が重複しているかチェック
        for (let i = 0; i < pref.adjacentPrefCodes.length; i++) {
            // 隣接する県と色が重複している場合、自分の色を消す
            let code = pref.adjacentPrefCodes[i];
            if (color === prefs[code].color) {
                pref.color = null;
                break;
            }
        }

        // 自分に色が塗られている状態の場合(= 隣接県と色が重複しなかった場合)、隣接する県に色を塗る
        if (pref.color != null) {
            for (let i = 0; i < pref.adjacentPrefCodes.length; i++) {

                let code = pref.adjacentPrefCodes[i];
                let isSuccess = paintColor(prefs[code]);

                // 隣接する県に色を塗れなかった場合、自分の色を消す
                if (isSuccess === false) {
                    pref.color = null;
                    break;
                }
            }

            // 隣接する県に色を塗れた場合、trueを返して処理を終える
            if (pref.color != null) {
                return true;
            }
        }
    }

    // ここまでたどり着くときは、自分に色を塗れなかった場合のみ
    return false;
}

paintColor(prefs[0]);

for (const code in prefs) {
    console.log(String(code) + "," + prefs[code].name + "," + prefs[code].color);
}

実行

0,海,red
1,北海道,blue
2,青森県,blue
3,岩手県,green
4,宮城県,blue
5,秋田県,yellow
6,山形県,green
7,福島県,yellow
8,茨城県,blue
9,栃木県,red
10,群馬県,green
11,埼玉県,yellow
12,千葉県,green
13,東京都,blue
14,神奈川県,yellow
15,新潟県,blue
16,富山県,green
17,石川県,blue
18,福井県,green
19,山梨県,green
20,長野県,red
21,岐阜県,yellow
22,静岡県,blue
23,愛知県,green
24,三重県,blue
25,滋賀県,red
26,京都府,yellow
27,大阪府,blue
28,兵庫県,green
29,奈良県,red
30,和歌山県,green
31,鳥取県,blue
32,島根県,yellow
33,岡山県,yellow
34,広島県,green
35,山口県,blue
36,徳島県,blue
37,香川県,green
38,愛媛県,yellow
39,高知県,green
40,福岡県,blue
41,佐賀県,green
42,長崎県,blue
43,熊本県,green
44,大分県,yellow
45,宮崎県,blue
46,鹿児島県,yellow
47,沖縄県,blue

Excelで確認してみる

japan.png

参考資料

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

海含む日本を4色で塗り分ける

ソースコード

main.js
let colors = ["red", "blue", "green", "yellow"];

let prefs = {
    0: {name: "", color: null, adjacentPrefCodes: [1, 2, 3, 4, 5, 6, 7, 8, 12, 13, 14, 15, 16, 17, 18, 22, 23, 24, 26, 27, 28, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47]},
    1: {name: "北海道", color: null, adjacentPrefCodes: [0]},
    2: {name: "青森県", color: null, adjacentPrefCodes: [0, 3, 5]},
    3: {name: "岩手県", color: null, adjacentPrefCodes: [0, 2, 4, 5]},
    4: {name: "宮城県", color: null, adjacentPrefCodes: [0, 3, 5, 6, 7]},
    5: {name: "秋田県", color: null, adjacentPrefCodes: [0, 2, 3, 4, 6]},
    6: {name: "山形県", color: null, adjacentPrefCodes: [0, 4, 5, 7, 15]},
    7: {name: "福島県", color: null, adjacentPrefCodes: [0, 4, 6, 8, 9, 10, 15]},
    8: {name: "茨城県", color: null, adjacentPrefCodes: [0, 7, 9, 11, 12]},
    9: {name: "栃木県", color: null, adjacentPrefCodes: [7, 8, 10, 11]},
    10: {name: "群馬県", color: null, adjacentPrefCodes: [7, 9, 11, 15, 20]},
    11: {name: "埼玉県", color: null, adjacentPrefCodes: [8, 9, 10, 12, 13, 19, 20]},
    12: {name: "千葉県", color: null, adjacentPrefCodes: [0, 8, 11, 13]},
    13: {name: "東京都", color: null, adjacentPrefCodes: [0, 11, 12, 14, 19]},
    14: {name: "神奈川県", color: null, adjacentPrefCodes: [0, 13, 19, 22]},
    15: {name: "新潟県", color: null, adjacentPrefCodes: [0, 6, 7, 10, 16, 20]},
    16: {name: "富山県", color: null, adjacentPrefCodes: [0, 15, 17, 20, 21]},
    17: {name: "石川県", color: null, adjacentPrefCodes: [0, 16, 18, 21]},
    18: {name: "福井県", color: null, adjacentPrefCodes: [0, 17, 21, 25, 26]},
    19: {name: "山梨県", color: null, adjacentPrefCodes: [11, 13, 14, 20, 22]},
    20: {name: "長野県", color: null, adjacentPrefCodes: [10, 11, 15, 16, 19, 21, 22, 23]},
    21: {name: "岐阜県", color: null, adjacentPrefCodes: [16, 17, 18, 20, 23, 24, 25]},
    22: {name: "静岡県", color: null, adjacentPrefCodes: [0, 14, 19, 20, 23]},
    23: {name: "愛知県", color: null, adjacentPrefCodes: [0, 20, 21, 22, 24]},
    24: {name: "三重県", color: null, adjacentPrefCodes: [0, 21, 23, 25, 26, 29, 30]},
    25: {name: "滋賀県", color: null, adjacentPrefCodes: [18, 21, 24, 26]},
    26: {name: "京都府", color: null, adjacentPrefCodes: [0, 18, 24, 25, 27, 28, 29]},
    27: {name: "大阪府", color: null, adjacentPrefCodes: [0, 26, 28, 29, 30]},
    28: {name: "兵庫県", color: null, adjacentPrefCodes: [0, 26, 27, 31, 33]},
    29: {name: "奈良県", color: null, adjacentPrefCodes: [24, 26, 27, 30]},
    30: {name: "和歌山県", color: null, adjacentPrefCodes: [0, 24, 27, 29]},
    31: {name: "鳥取県", color: null, adjacentPrefCodes: [0, 28, 32, 33, 34]},
    32: {name: "島根県", color: null, adjacentPrefCodes: [0, 31, 34, 35]},
    33: {name: "岡山県", color: null, adjacentPrefCodes: [0, 28, 31, 34]},
    34: {name: "広島県", color: null, adjacentPrefCodes: [0, 31, 32, 33, 35]},
    35: {name: "山口県", color: null, adjacentPrefCodes: [0, 32, 34]},
    36: {name: "徳島県", color: null, adjacentPrefCodes: [0, 37, 38, 39]},
    37: {name: "香川県", color: null, adjacentPrefCodes: [0, 36, 38]},
    38: {name: "愛媛県", color: null, adjacentPrefCodes: [0, 36, 37, 39]},
    39: {name: "高知県", color: null, adjacentPrefCodes: [0, 36, 38]},
    40: {name: "福岡県", color: null, adjacentPrefCodes: [0, 41, 43, 44]},
    41: {name: "佐賀県", color: null, adjacentPrefCodes: [0, 40, 42]},
    42: {name: "長崎県", color: null, adjacentPrefCodes: [0, 41]},
    43: {name: "熊本県", color: null, adjacentPrefCodes: [0, 40, 44, 45, 46]},
    44: {name: "大分県", color: null, adjacentPrefCodes: [0, 40, 43, 45]},
    45: {name: "宮崎県", color: null, adjacentPrefCodes: [0, 43, 44, 46]},
    46: {name: "鹿児島県", color: null, adjacentPrefCodes: [0, 43, 45]},
    47: {name: "沖縄県", color: null, adjacentPrefCodes: [0]}
};

function paintColor(pref) {
    // 既に自分に色が塗られている場合、処理を行わずにtrueを返す
    if (pref.color != null) return true

    for (let i = 0; i < colors.length; i++) {
        let color = colors[i];
        pref.color = color;

        // 隣接する県と色が重複しているかチェック
        for (let i = 0; i < pref.adjacentPrefCodes.length; i++) {
            // 隣接する県と色が重複している場合、自分の色を消す
            let code = pref.adjacentPrefCodes[i];
            if (color === prefs[code].color) {
                pref.color = null;
                break;
            }
        }

        // 自分に色が塗られている状態の場合(= 隣接県と色が重複しなかった場合)、隣接する県に色を塗る
        if (pref.color != null) {
            for (let i = 0; i < pref.adjacentPrefCodes.length; i++) {

                let code = pref.adjacentPrefCodes[i];
                let isSuccess = paintColor(prefs[code]);

                // 隣接する県に色を塗れなかった場合、自分の色を消す
                if (isSuccess === false) {
                    pref.color = null;
                    break;
                }
            }

            // 隣接する県に色を塗れた場合、trueを返して処理を終える
            if (pref.color != null) {
                return true;
            }
        }
    }

    // ここまでたどり着くときは、自分に色を塗れなかった場合のみ
    return false;
}

paintColor(prefs[0]);

for (const code in prefs) {
    console.log(String(code) + "," + prefs[code].name + "," + prefs[code].color);
}

実行

0,海,red
1,北海道,blue
2,青森県,blue
3,岩手県,green
4,宮城県,blue
5,秋田県,yellow
6,山形県,green
7,福島県,yellow
8,茨城県,blue
9,栃木県,red
10,群馬県,green
11,埼玉県,yellow
12,千葉県,green
13,東京都,blue
14,神奈川県,yellow
15,新潟県,blue
16,富山県,green
17,石川県,blue
18,福井県,green
19,山梨県,green
20,長野県,red
21,岐阜県,yellow
22,静岡県,blue
23,愛知県,green
24,三重県,blue
25,滋賀県,red
26,京都府,yellow
27,大阪府,blue
28,兵庫県,green
29,奈良県,red
30,和歌山県,green
31,鳥取県,blue
32,島根県,yellow
33,岡山県,yellow
34,広島県,green
35,山口県,blue
36,徳島県,blue
37,香川県,green
38,愛媛県,yellow
39,高知県,green
40,福岡県,blue
41,佐賀県,green
42,長崎県,blue
43,熊本県,green
44,大分県,yellow
45,宮崎県,blue
46,鹿児島県,yellow
47,沖縄県,blue

Excelで確認してみる

japan.png

参考資料

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

Node.js + JavaScript で HTTP リダイレクト先の URL を取得する

概要

  • Node.js + JavaScript で HTTP リダイレクト先の URL を取得する
  • 動作確認環境: Node.js 14.7.0 + macOS Catalina

ソースコード

get_redirect.js というファイル名で以下の内容を保存する。

get_redirect.js
'use strict'

const https = require('https')
const http  = require('http')

// リダイレクト先 URL を取得する関数
function get_redirect_url(src_url) {
  return new Promise((resolve, reject) => {
    try {
      // https と http で使うモジュールを変える
      const client = src_url.startsWith('https') ? https : http
      // 4xx や 5xx ではエラーが発生しないので注意
      client.get(src_url, (res) => {
        // HTTP レスポンスから Location ヘッダを取得 (ヘッダ名は小文字)
        resolve(res.headers['location'])
      }).on('error', (err) => {
        reject(err)
      })
    } catch(err) {
      reject(err)
    }
  })
}

(async () => {

  // コマンドライン引数を取得
  const src_url = process.argv[2]

  // リダイレクト先URLを取得
  const redirect_url = await get_redirect_url(src_url)
    .catch(err => {
      console.log(err)
    })

  // リダイレクト先URLを出力
  if (redirect_url) {
    console.log(redirect_url)
  }
})()

実行例。

$ node get_redirect.js https://bit.ly/3kmTOkc
https://t.co/yITSBp4ino
$ node get_redirect.js https://t.co/yITSBp4ino
https://qiita.com/niwasawa
$ node get_redirect.js https://qiita.com/niwasawa

参考資料

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

【JavaScript】数値範囲による分岐

前置き

ふと、数値の範囲で分岐させるのって面倒だったなぁと思い返したはいいけど、どう書いていたがすぐ思い出せなかったので自分用に記載。

コードはよくありそうな年代別の分岐。

コード

const getGeneration = age => {
    if (!Number.isFinite(age)) return console.error('数値以外が入力');
    if (age<0) return console.error('0歳未満?');
    if (age<12) return console.log('12歳未満');
    if (age<20) return console.log('12歳~19歳');
    if (age<30) return console.log('20歳代');
    if (age<40) return console.log('30歳代');
    if (age<50) return console.log('40歳代');
    if (age<60) return console.log('50歳代');
    if (age<70) return console.log('60歳代');
    if (age<80) return console.log('70歳代');
    if (age<100) return console.log('80歳以上');
    if (age<120) return console.log('100歳以上');
    return console.log('120歳以上');
};

getGeneration(-1);      // => 0歳未満?
getGeneration('14');    // => 数値以外が入力
getGeneration(14);      // => 12歳~19歳
getGeneration(200);     // => 120歳以上

Number.isFinite()の行は、数値以外の入力が絶対にない状態なら削除OK。

最後に

もっとスマートな書き方はないものか…

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

VisualforceでQRコード作成してみた その2

QR.gif
↑のGifは見づらいと思うので、こちらで動画を公開しています。

今回は、前回の記事VisualforceでQRコード作成してみたの続編です。

取引先責任者の画面でボタンクリックで、表示されているレコードの添付ファイルに保存しております。

取引先責任者の関連リストに表示するために、↑のGifでは画面リロードを行っています。

処理の手順としては
1.JavaScriptでQRコード作成
2.QRコードのバイナリデータをApexに渡す
3.ApexでContentDocument作成
となっております。

そのうちソースコードも公開すると思います。

使用事例

 作ってみたはいいものの、良い使用事例が思い浮かびませんでした。
というのも、例えば「イベントの出席確認用のQRコードを作成するためには、出席者すべてのレコードページでボタンクリックをしなければならず実用的ではないと思ったからです。ただし、LWCのScan Barcodesと合わせて、Salesforceのみで実装を完結できるところに将来性を感じるような気がします。
 良い使用事例が思いついた方は、コメント等頂けると幸いです。

まとめ

 他サービスと連携しなくても、Slaesforceだけで様々なことが出来るようになっていますね。次はLWCのScan Barcodesか上記の課題であるQRコード添付ファイルの一括作成に取り組んでみようかなと思います。

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

十字キーASCII入力

十字キーASCII入力

概要

十字キーでASCIIで文字入力する仕様とその実装。D-pad ASCII Input。

dpad_ascii_input.gif

動機、目的、特徴

  • 入力キーが少ないデバイスで入力できる
  • 仕様がシンプル
  • 実装がシンプル
  • 実用的な速度で入力できる
  • 画面を見なくても入力できる
  • 画面を見れば覚えていなくても入力できる

仕様

  • 十字キーを4回入力し、各2bitを合わせて1Byteにし、それをASCIIの文字コードとして文字入力する
  • ↑、←、→、↓がそれぞれ0、1、2、3になる
  • そのキーを押した場合の残り候補となる文字群が画面に表示される
  • キャンセルキーを設けて2ビットずつ取り消すようにしてもよい

  • Hello World
文字 入力
H ←↑→↑
e ←→←←
l ←→↓↑
l ←→↓↑
o ←→↓↓
Space ↑→↑↑
W ←←←↓
o ←→↓↓
r ←↓↑→
l ←→↓↑
d ←→←↑

実装例

JavaScriptで65行で実装した例。Escapeで2ビットずつキャンセルできる。

<!DOCTYPE html>
<meta charset="utf-8" />
<div>
  <textarea class="text" style="height: 4em; width: 80em;" autofocus></textarea>
</div>
<div><input class="progress" /></div>
<div class="candidate"></div>
<script>
  function codeToStr(i) {
    if (i === 0x09) return "[Tab]";
    if (i === 0x0a) return "[Enter]";
    if (i === 0x20) return "[Space]";
    if (i < 0x20 || 0x7f <= i) return "";
    return String.fromCodePoint(i);
  }
  function updateCandidates(codeStart, codeLen) {
    let html = '<table style="font-family: monospace;">';
    const rowCodeLen = codeLen / 4;
    for (let i = 0; i < 4; ++i) {
      const rowCodeStart = codeStart + rowCodeLen * i;
      html += "<tr>";
      html += `<td>${"↑←→↓"[i]}</td>`
      html += `<td>0x${rowCodeStart.toString(16).padStart(2, "0")}</td>`;
      html += "<td>";
      for (let j = 0; j < rowCodeLen; ++j) {
        const code = rowCodeStart + j;
        html += codeToStr(code);
      }
      html += "</td>";
      html += "</tr>";
    }
    html += "</table>";
    document.querySelector(".candidate").innerHTML = html;
  }
  function updateProgress(code2bs) {
    const progress = code2bs.reduce((a, c) => a + "↑←→↓"[c], "");
    document.querySelector(".progress").value = progress;
  }
  let code2bs = [];
  function onKeyDown(e) {
    const arrowKeys = ["ArrowUp", "ArrowLeft", "ArrowRight", "ArrowDown"];
    const idx = arrowKeys.indexOf(e.key);
    if (idx >= 0) {
      code2bs.push(idx);
      e.preventDefault();
    } else if (e.key === "Escape") {
      code2bs.pop();
      e.preventDefault();
    } else {
      code2bs = [];
    }
    updateProgress(code2bs);
    let code = code2bs.reduce((a, c, i) => a + (c << (2 * (3 - i))), 0);
    if (code2bs.length === 4) {
      document.querySelector(".text").value += String.fromCodePoint(code);
      code = 0;
      code2bs = [];
    }
    updateCandidates(code, 256 / (1 << (code2bs.length * 2)));
  }
  onload = () => {
    updateCandidates(0, 256);
    document.querySelector(".text").onkeydown = e => onKeyDown(e);
  };
</script>

メモ

入力 分類
↑→○○ 記号
↑↓○○ 数字
←↑○○ アルファベット大文字、前半
←←○○ アルファベット大文字、後半
←→○○ アルファベット小文字、前半
←↓○○ アルファベット小文字、後半
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

十字キーASCII入

十字キーASCII入力

概要

十字キーでASCIIを入力する仕様とその実装。D-pad ASCII Input。

動機、目的、特徴

  • 入力キーが少ないデバイスで入力できる
  • 仕様がシンプル
  • 実装がシンプル
  • 実用的な速度で入力できる
  • 画面を見なくても入力できる
  • 画面を見れば覚えていなくても入力できる

仕様

  • 十字キーを4回入力し、各2bitを合わせて1Byteにし、それをASCIIの文字コードとして文字入力する
  • ↑、←、→、↓がそれぞれ0、1、2、3になる
  • そのキーを押した場合の残り候補となる文字群が画面に表示される
  • キャンセルキーを設けて2ビットずつ取り消すようにしてもよい

  • Hello World
文字 入力
H ←↑→↑
e ←→←←
l ←→↓↑
l ←→↓↑
o ←→↓↓
Space ↑→↑↑
W ←←←↓
o ←→↓↓
r ←↓↑→
l ←→↓↑
d ←→←↑

実装例

JavaScriptで64行で実装した例。Escapeで2ビットずつキャンセルできる。

<!DOCTYPE html>
<meta charset="utf-8" />
<div>
  <textarea class="text" style="height: 4em; width: 80em;" autofocus></textarea>
</div>
<div><input class="progress" /></div>
<div class="candidate"></div>
<script>
  function codeToStr(i) {
    if (i === 0x09) return "[Tab]";
    if (i === 0x0a) return "[Enter]";
    if (i === 0x20) return "[Space]";
    if (i < 0x20 || 0x7f <= i) return "";
    return String.fromCodePoint(i);
  }
  function updateCandidates(codeStart, codeLen) {
    let html = '<table style="font-family: monospace;">';
    const rowCodeLen = codeLen / 4;
    for (let i = 0; i < 4; ++i) {
      const rowCodeStart = codeStart + rowCodeLen * i;
      html += "<tr>";
      html += `<td>${"↑←→↓"[i]}</td>`
      html += `<td>0x${rowCodeStart.toString(16).padStart(2, "0")}</td>`;
      html += "<td>";
      for (let j = 0; j < rowCodeLen; ++j) {
        const code = rowCodeStart + j;
        html += codeToStr(code);
      }
      html += "</td>";
      html += "</tr>";
    }
    html += "</table>";
    document.querySelector(".candidate").innerHTML = html;
  }
  function updateProgress(code2bs) {
    const progress = code2bs.reduce((a, c) => a + "↑←→↓"[c], "");
    document.querySelector(".progress").value = progress;
  }
  let code2bs = [];
  function onKeyDown(e) {
    const arrowKeys = ["ArrowUp", "ArrowLeft", "ArrowRight", "ArrowDown"];
    const idx = arrowKeys.indexOf(e.key);
    if (idx >= 0) {
      code2bs.push(idx);
      e.preventDefault();
    }
    if (e.key === "Escape") {
      code2bs.pop();
      e.preventDefault();
    }
    updateProgress(code2bs);
    let code = code2bs.reduce((a, c, i) => a + (c << (2 * (3 - i))), 0);
    if (code2bs.length === 4) {
      document.querySelector(".text").value += String.fromCodePoint(code);
      code = 0;
      code2bs = [];
    }
    updateCandidates(code, 256 / (1 << (code2bs.length * 2)));
  }
  onload = () => {
    updateCandidates(0, 256);
    document.querySelector(".text").onkeydown = e => onKeyDown(e);
  };
</script>

メモ

入力 分類
↑→○○ 記号
↑↓○○ 数字
←↑○○ アルファベット大文字、前半
←←○○ アルファベット大文字、後半
←→○○ アルファベット小文字、前半
←↓○○ アルファベット小文字、後半
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JS URLのクエリをオブジェクトに変換する

本文

JavaScriptでURLのクエリ部分をオブジェクトに変換するための関数群です。車輪の再発明だと思いますが毎回調べるので記録します。引数は型チェックしていません。

// URLからクエリ(?以降)を抜き出します。
function urlGetQuery(url) {
    const i = url.indexOf("?");
    return (i != -1) ? url.substring(i + 1) : null;
}

// URLのクエリから「=」より前をキー、後を値に持つオブジェクトを作成します。
function createQueryObjectForUrlQuery(query) {
    return Object.fromEntries(query.split("&").map(s => s.split("=")));
}

// URLからクエリを抜き出して、クエリの「=」より前をキー、後を値に持つオブジェクトを作成します。
function createQueryObjectForUrl(url) {
    return createQueryObjectForUrlQuery(urlGetQuery(url));
}

Google検索でYahooと調べた結果に適用すると次のようになります。

// Google検索でYahooを調べたときのYahoo!のURL(改変)
const url = "https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&url=https%3A%2F%2Fwww.yahoo.co.jp%2F&usg=...";
// URLからクエリ部分のオブジェクトを作成
const query = createQueryObjectForUrl(url);

// 全体を出力
console.log(query);
//>> Object { sa: "t", rct: "j", q: "", esrc: "s", source: "web", url: "https%3A%2F%2Fwww.yahoo.co.jp%2F", usg: "..." }

// urlをURIデコードして出力
console.log(decodeURIComponent(query.url));
//>> https://www.yahoo.co.jp/

蛇足

  • Object.fromEntriesにキーと値の配列を与えることで対応するオブジェクトが作成できます。
  • URLをデコードする関数はdecodeURIdecodeURIComponentがあります。今回は「:」「/」のような特殊文字もデコードしたいのでdecodeURIComponentを使用しています。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む