2015年10月8日木曜日

Swift2で二地点の緯度・経度からその距離を計算するには?

二地点の緯度・経度からその距離を計算する(日本は山だらけ〜)

のJava版をSwift2に移植しました。数学は分かりません。理論も分かりません。でもSwiftとJavaは分かるので移植はできるのです(^^);

オリジナルがMITライセンスなので、このコードもリスペクトを込めてMITライセンスとします。


//
//  GeoUtil.swift
//

import Foundation


/**
*  二地点の緯度・経度からその距離を計算する(日本は山だらけ〜) - http://yamadarake.jp/trdi/report000001.html
*/
@objc(GeoUtil) class GeoUtil: NSObject {
    
    
    static let BESSEL_A: Double = 6377397.155;
    static let BESSEL_E2: Double = 0.00667436061028297;
    static let BESSEL_MNUM: Double = 6334832.10663254;
    
    static let GRS80_A: Double = 6378137.000;
    static let GRS80_E2: Double = 0.00669438002301188;
    static let GRS80_MNUM: Double = 6335439.32708317;
    
    static let WGS84_A: Double = 6378137.000;
    static let WGS84_E2: Double = 0.00669437999019758;
    static let WGS84_MNUM: Double = 6335439.32729246;
    
    static let BESSEL: Int = 0;
    static let GRS80: Int = 1;
    static let WGS84: Int = 2;
    
    
    static func deg2rad(deg: Double) -> Double {
        return deg * M_PI / 180.0;
    }
    
    
    static func calcDistHubeny(lat1: Double, lng1: Double, lat2: Double, lng2: Double, a: Double, e2: Double, mnum: Double) -> Double {
        
        let my: Double = deg2rad((lat1 + lat2) / 2.0);
        let dy: Double = deg2rad(lat1 - lat2);
        let dx: Double = deg2rad(lng1 - lng2);
        
        let sinVal: Double = sin(my);
        let w: Double = sqrt(1.0 - e2 * sinVal * sinVal);
        let m: Double = mnum / (w * w * w);
        let n: Double = a / w;
        
        let dym: Double = dy * m;
        let dxncos: Double = dx * n * cos(my);
        
        return sqrt(dym * dym + dxncos * dxncos);
        
    }
    
    
    static func calcDistHubeny(lat1: Double, lng1: Double, lat2: Double, lng2: Double) -> Double {
        return calcDistHubeny(lat1, lng1: lng1, lat2: lat2, lng2: lng2, a: GRS80_A, e2: GRS80_E2, mnum: GRS80_MNUM);
    }
    
    
    static func calcDistHubery(lat1: Double, lng1: Double, lat2: Double, lng2: Double, type: Int) -> Double {
        switch(type) {
        case BESSEL:
            return calcDistHubeny(lat1, lng1: lng1, lat2: lat2, lng2: lng2, a: BESSEL_A, e2: BESSEL_E2, mnum: BESSEL_MNUM);
        case WGS84:
            return calcDistHubeny(lat1, lng1: lng1, lat2: lat2, lng2: lng2, a: WGS84_A, e2: WGS84_E2, mnum: WGS84_MNUM);
        default:
            return calcDistHubeny(lat1, lng1: lng1, lat2: lat2, lng2: lng2, a: GRS80_A, e2: GRS80_E2, mnum: GRS80_MNUM);
        }
    }
    
    
}

iOS9で位置情報を使う定期バックグラウンド処理をする場合のバッテリー消費を抑えるには?

iOSでのバックグラウンド処理の制限

iOSでのバックグラウンド処理は非常に制限されているが、いくつか方法はある。しかし、「定期的にバックグラウンド処理をする」という条件になるとかなり限られる。

どのような方法を選択できるかはアプリのタイプや目的によるが、アプリによらず選択できるであろう方法は少なくとも2つある。

Background fetch

iOSがアプリの利用頻度などから「非定期に」バックグラウンド処理を動作させるので使えない。

Remote Notifications

サーバーからのNotificationが届くタイミングでバックグラウンド処理を起動する方法だが、サーバーサイドの実装が必要な上、Notificationが届くタイミングはまちまちで、最悪の場合は届かないこともあるので、苦労の割に確実性に欠ける。


AndroidのAlarmManagerの代替は無いのか?

調べた限り、iOSではAndroidでいうところのAlarmManagerのようなバックグランド処理はできない。iOSで言うところの「バックグラウンド」とは、タスクマネージャーにプロセスが残っている状態のことである。つまり、ユーザーがタスクマネージャーからアプリを終了させたらバックグラウンド処理はできない。


位置情報の更新を利用した定期バックグラウンド処理を採用

幸い今回開発中のアプリは位置情報を利用するタイプのアプリなので、「Location updates」のcapabilityを利用して定期的にバックグラウンド処理をさせることができた。


位置情報の更新によるバッテリー消費を抑えるために考えた2つのこと(失敗)

しかし、ここで気になるのは、バックグラウンドで位置情報を更新し続けることによるバッテリーの消費がどれだけのものなのかということ。

そこで、このあたりを参考にした。

Energy Efficiency Guide for iOS Apps: Reduce Location Accuracy and Duration

一番始めに考えたのは、iOS9から導入されたrequestLocation()を利用することだった。タイマーで必要な時だけrequestLocation()を使って位置情報を取得すればバッテリーの消費を抑えられるのではないかと。

How to request a user's location only once using requestLocation – Swift 2 example code

しかし、この方法はダメ。requestLocation()は「Location updates」の処理のうちに入らないらしく、タイマー(プロセス)が止まってしまって定期処理を実現できなかった。

次に考えたのは、beginBackgroundTaskWithExpirationHandler()を使う方法。アプリがバックグラウンドになった時に一定の期間でstartUpdatingLocation()を呼び、すぐに停止させればバッテリーの消費を抑えられるのではないかと。

この方法もダメ。beginBackgroundTaskWithExpirationHandler()によるバックグラウンド処理は約3分間しかもたないうえ、処理が終わった時に新たにbeginBackgroundTaskWithExpirationHandler()を使えないので定期処理を実現できなかった。


startUpdatingLocation()を使いつつ、精度でバッテリー消費をコントロール

やはりタイマー(プロセス)をバックグラウンドで動作させ続けるためにはlocationManager.startUpdatingLocation() を使うことが必要。そこで、「Reduce Accuracy of Standard Location Updates Whenever Possible」のTIPsを使うことにした。つまり、ギリギリまで精度を下げて、できるだけバッテリー消費を抑える作戦。

ところで、pausesLocationUpdatesAutomatically = falseにしないとstartUpdatingLocation()を使っていても15分程度で止まるので注意。また、startMonitoringSignificantLocationChanges()ではバックグラウンド動作しなかったので注意。


実験

指定できるオプションはいくつかあるが、上に行くほどバッテリー消費が激しくなる。

Accuracy Constants - Core Location Constants Reference

ここでは、バッテリー100%の状態からstartUpdatingLocation()して、タイマーを使って定期的にWEB APIを叩いてその結果をNotificationするという作業を12時間続けて何パーセントまで減るかを見た。使用した機体はiPhone5。

まず、この処理をさせない場合は12時間放置で100%だった。実際は100%のマージンが大きすぎるのだろうと思うが、これがユーザーが見る現実だ。

次に「数百メートル」(kCLLocationAccuracyHundredMeters)に設定して15分に一回動作するケースを見たら76%だった。30分に1回にしたら78%。少しは効果があるが、やはりほとんどは位置情報の更新にエネルギーが費やされていると考えられる。

次はいきなり「3キロ」(kCLLocationAccuracyThreeKilometers)に設定して15分に一回動作するケースを見たら84%だった。かなり消費を抑えられる。

問題はkCLLocationAccuracyThreeKilometersに設定した場合の位置情報の精度がどれくらいのものなのかということだ。

調べた限りでは、かなり正確な場合もあるし、数百メートルずれている場合もあったが、キロ単位でずれていることは無かった


結論

位置情報の更新を使って定期的なバックグラウンド処理をする場合は、できるだけkCLLocationAccuracyThreeKilometersを設定し、タイマーでの動作回数を少なくすると良いだろう。

2015年9月15日火曜日

SwiftでCordova(ionic)のpluginを開発するには?

Swiftでcordovaのプラグインを開発することはできるのだろうか?

このあたりを見ると開発できそうだ。

Chris Dell - Software Developer :: Writing an iOS Cordova plugin in pure Swift

How to write Cordova plugin in Swift? - Stack Overflow

やってみよう。

今回はChrisさんのコードを参考に、HTMLからネイティブに小文字のメッセージを送って大文字変換されたメッセージを受信するというサンプルを作る。これが完成形。


ビルドシステムはionicを使うが、プラグイン部分の開発はcordovaでも通用するはず。

まずはblankプロジェクトを作ろう。

$ ionic start CDVEchoPlugin blank

CDVEchoPlugin/www/index.htmlをこのように編集する。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, width=device-width">
    <title>CDVEchoPlugin</title>

    <link href="lib/ionic/css/ionic.css" rel="stylesheet">
    <link href="css/style.css" rel="stylesheet">

    <!-- IF using Sass (run gulp sass first), then uncomment below and remove the CSS includes above
