はじめ
みなさんこんにちは。
Qiita Engineer Festa 2024で19記事か38記事投稿した人全員にプレゼントがもらえるとかなんとからしいので、せめて19記事には到達したと思い、全力で記事を書いております。
そんな祭りがなくても自分から記事書けやとどこかから言われそうですが…
前回したこと
前回⑤ではなにをしましたっけ。
~地震を選択可能にしよう~ なので表示したい地震情報をリストから選択して地図描画できるようになったんですよね。
今回すること
今回⑥では、内部で情報を更新できるようにしましょう。
現状のままだと、情報を更新するには、ブラウザでのリロード、再読み込みが必要です。
このままだと、アホなブラウザは地図データからフォントデータからと、重いデータをすべてネットから引っ張ってきてしまいます。(もうそんなアホはないでしょうが。)
更新したいのは地震情報のJSONだけですので、リロードしなくてもJSONをもう一度持ってきたいわけです。
開発スタート
前回⑤までのコードは
に貼ってありますので使用ください。
①更新ボタンの設置
まずは更新ボタンを設置していきましょう。
更新と同時にもっと多くの件数の地震情報を取得したいよーという声も出できそうで出てきていないので、横に取得件数を調整できるテキストボックスも設置しておきましょう。
ソースコード
<body>
<div id="map"></div>
<div class="btns">
<select id="quakelist"><option>地震情報の取得中…</option></select>
+ <span class="setsumei" style="padding: 0;padding-right: 5px;"><button id="reload">情報更新</button> <input type="number" id="reload_num" value="20" max="100">件</span>
</div>
<script src="index.js"></script>
</body>
CSSはかなりの量を追加したのでそのままコピペしてください。
追加箇所は#reload, #autoreload
から.setsumei
です。
html, body, #map {
width: 100%;
height: 100%;
margin: 0;
}
#map {
background: #1d1d1d;
}
.btns {
position: absolute;
bottom: 10px;
left: 10px;
z-index: 10000;
user-select: none;
}
.setsumei, #quakelist, #reload, #map_ichi, #test, #btn_shindo_ichiran, #autoreload, #display_onoff_point_check, #view_info {
display: inline-block;
background: #00000088;
border: white 2.5px solid;
border-radius: 5px;
color: white;
padding: 5px;
font-family: "ヒラギノ角ゴ-Pro",'Noto Sans JP';
font-weight: 500;
font-size: 0.8rem;
cursor: pointer;
}
#reload, #autoreload {
background: #00000000;
padding: 5px;
border: none;
}
#reload_num, #autoreload_num {
background: #00000000;
border: 0;
width: 2rem;
font-family: "ヒラギノ角ゴ-Pro",'Noto Sans JP';
font-weight: 500;
font-size: 0.9rem;
color: white;
border-bottom: #ffffff 1px solid;
}
select, input, button {
outline: none;
}
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type="number"] {
-moz-appearance:textfield;
}
.setsumei {
cursor: default;
}
#quakelist {
width: 25em;
height: calc(2em + 5px);
}
.leaflet-fade-anim .leaflet-popup {
transition: 0s;
}
.leaflet-popup-content-wrapper {
background-color: rgba(255, 255, 255, 0.85);
box-shadow: 0px 3px 7px 2px rgba(0, 0, 0, 0.4);
border-radius: 0!important;
font-family: "ヒラギノ角ゴ-Pro",'Noto Sans JP';
font-weight: 500;
user-select: none;
margin-bottom: 0;
}
.leaflet-popup-content {
color: black !important;
font-size: 1.2rem;
margin: 10px 8px 8px 8px;
}
.leaflet-popup-tip-container {
display: none;
}
②情報更新させる
続いて、情報更新をさせるようにJavaScriptを書いていきましょう。
ソースコード
}).addTo(map);
});
+//ボタン押下時のイベント設定とローカルストレージの設定
+document.getElementById('reload').addEventListener("click",()=>{
+ if (document.getElementById('reload_num').value != "") {
+ if (document.getElementById('reload_num').value > 100 || document.getElementById('reload_num').value <= 0) {
+ GetQuake(100);
+ } else {
+ GetQuake(document.getElementById('reload_num').value);
+ }
+ } else {
+ GetQuake();
+ }
+ document.getElementById('reload').innerText = "更新中…";
+ setTimeout(() => {
+ document.getElementById('reload').innerText = "更新完了";
+ setTimeout(() => {
+ document.getElementById('reload').innerText = "情報更新";
+ }, 1000);
+ }, 1000);
+});
+function GetQuake(option) {
+ var url;
+ if (!isNaN(option)) {
+ url = "https://api.p2pquake.net/v2/history?codes=551&limit="+option;
+ } else {
+ url = "https://api.p2pquake.net/v2/history?codes=551&limit=20";
+ }
+ $.getJSON(url, function (data) {
while (document.getElementById('quakelist').lastChild) {
document.getElementById('quakelist').removeChild(document.getElementById('quakelist').lastChild);
}
var forEachNum = 0;
data.forEach(element => {
var option = document.createElement("option");
var text;
let maxInt_data = element['earthquake']['maxScale'];
let maxIntText = hantei_maxIntText(maxInt_data);
let Name = hantei_Name(element['earthquake']['hypocenter']['name']);
let Time = element['earthquake']['time'];
if (element["issue"]["type"] == "ScalePrompt") {
text = "【震度速報】" + element["points"][0]["addr"] + "など " + "\n" + Time.slice(0, -3) + "\n最大震度 : " + maxIntText;
} else if (element["issue"]["type"] == "Foreign") {
text = "【遠地地震】" + Time.slice(0, -3) + " " + Name;
} else {
text = Time.slice(0, -3) + " " + Name + " " + "\n" + "\n最大震度 : " + maxIntText;
}
option.value = "" + forEachNum + "";
option.textContent = text;
document.getElementById('quakelist').appendChild(option);
forEachNum++;
});
//地震情報リストをクリックしたときの発火イベント
var list = document.getElementById('quakelist');
list.onchange = event => {
QuakeSelect(list.selectedIndex);
}
var shingenIcon;
function QuakeSelect(num) {
if (shingenIcon) {
map.removeLayer(shingenIcon);
}
let maxInt_data = data[num]['earthquake']['maxScale'];
var maxIntText = hantei_maxIntText(maxInt_data);
var Magnitude = hantei_Magnitude(data[num]['earthquake']['hypocenter']['magnitude']);
var Name = hantei_Name(data[num]['earthquake']['hypocenter']['name']);
var Depth = hantei_Depth(data[num]['earthquake']['hypocenter']['depth']);
var tsunamiText = hantei_tsunamiText(data[num]['earthquake']['domesticTsunami']);
var Time = data[num]['earthquake']['time'];
var shingenLatLng = new L.LatLng(data[num]["earthquake"]["hypocenter"]["latitude"], data[num]["earthquake"]["hypocenter"]["longitude"]);
var shingenIconImage = L.icon({
iconUrl: 'source/shingen.png',
iconSize: [40, 40],
iconAnchor: [20, 20],
popupAnchor: [0, -40]
});
shingenIcon = L.marker(shingenLatLng, {icon: shingenIconImage }).addTo(map);
shingenIcon.bindPopup('発生時刻:'+Time+'<br>最大震度:'+maxIntText+'<br>震源地:'+Name+'<span style=\"font-size: 85%;\"> ('+data[num]["earthquake"]["hypocenter"]["latitude"]+", "+data[num]["earthquake"]["hypocenter"]["longitude"]+')</span><br>規模:M'+Magnitude+' 深さ:'+Depth+'<br>受信:'+data[num]['issue']['time']+', '+data[num]['issue']['source'],{closeButton: false, zIndexOffset: 10000, maxWidth: 10000});
shingenIcon.on('mouseover', function (e) {this.openPopup();});
shingenIcon.on('mouseout', function (e) {this.closePopup();});
}
});
+}
まず、document.getElementById('reload').addEventListener("click",()=>{}
で、#reload
がクリックされたときに発火するトリガーを設定しています。
この中の処理ですが、P2P地震情報は101件以上の地震情報をリクエストするとエラーが出ますので、100件までという制限をこちらでかけなければ、エラーを返してきます。
よって、その判定処理をしています。
if (document.getElementById('reload_num').value != "")
で、更新件数の内容が空白ではない = なにかが入っているかどうかを判定
その下のif (document.getElementById('reload_num').value > 100 || document.getElementById('reload_num').value <= 0)
でその数値が「100以上」または「0以下」かというのを判定しています。この場合が「正」になる場合はAPIにはリクエストしてはいけないので、無理やりGetQuake(100)
で100件をリクエストしています。
この場合が「誤」である場合はそのままの数字でリクエストしています。GetQuake(document.getElementById('reload_num').value);
setTimeout
のところでは、更新中だよ!とわかるようにボタンの内容を変更しています。
setTimeout(() => {}, 1000);
は{}
の内容を1000ミリ秒以上経過した際に実行するというメソッドです。
この際指定するミリ秒は必ずしも正確な時間が経過してから実行されるわけではありません。処理が重ければ1000ミリ秒を大幅に超えて実行される可能性もあるので、ちょうどに実行させたいときにはsetTimeout
は使えません。
そして、関数の部分ですが、function GetQuake(option) {}
をすることで、GetQuake()
を実行すると関数を実行させられるようにしています。
また、option
があるかないかどうかを判定してURlを変更させ、$getJSON()
でJSONを取得しています。
これで情報更新の動作は追加できました。
ここからは少し改修をしていきましょう。
③震源位置に飛べるようにする
次は、改修として、震源の位置に飛べるようにいしておきましょう。
…
shingenIcon.on('mouseover', function (e) {this.openPopup();});
shingenIcon.on('mouseout', function (e) {this.closePopup();});
+map.flyTo(shingenLatLng, 7.5, { duration: 0.5 })
map.flyTo(LatLng, Zoom, {duration:秒})
で、マップのLatLng
の位置にズーム倍率Zoom
で、duration
秒でマップを飛ばすことができます。
こんかい震源の位置にマップを飛ばしたいので、shingenLatLng
としています。
④関数の位置を整理する
現在のGetQuake()
とQuakeSelect()
の位置関係はこうなっています。
この画像から分かる通り、QuakeSelect()
はGetQuake()
の外部からアクセスできません。
これはだめなことです。
なぜだめかというと、将来ユーザー操作など、GetQuake()
の外からQuakeSelect()
を操作しなければいけなくなった際、依存関係などの問題が多量に発生してしまうかもしれないからです。
よって、
こうしたいわけです。
しかし、JSONの絵があるように、そのまま出すだけではJSONを参照できなくなってしまいます。
よって、
//一番初め
+var QuakeJson;
function GetQuake(option) {
…
$.getJSON(url, function (data) {
+ QuakeJson = data;
…
});
}
GetQuake()
の外にQuakeJson
などという、取得したJSONの中身をいれる変数を作っておき、取得した段階で変数の中へ代入するようにしておきます。
これでGetQuake()
の外でも、JSONの中身を使うことができます。
そして、
function GetQuake(option) {
…
$.getJSON(url, function (data) {
QuakeJson = data;
…
- var shingenIcon;
- function QuakeSelect(num) {
- …
- }
});
}
+var shingenIcon;
+function QuakeSelect(num) {
+ …
+}
QuakeSelect()
をGetQuake()
の外へ持ってくるわけですが、QuakeSelect()
の中身はGetQuake()
の取得したJSONである、data[num]['earthquake']['maxScale']
などのようにdata
に依存してしまっています。
よって、data
を新たに設置した変数QuakeJson
に変更します。
function QuakeSelect(num) {
if (shingenIcon) {
map.removeLayer(shingenIcon);
}
let maxInt_data = QuakeJson[num]['earthquake']['maxScale'];
var maxIntText = hantei_maxIntText(maxInt_data);
var Magnitude = hantei_Magnitude(QuakeJson[num]['earthquake']['hypocenter']['magnitude']);
var Name = hantei_Name(QuakeJson[num]['earthquake']['hypocenter']['name']);
var Depth = hantei_Depth(QuakeJson[num]['earthquake']['hypocenter']['depth']);
var tsunamiText = hantei_tsunamiText(QuakeJson[num]['earthquake']['domesticTsunami']);
var Time = QuakeJson[num]['earthquake']['time'];
var shingenLatLng = new L.LatLng(QuakeJson[num]["earthquake"]["hypocenter"]["latitude"], QuakeJson[num]["earthquake"]["hypocenter"]["longitude"]);
var shingenIconImage = L.icon({
iconUrl: 'source/shingen.png',
iconSize: [40, 40],
iconAnchor: [20, 20],
popupAnchor: [0, -40]
});
shingenIcon = L.marker(shingenLatLng, {icon: shingenIconImage }).addTo(map);
shingenIcon.bindPopup('発生時刻:'+Time+'<br>最大震度:'+maxIntText+'<br>震源地:'+Name+'<span style=\"font-size: 85%;\"> ('+QuakeJson[num]["earthquake"]["hypocenter"]["latitude"]+", "+QuakeJson[num]["earthquake"]["hypocenter"]["longitude"]+')</span><br>規模:M'+Magnitude+' 深さ:'+Depth+'<br>受信:'+QuakeJson[num]['issue']['time']+', '+QuakeJson[num]['issue']['source'],{closeButton: false, zIndexOffset: 10000, maxWidth: 10000});
shingenIcon.on('mouseover', function (e) {this.openPopup();});
shingenIcon.on('mouseout', function (e) {this.closePopup();});
map.flyTo(shingenLatLng, 7.5, { duration: 0.5 })
}
とすればOKです。
⑤読み込み時に自動で実行できるようにする
今のままでは、読み込んでも何も起こりません。
これでは動作しないのか、と思われるかもしれませんので、JSONの取得が終わった後に関数を自動実行するようにしたいと思います。
「JSONの取得が終わった後に関数を自動実行」と聞くと、難しそうに聞こえますが、簡単です。
+GetQuake();
function GetQuake(option) {
…
}
まずは、JSONを取得しましょう。
そうですね。「JSONの取得が終わった後に」とか言ってましたが、まずJSONも取得できていませんでしたね。
次にJSONの取得が終わった後に自動で地震を選択できるようにします。
まあとりあえず一番最近の地震を見とけばいいでしょう。
function GetQuake(option) {
…
$.getJSON(url, function (data) {
…
+ QuakeSelect(0);
});
}
これで、JSONを取得し、あれやこれやごにゃごにゃしたあとのJSONを使って、地震選択の関数を動作させることができるようになりました。
終わりに
今回では、情報更新ができるようにしたのと、地図を震源位置に飛ばすなどの改修をしました。
次回からはようやく本題の観測点の描画に舵を切っていきましょう!
今回のサンプル
https://nanka.cloudfree.jp/bin/webapps/shindobunpu_qiita/6/
今回のソースコード
<!DOCTYPE html>
<html lang="ja">
<head>
<title>震度分布図</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
<link rel="stylesheet" href="https://nanka.cloudfree.jp/static/font/fonts.css">
<link rel="stylesheet" href="index.css">
</head>
<body>
<div id="map"></div>
<div class="btns">
<select id="quakelist"><option>地震情報の取得中…</option></select>
<span class="setsumei" style="padding: 0;padding-right: 5px;"><button id="reload">情報更新</button> <input type="number" id="reload_num" value="20" max="100">件</span>
</div>
<script src="index.js"></script>
</body>
</html>
var QuakeJson;
var map = L.map('map').setView([36.575, 137.984], 6);
L.control.scale({ maxWidth: 150, position: 'bottomright', imperial: false }).addTo(map);
map.zoomControl.setPosition('topright');
var PolygonLayer_Style_nerv = {
"color": "#ffffff",
"weight": 1.5,
"opacity": 1,
"fillColor": "#3a3a3a",
"fillOpacity": 1
}
$.getJSON("prefectures.geojson", function (data) {
L.geoJson(data, {
style: PolygonLayer_Style_nerv
}).addTo(map);
});
//ボタン押下時のイベント設定とローカルストレージの設定
document.getElementById('reload').addEventListener("click",()=>{
if (document.getElementById('reload_num').value != "") {
if (document.getElementById('reload_num').value > 100 || document.getElementById('reload_num').value <= 0) {
GetQuake(100);
} else {
GetQuake(document.getElementById('reload_num').value);
}
} else {
GetQuake();
}
document.getElementById('reload').innerText = "更新中…";
setTimeout(() => {
document.getElementById('reload').innerText = "更新完了";
setTimeout(() => {
document.getElementById('reload').innerText = "情報更新";
}, 1000);
}, 1000);
});
GetQuake();
function GetQuake(option) {
var url;
if (!isNaN(option)) {
url = "https://api.p2pquake.net/v2/history?codes=551&limit="+option;
} else {
url = "https://api.p2pquake.net/v2/history?codes=551&limit=20";
}
$.getJSON(url, function (data) {
QuakeJson = data;
while (document.getElementById('quakelist').lastChild) {
document.getElementById('quakelist').removeChild(document.getElementById('quakelist').lastChild);
}
var forEachNum = 0;
data.forEach(element => {
var option = document.createElement("option");
var text;
let maxInt_data = element['earthquake']['maxScale'];
let maxIntText = hantei_maxIntText(maxInt_data);
let Name = hantei_Name(element['earthquake']['hypocenter']['name']);
let Time = element['earthquake']['time'];
if (element["issue"]["type"] == "ScalePrompt") {
text = "【震度速報】" + element["points"][0]["addr"] + "など " + "\n" + Time.slice(0, -3) + "\n最大震度 : " + maxIntText;
} else if (element["issue"]["type"] == "Foreign") {
text = "【遠地地震】" + Time.slice(0, -3) + " " + Name;
} else {
text = Time.slice(0, -3) + " " + Name + " " + "\n" + "\n最大震度 : " + maxIntText;
}
option.value = "" + forEachNum + "";
option.textContent = text;
document.getElementById('quakelist').appendChild(option);
forEachNum++;
});
//地震情報リストをクリックしたときの発火イベント
var list = document.getElementById('quakelist');
list.onchange = event => {
QuakeSelect(list.selectedIndex);
}
QuakeSelect(0);
});
}
var shingenIcon;
function QuakeSelect(num) {
if (shingenIcon) {
map.removeLayer(shingenIcon);
}
let maxInt_data = QuakeJson[num]['earthquake']['maxScale'];
var maxIntText = hantei_maxIntText(maxInt_data);
var Magnitude = hantei_Magnitude(QuakeJson[num]['earthquake']['hypocenter']['magnitude']);
var Name = hantei_Name(QuakeJson[num]['earthquake']['hypocenter']['name']);
var Depth = hantei_Depth(QuakeJson[num]['earthquake']['hypocenter']['depth']);
var tsunamiText = hantei_tsunamiText(QuakeJson[num]['earthquake']['domesticTsunami']);
var Time = QuakeJson[num]['earthquake']['time'];
var shingenLatLng = new L.LatLng(QuakeJson[num]["earthquake"]["hypocenter"]["latitude"], QuakeJson[num]["earthquake"]["hypocenter"]["longitude"]);
var shingenIconImage = L.icon({
iconUrl: 'source/shingen.png',
iconSize: [40, 40],
iconAnchor: [20, 20],
popupAnchor: [0, -40]
});
shingenIcon = L.marker(shingenLatLng, {icon: shingenIconImage }).addTo(map);
shingenIcon.bindPopup('発生時刻:'+Time+'<br>最大震度:'+maxIntText+'<br>震源地:'+Name+'<span style=\"font-size: 85%;\"> ('+QuakeJson[num]["earthquake"]["hypocenter"]["latitude"]+", "+QuakeJson[num]["earthquake"]["hypocenter"]["longitude"]+')</span><br>規模:M'+Magnitude+' 深さ:'+Depth+'<br>受信:'+QuakeJson[num]['issue']['time']+', '+QuakeJson[num]['issue']['source'],{closeButton: false, zIndexOffset: 10000, maxWidth: 10000});
shingenIcon.on('mouseover', function (e) {this.openPopup();});
shingenIcon.on('mouseout', function (e) {this.closePopup();});
map.flyTo(shingenLatLng, 7.5, { duration: 0.5 })
}
function hantei_maxIntText(param) {
let kaerichi = param == 10 ? "1" : param == 20 ? "2" : param == 30 ? "3" : param == 40 ? "4" :
param == 45 ? "5弱" : param == 46 ? "5弱" : param == 50 ? "5強" : param == 55 ? "6弱" :
param == 60 ? "6強" : param == 70 ? "7" : "不明";
return kaerichi;
}
function hantei_Magnitude(param) {
let kaerichi = param != -1 ? param.toFixed(1) : 'ー.ー';
return kaerichi;
}
function hantei_Name(param) {
let kaerichi = param != "" ? param : '情報なし';
return kaerichi;
}
function hantei_Depth(param) {
let kaerichi = param != -1 ? "約"+param+"km" : '不明';
return kaerichi;
}
function hantei_tsunamiText(param) {
let kaerichi = param == "None" ? "なし" :
param == "Unknown" ? "不明" :
param == "Checking" ? "調査中" :
param == "NonEffective" ? "若干の海面変動" :
param == "Watch" ? "津波注意報" :
param == "Warning" ? "津波警報" : "情報なし";
return kaerichi;
}
html, body, #map {
width: 100%;
height: 100%;
margin: 0;
}
#map {
background: #1d1d1d;
}
.btns {
position: absolute;
bottom: 10px;
left: 10px;
z-index: 10000;
user-select: none;
}
.setsumei, #quakelist, #reload, #map_ichi, #test, #btn_shindo_ichiran, #autoreload, #display_onoff_point_check, #view_info {
display: inline-block;
background: #00000088;
border: white 2.5px solid;
border-radius: 5px;
color: white;
padding: 5px;
font-family: "ヒラギノ角ゴ-Pro",'Noto Sans JP';
font-weight: 500;
font-size: 0.8rem;
cursor: pointer;
}
#reload, #autoreload {
background: #00000000;
padding: 5px;
border: none;
}
#reload_num, #autoreload_num {
background: #00000000;
border: 0;
width: 2rem;
font-family: "ヒラギノ角ゴ-Pro",'Noto Sans JP';
font-weight: 500;
font-size: 0.9rem;
color: white;
border-bottom: #ffffff 1px solid;
}
select, input, button {
outline: none;
}
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type="number"] {
-moz-appearance:textfield;
}
.setsumei {
cursor: default;
}
#quakelist {
width: 25em;
height: calc(2em + 5px);
}
.leaflet-fade-anim .leaflet-popup {
transition: 0s;
}
.leaflet-popup-content-wrapper {
background-color: rgba(255, 255, 255, 0.85);
box-shadow: 0px 3px 7px 2px rgba(0, 0, 0, 0.4);
border-radius: 0!important;
font-family: "ヒラギノ角ゴ-Pro",'Noto Sans JP';
font-weight: 500;
user-select: none;
margin-bottom: 0;
}
.leaflet-popup-content {
color: black !important;
font-size: 1.2rem;
margin: 10px 8px 8px 8px;
}
.leaflet-popup-tip-container {
display: none;
}
次回予告
震度分布図を作ろう⑦ ~観測点を描画しよう~