バフ

非常事態宣言の最中、ランニングやサイクリング中もマスクをしましょうという話になってきてますね。
症状が無くてもウイルス持ってる可能性があるからということなんでしょう。
しかし、正直言って、マスクしながら走ったり自転車乗ったり、息がきつくつてしんどい。特に自分は喘息持ちで他の人よりもそういった部分については耐性が低いので。
ただ、エチケットとしてはやはり、人と人との距離が近いときなんかは何らなの対策をしないといけないのだろう。
山中教授はマスクの代わりにバフというのを推奨していたので、さっそく買って試してみた。
買ったのはワコールのCW-Xのやつ。

正式名称はマルチウェイカバーというらしい。その名の通り、ネックカバー、マスク、帽子、など色々な使い方ができるようだ。
で、つけてみたところはこんな感じ。
f:id:alasixOsaka:20200426151500j:plain
バフとつけてみた。めちゃめちゃ怪しい。
とっても怪しいおっさんって感じですね。まあ、仕方がないので、これでいつものコースを走ってみた。
走った感じは、やっぱり大きく呼吸をしないといけない、劇坂とかではしんどくってやってられない。いつも行くコースは、チャリも人もほとんどいないので、そういうところでは外しても良いと思う。こいつの良いところは、外したりつけたりが比較的簡単にできるというところ。したがって、家の近くなど周りに人がいそうなところではつけておいて、坂道に入って周りに人がいなくなったら外すというような使い方かなと思う。
首回りを完全に覆うようなものなので、夏場はこのままではちょっと暑くてやってられなさそう。そういう時は、水をかけておけば気化熱でかえって冷やしてくれることが期待できる。
ランのときも、たぶん坂道トレやスピード走、インターバル走など追い込んだときはつけるのがキツイと思うので、公園とか河川敷なんかは避けて、人がなるべくいないところを選んで走るしかないのかな。河川敷は信号もなく、車も来ないので、確かに走りやすいし、距離を延ばすトレーニングには最適なんですが、走りやすいということはイコール、他のランナーもいるということなので、しばらくはいかない方が良いのでしょうね。
なんにしても早くこの状態が元に戻ってくれることを期待するしかないですね。

Viewを分割して高低図の領域を作る(アンドロイド地図アプリの開発 その22)

前回は、ちょっと一休みで軽い話題でしたが、いよいよ、高低図の表示に取りかかります。
alasixosaka.hatenablog.com

Viewを分割する

高低図を表示するために、まず地図の画面の下に高低図を表示する領域を作成する。まあ、なくても重ね書きすることもできるけど、見辛いので、専用の領域を作成することにしました。

レイアウトを変更

地図を表示しているレイアウトファイルは、rotatemapviewer.xmlなので、これを修正する。Viewを分割するには、LinearLayoutにViewを並べて、weightを設定するのがやり易そうだ。もとのViewはRelativeLayoutで作成されているので、こいつの外側にLinearLayoutを配置する。2つのViewは縦に配置するので、orientationはverticalにする。

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_height="fill_parent"
    android:layout_width="fill_parent"
    android:orientation="vertical">
    <RelativeLayout
        android:id="@+id/mainView"
        android:layout_width="fill_parent"
        android:layout_height="0dp"
        android:layout_weight="4">

そして、下側にcanvasを配置する。
高低図を書くやり方もいろいろ考えたがcanvasで書くことにした。mapsforgeのサンプルプログラムにdualmapviewerというのがあって2つの地図を表示できるみたいだ、こいつを使うという方法も考えた。だが、地図上にプロットしようとすると、経度、緯度で座標を入力する必要があり、計算が面倒になる。メリットとしては現在地を表示するのが楽になるという点。だが結局canvasを使う方法にすることにした。
2つの画面の大きさの比率は4:1にした。比率の設定はweightに値を入力するだけなので、楽に設定できる。
下の画面にcanvasを書くために、Paintviewという専用のクラスを作成し、レイアウトファイルを書き換える。mpf_rotaitonCはこのプロジェクトの名称。

    </RelativeLayout>
    <com.example.mpf_rotationC.PaintView
        android:id="@+id/height"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"/>
</LinearLayout>

rotatemapviewerが呼ばれるとpaintviewのonDrawが実行される。onDrawにグラフの枠と縦軸を書くようにした。まず、Paint paint = new Paint() でPaintをnewし、paint.setColor(Color.BLACK)で線の色を黒に指定。
paint.setStyle(Paint.Style.STROKE)で線を実線にし、paint.setStrokeWidth(3)で線の幅を指定する。
Canvasの幅と高さはそれぞれ、canvas.getWidth()、canvas.getHeight()で得ることができる。これを4分割して、ループでcanvas.drawLine() コマンドで線を引いた。また、外枠はcanvas.drawRect() コマンドで描いている。

public class PaintView extends View {
    
    public PaintView(Context context, AttributeSet attribute){
        super(context,attribute);
    }
    @Override
    public void onDraw(Canvas canvas){
        Paint paint = new Paint();
        paint.setColor(Color.BLACK);
        paint.setStyle(Paint.Style.STROKE);
        paint.setStrokeWidth(3);
        int cheight = canvas.getHeight();
        int cwidth = canvas.getWidth();
        
        try {
            for (int i=0;i<4;i++){
                canvas.drawLine(i*cwidth/4,0,i*cwidth/4,cheight,paint);
            }
            canvas.drawRect(new Rect(0, 0, cwidth, cheight), paint);
            
        }catch (Exception e){
            Log.e("PaintView","error");
        }        
    }
}

