2015年4月
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30    
無料ブログはココログ

« 2013年9月 | トップページ | 2013年11月 »

2013年10月の5件の記事

2013年10月31日 (木)

手書き要素の配置換えを「ちょっとだけ」便利にするシール

画面に文字や図を手書きした後で、それらの配置を入れ替えたり並べ直したりしたいことがあります。

こういう場合は、手書きしたものを Link シールにして、画面上でドラッグするという(とりあえずの)ノウハウがあります。これはこれでよいのですが、動かした手書き要素がいつまでもシールに張り付いていて、上から線を重ね書きするときに邪魔になったり、と、時々困ることもあります。

Link シールのドラッグで配置換えした手書き要素を、もう一度ページの上に書き戻して、シールは剥がしてしまいたい。書き戻せば、また、他のシールで処理することもできるだろうし。ということで作ったシールです。

名前は一応「定着シール」とします。

(2014/8/24 : MOONPhase v.2.10.0 対応済み)

Download : [定着シール]

使い方

  • まず、普通に画面にいろいろ手書きします。
  • 配置を変えたいパーツがあれば、指で囲んで、"Link" でシールにします。
    • Link 先はどこでもよいですが、この目的の場合は、「自分のページ」への Link がお勧めです。これだと間違って tap 操作をしてしまっても、他所のページに移動しないのでストレスになりません。
  • 配置が決まったら「定着シール」を実行します。シールのストロークをベースのページに書き出します。
    • このとき、web からの切り取りなど、背景画像を持っているシールのストロークは無視しますが、手書きだけのシールは Link シールと MOONBlock などのシールの区別がつきませんので、全て書き出してしまいます。手書きラベルのシールは先に剥がしておく方がいいと思います。
    • 一旦上記の仕様で作りましたが、背景画像のあるものだけを対象外にしても、あまりメリットがありません。手書きラベルのストロークの「定着」を回避できるならよいのですが。結局意味がないので区別しないことにしました。すべてのシールのストロークが「定着」されて、「ふやけ」ますので、定着させたくないシールは先に剥がしてから使用してください。
    • 背景画像に拘らないようにしたので、付箋シールと併用できるようになりました。詳細は明日か明後日の記事に書きます。
  • 残念ながら、Link シールは手作業で剥がさなければなりません(後述)。
    • 無事に書き出されたシールは、ストロークが「ぶわっと」ふやけますので直ぐ分ります。一つ一つ手作業で剥がしてください。
  • 更に配置を変えたければ、以上を繰り返します。

以上のように、「ちょっとだけ便利にする」程度の代物です。

注意:やっぱり動作が安定しません。ロジックとしては、Link シールが何枚あっても大丈夫なはずなのですが、枚数が多いか、一枚でも大きくてストロークがたくさん含まれているシールだとシステムの負荷が大きいらしく、止まったりします。

小さ目のシールを 1-2 枚ずつ動かして、こまめに「定着」させていった方が安全です。

…実はあんまり便利になってないかも。

ホントはやりたかったのに、できなかったこと

配置調整を Link シールでやるのは、現状では許容します。バッド・ノウハウだけれども現実的。

「定着シール」では、シールのストロークをページに書き出した上で、元のシールを自動的に剥がしたかった。でも、他所のシールから勝手に別のシールが剥がせるというのはユーザーレベルからでは特権過ぎるので、当然不可能でした。

こっちは可能じゃないかと考えたのは、ページのストロークデータと、シールのストロークデータをまとめて、別のページに貼りこむこと。白紙のページか、新規ページに張り込めば、元のページでまた配置調整をやり直すのも簡単だし、実はこちらの方が有用かもしれない。

で、まず実験的に以下のようなコードを書きました。

    sticker.ontap = function(event) {
        // 移動元のページの storage.json に {"before":1} を書き出す
        localStorage[ "before" ] = 1;

        // ノート選択画面から別のページを選んで
        MOON.openNotebook( function( pageID ) {

            // そのページに移動し
            MOON.openPage( pageID );

            // storage.json に {"after":2} を書き出す
            localStorage[ "after" ] = 2;
            MOON.finish();
        });

    };

「双方向リンクシール」の時に、ページを移動してから peel() するとうまくいかないことを経験していましたから、今度はこれで、移動前のページの storage.json と、移動後のページの storage.json にそれぞれデータが書き込まれるはずです…。

…できませんでした。

実際には移動元ページの storage.json が {"after":2,"before":1} になります。他所のページに移動したからといって、そのページの情報にはアクセスできないのです。まあ、セキュリティを考えると、自ページに張り付けていないシールから、自ページのデータを改変されてしまってはかなわないので正しい仕様なのですが。

・・・というわけで、シールも剥がせない、他所のページにデータを張り込むこともできない。だったら、せめて剥がすべきシールが視認しやすくなるように、見た目を変えるくらいの工夫はしよう・・・、と考えた結果、現在のような仕様になりました。

このシールもまた、「ないよりマシ」くらいには役に立ってくれれば幸いですが。

2013年10月29日 (火)

enchantMOON : 双方向リンクシール : 一応公開

[2013/12/06 追記] バージョンアップに伴い、[双方向リンク元検索シール(1)], [双方向リンク元検索シール(2)]は廃盤としました。>詳細


MOONPhase を  ver 2.7.0 に update しました。