<link href="css/ionic.app.css" rel="stylesheet">
-->

    <!-- ionic/angularjs js -->
    <script src="lib/ionic/js/ionic.bundle.js"></script>

    <!-- cordova script (this will be a 404 during development) -->
    <script src="cordova.js"></script>

    <!-- your app's js -->
    <script src="js/app.js"></script>
  </head>
  <body ng-app="starter" ng-controller="controller">

    <ion-pane>
      <ion-header-bar class="bar-stable">
        <h1 class="title">CDVEchoPlugin</h1>
      </ion-header-bar>

      <ion-content padding="true">
        <label class="item item-input">
          <span class="input-label">msg.send</span>
          <input type="text" ng-model="msg.send">
        </label>
        <label class="item item-input">
          <span class="input-label">msg.return</span>
          <input type="text" ng-model="msg.return">
        </label>
        <button class="button button-block button-positive" ng-click="onClickBtnEcho();">echo</button>
      </ion-content>

    </ion-pane>
  </body>
</html>

CDVEchoPlugin /www/js/app.jsをこのように編集する。

/* jshint -W117, -W035, -W072, -W003 */
'use strict';


// Ionic Starter App

// angular.module is a global place for creating, registering and retrieving Angular modules
// 'starter' is the name of this angular module example (also set in a <body> attribute in index.html)
// the 2nd parameter is an array of 'requires'
angular.module('starter', ['ionic'])

  .run(function($ionicPlatform) {
  $ionicPlatform.ready(function() {
    // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
    // for form inputs)
    if(window.cordova && window.cordova.plugins.Keyboard) {
      cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
    }
    if(window.StatusBar) {
      StatusBar.styleDefault();
    }
  });
}).controller('controller', function($scope) {

  console.log('controller init');

  $scope.msg = {
    'send': 'message to send',
    'return': ''
  };

  $scope.onClickBtnEcho = function() {
    console.log('onClickBtnEcho: msg.send=' + $scope.msg.send);
    if (window.EchoPlugin) {
      EchoPlugin.echo($scope.msg.send, function(returnMsg) {
        $scope.msg.return = returnMsg;
        console.log('onClickBtnEcho: msg.return=' + $scope.msg.return);
        $scope.$applyAsync();
      });
    } else {
      console.log('NO EchoPlugin');
    }
  };

});

ここまでで一応起動しておこう。

$ cd /path/to/CDVEchoPlugin/
$ ionic run ios


カスタムプラグインのディレクトリを作る。

$ mkdir plugins-dev
$ cd plugins-dev
$ mkdir EchoPlugin

次の内容でCDVEchoPlugin/plugins-dev/EchoPlugin/plugin.xmlを作る。

<?xml version="1.0" encoding="UTF-8"?>
<plugin xmlns="http://apache.org/cordova/ns/plugins/1.0"
        id="echo-plugin"
        version="0.1">

  <name>EchoPlugin</name>
  <description>This plugin just echoes uppercased string.</description>

  <js-module src="echo-plugin.js">
    <clobbers target="window.EchoPlugin" />
  </js-module>

  <!-- iOS -->
  <platform name="ios">
    <config-file target="config.xml" parent="/*">
      <feature name="EchoPlugin">
        <param name="ios-package" value="EchoPlugin" />
      </feature>
    </config-file>
    <source-file src="src/ios/EchoPlugin.swift" />
  </platform>

</plugin>

さらに次の内容でCDVEchoPlugin/plugins-dev/EchoPlugin/echo-plugin.jsを作る。

'use strict';

var exec = require('cordova/exec');

var EchoPlugin = {

  echo: function(sendMsg, onSuccess, onFail) {
    return exec(onSuccess, onFail, 'EchoPlugin', 'echo', [sendMsg]);
  }

};

module.exports = EchoPlugin;

Swiftのソースディレクトリを作る。

$ mkdir src
$ cd src
$ mkdir ios
$ cd ios

次の内容でCDVEchoPlugin/plugins-dev/EchoPlugin/src/ios/EchoPlugin.swiftを作る。

import Foundation

@objc(EchoPlugin) class EchoPlugin : CDVPlugin {

    func echo(command: CDVInvokedUrlCommand) {

        var message = command.arguments[0] as! String
        message = message.uppercaseString // Prove the plugin is actually doing something

        var pluginResult = CDVPluginResult(status: CDVCommandStatus_OK, messageAsString: message)
        commandDelegate.sendPluginResult(pluginResult, callbackId:command.callbackId)

    }

}

プロジェクトのトップディレクトリに移動し、作ったプラグイン「EchoPlugin」をプロジェクトにインストールする。

$ cd /path/to/CDVEchoPlugin
$ ionic plugin add plugins-dev/EchoPlugin/

ここで次の項目を確認する。


  • plugins-dev/EchoPluginの内容がplugin/EchoPluginにコピーされているか?
  • CDVEchoPlugin/platforms/ios/CDVEchoPlugin/config.xmlにplugin.xmlで設定したfeatureの内容が追加されているか?
  • CDVEchoPlugin/platforms/ios/CDVEchoPlugin/Plugins/echo-plugin/EchoPlugin.swiftがあるか?
  • CDVEchoPlugin/platforms/ios/www/plugins/echo-plugin/echo-plugin.jsがあるか?


SwiftはiOS8以降なので最低OSを8にする必要がある。そこで、CDVEchoPlugin/config.xmlに次のように「<preference name="deployment-target" value="8.0" />」の記述を加える。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<widget id="com.ionicframework.cdvechoplugin107512" version="0.0.1" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
・・・・・・
  <preference name="deployment-target" value="8.0" />
</widget>

そして、この設定をCDVEchoPlugin/platforms/ios/CDVEchoPlugin.xcodeprojに反映させる。

$ ionic prepare

次の内容でCDVEchoPlugin/platforms/ios/CDVEchoPlugin/Classes/Bridging-Header.hを作る。

//
//  Use this file to import your target's public headers that you would like to expose to Swift.
//

#import <Cordova/CDV.h>

これはSwiftからObjective-Cのライブラリ(Cordovaなど)にアクセスするために必要。でなければコンパイルエラーになる。ただ作っただけではダメで、Xcodeにて手動で設定する必要がある。

XcodeでCDVEchoPlugin/platforms/ios/CDVEchoPlugin.xcodeprojを開く。

先ほどターゲットOSを指定するためにprepareした効果は、General→Deployment Info→Deployment Targetで確認できる。



「Build Settings」タブに移り、「All」をクリックして、「Objective-C Bridging」で検索フィルターをかけ、「Objective-C Bridging Header」に「CDVEchoPlugin/Classes/Bridging-Header.h」を設定する。

追記15.09.16: ※ここでObjective-C Bridging Headerが見つからない場合は、Xcode上でClassesに適当にswiftファイルを追加すると「自動的に設定しますか」的なことを言われる場合がある。OKすると勝手にClasses/プロジェクト名-Bridging-Header.hを作ってくれる。



ここまでで、とりあえずProduct→Buildでコンパイルできるはず。

しかし、Bridging Headerにはさらに落とし穴があって、この状態で実行するとこのようなエラーが出る。

dyld: Library not loaded: @rpath/libswiftCore.dylib
Referenced from: /private/var/mobile/Containers/Bundle/Application/2E6CCCD1-E46C-43F8-8F1F-1204A1991455/CDVEchoPlugin.app/CDVEchoPlugin
Reason: image not found

なぜ出るのか?解決方法は?

iphone - dyld: Library not loaded: @rpath/libswiftCore.dylib - Stack Overflow

一番usefulがついている"Embedded Content Contains Swift Code"の方法では解決しなかったが、"Runpath Search Paths"の方法で解決した。

「Build Settings」タブに移り、「All」をクリックして、「runpath」で検索フィルターをかけ、「Runpath Search Paths」に「@executable_path/Frameworks」を設定する。


ここまでやればProduct→Runで実行できるはず。


「echo」ボタンをクリックすると「msg.return」に大文字になってエコーが返ってくる。単純ではあるが、Swiftで開発したプラグインが正常に動作している証拠だ。

これでSwiftでプラグインを書ける!何でもデキる気がしてきた∠( ゚ω゚)/

2015年8月26日水曜日

これがAngularJSの$apply()や$digest()が低速な理由のひとつか?

昨日書いた記事 「琴線探査: Dr. GlebのAngularアプリ最適化TIPSまとめ」 に、try-catchがあるとV8では最適化されないため処理が遅くなると書いた。

今日は自分で書いたアプリをプロファイルしてみた。このように、$apply()や$digest()に相当な時間がかかっていることが分かった。


これは予想通りだったが、驚いたのは$apply()や$digest()でtry-catchを使っているらしいことだ。

「Not optimized: TryCatch Statement」とツールチップが出ている。ひょっとすると、これが$apply()や$digest()が低速な理由のひとつなのかもしれない。