Canvasの外枠が画面の淵と重なってよくわからなくなっているが、画面はこんな感じになった。

f:id:alasixOsaka:20200405183128j:plain
画面を分割し、地図の下にCanvasを貼りつけた

ここに高低図と現在地を表示すれば完成になるが、書くのは簡単だが、そう簡単ではない。

高低図を書くやり方は次回にして、今回はレイアウトの勉強をしたついでに、メニュー画面のレイアウトもconstraintLayoutを使って書き直した。
ConstraintLayoutの詳細は参考サイトに詳しく書いてあるのでそちらを見て下さい。
今までは見映えより、実際の機能を搭載することを重視したので、メニュー画面についてはお世辞にも見映えが良い画面とは言えなかった。今回は、各ボタンが縦に均等に、かつ真ん中に配置するようにした。また、高低図を表示するかしないかを選択するラジオボタンを追加した。さらに、紛らわしい表現だった、GPSOn/OffのラジオボタンとRotationalViewのボタンの名称も変更し、それぞれRotateOn/OffとMap Viewとした。

ConstraintLayoutはそれぞれの部品の配置を相対的な制約によって記述するので、RelativeLayoutに似ている。違いは更に柔軟に記述ができるということだが、詳細に説明できるほど理解していないので、やったことだけを書いておく。
まず、一番上の"Map File Select"のボタンを基準点にした。幅は各ボタンのサイズを揃えるために200dpとしている。高さは”wrap content"で揃えた。
画面の一番上からのマージンを50dpとしている。左右は均等になるように(つまりセンタリングするために)、 app:layout_constraintEnd_toEndOf="parent"と app:layout_constraintStart_toStartOf="parent"としている。これは、画面の右端と左端とボタンをそれぞれ制約するということで、これでセンタリングになる。片側だけに制約をつけるとどちらかに寄った表示(つまり左寄せや右寄せ)になる。
また、各パーツを均等に配置するためにこの基準パーツにだけ、app:layout_constraintVertical_chainStyle="spread"としている。これで、各パーツが上下に均等に配置される。
2番目のパーツ”GPX File Select”ボタンは、上を先ほどのパーツ”Map File Select”で制約し、下を次のパーツ”POI File Select”で規定している。このようにして、各パーツを数珠つなぎにして記述すると、きれいに均等配置になった。

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_centerHorizontal="true">

    <Button
        android:id="@+id/btMapFileSelect"
        android:layout_width="200dp"
        android:layout_height="wrap_content"
        android:layout_marginTop="50dp"
        android:text="@string/map_file_select"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_chainStyle="spread" />

    <Button
        android:id="@+id/btGPXSelect"
        android:layout_width="200dp"
        android:layout_height="wrap_content"
        android:text="@string/gpx_file_select"
        app:layout_constraintBottom_toTopOf="@id/btPOISelect"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/btMapFileSelect" />

    <Button
        android:id="@+id/btPOISelect"
        android:layout_width="200dp"
        android:layout_height="wrap_content"
        android:text="@string/poi_file_select"
        app:layout_constraintBottom_toTopOf="@id/btRotationalView"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/btGPXSelect" />

    <Button
        android:id="@+id/btRotationalView"
        android:layout_width="200dp"
        android:layout_height="wrap_content"
        android:text="@string/RotationalView"
        app:layout_constraintBottom_toTopOf="@id/radiogroup"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/btPOISelect" />

    <Button
        android:id="@+id/btSelectTheme"
        android:layout_width="200dp"
        android:layout_height="wrap_content"
        android:text="@string/select_theme"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/radiogroup" />

    <RadioGroup
        android:id="@+id/radiogroup"
        android:layout_width="350dp"
        android:layout_height="wrap_content"
        android:background="#df7401"
        android:orientation="horizontal"
        android:paddingLeft="10dp"
        android:paddingTop="10dp"

        android:paddingRight="10dp"
        android:paddingBottom="10dp"
        app:layout_constraintBottom_toTopOf="@id/radiogroup2"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/btRotationalView">

        <RadioButton
            android:id="@+id/rbOn"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:layout_marginLeft="25dp"
            android:layout_marginRight="25dp"
            android:background="#ffffff"
            android:text="@string/onGPS" />

        <RadioButton
            android:id="@+id/rbOff"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:layout_marginLeft="25dp"
            android:layout_marginRight="25dp"
            android:background="#ffffff"
            android:text="@string/offGPS" />

    </RadioGroup>

    <RadioGroup
        android:id="@+id/radiogroup2"
        android:layout_width="350dp"
        android:layout_height="80dp"
        android:background="#df7401"
        android:orientation="horizontal"
        android:paddingLeft="10dp"
        android:paddingTop="15dp"

        android:paddingRight="10dp"
        android:paddingBottom="0dp"
        app:layout_constraintBottom_toTopOf="@id/btSelectTheme"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/radiogroup">

        <RadioButton
            android:id="@+id/rb2On"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:layout_marginLeft="25dp"
            android:layout_marginRight="25dp"
            android:layout_gravity="center_vertical"
            android:background="#ffffff"
            android:text="@string/on" />

        <RadioButton
            android:id="@+id/rb2Off"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:layout_gravity="center_vertical"
            android:layout_marginLeft="25dp"
            android:layout_marginRight="25dp"
            android:background="#ffffff"
            android:text="@string/off" />

    </RadioGroup>

    <TextView
        android:id="@+id/heightMap"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/heightMap"
        android:textColor="#3F51B5"
        android:textSize="18sp"
        android:textStyle="bold"
        app:layout_constraintStart_toStartOf="@id/radiogroup2"
        app:layout_constraintTop_toTopOf="@id/radiogroup2"
        tools:ignore="MissingConstraints" />

