まだプログラマーですが何か?

プログラマーネタ中心。たまに作成したウェブサービス関連の話も https://twitter.com/dotnsf

2020/04

オープンな地図データである OSM(OpenStreetMap) と、オープンな地図操作 JavaScript ライブラリである Leaflet.js を使うことで、ライセンスフリーな地図アプリを作ることができます。このブログでも過去に何度か扱ったことがあります:
http://dotnsf.blog.jp/tag/leaflet

例えば、単に地図を全画面表示するのであればこんな感じのコードを書くと実現できちゃいます:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3c.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml">
<head>
<meta charset="utf8"/>
<meta http-equiv="pragma" content="no-cache"/>
<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.2.0/leaflet.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.2.0/leaflet.js"></script>

<meta name="viewport" content="width=device-width,initial-scale=1"/>
<meta name="apple-mobile-web-app-capable" content="yes"/>
<meta name="apple-mobile-web-app-status-bar-style" content="black"/>
<meta name="apple-mobile-web-app-title" content="OSM Sample"/>

<title>OSM Sample</title>
<script>
</script>
<script>
//. 初期中心位置
var lat = 35.681377778;
var lng = 139.76736389;

//. 初期ズームレベル
var zoomlevel = 9;

var map = null;

$(function(){
  //. 初期位置を中心とした地図を OpenStreetMap データで表示
  map = L.map('demoMap', {}).setView( [ lat, lng ], zoomlevel );
  L.tileLayer(
    'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
      attribution: 'Map data © <a href="//openstreetmap.org/">OpenStreetMap</a>',
    }
  ).addTo( map );
});
</script>
<style>
html, body {
  width: 100%;
  height: 100%;
  padding: 0px;
  margin: 0px;
}
#demoMap{
  position: fixed;
  width: 100%;
  height: 100%;
}
</style>
</head>
<body>
  <div>
    <div id="demoMap"></div>
  </div>
</body>
</html>

(東京あたりを中心に、ズームレベル9で地図を表示)
2020041501


さて、上図の左上にズームコントロールがあります(上記コードでは特に指定していませんが、明示的に非表示にしない限り、デフォルトでズームコントロールが表示されます)。スマホであればピンチイン・アウトの操作でズームイン・アウトができるのですが、タッチスクリーンに対応していない PC のブラウザで地図を見ている場合は、このコントールを使ってズームイン・アウトを行うことになります。

ただ例えば画面上部に常に表示されるメッセージがある場合など、メッセージとズームコントロールが重なってしまい、操作しにくくなってしまうことがあります:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3c.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml">
<head>
<meta charset="utf8"/>
<meta http-equiv="pragma" content="no-cache"/>
<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.2.0/leaflet.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.2.0/leaflet.js"></script>

<meta name="viewport" content="width=device-width,initial-scale=1"/>
<meta name="apple-mobile-web-app-capable" content="yes"/>
<meta name="apple-mobile-web-app-status-bar-style" content="black"/>
<meta name="apple-mobile-web-app-title" content="OSM Sample"/>

<title>OSM Sample</title>
<script>
</script>
<script>
//. 初期中心位置
var lat = 35.681377778;
var lng = 139.76736389;

//. 初期ズームレベル
var zoomlevel = 9;

var map = null;

$(function(){
  //. 初期位置を中心とした地図を OpenStreetMap データで表示
  map = L.map('demoMap', {}).setView( [ lat, lng ], zoomlevel );
  L.tileLayer(
    'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
      attribution: 'Map data © <a href="//openstreetmap.org/">OpenStreetMap</a>',
    }
  ).addTo( map );
});
</script>
<style>
html, body {
  width: 100%;
  height: 100%;
  padding: 0px;
  margin: 0px;
}
#demoMap{
  position: fixed;
  width: 100%;
  height: 100%;
}
#message{
  position: absolute;
  top: 10px;
  font-size: 20px;
  color: red;
  font-weight: bold;
}
</style>
</head>
<body>
  <div>
    <div id="demoMap"></div>
    <div id="message">このメッセージが邪魔でズーム制御ができない?</div>
  </div>
</body>
</html>

(画面上のメッセージとズームコントロールが重なってしまう例)
2020041502


この状態をなんとか回避できないか、というのが今回のブログエントリのテーマです。メッセージ側の位置を変えるのは簡単ですが、当然意図があって画面上部にメッセージを表示しているわけですから、できればズームコントロールの位置を変えたい。一方、なまじデフォルトで表示されてしまっているズームコントロールだけにどうすると位置を変更できるのか、がわかりにくいのでした。

正解の一例を先にお見せすると、こんな感じになります:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3c.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml">
<head>
<meta charset="utf8"/>
<meta http-equiv="pragma" content="no-cache"/>
<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.2.0/leaflet.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.2.0/leaflet.js"></script>

<meta name="viewport" content="width=device-width,initial-scale=1"/>
<meta name="apple-mobile-web-app-capable" content="yes"/>
<meta name="apple-mobile-web-app-status-bar-style" content="black"/>
<meta name="apple-mobile-web-app-title" content="OSM Sample"/>

<title>OSM Sample</title>
<script>
</script>
<script>
//. 初期中心位置
var lat = 35.681377778;
var lng = 139.76736389;

//. 初期ズームレベル
var zoomlevel = 9;

var map = null;