ですが、相変わらず「誤動作」します。正常動作する場合も、その時々で動作速度が異なり、「もしかしてハングしたか」と思うほど時間がかかることもあります。しかし、「害」を与えるシールではないので、一旦公開することにします。ダウンロードする方は、申し訳ありませんが、うまく動作しないこともあることを覚悟で試用をおねがいします。

Download : [双方向リンク登録シール], [双方向リンク元検索シール(1)], [双方向リンク元検索シール(2)], [双方向リンク元検索シール(更新版)]

 

使い方

二枚のシールを組み合わせて使います。他のページにリンクを張って登録するシール(registBiLinkTo - 以降「登録シール」と記述)、とリンク元ページの一覧を検索するシール(searchBilinkFrom - 以降「検索シール」と記述)の二枚です。

「登録シール」をページに貼ってから、タップするとリンク先選択画面が開きます。リンク先を選ぶと、そのシールに情報が登録され、以降はタップするとリンク先ページに移動します。(遅いですが)何かの誤動作で、リンク先情報が消失している場合は、タップすると改めてリンク先選択画面が開きます。「登録シール」は、一つのページに何枚でも貼り込むことができますので、画面イメージは以下のようになるでしょうか。

Bilinksticker_2

「登録シール」をいくつかのページに貼ってリンクを形成した後で、「検索シール」を使用します。特定のページで「検索シール」を貼ると、そのページにリンクを張っているページの一覧を表示します。この時表示されるのは「登録シール」でリンクを張ったページだけで、通常の、指で丸を描いて出てくるメニューから張ったリンクは検索できません。(システムレベルで作成したリンクの情報を拾い上げることは、今の処無理でした)

使い方はこれだけです。動作が速くて、システムが不安定にさえならなければ、そこそこ使えるとは思います。

誤動作パターンの説明(愚痴)

技術的な詳細や、ソースコードは、ここ 2-3 日の記事を参照ください。

自分で使っていて遭遇する不具合は

  • シールの貼り付け、実行、剥がす操作で、相変わらず "Sticker is not defined" エラーがでる。
  • 移動先、リンク先ページを表示するまで、「ハングしたか」と思うほど時間のかかることがある。
  • 「検索シール(1)」で、tap すれば剥がれるはずなのに、剥がれない。この場合は、指で囲んで "delete" で剥がすことはできる。
  • ページ移動で、「あれ?」と思うほど時間のかかることがある。
  • ページ移動で、(ものすごく時間がかかっているだけかもしれないですが)動作が止まることがある。電源ボタン長押しで shutdown → reboot しかなくなることがある。
  • リンク先選択、リンク元選択、の画面で、本来表示されるページが表示されなかったり、対象でないページが表示されたりする。何か、直前の動作に影響を受けている印象。
  • 規定の動作をしなかった場合、その後の操作を受け付けないことがある。
    • この場合、画面に指で輪を描けるか試してください。描けなければ、シールの動作が終了していないことになります。3 本指スワイプでキャンセル動作をすれば、入力を受け付けるようになります。

基本的に、いずれも、「誤動作」というより、「動作しない」形です。ですので、期待した動きをしなくてストレスはたまりますが、何度か tap を繰り返したり、最終的には強制的にシールを剥がして張り直せば何とか動くので、「プラスマイナス 0 」くらいにはなると思います。

そんなに複雑なコードは書いていないので、やはり OS の問題かなぁ、と愚痴りたくなりますが、一点自信のないのが、非同期処理のクセがわからなくて、やたらあちこちに MOON.finish() を仕込んであることです。もしかしたらこれが誤動作の原因かもしれません。その場合は、開発スタッフを批判してしまってごめんなさい、と謝罪しなければならないのですが、正直今のところ、MOON.finish() の入れ場所がわかりません。問題点がわかる方がいれば、教えてください。

もし、私の書いたコードにまったく問題がないとしたら・・・、searchStorage() や、openPage() など、ページの移動が発生する API のあとで不具合が生じているような「印象」を受けます。言いがかりでなければ、気長に修正をお願いしたく思います。

2013年10月28日 (月)

enchantMOON : 双方向リンクシールを作って考えた

「双方向リンクシール」やっぱりまだまだ不安定です。MOONPhase ver 2.7.0 で、「シールを貼付けた時と剥がした時にプログラムが実行されないことがある不具合を修正」ということなので、これで改善することを期待して、誤動作が減ったら Bookmarks にも公開することにします。

さて、シールについて、@shi3z 社長からもいろいろとコメントをいただきましたので、何か MOON に前向きな提案ができるように、ちょっと考えてみます。思いつくことをあれこれ書いて、2~3 日寝かせて整理できたら Issue Tracker に要望してみましょう。

大きく、(1)きれいに剥がれて終わるシールの実現方法、(2)システムレベルで作成したリンクの情報活用案、(3)今後の検索 API への要望、に分けて書いてみます。

(1)きれいに剥がれて終わるシールの実現方法

昨日の記事に書いたように、「検索シール」は元々目的のページに移動してからきれいに剥がれて跡を残さないつもりでした。しかし script の実行環境(カレントディレクトリ? カレントページ? カレントオブジェクト?)が移動してから peel() を実行すると、期待した動作にはなりません。

対応案の一つは、peel() が、途中でページを移動しても、実行を開始したときのカレントを保持しておいて ( binding ですね) 元のページのシールを剥がすように実装を変更すること。或いは、ページを移動してシールを剥がして終了という新しい API を追加すること。技術的難易度が同等なら、前者の方が先々使い回しが効くように思います。