</android.support.constraint.ConstraintLayout>

f:id:alasixOsaka:20200405183648j:plain
高低図表示のラジオボタンを追加し、全体の配置を整えた


参考にしたサイト
比率で幅や高さを指定する方法 - レイアウトの weight - ユーザーインターフェイス - Android 開発入門

AndroidのCanvasを使いこなす! – 基本的な描画 – PSYENCE:MEDIA
[Android] Custom Canvas をレイアウトに挿入する

XMLで始めるConstraintLayout - Qiita
[Android] ConstraintLayout レイアウト逆引きまとめ - Qiita

パン作り その4

ホシノ天然酵母で1.5倍起こし

前回、ホシノ天然酵母で間違って3倍起こしをしてしまった記事を書きました。

alasixosaka.hatenablog.com

今度は間違えないように1.5倍起こしをやってみました。
酵母50gに対し、75gの水を入れて、室温で放置。20℃だとだいたい一週間で種起こしができるそうですが、少し室温が高いので5日くらいでできました。

1.5倍起こしは発酵時間がめちゃ長い

早速パンを焼いてみようと、食パン1斤分の生地を作ってみました。
小麦は、春よ恋のブレンド
HBで天然酵母のねり+発酵コースで発酵時間を3時間にして1次発酵。どうも膨らみが今一で、発酵不足気味。フィンガーテストでも完全に穴が塞がらないものの、あけた穴が半分くらい埋まる感じ。実際にどの程度なら良いのかがいまいちわかってないので、まあいいかと次の工程に進むことにした。
2つに分割して、ベンチタイム20分。だいたい、ベンチタイムでも生地が膨らんでいくのだが、ほどんど膨らまない。嫌な予感がしたが、とりあえず突き進む。
生地をロール状に丸めて、食パン型に入れ、オーブンで35℃で二時間ほど二次発酵した。しかし、嫌な予感が的中してほとんど膨れていない。
これは失敗と思い、あとでピザにでもしようかとボウルに入れて冷蔵庫に入れておいた。

何故か冷蔵庫でも発酵が進む

翌日、ボウルを見てみると生地が膨らんでいる。さらに翌日になるともっと膨れていた。これはパンにできるかもということで、冷蔵庫から出して室温にしばらく放置。その間にも生地が膨れていく。
どうも、1.5倍起こしの場合は、低温で長時間発酵させないといけないみたいだ。本にも20℃で12時間と書いてある。
ということは、水の量が多いと酵母は活発に発酵しやすく、水を減らすと穏やかな発酵になるみたいだ。2倍起こしなら生種は冷蔵庫での保存が1週間程度といわれていて、1.5倍起こしは1か月もつというのもその辺が関係してそうだ。

常温に戻った生地を4つに分けて、丸めてベンチタイムを20分。それから、オーブンで35℃で80分2次発酵した。始めは60分でやってみたが、膨らみが足りないので、20分追加したところ、まずまず膨らんだので、オーブンのフランスパンモードで焼いてみた。焼に入ると生地がどんどん膨らんで普通にパンとして焼きあがった。
焼き上がりは、少し表面が焦げた感じで、もう少し焼き時間を短くした方がよかったかもしれないが、味の方はかなり美味しいパンになった。

田舎パンで再挑戦

前回は食パン用の生地でフランスパンモードでパンを焼くことになってしまった。今回は材料をシンプルにして田舎パンを焼いて見る。
材料は

  • 小麦粉(春よ恋ブレンド)240g
  • 全粒粉(春よ恋)40g
  • 生種 16g
  • 砂糖 8g
  • 塩 4g
  • 水 165cc

材料を混ぜてHB のねりモードで20分。途中で水が少なめだったので少し(だいたい20gくらい)追加。
1200cc のタッパーに入れて室温で放置。最近はほぼ20℃なので12時間くらいでいけるかと思ったがまったく膨らんでない。仕方ないので更に放置。22時間くらいのところでタッパーの上淵ギリギリまで膨らんでいた。
6つに分けて、丸めてベンチタイム、約30分。このへんは適当です。
オーブンに入れて30℃で2次発酵90分。2次発酵も時間がかかる。始めは60分で様子を見たがあまり膨らんでないので、10分ずつ追加で結局90分かかった。
クープを入れて、フランスパンモードで30分焼成。今回も前回ほどではないものの、ちょっと焼き色が濃くなってしまった。20-25分くらいで良かったかもしれない。