だとすると、かなり根本的なレベルで高速化できていないことになる…内部的にtry-catchを使わないようにできないのかなぁ(´・ω・`)

2015年8月25日火曜日

Dr. GlebのAngularアプリ最適化TIPSまとめ

Dr. Glebのブログ記事 Improving Angular web app performance example. | Better world by better software を簡単にまとめておこうと思う。


try-catchはできるだけ使わない

V8ではtry-catchを使った処理は最適化されないので、特にループ処理ではできるだけ使わないようにする。Primesの例ではtry-catchを無くしただけで全体の処理は2倍以上、メソッド単体では200倍近く高速化している。

追記15.08.26:$apply()や$digest()でtry-catchが使われていることが判明(ノ∀`)
琴線探査: これがAngularJSの$apply()や$digest()が低速な理由のひとつか?


ダイジェストサイクルをできるだけ速くする

$scope.$apply()した時のあれ。ダイジェストサイクルはフレームレートと同じで、長くなるほどUIが固まってしまうことになる。短くする方法は色々ある。


バインドさせる変数をできるだけ少なくする

ng-modelとか$scope.valueとか。バインドさせる変数の数だけダイジェストサイクルは遅くなる。


filterはできるだけ使わない

filterはダイジェストサイクルを遅くさせる。「{{ "index" | lowercase }}」的な無駄なフィルターはもってのほか。


one-time-bindingを使う

サーバーから持ってくるデータは大抵の場合は大量だ。これをページにレンダリングするために2wayバインディングを使ってしまうとダイジェストサイクルが大幅に遅くなる。

そこでAngular1.3から導入されたone-time-bindingを使う。これを使えば、始めだけはバインディングするのでダイジェストサイクルに影響するが、その後は影響が無くなる。

例えば「{{name}}」としている所を「{{::name}}」とするだけでone-time-bindingになる。

AngularJS: Developer Guide: Expressions


React的なレンダリング方法に変更する

特にtableの中身などの反復部分で。要するに、こういった部分を

<tr ng-repeat="prime in primes | orderBy:$index " bindonce>
  <td>index</td>
  <td bo-text="$index + 1 | number:0" />
  <td>prime number</td>
  <td bo-text="prime | number:0" />
  <td>is prime? true</td>
</tr>

こうする。

// use AngularJs built-in filter
var number = $filter('number');
function generateTableRows() {
  var k;
  var str = '';
  for(k = 0; k < $scope.n; k += 1) {
    str += '<tr><td>index</td>';
    str += '<td>' + number(k + 1, 0) + '</td>';
    str += '<td>prime number</td>';
    str += '<td>' + number($scope.primes[k], 0) + '</td>';
    str += '<td>is prime? true</td></tr>';
  }
  document.getElementsByTagName('table')[0].innerHTML = str;
}
$scope.find = function () {
  // generate primes list as before
  generateTableRows();
}

これだけで10倍速くなる。

※後に出てくるバーチャルスクローリングの方が現実的と思われ


表示できるものはできるだけ早く表示する

素数の計算は分単位の時間がかかる場合があるが、始めの100個は非常に速く計算できる。まずこれを表示し、残りは後で表示するようにする。

例えば、setTimeout()や$timeoutを使って長い処理を分割する。

実際に処理が速くなるわけではないが、心理的な処理速度は大幅に速くなる。UX的な配慮と言えるだろう。


UIの更新サイクルを33msec以内に保つ

UIの更新サイクルには

・JSコード実行
・レイアウト計算
・各コンポーネントをバックバッファーにレンダリング
・各コンポーネントを全部合わせてペイント

の4つのステップがあるが、これを最低33msec以内に保つようにする。これで30fps。60fpsが必要なら16msec以内に保つ必要がある。

ブラウザの性能やハードウェアの性能に依存する部分も多いが、コーダーとして最もコントロールできる部分はJSコードの実行だろう。ここをできるだけ速くすることが大事。

33msec以上かかるような処理は$qや$timeoutを駆使して処理を分割し、計算>表示>計算>表示>…と少しずつ計算しては表示するというサイクルを作る。


テーブルに行を追加する時はテーブル自体を追加する

大量にデータを含むテーブルにこのような処理をすると、全ての行でレイアウトの再計算が行われてしまい非常に遅くなる。

function generateTableRows(first, last) {
  // generate new rows HTML markup into variable str
  document.getElementsByTagName('tbody')[0].innerHTML += str;
}

そこで、行を追加せずテーブル自体を追加する。

function generateTableRows(first, last) {
  var k, txt = angular.bind(document, document.createTextNode);
  var table = document.createElement('table');
  for(k = first; k < last; k += 1) {
    var row = table.insertRow();
    row.insertCell().appendChild(txt('index'));
    row.insertCell().appendChild(txt(k + 1));
    row.insertCell().appendChild(txt('prime number'));
    row.insertCell().appendChild(txt($scope.primes[k]));
    row.insertCell().appendChild(txt('is prime? true'));
  }
  // schedule DOM update by attaching new table element to the body
  document.body.appendChild(table);
}

※後に出てくるバーチャルスクローリングの方が現実的と思われ


時間のかかる処理をWebWorkerに投げてしまう

結構面倒だが、奥の手としては十分検討の価値がある。


Array.pushはできるだけ使わない

Array.pushを使うとメモリを効率的に使えないため頻繁にGCが発生し、パフォーマンスに影響する。

var k, n = numbers.length;
for(k = 0; k < n; k += 1) {
  $scope.primes.push(numbers[k]);
}

配列の大きさが決まっている場合は、先にその大きさで配列を確保して値をコピーするようにする。

// initialize the array length
$scope.primes = new Array($scope.n);
$scope.computedN = 0;
// copy numbers
var k, n = numbers.length;
for(k = 0; k < n; k += 1) {
  $scope.primes[$scope.computedN] = numbers[k];
  $scope.computedN += 1;
}


必要になった時に必要なだけ計算(表示)する

全てのデータが必要とは限らないので、「ngInfiniteScroll」などを使ってリストの最後まで行ったら素早く計算して表示するようにする。


$scope.$watch()をやたらと使わない

// instead of individual actions for same watcher
$scope.$watch(function () {
  return $scope.primes;
}, foo, true);
$scope.$watch(function () {
  return $scope.primes;
}, bar, true);
$scope.$watch(function () {
  return $scope.primes;
}, baz, true);
// use single watcher and fire off multiple actions
$scope.$watch(function () {
  return $scope.primes;
}, function () {
  foo();
  bar();
  baz();
}, true);


バーチャルスクローリングを使う

ngRepeatでバーチャルスクロール機能を実現するライブラリ 「kamilkp/angular-vs-repeat」。万単位のリストであっても、実際に見えるデータのDOMのみを保持するようにすることでメモリ面でも速度面でもパフォーマンスの向上が期待できる。Dr.Glebの実験でもかなりのパフォーマンスを叩き出しているようだ。


Angularのバージョンを上げる

1.2で850msかかっていた処理が1.3にしただけで250msに!


ng-classをやたらと使わない

ng-class内のexpressionを処理するのに時間がかかるのでやたらと使わない。例えばこのような処理は

<td>
  <input type="number"
         ng-model="prime" name="primeNumber" required=""
         ng-class="{ 'small': prime < 10, 'exact': prime == 10, 'large': prime > 10 }"
         />
</td>

このようにmaxを設定して

<td>
  <input type="number" max="10"
         ng-model="prime" name="primeNumber" required=""
         />
</td>

フォームのバリデーションでng-validやng-invalidのCSSクラスが付加されるようにして、そこで必要なスタイルをつける。


まとめ

なんだかAngular独特の機能をできるだけ使わない方がいいという感じを受けた(^^);

Angularが便利だからとあまり依存しすぎないようにして、必要最小限の利用にとどめた方が、コードのメンテナンス性も含めた全体のパフォーマンスが上がりそうだなと思った。

これらのTIPSの中では特にone-time-bindingとバーチャルスクローリングの効果が高そう。


こちらも良記事。

Ultimate AngularJS and Ionic performance cheat sheet | Julien Renaux Blog

やっぱりone-time-bindingは共通して言及されている。

2015年8月13日木曜日

AngularMaterial VS Polymer - UI部分だけそれぞれで実装・比較して分かったこと

あるWEBアプリをAngularMaterialを使って実装しているが、最近Polymerが1.0になったので、実験的にUI部分をPolymerで実装し直して比較してみることにした。

ここに分かったことをまとめておこうと思う。


UIコンポーネントの数

AngularMaterialの方がベーシックに使えるものが多い。最近ではFAB Speed Dialなどのコンポーネントが増え、益々充実してきている。Polymerにもgoogle-mapなどのコンポーネントが増えてはいる。


UIの動作速度

Polymerの方が速い。PolymerはアニメーションにWebAnimationを使用しているが、AngularMaterialはngAnimateに依存しているからだと思われ。


レイアウトのしやすさ

Polymerの方が分かりやすい。Polymerではvertical, horizontalとするところを、AngularMaterialではrow、columnとなっている。


レイアウト処理の軽さ

Polymerの方が軽いのではないかと思われる。Polymerではレイアウト関連の指定は「class="layout horizontal"」のように全てCSSのclassだが、AngularMaterialでは「layout="row"」のようにタグのアトリビュートで行われるので内部でカスタムタグの処理をしているだろうから。