但し、この場合、ondetach に割り付けた処理の実行をどうするかが問題です。

シールが剥がれるので、detach event が発生して、ondetach 処理が実行されるべきなのですが、その時のカレント環境がどうなるのか。ondetach には何らかの後処理を記述することになりそうですが、たとえば処理中で getCurrentPage() を実行したら、どのページの ID が返るのか。もし、ページを移動してから peel() した場合、ondetach に peel() と同じ binding が渡せるのであれば問題にはならないはずですが、そもそも移動して剥がすコードを書くならば、移動する前に後処理を済ませてしまうことも可能なはずです。

いっそのこと、「移動してからシールを剥がして終了」な API では、detach event を発生させないことにして、API リファレンスに「ondetach 無効」と書いてしまうのもありな気がします。peel() は実行開始時の binding で動作して、ondetach にも開始時の binding が渡るというのがすごくエレガントですが、安全に開発するには、ondetach 禁止、の方が確実でしょう。

視点を変えてみますと、そもそも移動して剥がすといった面倒な要求が出てきたのは、張り付けて残す必要のないシールを transient に実行するために、貼って実行してすぐ剥がすというオペレーションが必要になるからです。であれば、いっそのこと「張り付けずに実行」というシール(?)を作ることができれば、移動 peel() が不要になるのでは?

とも考えたのですが、@shi3z 社長によると、これは MOONPhase のモデルからすると、オブジェクトのないメッセージセンディングになってしまうらしく、(ほぼ)不許可とのことです。

それならば、逆転の発想ですが、「貼ると同時に実行してすぐ剥がれる」という動作を実現する、別の Sticker オブジェクトを作る手もあります。たとえば ImmediateSticker とか。実際には、Sticker を継承して、勝手に peel() するだけなのですが、Sticker と異なる点として onattach プロパティのみを許し、ontap, ondetach プロパティを定義できないというものにします。

開発段階では、普通の Sticker で、処理をすべて ontap に書く。onattach と ondetach は MOON.finish() だけ。で、ontap にも peel() は書かない。いちいち剥がれていたら開発できないので。機能設計が済んだら、Sticker を ImmediateSticker に変えて、処理を onattach に移せば、すぐに剥がれるシールの出来上がり。

この形でも ondetach 処理がどの binding で実行されるかによるバグの発生を回避できます。とにかく、ページを移動した後の peel() で ondetach が呼び出されてバグを紛れ込ませる、というコードを書きにくい解決策を希望します。

ですがよくよく考えると、ページを移動してシールを剥がすという動作が必要な場面は相当に限られます。優先度は低いかもしれません。

(2)システムレベルで作成したリンクの情報活用案

今回の「双方向リンクシール」では、専用の「登録シール」で作成したリンクに対してだけ、リンク元を検索します。可能であれば、システムレベルで作成したリンクに対しても、リンク元を検索したいものです。

そうするのが良いかどうかの議論は横において、もしそうするならどんな方法が考えられるか。

最も安直で、面白くない方法としては、システムレベルで指で囲んでリンクを作成する際に、私の作成したシールのように、localStorage に情報を残し、そのフォーマットを公開すること。そうすれば、searchStorage() で簡単に検索できます。但し、この情報はユーザーレベルからも改変できるので、あまり利口な方法ではありません。

そんなことをするくらいなら、シールのディレクトリの manifest.json のリンク情報を直接読みだして、searchStorage() と同等の動作をする API を追加してもらえれば、特に工夫しなくても「検索シール」同等の機能は簡単に実装できます。

但し、毎回全ページ・全シールの manifest.json をスキャンするというのは結構実行コストが高くつきます。実行コストを下げるなら、システム内部に全ページ ID を key とした hash を保持し、リンクの作成・削除ごとに entry を追加、その都度 SD card に link.json とでもいったファイル名で保存、OS のリブート時にはそこから読み込む、という方法もあります。

そのたび manifest.json をスキャンするのと、hash を内部に保持するのと、どちらが良いのかは需要次第でしょうか。いずれにしても気になるのは、スケーラビリティの問題です。現在の MOON のパフォーマンスと、SD card が 16Gbyte 程度であれば、どちらの方法でもそれほど問題にはならないのかもしれませんが、このプロジェクトが順調に推移すれば、もっと規模の大きなソリューションが出てくるかもしれません。(enchant office ?) 複数のデバイスをまたがったリンクまでサポートすることになると、どこかに twitter 並の key-value store サーバーを立てるという方法すらあります。

で、まぁ、とりあえず manifest.json 全スキャンでよいので、API だけ定義してもらって、今後の展開とリンク元検索の需要に応じて実装を変えていってもらえばよいのではないかと思います。

尤も、ここまでしてリンク元検索 API を用意して、貼ってすぐ剥がせるシールの作成環境を用意するならば、リンク元検索機能自体をシステムレベルで提供するのはそれほど無茶な要求ではないようにも感じます。

【2013/10/29 追記】