アイコンを変更してみる(アンドロイド地図アプリの開発 その21)

前回までで、高低図以外の基本的な機能ができました。
alasixosaka.hatenablog.com

今回は、アプリのアイコンを変更してみたというお話です。
Android Studioでアプリを作ると、もれなくドロイド君のアイコンになってしまう。ずっとこれだといかにも開発中のアプリのようで(実際に開発途中なんですが)面白くないので、オリジナルのアイコンを作ってみた。
地図アプリなので、日本地図のイラストをフリーの素材からダウンロードしてきて、それを加工して作ってみたといおう話です。
アプリのアイコンはフォアグランドとバックグランドで別々の画像を用意するようになっているが、素材を加工すると、フォアグランド用の画像の背景が白くなってしまい、バックグランドが無意味な状態になってしまう。
Jtrimとペイントでやってみたが結果は同じ。仕方ないので、Jtrimでフォアグランド用の画像の背景も適当に色を付けてバックグランドは無視するような感じで作ってみた。
f:id:alasixOsaka:20200329150541p:plain
アイコンを変更するやり方は、Image Assetというのを使う。詳細は参考サイトの方が詳しいので、ここではやったことだけを書いておく。
まず、AndroidStudioを起動して、アイコンを変更したいプロジェクトを開く。
左ペインのAppを右クリックし、Newー>Image Assetを選択しImage Assetを起動する。
f:id:alasixOsaka:20200329150841j:plain
すると始めはドロイド君が表示されているはずなので、ここで、自分で作った画像ファイルをPathのところで指定する。
f:id:alasixOsaka:20200329151048j:plain
ここで、画像のサイズも変更できる。また、バックグランド画像を使う場合はバックグランドを選択し、同様に画像ファイルを指定する。あるいは、単純に色だけを指定することもできる。
画像ができたらNextをクリック。

f:id:alasixOsaka:20200329151347j:plain
Finishをクリックすれば完成。ここでは、一度やった操作を再現しているので上書きされるファイルが赤字で記載されている。
適当な名前を入力してFinishをクリックすれば完成。ドロイド君がいらなければそのまま上書きでもよい。今回は、一応"ic_launcher2"という名前にした。

次にManifestを修正する。Applicationの下のところの”icon”と”roundicon"の名前を先ほどの名前に合わせる。

<application
        android:name="com.example.mpf_rotationB.SamplesApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher2"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher2_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme"
        >

そうして、もう一度アプリを書き込むと、アイコンが無事に変更された。

f:id:alasixOsaka:20200329151935j:plain
ドロイド君がいっぱい並んだ画面に、オリジナルアイコンが一つ表示された
いままで全然気づかなかったが、エミュレータのAPI26ではアイコンは丸い形をしていて、実機で使っているXperiaZ4(Android6)ではアイコンは四角かった。Manifestの修正で始めは"icon"だけを修正していて、エミュレータのアイコンが変わらないので変だなと思ってようやく気付いた。


参考にしたサイト
[Android] アイコンを簡単作成できる Image Asset

緊急事態宣言

ついに、コロナウィルスの影響で緊急事態宣言が出されました。
私の住んでいる大阪も指定地域に入っています。今のところ、身の回り大きな変化があったというような感じはないですが、子供たちは学校が引き続き休み、塾も習い事も全部休校となり、家にいるしかない状況です。
会社の方は、極力テレワークや時差出勤などで人込みを避けるようにという指示が出ていますが、といってもテレワークでできる仕事も限られるので、出社はせざるを得ない状況です。
これからどうなるのか心配ですが、とりあえず、5月にエントリー済みないしエントリーしようと考えていた大会はすべて中止か延期ということになりました。
トレランでは比叡山国際が中止との連絡が昨日来ました。また、オリエンテーリングワールドマスターズのプレ大会は秋に延期。自転車のグランフォンド京都も今年は中止が決まりました。
安倍総理は、ジョギング程度なら外に出てもよいと言っていますが、でも一人で走るということしかできないでしょう。家のあたりはそんなに人が密集しているわけでもないので、走りに行くのはできそうですが、とりあえず今週末は食料品の買い出し以外は家にいようと思っています。
そこで、前から買おうと思っていたローラー台を購入しました。

一番安いやつですが、まあ、上を見ればきりがないし、まずは使ってみようという感じです。一応セッティングはやってみました。これでどのくらいトレーニングになるかわかりませんが、コロナが収まっても平日の夜とかに活用していければと考えています。

画面をオフにしない、バックグランドでGPS を動かす、軌跡を残す。(アンドロイド地図アプリの開発 その20)

今回は、高低図以外の残りの宿題を一気に片付けてしまいます。

画面をオフにしない。

まず、画面をオフにしない設定ですが、これは簡単にできました。
今回は、地図を表示中に画面を自動でオフにしたくないので、OverlayMapviewer.javaのonCreateの下に下記のように一行加えます。