ライブラリとしての扱いやすさ

AngularMaterialの方が扱いやすい。ngAnimate、ngAria、ngMaterialの各モジュールとCSSをひとつ読み込むだけでたくさんのUIコンポーネントを使うことができる。逆に言えばムダが多い。bowerのコンポーネントの数で言えば3つだ。

一方、PolymerはHTML importの仕組みによって必要なコンポーネントを必要なだけ組み込めるようになっているのでムダが少ないが、小さいファイルをたくさん読み込む必要がある。例えばFAB一つ使うだけでもbowerのコンポーネントは3つでは済まない。


起動速度

AngularMaterialの方が速い。参考までに、今回のアプリでは10回平均の起動時間(msec)はこのようになった。

AngularMaterial Polymer
PC 1132 1665
PHONE 4892 6435

このように、PolymerはAngularMaterialに比べて約1.3〜1.5倍の起動時間がかかった。

原因として考えられるのは、依存ファイル読み込みのオーバーヘッド。AngularMaterialは読み込むファイル数が少ないがPolymerはとても多い。

解決策として考えられるのは、ファイルのconcatだが、HTML importの仕組みだと怖くてヘタにconcatするわけにもいかないし、concatする勇気があっても慎重にファイルの依存関係を調べる必要があり面倒過ぎる。

特にモバイルのような遅いネットワークでは、読み込むファイルの数が多いということは相当なオーバーヘッドになり、悪くすると数倍の起動時間が必要になる可能性もある。


AngularJSとの統合のしやすさ

これはもちろんAngularMaterialの方が統合しやすい。Polymerのコンポーネントはそのままではng-changeなどのマッピングができない。マッピングするためにはこのライブラリを使う。

GabiAxel/ng-polymer-elements

ただ、このライブラリを実際に使ってみたところ、paper-toggle-buttonのng-changeにマッピングされたメソッドでそのpaper-toggle-buttonのON/OFF状態を調べたところ、完全に状態が逆になっていた。


結論

特にPolymerのUI動作速度は捨てがたいが、AngularMaterialも我慢できないほど遅いということは無い。どちらかというと起動速度の方が問題なので、現状のAngularMaterialを引き続き採用することにした。

2015年8月6日木曜日

CSSのtranslate3dをパースして数字の配列にするには?

CSSのtranslate3dは例えば translate3d(5, -10, 0) のような文字列になっているが、このままでは計算するのに使いにくい。

そこで、これをパースして数字の配列にするにはどうすればいいだろう。例えば、このような正規表現を使うというのはどうでしょう?

/**
 * CSSのtranslate3d文字列を数字の配列に変換して返す
 */
function parseTranslate3d(string) {
  var array = string.replace('translate3d', '').match(/-?[\d\.]+/g);
  for (var i = 0; i < array.length; i++) {
    array[i] = Number(array[i]);
  }
  return array;
}

2015年7月11日土曜日

ionicでcrosswalkのビルド後のapkがarmv7とx86に分かれた件

最新版のionicでは、crosswalkのビルド後のapkがarmv7とx86に分かれるようになった。

これによって、armv7とx86両対応のために40MB以上になっていたアプリサイズが20MB程度まで軽量化された。

喜ばしい事ではあるけれど、リリース時にとても混乱してしまった。

リリーススクリプトを書いているのだけど、これまでは一つのapkにサインすればよかったものが二つのapkにサインする必要がでてきて、スクリプトがまともに動かなくなってしまったからだ。

しかし、このことに気づいた後は特に問題は無かった。

Google Playでは、すでに一つのリリースで複数のapkをアップロードできるようになっているので全く心配は無い。

2015年7月8日水曜日

ionicで最新版のcrosswalkをインストールする方法

ionicで最新版のcrosswalkをインストールする方法について。

Crosswalkのstableな最新版はここでチェックできる。

現在「14.43.343.17」が最新版だが、「ionic browser ls」すると


ionicでは14.x系はcanaryになっていることがわかる。

ここに最新版を追加するにはionicのbrowser.jsというスクリプトを編集する。

$ sudo vi /usr/local/lib/node_modules/ionic/node_modules/ionic-app-lib/lib/browser.js

Browser.crosswalkVersions = [
・・・・
{
version: '12.41.296.5',
publish_date: '2015-03-05 13:19'
},
{
//独自に追加
version: '14.43.343.17',
publish_date: '2015-06-30 14:12'
},
{
version: '13.42.319.6',
publish_date: '2015-04-14 14:24',
beta: true
},
{
version: '14.42.334.0',
publish_date: '2015-04-13 09:16',
canary: true
}
];

再度「ionic browser ls」すると


「14.43.343.17」が追加されていることがわかる。

あとは

$ ionic browser add crosswalk@14.43.343.17

とすれば最新版をインストールできる。

2015年6月27日土曜日

Angular2.x系が出るまで1.x系でES6やTypeScriptと共存させる方法についてのビデオ

Angular2.x系が出るまで1.x系でES6やTypeScriptと共存させるにはどうしたらよいのか?

この疑問に、このビデオ「Getting Ready for Angular 2.0」がある程度答えてくれていたのでメモ。



55:34あたりからThe question is…「But What Do We Do Now?」と始まる。そう。そこが聞きたかった!

ES6って何となく全く違う言語のような気がしていて、一体どんな風に書くの?と思っていた。

しかし、よくよく考えてみるとES6はES5の機能を受け継いでいるので、ES6で書いてもAngular1.x系は動くのである。というか、「ES5でES6の拡張機能が使えるようになったと考えた方がいい」ということに気づかされた。


例えば、これがおなじみの1.x系でのコントローラーの書き方だ。


そして、ES6と共存させて書くとこうなる。


より現実的には、このようにクラスをファイルに分けることになるだろう。


これを使う場合はこうなる。


その他、ディレクティブやNew Routerについてもメンションされている。

これらをヒントに、これからES6というかTypeScriptで書く実験をしてみたいと思う。

ES6はつい先日正式版になったばかりだ。ではES6を使い始めるのは一体いつか?それは今でしょう。

Thanks, Yuri Takhteyev!

2015年6月12日金曜日

ionicでインストールするcrosswalkを選択できない件

追記15.07.11:最新版のionic(1.6.1)では「ionic browser add crosswalk@バージョン」で指定通りインストールできるようになった。この記事も合わせて読むとionicの更新に依存することなくcrosswalkの最新版をインストールできるようになる。

琴線探査: ionicで最新版のcrosswalkをインストールする方法

----

最近ionicのCLIが1.5.0にバージョンアップしたのだが、その後platform/androidを削除してcrosswalkを再インストールしたら、localStorageに保存したはずの値がアプリの再起動時に保存前の状態に戻っているという動作になってしまった。

ChromeのDevToolsでは、crosswalkのバージョンは42.0.2311.135となっている。以前のバージョンが何だったのかは覚えていないが、このバグが出る前は確か41系だったような気がする。

「ionic browser ls」でインストールできるcrosswalkを見ると、crosswalk-liteが増えていたりするので多分アップデートされたと思う。

幸いなことに、ionicはcrosswalkのインストール時にバージョンを指定できるので、stableで一番古いものをインストールして実行し、再びDevToolsでバージョンを確認すると…全く変わっていない!

何かおかしい。ネットで調べると、どうやらcrosswalk-liteもインストールできないという話。

cordova - How to build android with Crosswalk lite using ionic-cli? - Stack Overflow

このページを参考に、「ionic browser add」で何をやっているのかソースを見てみることにした。場所としてはここ。

/usr/local/lib/node_modules/ionic/node_modules/ionic-app-lib/lib/browser.js

問題の場所はこの11行目だとわかった。



cordova 5.0.xがインストールされていると「Browser.installCordovaCrosswalk(appDirectory);」を実行するようになっていて、せっかく指定したバージョンを無視している!これではダメに決まってる。

そこで、11行目をコメントしてその下の処理に流してやるようにした。ついでにインストールしようとしているバージョンを表示するようにした。



これでcrosswalkの任意のバージョンをインストールできるようになった。

2015年6月11日木曜日

ionic(cordova)のカスタムプラグイン開発のワークフローを構築する

カスタムプラグイン開発のワークフローが必要だ!

ionic(cordova)のカスタムプラグインを開発するのはそれほど難しいことではない。しかし当然の事ながら、プラグインの開発は試行錯誤を重ねる必要がある。そのための円滑なワークフローを構築するのにかなり苦労したので、まとめておきたいと思う。

ここでは、ionic側からカスタムプラグインを叩いてネイティブのノーティフィケーションを表示するサンプルを作ろうと思う。とりあえずAndroid用を開発することにする。


ionicプロジェクトの作成

まず、ionicプロジェクトを作り、CordovaLib-debug.aarを作るため一度ビルドする。



AndroidStudioプロジェクトの作成

AndroidStudioで~/Documents/git/customplugin-androidに、パッケージ名ex.customplugin、Blank Activityを選択してプロジェクトを作る。

そして、このようなレイアウトを作る。