リンク元情報の保持は、今の enchantMOON の規模なら、以下の方法の方がバランスがいいかもしれない。

  • 各ページの下に "linked.json" という file を作る
  • リンクを作成した時、リンク先ページの linked.json にページの ID を追記する
  • リンクやページの削除の時は何もしない
  • リンク元ページの検索要求に対し、まず、linked.json を読み込む
  • 登録されている各ページ ID に対し
    • そのページが存在するか
    • そのページから自ページへのリンクが存在するか
  • をチェックして無効な entry を削除する
  • linked.json に書き戻す
  • linked.json の内容を検索要求元に返す

こうすれば、毎回全ページを総当たりするより動作は速く、hash を常に保持することによるメモリの消費も回避できる。システム全体で hash を保持して一つのファイルに書き出す形だと、USB で PC にマウントして、ページを直接削除されてしまった場合に情報の不整合が出るが、上記の動作だと、不整合を招かない。

(3)今後の検索 API への要望

enchantMOON において、検索機能の拡充には大きな可能性があると思います。私自身、手軽な手書きメモ端末を期待していますので、いろいろな検索機能が欲しいと感じます。また、そもそも「紙の表現力は素晴らしいが、検索できない」から enchantMOON を開発したのであれば、検索機能はむしろ key function に位置付けるべきです。もっと追及したい。

ところが、現在の MOONPhase の検索機能の実装は少し残念です。

ver 2.6.0 現在で提供されている検索 API は以下の三種類です。(openNotebook() は、「全ページ」を検索する API)

  • openNotebook()
  • searchPage()
  • searchStorage()

openNotebook() が中途半端ですが、これら検索 API はいずれも、「全ページを対象に」「指定した条件で」「検索したページを表示して選択」「指定されたページを表示する」といった機能がひとまとめになっています。これは惜しい。

既に Issue Tracker の #112 に投稿済みですが、これらはそれぞれ要素 API に分解するべきです。

「全ページを対象に」するのをデフォルトとしても、特定のページ群の中から検索できるように、MyNotebook1/info.json と互換の形式で母集団を指定し、検索結果も JSON で返す「検索 API」。JSON を与えると、ページを表示して選択する 「選択 API」。「指定のページに移動する API」は既に openPage() として提供されています。但し、選択されたページがサムネイルからぐぐっと拡大してページ全体の表示になるビジュアルは魅惑的ですので、「表示して選択して移動する API」も別に用意してよいと思います。

問題なのは、現状提供されている API が、「検索」と「表示・選択・移動」がひとまとめになっていることです。これが検索の組み合わせを不可能にしています。

JSON で与えて検索結果を JSON で返す API になっていれば、たとえば searchPage() だけでも、全ページから tag_a で検索して、その結果に対して更に searchPage() で tag_b で検索をかければ、結果的に tag_a & tag_b での絞り込み検索になります。今回の「リンク元検索」機能と、検索だけの searchPage() を組み合わせても絞り込みリンク元検索ができます。たとえば今後、各ページにプロパティが設定されて、作成日時で検索できるようになったとすると、JSON 渡しになっていれば、「30 日以内に作成された、このページにリンクしているページ」を検索することも容易です。

検索結果を表示して、選択ページに移動するのも、選択までと移動を分離しておけば、たとえばサムネイル表示でなく、リスト表示で選択するシールをユーザーが作成することもできます。

検索機能は enchantMOON の今後を大きく左右する可能性があると感じていますので、是非、「応用がきいて、展開の幅が広くなるように」機能要素を分解して API を設計してもらいたいと思います。

2013年10月27日 (日)

enchantMOON : 双方向リンクシールを作りました(解説&言い訳)

[2013/12/06 追記] バージョンアップに伴い、[双方向リンク元検索シール(1)], [双方向リンク元検索シール(2)]は廃盤としました。>詳細


昨日(というか、日付としては今日なので、前回)アップロードしたシールについて、あれこれ解説&言い訳をしてみます。

今回作ったシールは、pre-install の「お気に入りシール」をちょっとだけ拡張したものなので、そんなに高度なことはしていないのですが、結局ずいぶん苦労しました。そのうち半分くらいはタイプミスですが、残りのあれこれを書き出してみます。

やりたくてできなかったこと - ページを移動してからシールを剥がす

実現したかったのは、単にリンク元を探してリストアップすることでしたので、最初はシステムレベルで作成されたリンクを拾い出せないかと調べたのですが、さすがに他のページの情報を javascript で直接拾い出すこともできず、MOON.searchStorage() で検索できるように組み立てることになりました。

できることの範囲内で、できるだけ使いやすいように工夫しましたが、最終的にひとつだけ残念な仕様になってしまったのは、「検索が済んで用済みになったシールを自動で剥がすことができない」ことです。

リンク元を検索したいときは、事前にシールを貼って準備しておくというよりも、任意のページで、ふとリンクを逆にたどりたくなって検索するはずです。シール台帳から検索シールを選べば、すぐに検索動作になって、その後は余計なものを残さずに消えてくれるのが理想です。

「検索シール(1)」の "hack.js" の一部を以下に示しますが、

    sticker.onattach = function(event) {
        var query = queryForLinkedSearch();
        shrinkStickerClip();
        MOON.searchStorage( query );
//        MOON.peel();
        MOON.finish();
    };

当初は MOON.searchStorage() のあとに MOON.peel() でシールを剥がして終了、のつもりだったのですが、安定動作しません。リンク元へのページ移動の途中で止まってしまったり、シールの動作が終了せずに三本指キャンセルが必要になったりします。

よく考えてみると当然でした。