protected void onCreate(Bundle savedInstanceState) {
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

これで、画面は電源スイッチを操作しないとオフになりません。

バックグランドでGPS を動かす。

東海自然歩道のトレランの時に、時々地図を確認することをしていて、スリープするとGPS が切れてしまっていて、GPS をなかなか拾ってくれなくてイライラしたので、バッテリーの消費は大きくなりますがGPS を切らずにバックグランドで動き続けるようにします。自転車の場合はハンドルにスマホを取り付けて画面をオフにすることなく使うので問題ないのですが、トレランだとずっと持って走るということはなく、時々取り出して地図を確認するという使い方になるのでやっぱりGPS を切らずに使う方が良さそうです。GPS をなかなか拾わない原因の一つはSIMカードをさしてないので通信しないこともあります。スマホが現在地を素早く掴む仕組みとして近くの基地局の電波を拾って三角測量をして位置を決めることも併せてやっているからで、SIMカードの刺さったスマホならイライラ待つ必要はないのですが、何せ廃品利用というか、使わなくなった古いスマホをナビがわりに使おうという事なので仕方がありません。

Service を使う

バックグランドでGPS を動かし続けるにはService と言う仕組みを使うそうです。GPS でログを取るといったことは、比較的取り上げられることなので記事も簡単に見つかります。問題は実装の方法で、下記2つの記事を参考にしました。
medium.com
akira-watson.com
後者の方が実装は簡単そうだったので、始めはこちらの方法を試して見たのですが、GPS の測定結果を受け取るアクティビティが"AppCompactActivity"を継承しないと動かないっぽい。ところが自分の地図アプリの方は継承が複雑でたどっていくとmapsforgeのライブラリーにたどり着いてどうしようもないのでこのやり方は諦めました。
前者の方法だと継承は必要なく、その代わりといってはなんだがブロードキャストという、また新たな仕組みを導入することになり、ちょっとコードが複雑。

ブロードキャストとは

ブロードキャストを使うとイベントが発生したタイミング、この場合GPS の位置情報が更新されたタイミングでアプリ内でイベントの発生をブロードキャストして知らせます。受け取り側のアクティビティはブロードキャストによりイベントの発生を知り処理、この場合は現在地のマークを移動させます。
このイベント処理をonLocationChangedで行っている処理に変えることでバックグランドでGPS を動かし、地図上に反映させることができます。
ソースコードです。まず初めに、Serviceクラスを作成します。これは、通常の左ペインの右クリックでJavaを選ぶところをServiceを選択して作成します。選択肢が2つありますが、単なるServiceの方を選択します。
クラス名はLocationServiceとしました。このとき、Exportedのチェックボックスは外してOKをクリックして作成します。

package com.example.mpf_rotationB;
public class LocationService extends Service implements LocationListener, GpsStatus.Listener {
    public static final String LOG_TAG = LocationService.class.getSimpleName();

    private final LocationServiceBinder binder = new LocationServiceBinder();
    boolean isLocationManagerUpdatingLocation;

    private Context context;

    public LocationService() {
    }

    @Override
    public void onCreate() {
        isLocationManagerUpdatingLocation = false;
        context = getApplicationContext();
    }

    @RequiresApi(api = Build.VERSION_CODES.O)
    @Override
    public int onStartCommand(Intent i, int flags, int startId) {
        super.onStartCommand(i, flags, startId);
   
        return Service.START_STICKY;
    }

    @Override
    public IBinder onBind(Intent intent) {
        return binder;
    }

    @Override
    public void onRebind(Intent intent) {
        Log.d(LOG_TAG, "onRebind ");
    }

    @Override
    public boolean onUnbind(Intent intent) {
        Log.d(LOG_TAG, "onUnbind ");

        return true;
    }

    @Override
    public void onDestroy() {
        Log.d(LOG_TAG, "onDestroy ");
    }

    //This is where we detect the app is being killed, thus stop service.
    @Override
    public void onTaskRemoved(Intent rootIntent) {
        Log.d(LOG_TAG, "onTaskRemoved ");

        if(this.isLocationManagerUpdatingLocation == true){
            this.stopUpdatingLocation();
            isLocationManagerUpdatingLocation = false;
        }

        stopSelf();
    }

    /**
     * Binder class
     *
     * @author Takamitsu Mizutori
     *
     */
    public class LocationServiceBinder extends Binder {
        public LocationService getService() {
            return LocationService.this;
        }
    }

    /* LocationListener implemenation */
    @Override
    public void onProviderDisabled(String provider) {
        if (provider.equals(LocationManager.GPS_PROVIDER)) {
            notifyLocationProviderStatusUpdated(false);
        }
    }

    @Override
    public void onProviderEnabled(String provider) {
        if (provider.equals(LocationManager.GPS_PROVIDER)) {
            notifyLocationProviderStatusUpdated(true);
        }
    }

    @Override
    public void onStatusChanged(String provider, int status, Bundle extras) {
        if (provider.equals(LocationManager.GPS_PROVIDER)) {
            if (status == LocationProvider.OUT_OF_SERVICE) {
                notifyLocationProviderStatusUpdated(false);
            } else {
                notifyLocationProviderStatusUpdated(true);
            }
        }
    }

    /* GpsStatus.Listener implementation */
    public void onGpsStatusChanged(int event) {
    }

    private void notifyLocationProviderStatusUpdated(boolean isLocationProviderAvailable) {
        //Broadcast location provider status change here
    }

    public void startUpdatingLocation() {

        LocationManager locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);

        //Exception thrown when GPS or Network provider were not available on the user's device.
        try {
            Criteria criteria = new Criteria();
            criteria.setAccuracy(Criteria.ACCURACY_FINE);
            criteria.setPowerRequirement(Criteria.POWER_HIGH);
            criteria.setAltitudeRequired(false);
            criteria.setSpeedRequired(false);
            criteria.setCostAllowed(true);
            criteria.setBearingRequired(false);

            //API level 9 and up
            criteria.setHorizontalAccuracy(Criteria.ACCURACY_HIGH);
            criteria.setVerticalAccuracy(Criteria.ACCURACY_HIGH);
            //criteria.setBearingAccuracy(Criteria.ACCURACY_HIGH);
            //criteria.setSpeedAccuracy(Criteria.ACCURACY_HIGH);

            Integer gpsFreqInMillis = 1000;
            Integer gpsFreqInDistance = 1;  // in meters

            locationManager.addGpsStatusListener(this);

            locationManager.requestLocationUpdates(gpsFreqInMillis, gpsFreqInDistance, criteria, this, null);

        } catch (IllegalArgumentException e) {
            Log.e(LOG_TAG, e.getLocalizedMessage());
        } catch (SecurityException e) {
            Log.e(LOG_TAG, e.getLocalizedMessage());
        } catch (RuntimeException e) {
            Log.e(LOG_TAG, e.getLocalizedMessage());
        }
    }

    public void stopUpdatingLocation(){
        LocationManager locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
        locationManager.removeUpdates(this);
    }

    @Override
    public void onLocationChanged(final Location newLocation) {
        Log.d(TAG, "(" + newLocation.getLatitude() + "," + newLocation.getLongitude() + ")");
        Intent intent = new Intent("LocationUpdated");
        intent.putExtra("location", newLocation);

        LocalBroadcastManager.getInstance(this.getApplication()).sendBroadcast(intent);
    }
}