CordovaLib-debug.aarを組み込む。これはAndroidStudioでCordova関連クラスをimportするのに必要。

File>New>New module>Import .JAR or .ARR Packege>Next。

File Name: ~/Documents/git/customplugin/platforms/android/CordovaLib/build/outputs/aar/CordovaLib-debug.aar

からのFinish。

左のツリーでGradle Scripts>build.gradle(Module app)を開き、dependency部にCordovaLib-debugの記述を追加。



ここでCordova関連クラスをimportできるようにするために一度ビルド。Build>Rebuild Project。


ノーティフィケーションアイコンの作成

適当なサイズのアイコン画像(512x512程度、アルファ付き、塗りは白、アイコン内のパディングは極力少なく)を用意する。

その画像をこちらにアップロードしてDownload Zip。

Android Asset Studio - Icon Generator - Notification icons


ダウンロードしたic_stat_notif_icon.zipを解凍するとresフォルダができるので、その配下のフォルダを全てcustomplugin-android/app/src/main/resにコピー。

これでカスタムプラグインを作る用意ができた。


カスタムプラグインの開発(ネイティブ側)

File>New>Java Class>Name: CustomPlugin。

CustomPlugin.javaをこのように編集する。



MainActivity.javaをこのように編集する。



カスタムプラグインをネイティブで実行確認

Ctrl+Rで実行。


ボタンをクリックするとこのようにノーティフィケーションが表示された。


これでネイティブ側でのカスタムプラグイン開発は終了。ここからが本番。


ionicプロジェクトにカスタムプラグインを構築するフォルダを作成

ここではcustomplugin/plugins-custom/ex.custompluginとする。pluginsというフォルダが元からあるが、ここに作ってはいけない。pluginsはionicのシステムにマネージメントさせる。


ライブラリ(jar)をコピー

次にプラグインに必要なライブラリ(jar)をコピーしておく。



プラグインJSを作成

plugins-custom/ex.customplugin/customplugin.jsをこのように編集する。これがネイティブとの橋渡し役をする。


plugin.xmlを作成

plugins-custom/ex.customplugin/plugin.xmlをこのように編集する。



開発中のプラグインを素早くionicに反映させるgulpスクリプトの作成

次に、開発・編集したネイティブカスタムプラグインをionicプロジェクトへ素早く反映させるためのgulpスクリプトを書く。

まず必要なnodeモジュールをプロジェクトにインストールする。



ところで、ionicはビルド時にprepareを行うが、これはplatform配下に様々なファイルをコピーしたりする処理だ。hooks/after_prepare/010_add_platform_class.jsはそのprepare後に行われる処理。

カスタムプラグイン関連の処理はこのprepareの前に行わなければならないので、スクリプトをhooks/before_prepare/010_copy_custom_plugins.jsと作成してこのように編集する。



gulpスクリプトの動作確認

ここで「ionic prepare android」して次の項目を確認する。

  • plugins-custom/ex.customplugin/src/android/CustomPlugin.javaが存在するか?
  • platforms/android/src/ex/customplugin/CustomPlugin.javaが存在するか?
  • platforms/android/res/drawable-hdpi-v11/ic_stat_notif_icon.pngなどのリソースが存在するか?

これでAndroidStudioで編集したカスタムプラグインのソースをプラグインの追加と削除をせずとも即座に、また自動的にionicプロジェクトに反映させることができるようになった。


カスタムプラグインのインストール

ついにionicプロジェクトにカスタムプラグインをインストールするが、その前にplatforms/android/src/ex/customplugin/CustomPlugin.javaが存在するとインストール時にエラーになるので削除しておく。

そして「ionic plugin add plugins-custom/ex.customplugin/」でインストール。次の項目を確認する。

  • platforms/android/assets/www/plugins/ex.customplugin/customplugin.jsが存在するか?
  • platforms/android/libs/配下にandroid-support-v4.jarとandroid-support-v7-appcompat.jarは存在するか?
  • platforms/android/res/xml/config.xml内に「ex.customplugin.CustomPlugin」が存在するか?

asssets配下のcustomplugin.jsはprepare・ビルド時にplugins/ex.customplugin/customplugin.jsが毎回上書きコピーされる。before_prepareでplugins-custom配下のjs(マスター)をplugins配下にコピーしているのはそのためなので、編集はplugins-cutom配下のものを編集する。

java/jarファイルのコピーとplugin.xmlの設定によるconfig.xmlの変更はプラグインをインストールした時にしか行われない。jarの追加やplugin.xmlの変更は比較的少ないのでまぁそれでも良いが、javaファイルの変更は頻繁に行われるはずで、それではまずい。before_prepareでjavaファイルのコピーをしているのはこのため。


カスタムプラグインのアンインストール

ここで一度「ionic plugin rm ex.customplugin」でアンインストールして次の項目を確認する。

  • platforms/android/assets/www/plugins/ex.customplugin/customplugin.jsが削除されているか?
  • platforms/android/libs/内のandroid-support-v4.jarとandroid-support-v7-appcompat.jarは削除されているか?
  • platforms/android/res/xml/config.xml内の「ex.customplugin.CustomPlugin」は削除されているか?


カスタムプラグインの再インストールスクリプトを作成

plugin.xmlの編集やjava/jarファイルの追加などを行う場合にはプラグインのインストール・アンインストールを繰り返すことになるので、このようなスクリプトを用意しておく。



「chmod 744 reinstall-custom-plugins.sh 」で実行権限を与えておくのを忘れずに。


カスタムプラグインをionicで動作確認

これでカスタムプラグインの導入は完了!最後にionicプロジェクトでの動作確認をする。

www/index.htmlをこのように編集する。



www/js/app.jsをこのように編集する。



「ionic run android」でAndroidの実機で実行。するとこのように表示される。


「ノーティフィケーション表示」のボタンをクリックすると、このようにノーティフィケーションが表示される。


つまり、ネイティブアプリケーションと同様に、カスタムプラグインを使ってionicからノーティフィケーションを表示できるようになったわけだ。


ワークフローの再確認

ここで安心してはいけない。目的はあくまでワークフローの構築。カスタムプラグインを変更して素早くionicのプロジェクトに反映させられるかを確認しなくてはならない。

AndroidStudioに戻り、CustomPlugin.javaのshowNotification()メソッドの1行目に「Toast.makeText(context, "ノーティフィケーション表示", Toast.LENGTH_LONG).show();
」を追加してネイティブのトーストを表示してみよう。実行するとこのようになる。


さて、ionicでも同様に動作するだろうか?「ionic run android」で実行。するとこのようになる。


このように、カスタムプラグインのJavaソースを変更してネイティブで動作を確認しつつ、その後にコマンド一発でionicでも動作を確認できるようになったわけだ。


各プロジェクトのダウンロード

いや〜大変(^^);

customplugin(ionicプロジェクト)とcustomplugin-android(AndroidStudioプロジェクト)はgithubにアップしてある。

junkoro/customplugin
junkoro/customplugin-android

2015年6月2日火曜日

コンテンツのレーティング(IARC)が理由でアプリが不承認(リジェクト)される件 @ Google Play

Google Playにて、コンテンツのレーティング(IARC)が理由でアプリが不承認(リジェクト)された!不承認を通知するメールにはこんな文言が…

不承認の理由: Google Play コンテンツ レーティング ポリシーへの違反。


提出したアプリの性質からして、どう考えてもリジェクトされる理由が分からなかった。こんなことは初めてだ。しかし、しばらくすると自動的に承認された(^^);

原因は、「IARC認定」(Certificate ID)が得られる前にアプリを提出したことだと分かった。

IARCのコンテンツレーティング認定アンケートなど、アプリの掲載情報を入力すると提出可能になる。

しかし、実際問題としてはアンケートに答えるだけではダメで、IARCからCertificate IDのメールが来て、Google Play Developer Consoleが次のような状態にならなければ承認されないようだ。


12:37に行ったアンケートにはIARC認定のCertificate IDが無い。15:35のはある。

焦ってググったが、この件についての記事が見つからなかったので書いておくことにした。

同様の状態にハマった方は、焦らず、しばらく待ちましょう(^^);

Google Playには、Certificate IDが得られるまで提出ができないようなバリデーションを行っていただきたい。

2015年5月13日水曜日

脱jQueryするにはdocument.querySelector()もあるけど状況どう?

AngularJS内蔵のjqLiteを使うことにしたが、DOMElementを取得するのにdocument.getElementById()というのは使いにくい場合もある。

ところで、jQueryライクなものとしてdocument.querySelector()もあるけど、状況はどうなのか?と。

Can I use... Support tables for HTML5, CSS3, etc

な〜んだ。全然大丈夫じゃん!なら、もはやjQueryも必須ではないし、getElementById()も要らないじゃん!と思ったが…

document.querySelector - Web API インターフェイス | MDN