検索シールを貼る → MOON.searchStorage() でページを検索し、移動先を選択・タッチする → ページを移動・表示されるページが変わる → (このとき、恐らくプロセスのカレントディレクトリ或いは OS の現在ページ管理情報が、移動したページのものに変化する) → ここで、元のシールの情報に基づいて MOON.peel() を実行しても、移動先の環境では該当するシールが存在しないために "Sticker is not defined" なエラーが出る。

かといって、MOON.searchStorage() の前に MOON.peel() を実行すると、当然そこでシールが終了してしまうので、リンク元検索動作を実行できません。

しかも、MOON.serchStorage() は非同期動作のようで(?)タイミングによって動作が微妙に違います。MOON.peel() と MOON.finish() をいろんな場所に埋め込んで試してみましたが、やっぱり無理でしたので、最終的に諦めました。

そうなると、リンク元に移動してシールを終了するので、検索を開始したページにはシールが張り付いたまま残ってしまいます。あとで改めて、検索シールを剥がす手間が発生します。はっきり言って美しくない。

せめて使い勝手を多少でも選んでもらえるように、検索シールは二種類作りました。

「検索シール(1)」は、張り付けると同時に検索動作に入ります。あとでシールを剥がす手間は少しでも簡単な方がよいので、シールをタッチするだけで剥がれてくれます。使用の手間は少なくなりますが、UI としては変則的でしょう。

「検索シール(2)」は、張り付けても何も起こりません。タッチすると検索します。剥がす時には普通に指で囲んで "delete" を選択してください。こちらの方が素直な UI ですが、(1) よりひと手間増えてしまいます。但し、同じページから何度もリンク元検索を繰り返すようなら、こちらの「普通に張り付く」シールの方が便利です。

考えてみると、このような、カレントディレクトリを移動してからシールを剥がすなどという機能は、そうそう必要にはなりません。妙なシールを作ってしまった私の方が irregular だったのでしょう。

MOON.searchStorage() 周り - 結構ハマりました

機能の要となる、MOON.searchStorage() には検索用の query として、関数を文字列化して渡すのですが、これが当初なかなか動かない。動かないこと以外にデバッグの方法もなかったので、あれこれ試行錯誤した結果、以下のようなことがわかりました。(私が勘違いしていなければ)

query に渡す関数は、無名関数では動作しない。しかも、関数名は何でもよいわけではなくて、"matches()" でなければ動作しない。

こんなことは、マニュアル類や、公式の API リファレンスにも書いてなかったのでハマりました。特に後者は、なかなか意表をついたトラップと言えるでしょう。そのうち改修するのかもしれませんし、まあ、情報さえあれば実害は乏しいので、そのままかもしれません。

リンク元ページの検索情報は、localStroage(その実体は storage.json ですが)に、"kasinSearch" というプロパティ名で埋め込んでいます。最初は柔軟性を考えて、連想配列(= object)で埋め込んだのですが、MOON.searchStorage() を実行すると、全てのページの storage.json を読み込んで check することになるので、あまり複雑な構造のデータだと、enchantMOON のパフォーマンスでは苦労しそうです。そう考えて、途中から、単一の文字列にデータを詰め込む形に設計を変えました。(一応、元の実装も注釈の形で残してあります)1ページに登録シールを何十枚も張り付けると、どちらの実装の方が軽くなるのかわかりませんが、数枚程度であれば、現在の実装の方が軽いと思います。

その他もろもろ

双方向 link の「登録シール」は、一つのページに複数枚張り込みたいので、シールごとにリンク先を区別して管理することが必要です。ぶっちゃけて言うと、シールそれぞれの ID を区別しなければならないのですが、以前 esmasui さんの処の blog で、「現行バージョン(2.5)のMOONPhaseは、実行されているシール自身のIDを取得する方法がありません。」とありましたので、当面あきらめていました。

しかし、@k_ohga さんの「見た目が国旗になるシール」 を読んでると、シール本体が格納されている directory 名を取得できてます。いつの間にかできるようになってた(?)んですね。ありがたく参考にさせていただいて、以下のような関数にしました。

function getStickerID() {
    var relURL = window.location.getAbsoluteURL("").split("/Data/")[1];
    return relURL.split( "/" )[2];
}

function getPageID() {
    var relURL = window.location.getAbsoluteURL("").split("/Data/")[1];
    return relURL.split( "/" )[1];
}

さて、上記の関数で、ページやシールの識別 ID を得ているのですが、この実装で気になるのが、"enchantMOON - Issue Tracker:69 - 機能要望 サムネイル画面でのページのコピー" が、どのように実現されるのか。

最も単純な実装は、複製元ページの directory 名が "XXX..." であった場合、複製先として新しいページ用の directory を作り(directory 名を "YYY..." とします)"XXX..." 以下の file, directory を丸ごと copy する方法です。ただ、これですと、MOON.getPaperJSON( pageID ) が機能しない。

元ページのストロークデータファイルが、"XXX.../xxx.../info.json" だったとして、単純丸々コピーすると、複製先は、"YYY.../xxx.../info.json" ということになります。ファイルツリーによって、名前空間の一意性は保たれています。しかし、MOON.getPaperJSON() では、どちらのストロークデータにアクセスする場合も MOON.getPaperJSON( "xxx..." ) になってしまうので一意性が確保できません。今の MoonPhase の設計では、階層を無視した各 directory 名のそれぞれが一意でないとならないのです。そのため、"XXX.../xxx.../info.json" の複製先は、例えば "YYY.../zzz.../info.json" と別の directory 名にしなければなりません。