ソースの方は参考サイトをほぼそのままコピペしたのであまりよくわかっていませんが、GPS周りの処理はすべてこのServiceクラスで行っています。重要なのはonLocationChangedの部分で、GPSで位置情報が更新されたらここに飛んできます。ここで、ブロードキャストを実行して、位置情報の更新を知らせます。
Serviceクラスを作成すると、Manifestに自動的に下記のようなServiceの記述が追記されます。

<service
            android:name="com.example.mpf_rotationB.LocationService"
            android:enabled="true"
            android:exported="false"></service>

そして、LocationOverlayViewer.javaのOncreateに下記のようにLocationSeviceのインテントを作成しバインドします。またlocationUpdateRecieverでブロードキャストの情報を受け取る記述を追記します。

protected void onCreate(Bundle savedInstanceState) {
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        super.onCreate(savedInstanceState);
        final Intent serviceStart = new Intent(this.getApplication(), LocationService.class);
        this.getApplication().startService(serviceStart);
        this.getApplication().bindService(serviceStart, serviceConnection, BIND_AUTO_CREATE);

        //initLocationManager();
        locationUpdateReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                Location newLocation = intent.getParcelableExtra("location");

                //drawLocationAccuracyCircle(newLocation);
                drawUserPositionMarker(newLocation);

            }
        };

更にServiceConnectionという内部クラスを作成し、StartUpdatingLocationで位置情報の更新を開始します。

private ServiceConnection serviceConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className, IBinder service) {
            String name = className.getClassName();

            if (name.endsWith("LocationService")) {
                locationService = ((LocationService.LocationServiceBinder) service).getService();

                locationService.startUpdatingLocation();
            }
        }

        public void onServiceDisconnected(ComponentName className) {
            if (className.getClassName().equals("LocationService")) {
                locationService.stopUpdatingLocation();
                locationService = null;
            }
        }
    };

ブロードキャストを受け取るとdrawUserPositionMarkerを呼び出し、地図上の現在地を更新します。
ここで、メニューのGPSをオンに選んでいると、現在地のマーカーを書き込みます。GPSをバックグランドで動かし続けるのでGPSオフを選んでいてもGPSは動き続け、位置が更新されればブロードキャストを実行しますので、if文で処理を分岐しています。これをやらないと、GPSオフを選んでいてもマーカーが表示され、地図の中心が現在地に固定されてしまいます。
if文の中は、元々のonLocationChangedと同じ処理で、マーカーを現在地に書き込み、地図の中心を現在地に移します。

private void drawUserPositionMarker(Location location){
        if (globals.setGPS==true) {
            Latitude = location.getLatitude();
            Longitude = location.getLongitude();
            
            this.myLocationOverlay.setPosition(Latitude, Longitude, location.getAccuracy());

            // Follow location
            this.mapView.setCenter(new LatLong(Latitude, Longitude));
        }
    }

軌跡を残す

今回、一番苦労したのが軌跡を残す処理でした。
バックグランドでのGPS 動作で参考にしたサイトでgoogle map に軌跡を残す記事があり参考にしましたが、さすがにベースのアクティビティが違い過ぎてうまくいかない。そこで、次に参考にしたのがmapsforge の例を紹介している次のサイト
www.programcreek.com
ここにやろうとしている処理とほぼ同じ処理を見つけました。軌跡を描くためのレイヤーを追加し、現在地がアップデートされたら軌跡を描いているレイヤーを特定し、線を追記しています。
レイヤーの追加は、CreateLayers()に軌跡を描画用にPolyline2というレイヤーを追記しています。線の色は赤色にしています。