$(function(){
  //. 初期位置を中心とした地図を OpenStreetMap データで表示
  map = L.map('demoMap', { dragging: true, zoomControl: false }).setView( [ lat, lng ], zoomlevel );
  L.control.zoom( { position: 'bottomright' } ).addTo( map );
  L.tileLayer(
    'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
      attribution: 'Map data © <a href="//openstreetmap.org/">OpenStreetMap</a>',
    }
  ).addTo( map );
});
</script>
<style>
html, body {
  width: 100%;
  height: 100%;
  padding: 0px;
  margin: 0px;
}
#demoMap{
  position: fixed;
  width: 100%;
  height: 100%;
}
#message{
  position: absolute;
  top: 10px;
  font-size: 20px;
  color: red;
  font-weight: bold;
}
</style>
</head>
<body>
  <div>
    <div id="demoMap"></div>
    <div id="message">このメッセージがあってもズーム制御できる</div>
  </div>
</body>
</html>

青字部分が肝になります。初期化時(L.map() 実行時)のオプションで zoomControl: false をつけることでズームコントロールを(一旦)非表示にします。なおもう一つ指定しているオプション dragging: true は地図のスクロール(ドラッグ)ができるようにするオプションで、ここを false にするとスクロールできない地図になります。

このままだとズームコントロールは表示されないのですが、直後の1行でズームコントロールを再び表示しています。そしてこの行では position: 'bottomright' というオプションをつけることで「右下」にズームコントロールを表示させています(デフォルトは 'topleft'):

2020041503

↑こんな感じになって、メッセージと重ならない位置にズームコントロールを移動できました。


(参考)
https://www.wrld3d.com/wrld.js/latest/docs/leaflet/L.Control.zoom/



HTML の <select> 要素は複数の選択肢から1つを選択させるためのパーツです。特にスマホなどでは入力の負担が大きいため、入力内容が長いケースでは選択肢の中から選ぶことができると利用者の負担を軽くすることができて便利です。

ただ必ずしも全てのケースで「選択肢から選ぶ」ことが可能とは限りません。めぼしい選択肢を用意した上で、それでも選択肢以外の答を入力したい/させたい場合、多くのケースでは「その他」という選択肢を用意した上で、「その他」を選んだ場合のみ、詳しい内容を別のテキストフィールドに入力させる、という方法が取られているように感じます。 これはこれで(最低限の目的を達成することができるという意味で)いいのですが、UI/UX の観点では例外的なパーツ処理となり、見た目にもスマートではありません。

HTML を使わないネイティブアプリケーションなどでは「任意文字列も入力可能なセレクトボックス」のようなパーツも用意されていて、これを使うことで選択肢の中から選ぶことも(ダブルクリックするなどして編集状態に切り替えた上で)任意文字列を入力することもできます。これだと見た目もスマートでいいですよね。というわけで、このようなパーツを HTML でも用意できないか挑戦してみました。目標はこのブログエントリのタイトルでもある『ダブルクリックすると <input> になって任意入力可能な <select> 』の実現です。

説明の前に、まずは実際に動くサンプルを使ってみてください。下の <select> から値を選び、"Value" ボタンをクリックすると、選択されている値が alert() で表示されます。<select> 部分をダブルクリックすると編集可能になって任意文字列が入力可能になり、その上で "Value" ボタンをクリックすると、入力されている値が alert() で表示される、というものです:




そんなに特別なことをしているわけではないのですが、以下解説です。まず HTML での該当箇所は以下のようになっています:
<select id="mySelect">
  <option value="12345">12345</option>
  <option value="23456">23456</option>
  <option value="34567">34567</option>
  <option value="45678">45678</option>
  <option value="56789">56789</option>
</select>
<input type="text" id="myInput" style="display:none;" value=""/>

<select>(id = "mySelect")と <input>(id = "myInput")を1つずつ配置しています。ただし <input> には style="display:none;" を指定して非表示にしています。つまりこの時点では <select> のみが表示されています。先にコツを言っておくと、<select> の値は常に <input> に引き継がれるように(この後で)設定しておき、"Value" ボタンクリック時には <select> ではなく <input> の値を取得するようにします。

次に JavaScript の解説です:
$(function(){
  //. select の値が変わった時のハンドリングを定義
  $('#mySelect').change( onSelectChange );
  onSelectChange();

  //. select がダブルクリックされたら select を非表示にした上で、同じ値が入っている input を表示する
  $('#mySelect').dblclick( function( e ){
    $('#mySelect').css( 'display', 'none' );
    $('#myInput').css( 'display', 'block' );
  });
});

//. select の値が変わったら、その値を隠しフィールドに代入しておく
function onSelectChange(){
  var v = $('#mySelect').val();
  $('#myInput').val( v );
}

まず画面ロード時に <select> の値が変わった時の処理を定義しています。ここでは <select> の値が変わるたびに onSelectChange() が実行され、<select> で選択された値が常に <input> の値として反映されるようにしています(またロード直後に1回実行することで <input> を初期化しています)。

また <select> がダブルクリックされた時の処理も記述しています。ダブルクリックされたら <select> を非表示に、<input> を表示状態に切り替えます。<input> には上述の処理によって常に <select> で選択されていた値が入っているので、その値が引き継がれた状態で編集可能なフィールドとして表示されることになります。

加えて、以下のスタイルシートを適用することで <select> と <input> が同じサイズになるように設定し、ダブルクリック時に <select> が自然に <input> に切り替わったように見えるよう調整しています:
#mySelect{
  height: 40px;
  width: 200px;
}
#myInput{
  height: 40px;
  width: 200px;
}

これでダブルクリックすると <input> に切り替わる <select> を作ることができました。 あとは "Value" ボタンですが、これは onClick 属性に以下のような関数を定義して、<input> に設定された値を取り出すようにしています:
<input type="button" class="btn btn-primary" value="Value" onClick="getValue();"/>
//. 現在の値を取り出す
function getValue(){
  //. select ではなく input に入っている値を取り出す
  var v = $('#myInput').val();
  alert( v );
}

実際のサンプルコードは github で公開しているので、こちらも参照ください:
https://github.com/dotnsf/select2input

 

このページのトップヘ