今回作ったシールでは、上記のように異なる directory 名で複製されてしまうと、リンク情報が辿れなくなってしまいます。一応、今後の可能性も考えて、そのような場合はリンク情報の再設定が可能、かつ、リンクの再設定かシールを剥がす操作をすれば、localStorage から無効なリンク情報を削除するように作ってありますので、システムに悪影響を与えることはないと思います。

ページのコピーという機能が、どのような形で実装されるのか興味のあるところです。

もっといろいろと苦労したような気がするのですが

思い出せないのでここまでにします。

enchantMOON : 双方向リンクシールを作りました(使い方のみ)

[2013/12/06 追記] バージョンアップに伴い、[双方向リンク元検索シール(1)], [双方向リンク元検索シール(2)]は廃盤としました。>詳細


MoonPhase が 2.6.0 に update されて、localStorage が使えるようになったので、前から作りたかったシールを作りました。

ですが、残念ながら、欲しい機能が実現できませんでした。それでも、「ないよりマシ」かもしれませんし、最低でも、「今後こんな機能が欲しい」というたたき台にはなると思いますので、以下の注意点をお読みの上、酔狂な方だけご使用下さい。

Download : [双方向リンク登録シール], [双方向リンク元検索シール(1)], [双方向リンク元検索シール(2)]

注意点

まず、不安定です。特に例の "電源投入時に「内部エラー」と頻繁に表示される問題につきまして" に該当する方は結構やばいかもしれません。うちの子(MOON)も入院しました。退院して随分ましになりましたが、残りの不安定さが、私のコードによるものなのか、OS(ver.2.6.0)によるものなのかわかりません。もし私のコードの不備が分かる方おられましたら、ぜひ教えて下さい。また、動作は大変遅いです。

不安定なことに目をつむっても、使い勝手が悪いです。下記の「使い方」にあるように、「リンク作成」シールと、「リンク元検索」シールがあるのですが、後者で、「検索して移動」と「シールを剥がす」を一度に行うことができません。必ず、用済みのシールが残ります。あまりに使い勝手が悪いので、「検索シール」は、(1)貼り付けで検索、タップで削除、と(2)タップで検索、指で囲んで削除、の 2 バージョン用意しました。

あくまでも「実証実験」と考えてください。

以下の「使い方」をお読みの上、使いやすい方をお使い下さい。

Download : [双方向リンク登録シール], [双方向リンク元検索シール(1)], [双方向リンク元検索シール(2)]

どんなものか

ページからページへ、一方向ではなく、双方向のリンクを可能にするものです。
enchantMOON が他のタブレットの手書きメモに勝る(?)部分は、ノートのページ相互がハイパーリンクを形成できることです。が、現在の実装では一方向のリンクしか作成できません。MOON を見て誰もが連想した BTRON では、実身/仮身で、双方向にリンクをたどれます。(使ったことないので、誤解してたらご容赦を

2013/10/27 追記: @shi3z 社長から直々に「BTRONでは実身から仮身を辿れなかった」とのコメントをいただきました。やはり、知らないものをイメージだけで語るのはだめですね。上記訂正します。)

まともに双方向リンクを作成するのは大変なので、できる範囲で実現しています。あるページからリンク元を検索すると、そのページに対して(今回のシールを使って)リンクを張ってあるページの一覧を表示します。一覧から選んだページに移動できます。

「このページに飛んできた元のページに戻る」という動作ではありませんが、リンク元をピックアップできるだけでも、ないよりマシでしょう。

OS レベルで手を入れれば、同様の機能を実現するのは難しくないはずですが、「どんな使い勝手にするのがよいか、そもそも、こういう機能にどの程度需要があるか」という実験、リサーチくらいの位置づけでとらえてください。(そもそも不安定ですので)具体的な意見がたくさん出れば、開発チームの機能設計も楽になるでしょう。

使い方

二枚のシールを組み合わせて使います。他のページにリンクを張って登録するシール(registBiLinkTo - 以降「登録シール」と記述)、とリンク元ページの一覧を検索するシール(searchBilinkFrom - 以降「検索シール」と記述)の二枚です。

「登録シール」をページに貼ってから、タップするとリンク先選択画面が開きます。リンク先を選ぶと、そのシールに情報が登録され、以降はタップするとリンク先ページに移動します。(遅いですが)何かの誤動作で、リンク先情報が消失している場合は、タップすると改めてリンク先選択画面が開きます。「登録シール」は、一つのページに何枚でも貼り込むことができますので、画面イメージは以下のようになるでしょうか。

Bilinksticker_2

 

「登録シール」をいくつかのページに貼ってリンクを形成した後で、「検索シール」を使用します。特定のページで「検索シール」を貼ると、そのページにリンクを張っているページの一覧を表示します。この時表示されるのは「登録シール」でリンクを張ったページだけで、通常の、指で丸を描いて出てくるメニューから張ったリンクは検索できません。(システムレベルで作成したリンクの情報を拾い上げることは、今の処無理でした)

使い方はこれだけです。動作が速くて、システムが不安定にさえならなければ、そこそこ使えるとは思います。

技術的注釈

長くなりそうなので、解説や注釈は、日を改めます。

ソース提示

登録シール : hack.js