【訳注: 上記ではクラスセレクタを用いた検索を例示していますが、この様な単発のクラス名の場合は getElementsByClassName() メソッドを用いた方が高速な動作となります。また、ID セレクタ ( # ) を対象とする検索も可能ですが、その場合は getElementById() メソッドを用いた方が高速です。対象セレクタが流動的で有る場合や、または隣接セレクタなどによるコンビネーションセレクタでの複雑な検索の場合に於いて、querySelector() や querySelectorAll() は真価を発揮します。】

ということなので、要らないということは無いようだ。

つまり、複雑なクエリ条件の場合はquerySelector()、単純な場合はgetElementById()とかgetElementsByClassName()と使い分けをした方が良いと。

angular.element()のjQueryを読み込んだ場合とそうでない場合(jqLite)の動作の違い

できるだけページを軽量化するためにjQueryを読み込まないようにしようと思った。しかし、jQueryは便利なので捨てがたいなと思っていた。そんな時、angular.element()というメソッドがあることに気がついた。

AngularJS: API: angular.element

AngularJSは内部でjqLiteというjQueryのサブセット版を持っていて、angular.element()はそれを使ってjQueryのオブジェクトを返してくれるらしい。

ただ実際に使ってみると、jQueryを読み込んだ場合とそうでない場合で動作が違うのでハマった。

jQueryを読み込んでいる場合はこのように#targetを取得できるが、そうでない場合、つまりjqLiteのみの場合は取得できない。

var $elm = angular.element('#target');

要するにjQueryを読み込んでいる場合は次と等価だ。

var $elm = $('#target');

一方、jqLiteのみの場合はこのようにする。

var elm = document.getElementById('target');
var $elm = angular.element(elm);

Wraps a raw DOM element or HTML string as a jQuery element.

というのはそういう意味だったのか!とやっと分かった(^^);

2015年5月5日火曜日

ionicプロジェクトのsplash.pngを自動でリサイズしながら9-patchする「make-9patched-splash-ionic」

先日、nodejsで9-patch画像を自動生成する方法について書いた。

琴線探査: nodejsで9-patch画像を自動生成する「FourSide1px9patcher」

その後、このコードを使いやすいようにnodeモジュール化した。

junkoro/four-sides-1px-9patcher

ここからが本番。ionicプロジェクトでsplash.pngを自動的にリサイズしつつ9-patchするコードを書くときがキタ(・∀・)!!

すでにnodeモジュール化して公開済み。

junkoro/make-9patched-splash-ionic

インストール方法やコード内容についてはgithubに譲るとして、ここでは開発時に困ったことについて書いておきたいと思う。

9-patchについては開発済みだったので何の問題も無かったが、画像のリサイズが困った。node-canvasでもリサイズはできるのだが、リサイズ後の画像のクオリティーがとても使えたものではないのだ(´・ω・`)

この問題はブラウザ上のJSでも同じで、以前に研究したことがあった。

琴線探査: HTML5 Canvasで画像のリサイズするならコレ!「JS-Image-Resizer」

というわけで、再びこの「JS-Image-Resizer」を使わせていただくことにした。結果は上々だ。

ただ、「JS-Image-Resizer」はブラウザ用のJSコードなので、node用に多少変更する必要があった。と言っても、「modules.exports」行の追加とエラーになる部分(WebWorker部)をコメントするくらいだったが。

これでこの件にコメントすることができるようになった。

Bad splashscreen ratio with Android using CLI new feature - ionic - Ionic

この件は自分も困っていたが、他にも多くの人が困っているようなので、このコードが助けになればと思う。

2015年5月3日日曜日

nodejsで9-patch画像を自動生成する「FourSide1px9patcher」



追記15.05.05:使いやすいようにnodeモジュール化した。
junkoro/four-sides-1px-9patcher

9-patchツールの現状 〜 開発意図

前から9-patchを作るのは面倒だし問題だと思っていた。そこで改めて現状を調べてみたが、やはり9-patchを作るには

・Android SDKのdraw9patch
・Android Asset Studio - Simple Nine-patch Generator
・Eclipseプラグイン「Draw 9-patch Tool
・Fireworksなど汎用の画像編集ソフトを使う

という感じ。nodejsで、特にgulpで9-patchを自動化できるツールが無いか探したが、これくらい。

gulp-9-patch

このツールはスケーリングしたり非9-patch化する機能しかなく、画像を9-patch仕様に加工することはできない。


開発要件

やはり独自開発するしかないか…もう我慢ならん!こんな ('A`)マンドクセ-ことイチイチやってられるかっての!

ただ、なぜこのような状況なのかは理解できる。9-patchは柔軟性が高く、画像によってパッチすべき場所は変わる。これが自動化ツールが存在しない理由なのだろう。

しかし、画像を作る側が9-patchしやすい条件を作ればどうだろう?たとえばこんな画像で、単純に4辺の周辺1ピクセルを画面にあわせて伸ばすように9-patchすることを考える。

画像はionicのデフォルトスプラッシュスクリーン用の画像

この条件を満たすケースは意外と多いのではないかと。自分的には自動化するためにデザインを犠牲にする覚悟である(^^);

このようなことをしてくれるCLIツールを、後々ionicなどgulpやgruntをビルドシステムとして使っているようなものに組み込むことを考えて、nodejsで開発する。


nodejsでの画像処理をどうする? 〜 技術調査

調べると、みんなよくimagemagickとかを使ってやっているようだが、できればコマンドラインツールに依存するような形にしたくない。

というわけで、前から名前だけは知っていたけれど、よく調べたことがなかったこれを使ってみよう。

EaselJS | A JavaScript library that makes working with the HTML5 Canvas element easy.

EaselJSはHTMLのCanvasをラップしてFlashライクに扱えるようにするライブラリだ。

CreateJSというFlashコンテンツをHTML5に変換するのをサポートするランタイムライブラリのひとつ。当然Adobeが関係してる。node版もある。

CreateJS/EaselJS-NodeJS

これで描画関連は問題無いだろう。画像ファイル化はどうか?

Automattic/node-canvas

node-canvasはtoBuffer()というメソッドを持っていて余裕でPNG化できるらしい。このBufferをfsモジュールでファイルに書き出せば終了だろう。楽ぅ〜(*^_^*)

しかもPDF化とかSVG化もできるらしい。なんかいつの間にか色々やりたい放題な状況になっていたんだね\(^o^)/

そもそもEaselJS-NodeJSがこのnode-canvasに依存しているので、かなり成熟しているライブラリなのだろうと思う。とりあえずこれで。


開発準備

$ mkdir FourSide1px9patcher
$ cd FourSide1px9patcher

node-canvasをインストールするには、結構ネイティブ側の準備が必要らしい。さすがにネイティブと無関係とはいかないか(^^);

Installation OSX · Automattic/node-canvas Wiki

HomebrewとかMacPortとかでインストールした方がいいだろう。自分はbrew派なのでbrewで。

MacOSX - パッケージ管理システム Homebrew - Qiita

$ brew install pkg-config
$ brew install pixman
$ brew install cairo
$ PKG_CONFIG_PATH=/opt/X11/lib/pkgconfig npm install canvas


開発

後はエディターでコーディング。

コーディング中に気づいたのだけど、EaselJSは必要無かった(^^); 描画の部分はCanvasだけで事足りてしまった。

やっていることは、元画像に透明の枠をつけ、左上、左下、右上をこのように加工して9-patchする作業。

左上

左下

右上

//モジュール読み込み
var fs = require('fs');
var Canvas = require('canvas');
var Image = Canvas.Image;


/**
 * ピクセル描画
 */
function drawPixel(context, x, y, color) {
  context.save();
  context.translate(x, y);
  context.fillStyle = color;
  context.fillRect(0, 0, 1, 1);
  context.restore();
}


/**
 * 4辺の周辺1ピクセルを9-patch
 */
function fourSide1px9patcher(src, dst) {

  //イメージ読み込み
  var img = new Image;
  img.src = src;

  //9-patchは元画像の周りに1pxの透明の枠を付ける
  var cv = new Canvas(img.width + 2, img.height + 2);

  //コンテキスト取得
  var ctx = cv.getContext('2d');

  //イメージを座標(1, 1)に描画
  ctx.drawImage(img, 1, 1);

  //9-patchの線は黒
  var color = 'rgba(0, 0, 0, 1.0)';

  //ドット描画 - 左上-TOP
  drawPixel(ctx, 1, 0, color);

  //ドット描画 - 左上-LEFT
  drawPixel(ctx, 0, 1, color);

  //ドット描画 - 左下-LEFT
  drawPixel(ctx, 0, cv.height - 2, color);

  //ドット描画 - 右上-TOP
  drawPixel(ctx, cv.width - 2, 0, color);

  //ファイル書き出し
  fs.writeFile(dst, cv.toBuffer());

}


//実行
fourSide1px9patcher('src.png', 'dst.9.png');


実行

$ node FourSide1px9patcher.js

生成されたdst.9.pngをdraw9patchで開くと、意図通りに9-patchされていることを確認できた。

右側で中央のionicロゴが歪んでいない

draw9patchは画像が正しく9-patch加工されていても、拡張子が「.9.png」になっていないと9-patch画像として認識しないので注意する。

次はionicプロジェクトのリソース構造に合わせて9-patchするラッパーを書こう。

追記15.05.05:書いた。
琴線探査: ionicプロジェクトのsplash.pngを自動でリサイズしながら9-patchする「make-9patched-splash-ionic」

2015年4月5日日曜日

迫り来る電子の声と人形に宿った凄まじい情念 〜 映画「ボーカロイドオペラ葵上with文楽人形」@アミューあつぎ映画.comシネマ

映画「ボーカロイドオペラ葵上with文楽人形」 を地元の映画館「アミューあつぎ映画.comシネマ」に見に行ってきた。




動機

一つの理由は、アミューあつぎ映画.comシネマの副支配人でおられる杉本穂高さんのこの記事を見たから。

「ボーカロイド™ オペラ 葵上 with 文楽人形」人ならざるものの魂の躍動 | 杉本穂高

もう一つの理由は、なぜか分からないけれど以前からアンドロイド的なものが気になっているから。

古くはbjörkの「all is full of love」。



近年では、冨田勲×初音ミク「イーハトーヴ交響曲」とか、オリエント工業のラブドール展とか、平田オリザ・石黒浩のアンドロイド演劇。

これらの作品には根源的に共通する何かがあるはずだと思うのだけど、いまだにそれが一体何なのか分からないので、今度こそハッキリさせよう!と。


「葵上」とは?

まず、「葵上」とはどう読むのか?「あおいのうえ」と読むらしい。そしてその「葵上」とは一体何なのか?源氏物語をベースにした能の演目だそうだ。

葵上 - Wikipedia


第一印象

この映画は普通の映画ではない。演劇を撮った映画というか…しかし、決してドキュメンタリーでもないし…初めて見るタイプの映画なので、正直どう評価していいのか分からなかったが、とにかく、電子の声と人形が凄い念をもって迫ってきた!という感じだった。


単純ではないプロット

パンフとサントラCDを買って帰ってきて、「あれは何だったんだろう…」と考えているうちに、源氏物語にも能にも文楽にも造詣が無いのと、約30分という短さもあって、全然話しについていけてなかった自分に気がついた。

要するに話はこうだ…と、まとめようとしたが、やっぱりこれは簡単にまとめられるような話じゃない。

そこで、表面的な「事実」だけ挙げていくとこうだ。

ボカロPのヒカル(光源氏)はボカロのミドリ(六条御息所・ろくじょうのみやすどころ)に曲を書いていた。その曲を聴き、あこがれて歌手になった生身のアオイ(葵上)はボカロPのパートナーとなった。その後のある日、生身の歌手が憑かれたようにあばれまくる!

という感じ。

ボカロPが生身の歌手を得てボカロを捨ててしまったので、ボカロがジェラスして生身の歌手に取り憑いて暴れる!という話なら分かりやすい。しかしよくよく考えると、怒っているのはむしろ生身の歌手の方ではないのか?という。

いや、本当にどういうことなのか分からない(^^);

この話は生身の歌手のマネージャーの回想という形なのだが、最後にマネージャーが「あれは何だったんだろう…」と一人佇む。

自分もまさにその心境だ。


文楽人形の「スーパーモード」

文楽人形には基本的に表情が無い。まさに能面だ。そう思っていた。しかし、文楽人形には「スーパーモード」が存在することが分かった。

何と文楽人形の口がぱかっと開いて、鬼?悪霊?とにかく凄い情念を表現していた。

後でよくよくポスターを見ると、口のところに裂け目があることが分かった。


ボーカロイドは言霊を載っけられる楽器

滑らかで完璧すぎる音階。そして広すぎる声域。ボーカロイドは言霊を載っけられる楽器だ。

その人間わざではない歌声で凄い情念を表現していた。


「共通する何か」は分かったか?

どうだろう…しかし、パンフにあったこの言葉はそれかもしれないと思った。

「命なきものに命を吹き込む」

ボーカロイドも、ラブドールも、アンドロイドも、文楽人形も、確かに人間ではないけれど、生きているように感じた。

つまり、「生きている」というのは物理的な問題ではなく、「生きている」と信じる心理的な問題だ、ということだろうか。


アミューあつぎ映画.comシネマの「前説」という新しい試み

アミューあつぎ映画.comシネマでは、どうやら映画の前に「前説」をするらしい。

これまで何度となく映画館に行ってきたけれど、前説など初めて見た。シネコンにばかり行ってきたからだろうか。

非常に好感を持った。

2015年3月24日火曜日

PCブラウザでネットワーク接続の有無を検知するには?

PCブラウザでネットワーク接続の有無を検知するにはどうしたらいいだろう?

一応「navigator.onLine」というAPIはある。

window.navigator.onLine - Web API インターフェイス | MDN

ブラウザのサポート具合を調べてみると、全てのブラウザというわけにはいかないが、まあまあサポートされていることがわかる。

Can I use... Support tables for HTML5, CSS3, etc

しかし、このAPIには色々と問題があることがすぐに分かった。

javascript - How to detect online/offline event cross-browser? - Stack Overflow


Chrome and Safari will detect when you go "offline" automatically - meaning that "online" events and properties will fire automatically when you unplug your network cable.

Firefox (Mozilla), Opera, and IE take a different approach, and consider you "online" unless you explicitly pick "Offline Mode" in the browser - even if you don't have a working network connection.


要するに、ChromeやSafariならネットワーク接続が切れた時点で自動的にオフラインイベントが来るが、FirefoxやOpera、IEはブラウザ側に「オフラインモード」という機能があるので、navigator.onLineの値は実際のネットワーク接続に有無ではなく「オフラインモードかどうか」の値になってしまう、という話。

つまり、全くもって役に立たない!

ではどうするか?

StackOverflowでも議論されているが、要するに「サーバーにリクエストを投げて返ってくればオンラインじゃね?」というシンプルな考え方が最も信頼できそうだ。

賢くやりたいなら、こういったライブラリもある。

cvan/navigator.onLine-that-actually-works

愚直にやりたいなら、ここが参考になる。シンプルにXMLHttpRequestを使う。

navigator.onLine alternative: serverReachable() | Louis-Rémi

このコードを参考に、少し改造してこのようにした。

/**
 * 実際にサーバーに接続できるかどうかを返す
 * URL指定が無ければ自ホストの「/? + Math.random()」
 */
function isServerReachable(url) {
  if (!url) {
    url = location.protocol + '//' + location.host + '/?' + Math.random();
  }
  console.log('ネットワーク接続を確認します:' + url);
  try {
    var req = new XMLHttpRequest();
    req.open("GET", url, false); //同期で接続
    req.send();
    if ((200 <= req.status && req.status < 300) || req.status == 304) {
      console.log('ネットワーク接続可能 status=' + req.status + '(' + req.statusText + ')');
      return true;
    } else {
      console.log('ネットワーク接続エラー status=' + req.status + '(' + req.statusText + ')');
      return false;
    }
  } catch (err) {
    console.log('ネットワーク接続エラー');
    return false;
  }
}

req.open()でのメソッドの指定はHEADにした方が高速化のためには好ましいのだろうが、ローカルのデバッグサーバーによっては著しくレスポンスが遅い場合があった。

そこで、サーバー上にゼロバイトのHTMLファイルを作り、HEADではなくGETを指定してそのファイルのURLを指定するようにした。恐らくこの方法は鉄板だと思われる。

2015年3月8日日曜日

「いずれ米国がこの人種差別主義を解決できると信じている」と他人事の黒人初の大統領

日経14.12.09夕 オバマ氏「人種差別根深い」黒人射殺不起訴 続く抗議デモ 心情に理解示す

【ワシントン=吉野直也】オバマ米大統領は8日放映の米テレビ番組のインタビューで「米社会で人種差別主義は根が深い」と表明…

2008年大統領選で「一つの米国」を掲げて黒人初の大統領になったオバマ氏はこれまで黒人側の立場を鮮明にするのを避けてきた…

「米国は過去50年間で公民権問題を前進させてきた。いずれ米国がこの人種差別主義を解決できると信じている」…

「オバマ氏が大統領になって人種間の対立が悪化した」と回答した人が53%と過半を超えた…

「米社会で人種差別主義は根が深い」?そんなことなら自分でも言える。

「一つの米国」を掲げて黒人初の大統領になったのに、米国民の53%はオバマ大統領になってから人種間の対立が悪化したと答えているそうだ。

「Yes, we can!」はどうした?鳩ちゃんの「Trust me」もしょーもなかったけど、「Yes, we can!」もしょーもないな。

しまいには、「いずれ米国がこの人種差別主義を解決できると信じている」だって。「いずれ」ね。どんだけ他人事なのか。

当初はすばらしい大統領が出てきたなーと思っていたけれど、すでに終わってるよ(´-`)

しかし、この大統領の時代があと2年以上は続くわけでして。。。

米国は日本を最重要パートナーとして再認識し始めている

日経14.11.27朝 米国民「日本が最重要」 「アジアのパートナー」中国を逆転 外務省調査

外務省が米国民を対象に今夏実施した日本に関する世論調査で、「アジアで最も重要なパートナー」に日本をあげた「一般国民」が46%に上り、2年ぶりに中国(26%)を逆転して首位となった。

有識者を対象とした調査でも日本を選んだ人が58%と、中国の24%を大きく上回った。

一般国民と有識者でいずれも日本が首位になったのは5年ぶりだ。中国が東シナ海や南シナ海で挑発的な海洋進出を続けていることなどが影響したと見られる…

「日米両国がアジア太平洋の平和と安全のために緊密に協力すべきか」という質問には、一般国民の91%、有識者の97%が同調した。

日米安全保障条約は「維持すべきだ」と答えた一般国民が81%(昨年67%)、有識者は85%(同77%)…

日本国民として名誉なことだ。中国の本性が米国民にもバレてきているのだろう。

さらに、米国人のほとんどが日米がアジア太平洋の平和と安全のために協力すべきだと考えているということも確認できた。

ただ一方で、日米安全保障条約の維持に関しては「協力」のパーセンテージより10ポイントほど低くなっている。

つまり、「協力はすべきだが、米国が一方的に日本を守るということに抵抗がある」という人がそれくらいはいるということなのではないだろうか。

本当に悩ましい地下水バイパスといわき漁業の復興のバランス

日経 14.11.12 いわき 遠い「大漁」 試験操業1年、残る出荷制限 安全性PRへ奮闘

…漁を自粛した福島県いわき市の漁業組合が昨年10月に試験操業を始めて1年余り。放射性物質の基準値を下回る魚種は増えているが、県漁連は約150の全魚種の安全性が確認されるまでは本格操業を再開しない方針だ…

今月9日、いわき市の小名浜港で、原発事故後初の地元漁協による「魚まつり」が開かれた…約1時間で完売…

「風評被害に苦しんでいると思うので、地元産を応援したい」…

「水揚げ量が少なく、とても商売になるレベルではない」…

「サブドレン」と呼ばれる井戸から、地下水をくみ上げ浄化後に海に流す…

「新たな風評被害を招きかねない」と反対意見が相次いだ…

震災1年後くらいの時期にいわきの漁港に行ったことがある。その時は本当に廃墟のようだった。それが束の間とはいえ、このように賑わっていることはとても喜ばしい。

日経記事より

福島第1原発近くの地下水は問題ないことを確認しつつ海に流すべきだと思う。そうしなければその分の水が原発に流れ込み、高濃度の汚染水となって海に流れ込むことになりかねないと思うから。

しかし、地元の漁師さんたちはこれに反対している。新たな風評被害を招きかねないとお考えだからだ。

この苦悩は過去の報道でも知っていたけれど、それでも流すほかないだろうと今でも考えている。万が一地下水が高濃度の汚染水になって海に流れ込むことになったら、より厳しい状況になり、本格操業はさらに遅れることになってしまうから。

さりとて漁師さんたちの気持ちもわかる。地下水を流すことにすると、風評被害が増えることは考えられても減ることは考えられないし、漁業者にとっての問題は何も解決しない。

ひとつの一時的な対処は「補償」という形の金なのだろうけれど、それだけで解決できる問題だろうか。生き甲斐とか張り合いとか、そういった人生に関わる問題も絡んでくるのではないだろうか。

本当に悩ましい。

「頑張る・努力する」は評価としてはゼロだし、当然のことだけど、簡単なことでもない

日経14.11.14朝 サッカー人として 三浦知良 全力でやるしかない

…僕自身のことをいえば今季は4分間しか出場していない厳しさのなかにいる…

「出場できなくても毎日頑張りました」。これは自分を納得させる限りのもので、評価の上ではゼロだ。頑張る。努力する。それは当たり前のこと。ただし普通のことを普通に毎日やるのも、やっぱり難しい。「当たり前のこと」だけど簡単なことでもないんだ…

サッカーに本当の"苦労"はないと思っている。全部プラスになることだから…

サッカー様々ですよ。だったら、サッカーに失礼のないようにやっていくしかないです。全力で…

「頑張る・努力する」は当然のことで、評価としてはゼロだと。しかし、簡単なことでもない、と。

何も言うことはない。

ただ、それでも「サッカー様々ですよ。」というキングカズは、とても素敵な人だと思う。

2015年3月6日金曜日

ツイートボタンをリロードする(ページが読み込まれた後に動的に再作成する)には?

ツイートボタンはこのページで自由にカスタマイズして作成できる。

Twitter Buttons | About

作成したコードをページに埋め込むと、そのページをブラウザでロードしたときに表示されるのは知っての通り。

基本的にツイートするアドレスや内容などはページをロードする時に決まっている必要があるが、ページをロードした後でそれらを変更するにはどうしたらいいだろうか?

ブログやホームページなどの静的なサイトで問題になることはあまりないだろうが、AngularJSなどで構築したシングルページアプリケーションでは大いに問題になり得る。

まず、ツイートボタンを表示するホルダーをこのように作成する。

<!-- ツイートボタンホルダー -->
<div id="tweetButtonHolder">
</div>

<!-- ツイートボタン作成サイト https://about.twitter.com/resources/buttons#tweet でコピーしたscriptタグ -->
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs');</script>

このscriptタグは window.twttr を生成する。このオブジェクトは様々なツイートボタンを作ることができる。

Scripting: Factory Functions | Twitter Developers

なので、このtwttrオブジェクトを使ってツイートボタンホルダーの場所にツイートボタンを作る(リロードする)には、例えばこのようなコードになるだろう。

function reloadTweetButton(url, text) {

  //ツイートボタンを消す(jQuery使用)
  $('#tweetButtonHolder').empty();

  //ツイートボタンを再作成
  twttr.widgets.createShareButton(
    url, //ツイートしたいURL
    document.getElementById('tweetButtonHolder'), //ツイートボタンを挿入する場所
    {
      lang: 'ja', //言語。jaだとボタンに「ツイート」と表示される
      text: text //ツイートしたいテキスト
    }).then(function (el) {
    console.log("ツイートボタン作成完了");
  });

}

2015年3月3日火曜日

macでsmb(samba)共有サーバーに別名で接続(別アカウント名で接続)する方法

macでsmb(samba)共有サーバーに別のアカウント名で接続したくなった。

接続を解除してもパスワードを保存してしまったために自動的に再接続してしまい、別アカウント名で接続できなくなってしまった(´・ω・`)

どうすれば別名で接続できるのか?

Finder>移動>サーバーへ接続>サーバアドレス に下記のように入力する。

smb://アカウント名@ホスト名

ノリでやってみたら、たまたまできちゃった(・∀・)

知らなかったわ〜

2015年2月18日水曜日

AngularJSのコードをminify対応にするためのベストプラクティス

AngularJSはDependency Injectionをするので書き方によってはminifyした時にコードが破損してしまう場合がある。

例えばこのようなコードは破損する。

var app = angular.module('app', []);
app.controller('AppCtrl', function ($scope, $location, $interval, $timeout) {
}

ベストプラクティスはこれ。(だと思う)

var app = angular.module('app', []);
app.controller('AppCtrl', ['$scope', '$location', '$interval', '$timeout',  function ($scope, $location, $interval, $timeout) {
}

面倒でもfunctionの引数の定義の通りをfunctionの前に記述する。

このことはAngularJS公式チュートリアルの「A Note on Minification」に書いてあった。

AngularJS: Tutorial: 5 - XHRs & Dependency Injection

こちらにも同じ議論がある。

javascript - Angularjs minify best practice - Stack Overflow

2015年2月17日火曜日

キーボードの記号フォント「Keyboard-JP」(n-yujiさん作)をWEBサイトで使う方法

先日、このような記事を見た。

keyboard-font·キーボードを表現した記号フォント MOONGIFT

元々はn-yujiさんの作品だ。

n-yuji/keyboard-font

ボタンアイコンのツールチップなどでのキーボードショートの説明に使うのにちょうどいいなと思ってWEBサイトで使ってみようとしたが、正常に表示できるようになるまでに意外と苦労したのでログを残しておこうと思う。

まずはデモ(Keyboard-JP Web Example)。こんな感じの表示になる。


苦労した最大のポイントは「リガチャー」の変換がうまくできなかったこと。

「リガチャー」というのは「合字」のことで、例えば上の例だと「POWER」とテキストで指定した場合に電源マークに変換されるようなことを言う。

最終的なフォント指定のCSSはこれ。ポイントは「font-feature-settings」と「text-rendering」プロパティー。


HTMLはこんな感じ。HTML内では左の列と右の列で同じ文字列を入れているが、ブラウザでの表示は異なることがポイント。


その他にどのような文字を使えるかは、n-yujiさんのオリジナルパッケージ内のPDFを見るとわかる。

ほとんどの文字はリガチャーによって変換されるが、一部の文字、例えばMacのコマンドキーマークは右に「U+2318」と記述されている。HTMLでこれを指定するには「&#x2318;」と指定する。

最後に、IEで表示するためにはWOFFファイルが必要だ。OTFファイルを下記のソフトでWOFFに変換する。

WOFFコンバータ - 武蔵ソフト

上のフォント指定のCSS(Keyboard-JP-Regular.css)にはすでにWOFFファイルの記述もしてあるのでIEでも動作を確認できる。

プロジェクト全体はここでダウンロードできる。

junkoro/Keyboard-JP_WebExample