@Override
    protected void createLayers() {
        super.createLayers();
      
   ~省略~
       
   polyline2 = new Polyline(Utils.createPaint(
                AndroidGraphicFactory.INSTANCE.createColor(Color.RED),
                (int) (4 * mapView.getModel().displayModel.getScaleFactor()),
                Style.STROKE), AndroidGraphicFactory.INSTANCE);

        ~省略~

    }

こうしておいて、drawUserPositonMarkerの中で、

latLong15 = new LatLong(Latitude,Longitude);
Layers x = mapView.getLayerManager().getLayers();
int i = layerX.indexOf(this.polyline2);
((Polyline) mapView.getLayerManager().getLayers().get(i)).getLatLongs().add(latLong15);

として、位置情報が更新されるたびに軌跡を追記するようにしました。

ところが、最初はうまくいくのですが、一旦メニュー画面に戻るとエラーが発生してアプリが停止してしまう。デバッグで原因を探るとどういう訳かメニューに戻った直後にレイヤーの情報がクリアされている。
Layers x = mapView.getLayerManager().getLayers();
int i = layerX.indexOf(this.polyline2); 
で取得した結果が-1になっている。-1ということは該当のレイヤが無いということを意味している。
デバッグモードでたどっていってもしっかりCreateLayersのところでレイヤーの追加をやっているにもかかわらずGPS がアップデートされたときの処理drawUserPositionMarkerにやって来るとレイヤー情報が消えてしまっている。まったく訳がわからないが、参考にしたサイトで
Layers layers = mapView.getLayerManager().getLayers();
if (startMarker != null) {
removeLayer(layers, startMarker);
}
としていたのを思い出して、レイヤー情報がない場合処理をスキップするようにして見たところ何故かうまくいった。どうもすっきりしないがとりあえず動いたのでよしとすることにする。

Layers x = mapView.getLayerManager().getLayers();
            //int i = mapView.getLayerManager().getLayers().indexOf(this.polyline2);
            int i = layerX.indexOf(this.polyline2);
            if (i>=0) {
                ((Polyline) mapView.getLayerManager().getLayers().get(i)).getLatLongs().add(latLong15);
            }

エミュレータで動かしてみた。青がGPXファイルの想定ルート、赤が軌跡。GPSの位置情報の入れ方が少しアバウトなので青のGPXと赤の軌跡が完全に重なっていないが、実際にもこんなくらいの誤差はありうるのでかえってリアルに見える。

f:id:alasixOsaka:20200327155222j:plain
青がGPXのルート、赤が軌跡
この軌跡は、メニュー画面に戻るとmapviewを再描画するので消えてしまう。これを防ぐにはログを記録しておいて再描画の際に書き直す処理をする必要があるが、とりあずこれで使い勝手を見てみたいと思っている。

Notificationについて

アンドロイドではバックグラウンドでアプリが動作していることなどを知らせるためのNotificationという仕組みがある。ところが、こいつの仕組みがOreo(Android8)から大きく変更になっていて、実装の仕方が全く異なっている。Oreo以降ではチャンネルという仕組みを使うらしいが、この仕組みを古いOreo以前のAndroidが搭載されているスマホに入れてもうまく動かない。自分の場合はエミュレータはAPI26にしているのでチャンネルを使ったNotificationがうまく動作したが、実機(XperiaZ4)はAndroid6なので、エラーになって動かない。一方で古い仕組みを使ってみるとエミュレータでは動かない。Androidデベロッパーサイトには、古い仕組みは推奨されないが、互換性があるので動作はするというようなことが書いてあるが実際にやってみると動かない。Notificationはあると便利だが、古いスマホを地図アプリ専用に使おうとしているのでなくても大きな不便はないと思われるので、今後の課題ということにして今回は実装しないことにした。また、スマホを買い換えたら考えてみようと思う。

さて残りは最難関の高低図の描画だ。一応、GPXファイルから高さ情報を拾ってきて、高低図のプロットまでは何とかできそうな目途がついてきている。ただ、その図を地図アプリの方に書き込むのができるのかどうか。このへんが大きなポイントになりそうだ。


参考にしたサイト
Androidで明示的に画面をOFFにさせない - Qiita
位置情報を正確にトラッキングする技術 – Medium
[Android] バックグラウンドでGPSログを取り続けるには
MapHandler Java Source Code

パン作り その3

相変わらずパン作りにチャレンジしています。

 

ホシノ天然酵母3倍起こし