importJS(["lib/MOON.js", "main.js"], function() {

    function isStickerClipPlain() {
        var myID = getStickerID();
        var clip = MOON.getPaperJSON( myID ).clip;
        return ( clip.data[0] < 1.0 );
    }

    function shrinkStickerClip() {
        var myID = getStickerID();
        var paperJSON = MOON.getPaperJSON( myID );
        paperJSON.clip.data = [
72.0,40.2,0.05,71.83147,43.6306,0.05,
71.32748,47.02816,0.05,70.49291,50.35996,0.05,
69.33578,53.59392,0.05,67.86724,56.69889,0.05,
66.10144,59.64496,0.05,64.05537,62.40376,0.05,
61.74874,64.94874,0.05,59.20376,67.25537,0.05,
56.44496,69.30144,0.05,53.49889,71.06724,0.05,
50.39392,72.53578,0.05,47.15996,73.69291,0.05,
43.82816,74.52748,0.05,40.4306,75.03147,0.05,
37.0,75.2,0.05,33.5694,75.03147,0.05,
30.17184,74.52748,0.05,26.84004,73.69291,0.05,
23.60608,72.53578,0.05,20.50111,71.06724,0.05,
17.55504,69.30144,0.05,14.79624,67.25537,0.05,
12.25126,64.94874,0.05,9.94463,62.40376,0.05,
7.89856,59.64496,0.05,6.13276,56.69889,0.05,
4.66422,53.59392,0.05,3.50709,50.35996,0.05,
2.67252,47.02816,0.05,2.16853,43.6306,0.05,
2.0,40.2,0.05,2.16853,36.7694,0.05,
2.67252,33.37184,0.05,3.50709,30.04004,0.05,
4.66422,26.80608,0.05,6.13276,23.70111,0.05,
7.89856,20.75504,0.05,9.94463,17.99624,0.05,
12.25126,15.45126,0.05,14.79624,13.14463,0.05,
17.55504,11.09856,0.05,20.50111,9.33276,0.05,
23.60608,7.86422,0.05,26.84004,6.70709,0.05,
30.17184,5.87252,0.05,33.5694,5.36853,0.05,
37.0,5.2,0.05,40.4306,5.36853,0.05,
43.82816,5.87252,0.05,47.15996,6.70709,0.05,
50.39392,7.86422,0.05,53.49889,9.33276,0.05,
56.44496,11.09856,0.05,59.20376,13.14463,0.05,
61.74874,15.45126,0.05,64.05537,17.99624,0.05,
66.10144,20.75504,0.05,67.86724,23.70111,0.05,
69.33578,26.80608,0.05,70.49291,30.04004,0.05,
71.32748,33.37184,0.05,71.83147,36.7694,0.05
        ];
        MOON.setPaperJSON( myID, paperJSON );
    }

    var sticker = Sticker.create();

    sticker.ontap = function(event) {
        var linkToID = referMyLinkToID();
        if ( linkToID ) {
            moveToPage( linkToID );
            MOON.finish();
        } else {
            if ( isStickerClipPlain() ) {
                shrinkStickerClip();
                selectPageToLink();
                MOON.finish();
            } else {
                MOON.alert( "link 先情報が登録されていません", function() {
                    selectPageToLink();
                    MOON.finish();
                });
            }
        }
        MOON.finish();
    };

    sticker.onattach = function(event) {
        MOON.finish();
    };

    sticker.ondetach = function(event) {
        removeMyLink();
        MOON.finish();
    };

    sticker.register();
});

検索シール(1) : hack.js

importJS(["lib/MOON.js", "main.js"], function() {
    var sticker = Sticker.create();

    function shrinkStickerClip() {
        var myID = getStickerID();
        var paperJSON = MOON.getPaperJSON( myID );
        paperJSON.clip.data = [0.0,0.0,0.05,104.0,0.0,0.05,
          104.0,85.0,0.05,0.0,85.0,0.05,0.0,0.0,0.05
        ];
        paperJSON.width = 105;
        paperJSON.height = 86;
        paperJSON.image = "peel.png";
        MOON.setPaperJSON( myID, paperJSON );
    }

    sticker.ontap = function(event) {
//        MOON.finish();

        MOON.peel();
    };

    sticker.onattach = function(event) {
        var query = queryForLinkedSearch();
        shrinkStickerClip();
        MOON.searchStorage( query );
//        MOON.peel();

        MOON.finish();
    };

    sticker.ondetach = function(event) {
        MOON.finish();
    };

    sticker.register();
});

検索シール(2) : hack.js

importJS(["lib/MOON.js", "main.js"], function() {
    var sticker = Sticker.create();

    sticker.ontap = function(event) {
        var query = queryForLinkedSearch();
        MOON.searchStorage( query );
//        MOON.peel();

        MOON.finish();
    };

    sticker.onattach = function(event) {
        MOON.finish();
    };

    sticker.ondetach = function(event) {
        MOON.finish();
    };

    sticker.register();
});

全体での共用ルーチン : main.js

var JISSIN_LINK = 'jissinLink';
var KASIN_SEARCH = 'kasinSearch';

function getStickerID() {
    var relURL = window.location.getAbsoluteURL("").split("/Data/")[1];
    return relURL.split( "/" )[2];
}

function getPageID() {
    var relURL = window.location.getAbsoluteURL("").split("/Data/")[1];
    return relURL.split( "/" )[1];
}

