消毒液の味
最近は例のアレのせいで、どこに寄っても入り口に消毒液が置いてある。
湿気が多い時期だと中々消毒液が乾かなくて嫌だよね、とかまあそれはいいとして、個人的に「消毒液」の文字を見るたびに思い出してしまう中学生の時のエピソードがある。
消毒液の味がする
中学生の時の話。
給食の時間に、クラスの中のひとりが、おかずの漬け物を指差して「これ、消毒液の匂いがする!」と言い出した。
すると、周りの生徒たちも皆口々に「たしかに」とうなずいた。
しまいには、先生まで「変な匂いがするなら食べなくていいから」と言い出す始末だった。
さて、その中で一人、俺は全くその感性が理解できないでいた。
それもそのはずで、俺はその時、「消毒液」を「ショートケーキ」と聞き間違えていた。
ショートケーキは甘くない
俺は自分の味覚だけおかしいのかと不安になって、隣の席のやつに「ショートケーキの味なんかしないよね?ショートケーキってさあ、ほら、もっと甘いよね?」と率直な疑問を聞いてみた。
すると隣のやつは、先ず驚き、次に大笑いして「いやそもそも消毒液は甘くないだろ」と言ってきたので、俺はますます混乱した。
みんなにとっては、漬け物はショートケーキの味がする、しかもショートケーキは漬け物ほどには甘くないらしい。
もう軽い疑心暗鬼状態だ。
さらに悪いことに、その隣の席のやつが「こいつ、消毒液が甘いって言うんだけど」と周りに尋ね始めたからもう大変だ。
俺は「いや、いいから、今のなしで」とそいつを抑えて、なんとかその場はことなきを得た。
その後、ふと、有史以前から社会的認知は多数決により決定されてきたのだった、ということを思い出したので、消毒液の話題についてはそれ以上喋らないようにしてやり過ごした。
ここからは話盛ってる
さて、俺がいた中学校では、給食の時間が終わるとみんなで食器を配膳室に片付けに行く制度があったのだが、運の悪い(?)ことに、配膳室には消毒液が置いてあった。
配膳室で片付けが終わると、一緒に片付けていたやつの一人が、俺に消毒液を突きつけてきて「これ、本当に甘く感じるの?」と聞いてきた。
その瞬間、脳みその中で全てが繋がった。
ああ!ショートケーキって、消毒液ね、と。
もやもやが一気に晴れた。
しかし中学生というのは、簡単に間違いを認められるほど素直じゃない。
中学生はメンツで生きているのだ。
俺は脊髄反射で嘘をついて「まあ、甘いね」とこたえた。
すると、そいつもなんだか自分の味覚が信じられないような顔になって「マジ?」と興味津々で聞いてくる。
マジなわけねえだろ。
しかし俺は黙ってうなずいた。
「本当かな…」とそいつは手に消毒液を垂らし、おそるおそる舌で舐めた。
本気か?と思いつつ、俺もそれを固唾を飲んで見守る。
実際、消毒液を直接なめる人間を見るのは後にも先にもこれきりだったし、どんな味がするのか気になった。
そいつはしばらく口の中で舌を動かし、難しそうな顔をしていた。
「どう?」と聞くと、「まあ確かにちょっと甘いのかな〜言われてみれば」と。
言ってみるもんだな。
それで、逆に驚いたのはこっちだった。
嘘から出たまことか?と思いつつ、消毒液を手に取ってさっと舐めた。
が、まあ、結果は言うまでもなく。
それで中学生ながらに、感じる事とか言う事とか、ずいぶん適当なものだと思ったのだった。
WordPress on Google Cloudのメモ。SSH接続とかテーマ編集とか。
最近WordPressをGoogle Cloud Platform(以下GCP)で動かしてまして、初期で色々つまったポイントとかのメモを置いておきます。
環境は、マーケットプレースパッケージのWordPress Google Click to Deployを使っています。
SSH in Browserは便利だけど結局つかわなかった
GCEはブラウザのコンソールからワンクリックでSSH接続を開始できて、それがまあ便利なのですが。
cloud.google.com
はじめの方はもっぱらこれのお世話になってたんですが、Debianのバージョンが古いせいか、IAMにSSH in Browser権限を付与しようとするとうまくいかないんですよね。
本当はよく調べるべきなんだろうけど、一度自前のSSHの公開鍵を登録してしまえばもうそれで接続すればいいかな〜という感じで、それっきりでした。
DNS設定のこと
公式ドキュメント通り。
cloud.google.com
テーマはどうやっていじろう
GCEには、レンタルサーバーのようにブラウザから使える便利なFTPソフトはありません。
そうするとまあGitHub経由でファイルを更新しようとなるわけです。
これはWordPressのAjaxアクションを利用して解決します。
developer.wordpress.org
手順としては
- 対象のテーマディレクトリをGit管理する
- SSH鍵を用意する
- テーマのfunctions.phpに、SSH鍵を使ってリモートリポジトリからPullするwp_ajaxアクションを追加する
- GitHubのリモートリポジトリにWebHookを追加してSSH鍵を登録する。
具体的な方法は以下のリンクが明快です。
qiita.com
サイトに「技術的な問題が〜」が出てしまったら
ワードプレスのログファイルを見てもいいんですけど、GCP のLoggingに集積されてるので基本的にここからみるのがよさそうです。
インスタンスの詳細の「Stackdriver Logging」から閲覧できます。
善行
2020年6月くらいまで随時追加・加筆します。
Youtubeライブのコメント読み上げをゆるく実装する
動機と概要
・Youtube のライブ配信を聴きながら作業をすることが多い
・でもやっぱりライブ配信ってコメントありきだし、コメントも音声で聴きたいですよね〜
・「Youtubeコメント読み上げ」で調べてみたら、棒読みのインストールとかコメントビュアーとか面倒な準備が多い。
・それならJavaScriptで読み上げさせればよくね?
というわけで今回作ったものは、ブラウザのコンソール(画像の赤丸)からjavascriptでコメント(画像の緑丸)を取得し、Web Speech APIで読み上げる、というスクリプトです。
特徴はなんといっても事前準備一切不要で使えることです。ブラウザの機能のみを使っているため、スクリプトをコンソールにべた貼りするだけでOKです。
使用方法
まずはブラウザのコンソールを開きます。WindowsならCtrl+Shift+J、MacならCmd+Shift+Jで開くことができます(開けない場合は、使っているブラウザー名+"コンソール 開き方"とかで検索すると解決します)。
コンソールが開けたら、以下のスクリプトをコピーしてコンソールに貼り付けます。
const main=()=>{const e=(e,t)=>{let n=e;return t.map(e=>n=n&&e(n)),n},t=[e=>e.childNodes[1],e=>e.childNodes[3]],n=new SpeechSynthesisUtterance;n.voice=speechSynthesis.getVoices()[18],n.volume=.25;const s=e(document,[e=>e.getElementsByTagName("iframe")[0],e=>e.contentDocument,e=>e.getElementById("item-offset"),e=>e.childNodes[1]]);if(void 0===s)return void console.log("コメントのコンテナを取得できませんでした。");let o="";setInterval(()=>{const c=e((e=>e[e.length-1])(s.childNodes),t);if(void 0===c)return;const i=c.innerHTML.split("<")[0];i!==o?(o=i,speechSynthesis.cancel(),n.text=i,speechSynthesis.speak(n)):console.log("コメントの内容を取得できませんでした。")},3e3)};main();
コメントの更新があれば、ただちに読み上げが始まります。
スクリプトの解説
解説付き版のスクリプトを載せておくので参考までに。
やっていること自体は、DOMツリーからコメントをとってくる処理と、Web Speech APIで読み上げさせる処理の二つだけなので特に目新しいこともなく、JavaScript使ってる人なら誰でも自作できると思います。
branchPoints 定数をいじるだけで様々な配信・動画サイトにも応用できます。
const main = () => { // immutableなpop関数。手癖です。 const immpop = (array) => array[array.length-1]; // targetを基点として、DOMツリーの分岐点のリスト(branch points)をたどって目的の要素を返します。 // たどっている途中でundefinedになった場合もエラーをはかずにundefinedを返してくれます。 const getElementOrUndefined = (target, branchPoints) => { let result = target; branchPoints.map((f) => result = result && f(result)); return result; }; // window.documentから、コメントのコンテナまでを探索する分岐点のリスト。 // これはYoutube(2020/04/30現在)用のリストですが、ここを変更することで各種配信サイトに対応できる // はずです。 const branchPointsForContainer = [ e => e.getElementsByTagName('iframe')[0], e => e.contentDocument, e => e.getElementById('item-offset'), e => e.childNodes[1], ]; // コメントのコンテナからコメントの内容までを探索する分岐点のリスト。 const branchPointsForContent = [ e => e.childNodes[1], e => e.childNodes[3], ]; // Web Speech APIのコントローラです。詳しくは以下から。 // https://developer.mozilla.org/en-US/docs/Web/API/SpeechSynthesis const utterance = new SpeechSynthesisUtterance(); utterance.voice = speechSynthesis.getVoices()[18]; utterance.volume = 0.25; // 先に定義した関数を使ってコメントのコンテナを入手します。 const container = getElementOrUndefined(document, branchPointsForContainer); if (container === undefined) { console.log('コメントのコンテナを取得できませんでした。') return; } // 同じコメントを何度も読み上げないように、一つ前のコメントを記録しておく変数。 let previousComment = ''; // setInterval関数で三秒毎にコメントのコンテナを観察して読み上げを実行します。 setInterval(() => { // コメントの内容を取得します。undefinedならパス。前回と同じコメントもパス。 const content = getElementOrUndefined(immpop(container.childNodes), branchPointsForContent); if (content === undefined) return; const comment = content.innerHTML.split('<')[0]; if (comment === previousComment) { console.log('コメントの内容を取得できませんでした。') return; } previousComment = comment; // 読み上げ speechSynthesis.cancel(); utterance.text = comment; speechSynthesis.speak(utterance); }, 3000); }; main();
それでは。
素のHTML+JavaScriptで複素関数を描画する
はじめに
特別なライブラリを使わなくても、HTMLのcanvas要素を使ってJavaScriptで色々描ける。
例えば複素対数関数とかをこんな感じで。
canvas要素の取り扱いについてはこちらから詳しくCanvasRenderingContext2D - Web API | MDN
とりあえずなんか描く
body要素にcanvasを設置してグラデーションを描いてみる
👇コード全文
<!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <title>famous canvas</title> <style>canvas {border: 1px solid black;}</style> <script> window.onload = () => { // 👇JavaScriptでcanvas要素を取得、キャンバスサイズの定義とコンテキストの取得 const canvas = document.getElementById('famousCanvas'); const size = 256; [canvas.width, canvas.height] = [size, size]; const context = canvas.getContext('2d'); // 👇グラデーションを描画。fillStyleで色指定、fillRectで描画座標指定。 for(let i = 0; i < size; i++){ for(let j = 0; j < size; j++){ context.fillStyle = `rgba(${i}, ${j}, 128, 1)` context.fillRect(i, j, 1, 1); } } }; </script> </head> // 👇canvas要素を置く。 <body><canvas id="famousCanvas"/></body> </html>
👇結果
偏角を色に変換する
直交座標系x, yの偏角を色環に変換する。
原点(0,0)を視点としてある点(x, y)を通る半直線が下に示す六角形のどの辺に交わるかでRGBの値を決定する。
👇コード(script部分のみ、rectToRgba関数が該当部分。)
const rectToRgba = (x, y) => { const max = 255; const sqrt3 = Math.sqrt(3); // 0点は例外処理。 if (x == 0 && y > 0) return [max*0.5, max, 0]; if (x == 0 && y < 0) return [max*0.5, 0, max]; if (x > 0 && y == 0) return [max, 0, 0]; if (x < 0 && y == 0) return [0, max, max]; const tan = y / x; const cot = x / y; // 場合わけしてrgb算定(上の六角形と見比べるとわかりやすいかも) if (0 < tan && tan < sqrt3) return y > 0 ? [max, tan/sqrt3*max, 0] : [0, (1-tan/sqrt3)*max, max] else if (-1/sqrt3 < cot && cot < 1/sqrt3) return y > 0 ? [(1+cot*sqrt3)*max*0.5, max, 0] : [(1-cot*sqrt3)*max*0.5, 0, max] else return y > 0 ? [0, max, (1+tan)*max] : [max, 0, -tan*max] }; window.onload = () => { const canvas = document.getElementById('famousCanvas'); const size = 256; [canvas.width, canvas.height] = [size, size]; const context = canvas.getContext('2d'); for(let i = -size*0.5; i < size*0.5; i++){ for(let j = -size*0.5; j < size*0.5; j++){ const rgbArray = rectToRgba(i, j); context.fillStyle = `rgba(${rgbArray[0]}, ${rgbArray[1]}, ${rgbArray[2]}, 1)` context.fillRect(i+size*0.5, -j+size*0.5-1, 1, 1); } } };
👇結果(恒等写像f(z) = zの図示になっている)
いろんな複素関数を描いてみる
先ほどのコードにもう少してを加えて、いろいろ複素関数を描いてみる
👇コード全文 (complexFunction()で複素関数の定義するようにした。)
const complexFunction = (x, y) => { // ここに複素関数の定義を書く。たとえばf(z)=z^2なら以下の通り。 return [x*x - y*y, 2*x*y]; }; const rectToRgba = (x, y) => { const max = 255; const sqrt3 = Math.sqrt(3); if (x == 0 && y > 0) return [max*0.5, max, 0]; if (x == 0 && y < 0) return [max*0.5, 0, max]; if (x > 0 && y == 0) return [max, 0, 0]; if (x < 0 && y == 0) return [0, max, max]; const tan = y / x; const cot = x / y; if (0 < tan && tan < sqrt3) return y > 0 ? [max, tan/sqrt3*max, 0] : [0, (1-tan/sqrt3)*max, max] else if (-1/sqrt3 < cot && cot < 1/sqrt3) return y > 0 ? [(1+cot*sqrt3)*max*0.5, max, 0] : [(1-cot*sqrt3)*max*0.5, 0, max] else return y > 0 ? [0, max, (1+tan)*max] : [max, 0, -tan*max] }; window.onload = () => { const canvas = document.getElementById('famousCanvas'); const size = 256; [canvas.width, canvas.height] = [size, size]; const context = canvas.getContext('2d'); for(let i = -size*0.5; i < size*0.5; i++){ for(let j = -size*0.5; j < size*0.5; j++){ const [x, y] = [i/size*5, j/size*5]; const rgbArray = rectToRgba(...complexFunction(x, y)); context.fillStyle = `rgba(${rgbArray[0]}, ${rgbArray[1]}, ${rgbArray[2]}, 1)` context.fillRect(i+size*0.5, -j+size*0.5-1, 1, 1); } } };
👇結果
色環が2周しているのがわかる🤔
二次元ではリーマン面が表示しきれない🤔
偏角はyだけに依存していることがわかる🤔
🤔
以上
React.jsで一通り地図を扱う【React-Leaflet】
目次
用語説明
Reactとは?
→JavaScriptフレームワーク御三家の一。
Leafletとは?
→Webアプリで地図を扱うためのライブラリ。
React-Leafletとは?
→Reactに入れて使うLeafletのモジュール。
はじめに
Reactで地図を使ってる記事が少なかったので。
素のJavaScriptで地図を使おうとすると、画面の状態管理がめちゃくちゃしんどいですが、Reactの力である程度のりきることができます。
(Leafletが2.0に更新されてReactのContext APIをサポートしてたり、国土地理院がベクトルタイルを公開し始めたりと、地図を使ったWebアプリを作るのになにかと追い風な状況です。)
皆さんも地図を使ったWebアプリ始めましょう。
⭐️とりあえず地図を表示する⭐️
Reactアプリを立ち上げて、react-leafletを導入します。
npx create-react-app app cd app npm install react-leaflet leaflet
まずは、LeafletのMap要素だけを表示してみましょう。
説明のため、コードにコメントをつけています。
/* src/App.js */ import React from 'react'; import './App.css'; import {Map} from 'react-leaflet'; // leafletのCSSを別に読み込む必要があります(これ公式に書いてなくて結構詰まった) import '../node_modules/leaflet/dist/leaflet.css'; const App = () => ( // Map要素は画面読み込み時の座標(center: LatLngTupple)と縮尺(zoom: number)を指定する必要があります。 // viewport: {center, zoom} 形式で指定することもできます。 <Map center={[34.9843, 135.7596]} zoom={7}/> ); export default App;
/* src/App.css */ .leaflet-container { height: 100vh; width: 100vw; } /* デフォルトではMap要素の高さが0なので、なんらかの高さを指定する必要があります。Map要素のクラス名前はleaflet-containerです。 */
こんな感じで灰色の空っぽの地図が表示されます。
このままでは地図としての役割を果たさないので、地図タイル(説明は後述)を追加します。
import React from 'react'; import './App.css'; import '../node_modules/leaflet/dist/leaflet.css'; import { Map, TileLayer } from 'react-leaflet'; // 国土地理院の地図タイルを利用します。 // 国土地理院に限らず、Attributionの記述は必須です。 const tileAttribution = '<a href="https://maps.gsi.go.jp/development/ichiran.html">地理院タイル</a>'; const tileUrl = 'https://cyberjapandata.gsi.go.jp/xyz/pale/{z}/{x}/{y}.png'; const App = () => ( <Map center={[34.9843, 135.7596]} zoom={7}> <TileLayer attribution={tileAttribution} url={tileUrl} /> </Map> ); export default App;
ここまで書くと、いい感じで地図が表示されます。
地図タイルの説明😉✨↓
そこで、小さな正方形の画像に区切ってサーバーに保存しておき、必要な領域の画像だけサーバーから受け取る方法が考え出されました。
この区切られた正方形の画像が地図タイルで、地図タイルを配布しているサーバーをタイルサーバーといいます。
TileLayer要素にタイルサーバーのURLを指定することで、Leafletがユーザーの操作に応じて地図タイルをリクエストして表示してくれます。
地図タイルの概念はこちらのページがわかりやすいと思いますタイル座標 | 国土地理院。
⭐️Leaflet要素いろいろ⭐️
Map要素の内側にReact-Leafletの要素を追記することで、地図に色々書くことができます。
・範囲円を表示
<Circle center={[34.9843, 135.7596]} radius={100000} />
・線を表示
<Polyline positions={[[35, 136], [34, 135], [34.5, 137]]} color='red' />
・マーカーを表示
import {icon} from 'leaflet'; import iconImage from './marker.png' ... const markerIcon = icon({ iconUrl: iconImage, iconSize: [20, 32], iconAnchor: [20/2, 32], }); ... <Marker position={[34.9843, 135.7596]} icon={markerIcon} />
marker.pngの部分は好きな画像に置き換えることができます。
ここらへんの詳しいことはReact-Leaflet ComponentsとLeaflet API referenceを参考にしてみてください。
最後に1つ、GeoJsonというモノを紹介しておきます。
GeoJsonは2016年ごろにRFC 7946で標準化された、地理情報をJSON形式で扱うためのフォーマットです。
LeafletでももちろんGeoJSON形式を読み込むことができます。
地理情報のデータセットを表示する場合は、MarkerやCircleをArray.mapするより、GeoJSON形式で表示してしまうほうが、コードの見通しがよくなります。
・GeoJSONを表示
今回は例として京都駅のバス停のデータセットを利用します。(国土数値情報 バス停留所データの詳細)
データをこんな感じでGeoJSON形式に整形します。
Reactのソースコード
... import { Map, TileLayer, GeoJSON } from 'react-leaflet'; import {icon, marker} from 'leaflet'; import geoData from './geodata.json'; import iconImage from './marker-icon.png'; ... const markerIcon = icon({ iconUrl: iconImage, iconSize: [20, 32], iconAnchor: [20/2, 32], popupAnchor: [0, -32], }); ... <Map ... > ... <GeoJSON data={geoData} // pointToLayerは、GeoJSONの中のそれぞれのFeature(要素)ごとにLeaflet要素を返し、レイヤに追加する。 pointToLayer={(_feature, latlng) => marker(latlng, {icon: markerIcon})} // onEachFeatureは、それぞれのFeatureに対してイベントを指定したり設定を適用したりできる。 onEachFeature={(feature, layer) => layer.bindPopup(feature.properties.label)} /> </Map> ...
⭐️イベント管理いろいろ⭐️
後日追記
JavaScript+Python(Django)で京都市バスの路線図を描画する処理
これまでのいきさつ
京都市バスのオープンデータを使ってバス路線図を描いた。
京都の市バス路線図のレプリカ作成、途中経過
— 人生完全に理解した (@tatsusaki98) 2019年7月2日
バス停をクリック→バス停の名前とバス停を通過する路線を表示
路線をクリック→路線名を表示 pic.twitter.com/YdEcG3ls2h
現在以下のリンクにて公開中。
https://replica-of-kyotobusmap.herokuapp.com/main/
使用したライブラリ
Leaflet JavaScriptで地図を扱うライブラリ
https://leafletjs.com/
Django PythonのWebフレームワーク
https://www.djangoproject.com/
JavaScript側の処理(リクエスト送信)
バス停(画像の緑色の円)をクリックすると、下の関数を呼び出す。
すでに表示されてる線(pline)があれば一度全て削除。
その後、サーバーに非同期通信でクリックした緯度と経度を送信する。
CSRFトークンに関しては前記事で。
function circleClick(event) { while(plines.length > 0){ map.removeLayer(plines.pop()); } let lat = event.latlng.lat; let lng = event.latlng.lng; let csrftoken = document.cookie.substring('csrftoken='.length); $.ajax({ url: '/main/get_stop_info/', data: {'lat': lat, 'lng': lng}, beforeSend: function (xhr) { xhr.setRequestHeader("X-CSRFToken", csrftoken); }, type: "POST", dataType: 'json', success: getStopInfo, }); }
Django側の処理(レスポンス送信)
受け取った経度と緯度(lng, lat)から、該当するバス停を検索する。
バス停を通る路線を、順番通りに並び替えてリスト化。
JSON形式でJavaScript側に送り返す。
ここはがんばればもう少し簡潔に書けそう。
def get_stop_info(request): lat, lng = float(request.POST['lat']), float(request.POST['lng']) stop = 0 for s in Stop.objects.all(): if abs(s.lat - lat) < 0.0007 and abs(s.lng - lng) < 0.0007: stop = s break if stop == 0: return JsonResponse({'StopNotFound': 1}) route_set = [] for line in stop.line_set.all(): route = [] for stop_pk in line.route.split(','): s = Stop.objects.get(pk=int(stop_pk)+1) route.append([s.lat, s.lng]) route_set.append([route, line.line_name]) line_name_count = Counter([route[1] for route in route_set]) response = { 'stop_name': stop.stop_name, 'route_set': route_set, 'line_name_set': sorted(list(line_name_count.keys())), } return JsonResponse(response)
JavaScript側の処理(路線描画)
レスポンスが無事に返ってきたら以下の関数が実行される。
route_setのデータに沿って線を描画する。
基本的にはLeafletの説明書通り。
function getStopInfo(response){ for(let route of response['route_set']) { let pline = L.polyline(route[0], {weight: 5, color:'#FF0000'}).addTo(map); pline.on('click', function(e){clickLine(e, route)}); plines.push(pline); } $('h3').text(response['stop_name']); } function clickLine(event, route){ while(plines.length > 0){ map.removeLayer(plines.pop()); } let pline = L.polyline(route[0], {weight: 5, color:'#FF0000'}).addTo(map); $('h3').text(route[1]); plines.push(pline); }
無事に表示されている。
今後のこと
時刻表の追加。これは近いうちに。
路線を描画する動作が重いので、重複する線をあらかじめデータベース側で省略するなどして、最適化する。
見た目も今のままだた少し無骨すぎるのでちょくちょく直していきたい。