ホシノ天然酵母は普通、酵母1に対し水を2の割合で混ぜて種起こしをします。これに対し酵母1に対し水1.5で起こす方法があります。この間買った本に書いてあったので、早速やってみようとしましたが、計算ミスで水が3倍入ってしまった。何でこんなことになったかと言うと、本では酵母を100g 、水を150gの割合で混ぜていたんですね。でも、我が家の酵母は50gずつ小分けにされているので、それを使って、水だけ150g 入れてしまったというわけ。1日経って様子を見たら、本ではおからのような感じになっているのに、我が家ではしゃばしゃばで、ここでようやくおかしいと気がついた。でも後の祭り。仕方ないのでそのまま様子を見ることに。1.5倍起こしだと室温で1週間くらいかかるそうだが、途中で味を嘗めて確認したところ、2日位で甘味が消えたので、一応完了と判断した。

フランスパンを焼いてみる

ものは試しにフランスパンを焼いてみた。今回は小麦粉に外国産のスーパーカメリアを使った。国産小麦があまりに膨らまないので、比較実験のために買っておいたのだが、パネトーネマザーのお陰で国産小麦でもしっかり膨らんだパンが焼けるようになっていたので出番がなかった。

材料は次の通り

小麦(スーパーカメリア)200g

生種

砂糖

レモン汁

本当はフランスパンは中力粉を使って焼くものだが、今回はスーパーカメリアを使いたかったのであえて強力粉100%電焼いてみた。

材料をHBに投入し天然酵母のねり+発酵モードで発酵実験を3時間に設定。うまく膨らまない場合はピザにしようと思いながら開始。でも、ちゃんと膨らんでフィンガーテストもOK 。

2分割してベンチタイム20分。フランスパンの形に成形して、オーブンで2次発酵、30℃で1時間。やや膨らみが足りないので追加で10分発酵。焼き上げはオーブンのフランスパンモードで焼く30分。うまく焼けました。

味も良好で何の問題もない。あとは種がどのくらいもつか。2倍起こしだと約1週間と言われている。それに対し1.5倍起こしは1ヶ月もつとのこと。とすると水分の多い3倍起こしは更に短いのかも。

f:id:alasixOsaka:20200301145822j:plain

フランスパンはうまく焼けた

角食パンに挑戦

酵母が使えることが分かったので、翌週、今度は角食パンを焼いてみることにした。

材料

  • 小麦 280g
  • 生種 38g
  • 砂糖 20g
  • 塩  4g
  • 水  140g
  • スキムミルク 6g
  • 無縁バター 10g

春よ恋の残りが100gほどだったので、残りは別に買ってあった春よ恋ブレンドを使用

HBのホシノ天然酵母メニューのねり+発酵コースで発酵時間3時間で一次発酵。1週間たってもちゃんと発酵してくれた。

2つに分割し、ベンチタイム20分。ベンチタイム後、細長く伸ばして、端から丸めてパン型に入れ、レンジの発酵メニューで30℃、2時間、二次発酵。パン型の上端から2-3cmまで膨らんだことを確認して、蓋をし、レンジの山形食パンメニューで焼き上げ。奇麗な角食パンが出来上がった。味もグッド。

f:id:alasixOsaka:20200307153349j:plain

角食パンも奇麗に焼けた。

プチパンを作ってみる。

酵母がまだ生きているうちにと、翌日にプチパンを作ってみる。

材料

  • 小麦(リスドゥオル)240g
  • 全粒粉(春よ恋)40g
  • 砂糖 6g
  • 生種 38g
  • 塩 3g
  • 水 151g
  • レモン 4cc

水は、リスドゥオルの説明書きには65%と表示があったので、リスドゥオルに対し65%、全粒粉に対し60%で計算した。

角食パンと同じく、HBのホシノ天然酵母メニューのねり+発酵コースで発酵時間は3時間。今回は発酵中の温度を測ってみた。

f:id:alasixOsaka:20200308102642j:plain

ホシノ天然酵母メニューの発酵温度は30℃

だいたい予測通りだったが、発行時の釜の温度は30℃だった。この温度計アリババで買ったものだが、安いだけあって電源スイッチはなく、電池を入れたらスイッチが入るという代物。また、温度変化に対する追従もとっても遅く、安定するのに5分くらいかかる。まあ、安いし一応使えるので良しとする。

 

種起こしから2週間後の酵母を使う

種を起こしてから2週間たった、いつまで酵母が活きているのか気になるが、とりあえずパンを焼いてみることに。

今回から、小麦粉が春よ恋から春よ恋とゆめちからのブレンドに変わっている。実は、パネトーネでHBの予約コースで食パンを焼いてみたが、過発酵気味であまりうまくいかなかった。水分量があってなかったのかもしれないので、こねながら水分量をチェックしてみた。

材料

小麦粉(春よ恋ブレンド)280g

砂糖 6g

塩 4g

生種 37g(1対3起こし)

水 140ml + 5ml(後で追加)

水を140mlにしてねりから開始したが、どうも粉が残っている感じで、水が足りないようだ。水5ml追加することで、いい感じになった。

HBのホシノ天然酵母のねり+発酵モードで発酵時間を3時間に設定して1次発酵。

今回は、2つに分けて少し大きめの丸パン(いなかパン)にしてみた。ベンチタイム20分で、オーブンで手動の230℃で20分焼いた。焼き上がりは思ったよりもソフトで、もう少しハードな感じを想像してたのでちょっと予想外。とりあえず、2週間たってもちゃんとパンになった。味の方も落ちていない。

f:id:alasixOsaka:20200322082153j:plain

今回はいなかパンを焼いてみた。