//
//
//
function getFromLocalStorage( tag, nullDefault ) {
    nullDefault = nullDefault || {};
    var obj = localStorage[ tag ];
    if ( obj == null ) {
        return nullDefault;
    } else {
        return obj;
    }
}

//
// localStorage に、値 obj を 名 tag で登録する。
// 但し、値が null, {}, [] の場合は削除する。
//
function setToLocalStorage( tag, obj ) {
    if ( (typeof obj).match( /number/i ) ) obj = obj.toString();
    var isObj = (typeof obj).match( /string/i );

    if ( (obj == null)
      || ( (typeof obj).match( /object/i ) && Object.keys( obj ).length <= 0 )
    ) {
        if ( localStorage[ tag ] ) localStorage.removeItem( tag );
    } else {
            localStorage[ tag ] = obj;
    }
}

//
// 自シールに対して登録されているリンク先ページの ID を参照
//
function referMyLinkToID() {
    var jissinLink = getFromLocalStorage( JISSIN_LINK, {} );
    return jissinLink[ getStickerID() ];
}

//
// { myStikerID => linkToID } から成る object を scan.
// 自ページのシール以外からの link 情報が紛れていたら
// ゴミとして削除する。
//
function sweepJissinLinkInfo( jLink ) {
    var stickers = MOON.getCurrentPage().papers;
    var keys = Object.keys( jLink );
    for ( var i = 0, l = keys.length; i < l; i++ ) {
        if ( stickers.indexOf( keys[i] ) < 0 ) delete jLink[ keys[i] ];
    }
    return jLink;
}

//
// { myStikerID => linkToID } から成る object を基に、
// link 先 ID を ':' 連結した文字列を作成して戻す。
// 処理するべき情報がない場合は null を返す。
//
function makeLinkedSearchInfo( linkToObj )  {
    var tmpObj = {}, tmpArray = [], k;

    for ( k in linkToObj ) {
        tmpObj[ linkToObj[ k ] ] = true;
    }
    for ( k in tmpObj ) {
        tmpArray.push( k );
    }

    return ( tmpArray.length > 0 ) ? ( ':' + tmpArray.join( ':' ) ) : null;
}

//
// MOON.searchStorage() に渡す、文字列化した query を返す。
// pageID に対する link 情報の存在する page が検索にかかる。
//
function getLinkedListQuery( pageID ) {
    return 'function matches(s){var ks = s[\"' + KASIN_SEARCH + '\"]; return ((ks != null) && (ks.indexOf(\":' + pageID + '\") >= 0));}';
}

/*-------------------------------------------------------------------------//
// makeLinkedSerchInfo を、文字列でなく、object (=連想配列) として
// 実装する案。こちらの方がカッコイイが、scalability を考えなければ
// 文字列案の方が、MOON.searchStorage() の際の実行コストが低くなるか?

//
// { myStikerID => linkToID } から成る object を基に、
// 検索用情報を格納した object を返す。
// 処理するべき情報がない場合は {} を返す。
//
function makeLinkedSearchInfo( linkToObj )  {
    var linkedListObj = {}, k;

    for ( k in linkToObj ) {
        linkedListObj[ linkToObj[ k ] ] = true;
    }
    return linkedListObj;
}

//
// MOON.searchStorage() に渡す、文字列化した query を返す。
// pageID に対する link 情報の存在する page が検索にかかる。
//
function getLinkedListQuery( pageID ) {
    return 'function matches(j){\
var ks = j[\"' + KASIN_SEARCH + '\"];\
if ( ks ) { return ks[\"' + pageID + '\"]; } else { return false; }\
            }';
}

//-------------------------------------------------------------------------*/

//
// linkToID で指定されたページへの link 情報を localStorage に登録する。
// 引数が null であれば、link 情報の削除処理を行う。
//
function registMyLink( linkToID ) {
    var myID = getStickerID();
    var jissinLink = getFromLocalStorage( JISSIN_LINK, {} );

    if ( linkToID ) {
        jissinLink[ myID ] = linkToID;
    } else {
        delete jissinLink[ myID ];
    }

    jissinLink = sweepJissinLinkInfo( jissinLink );
    setToLocalStorage( JISSIN_LINK, jissinLink );
    setToLocalStorage( KASIN_SEARCH, makeLinkedSearchInfo( jissinLink ) );
}

//
// 自 link シールが担当している link 情報を削除する
//
function removeMyLink() {
    registMyLink( null );
}

//-------------------------------------------------------------------------//

//
// 指定された link 先へ移る。
//
function moveToPage( linkToID ) {
    if ( linkToID ) {
        MOON.openPage( linkToID );
    } else {
        MOON.alert( 'Link 先ページが登録されていません', function() {
            MOON.finish();
        } );
    }
}

//
// link 先ページを選ぶ。link 情報を localStorage に登録する。
//
function selectPageToLink() {
    MOON.openNotebook( function( pageID ) {
        registMyLink( pageID );
        MOON.finish();
    });
}

//
// link 元ページの一覧を表示するための query を返す。
//
function queryForLinkedSearch() {
    return getLinkedListQuery( getPageID() );
}

解説と言い訳は、明日にでも。

Download : [双方向リンク登録シール], [双方向リンク元検索シール(1)], [双方向リンク元検索シール(2)]

« 2013年9月 | トップページ | 2013年11月 »