JavaScriptの部屋
週刊アスキー連載のプログラム紹介です。自分で誌面のプログラムを打ち込んでHTMLファイルを作ります。そのHTMLをブラウザで開いて動かすことができます。
打ち込みに時間がかかりますが、打ち込みながらJavaScriptやCSSなどが学べます。
ジャパンデーターサーチや国土地理院のAPIを使った二つのプログラムを紹介します。
著作権は週刊アスキーにあります。
文化財になっている建造物を検索、表示する
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>文化財建造物マップ</title>
<script>
let canvas, context;// キャンバス
let [lat, lng] = [35.6581, 139.74135];// 基準点の緯度、経度
let tx, ty, z, qz = 0;// 中心のタイル座標、ズームレベル
let points = new Array();// 建造物の緯度、経度、マーク
// 国土地理院 API、ジャパンサーチ API
const gsi_url = "https://cyberjapandata.gsi.go.jp/xyz"; //国土地理院タイルのURL
const jpsearch_url = "https://jpsearch.go.jp/api/item/search/jps-cross";//ジャパンサーチ
const param = "?f-type=architecture&f-cm=cultural";
const init = () => {
// IMG 要素、CANVAS 要素の作成
for (let x = 0; x < 3; x++) { //地図画像用3×3
for (let y = 0; y < 3; y++) {
const img = document.createElement("img");//画像を作成
img.id = `map_${x}_${y}`;
img.style.top = `${256 * y}px`;//表示座標をCSSでセット
img.style.left = `${256 * x}px`;
document.getElementById("map").appendChild(img);//地図エリア(157行)に挿入
}
}
canvas = document.createElement("canvas");//キャンバスを作成(円マーク表示用)
[canvas.width, canvas.height] = [256 * 3, 256 * 3];//キャンバスサイズ768
context = canvas.getContext("2d");
document.getElementById("map").appendChild(canvas);
// ズームイン/アウト
document.getElementById("map").addEventListener("wheel", event => {//マウスホール回転のとき
event.preventDefault();//画面のスクロールをキャンセル
if (event.deltaY < 0) document.getElementById("z").value++;//ズームスライダーを増減させる
if (event.deltaY > 0) document.getElementById("z").value--;
showMap();
});
// 基準点をセット
document.getElementById("map").addEventListener("click", event => {//地図をクリックしたとき
const x = (tx - 1 + event.offsetX / 256) / 2 ** z;//クリック場所のズームに応じた緯度経度を計算
const y = (ty - 1 + event.offsetY / 256) / 2 ** z;
lng = 360 * x - 180;
lat = 2 * Math.atan(Math.exp(Math.PI - 2 * Math.PI * y)) * 180 / Math.PI - 90;
qz = 0;
showMap();
});
showMap();//地図やマークを描画更新
}
const showMap = () => {
// 地図タイルを表示
z = document.getElementById("z").value;//ズームスライダーの値を取得
const [x, y] = getPosition(lat, lng);//緯度経度を座標に変換
[tx, ty] = [Math.floor(x / 256), Math.floor(y / 256)];//中心タイルの座標
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
const img = document.getElementById(`map_${i}_${j}`);//現在の地図画像を取得
img.src = `${gsi_url}/std/${z}/${tx + i - 1}/${ty + j - 1}.png`;//新しいマップを国土地理院から取得してセット
}
}
// マークの描画
context.clearRect(0, 0, canvas.width, canvas.height);
const [cx, cy] = [256 + x % 256, 256 + y % 256];//マークの中心座標
drawMark(cx, cy, 10, "#0000FF");//半径10 青色で円を描画
if (qz > 0) drawMark(cx, cy, 256 * 2 ** (z - qz), "#0000FF", false);//検索範囲の円を描画
points.sort((a, b) => a.mark - b.mark);//赤い円が上になるよう見つかった建造物を並べ替える
points.forEach(p => {
let [x, y] = getPosition(p.lat, p.lng);//建造物の座標を計算
x = (Math.floor(x / 256) - tx + 1) * 256 + x % 256;
y = (Math.floor(y / 256) - ty + 1) * 256 + y % 256;
let color = "#A000FF";//詳細表示中なら赤
if (p.mark) color = "#FF0000";//詳細表示中でないなら紫
drawMark(x, y, 20, color);//半径20の円を描画
});
}
const getPosition = (lat, lng) => {//緯度:lat 経度:lng
// 緯度経度を座標に変換
const r = 256 / (Math.PI * 2);//ズーム時の地球の半径
const [rLat, rLng] = [lat * Math.PI / 180, lng * Math.PI / 180];//緯度経度をラジアンに変換
const x = (256 / 2 + r * rLng) * 2 ** z;//緯度経度をxy座標にしてズームに応じて拡大
const y = (256 / 2 - r * Math.log(Math.tan(Math.PI / 4 + rLat / 2))) * 2 ** z;
return [x, y];
};
const drawMark = (x, y, r, color, fill = true) => {
// マークの描画
context.strokeStyle = color;//描画線の色
context.fillStyle = "rgba(100, 100, 255, 0.3)";//塗りつぶし色 半透明
context.lineWidth = 5;//線の太さ
context.beginPath();
context.arc(x, y, r, 0, Math.PI * 2);//円(パス)を描く
if (fill) context.fill();//因数fillがtrueなら半透明青で塗りつぶす
context.stroke();
}
const query = async () => {
// 文化財建造物検索
let size = document.getElementById("size").value;//最大件数171行の値を取得
size = Math.min(Math.max(size, 20), 500);//範囲を20から500の間に限定
document.getElementById("size").value = size;
const r = 111 * Math.cos(lat * Math.PI / 180) * 360 / 2 ** z;//検索範囲の半径を計算
const aParam = `&size=${size}&g-coordinates=${lat},${lng},${r}km`;//パラメーターをセット
const response = await fetch(jpsearch_url + param + aParam);//ジャパンサーチからデーター取得
const data = await response.json();//JSONオブジェクトに変換
document.getElementById("list").innerHTML = "";
points = [];
data.list.forEach(item => {
// タイトル、説明文を表示
const building = document.createElement("details");//details要素を取得
const title = document.createElement("summary");//summory要素を取得
title.innerText = item.common.title;//建物名を表示
building.appendChild(title);
const desc = document.createElement("div");//div要素を取得
desc.innerText = item.common.description;//説明があるならセット
if (item. common. description == undefined) desc. innerText = "";
building.appendChild(desc);
document.getElementById("list").appendChild(building);//建造物一覧176行に追加
// 緯度、経度を格納
const pLat = item.common.coordinates.lat;//建造物の緯度経度マークとフラグを配列pointsに追加
const pLng = item.common.coordinates.lon;
const point = { lat: pLat, lng: pLng, mark: false };
points.push(point);
// マークの切替
building.addEventListener("toggle", () => {//details要素の状態が変わったとき
point.mark = building.open;//状態(詳細表示中ならtrue閉じているときfalse)をマークフラグにセット
showMap();
});
});
qz = 2;//検索時のズーム値を保存
showMap();
}
</script>
<style>
#wrapper {display: flex;}
#map {
position: relative;
width: 768px;
height: 768px;
margin-right: 10px;
}
img {
position: absolute;
width: 256px;
height: 256px;
}
canvas {
position: absolute;
border: thin solid #CCCCCC;
}
#list {
width: 250px;
max-height: 768px;
overflow: auto;
}
summary {
color: #FFFFFF;
padding: 1px 2px;
background-color: #336633;
border: thin solid #CCCCC;
}
details div {
font-size: small;
padding: 2px 5px;
background-color: #EEFFEE;
}
</style>
</head>
<body onload="init()">
<p>文化財建造物マップ</p>
ズーム: <input type="range" id="z" value="12" min="5" max="18" onchange="showMap()">
最大件数: <input type="number" id="size" value="20" min="20" max="500" step="10">
<input type="button" value="検索" onclick="query()">
<hr>
<div id="wrapper">
<div id="map"></div>
<div id="list"></div>
</div>
</body>
</html>
我が国にある多様な絵画・版画を鑑賞しよう
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>絵画アーカイブ検索</title>
<link href="https://unpkg.com/gridjs/dist/theme/mermaid.min.css" rel="stylesheet" />
<script src="https://unpkg.com/gridjs/dist/gridjs.umd.js"></script>
<script>
let grid; // グリッド表
const api = "https://jpsearch.go.jp/api/item/search/jps-cross?f-type=paint";
const init = () => {
grid = new gridjs.Grid({
columns: [
{name: "タイトル", sort: true},
{name: "作者", sort: true},
{name: "時代", sort: true},
{name: "画像 URL", hidden: true},
{name: "説明", hidden: true},
{
name: "詳細",
width: "100px",
formatter: (cell, row) => {
return gridjs.h("button", {
onClick: () => showData(row)
}, "開く");
}
}
],
search: true,
pagination: {limit: 5},
resizable: true,
data: [],
language: {
search: {placeholder: "絞り込みキーワード"},
pagination: {
previous: "前",
next: "次",
showing: " ",
to: "~",
of: "件目を表示(全",
results: "件)"
},
loading: "検索中・・・",
noRecordsFound: "検索キーワードを入力してください。",
error: "情報が取得できませんでした。"
}
}).render(document.getElementById("table"));
};
const query = () => {
const keyword = document.getElementById("keyword").value;
let size = document.getElementById("size").value;
size = Math.min(Math.max(size, 20), 500);
document.getElementById("size").value = size;
if (keyword != "") {
const url = `${api}&keyword=${keyword}&size=${size}`;
grid.updateConfig({
server: {
url: url,
then: data => data.list.map(item => [
item.common.title,
arrayToText(item.common.contributor),
arrayToText(item.common.temporal),
item.common.contentsURI,
item.common.description,
null
])
}
}).forceRender();
} else {
alert("検索キーワードを入力してください。");
}
};
const arrayToText = array => {
let text = "";
if (array != undefined) {
if (Array.isArray(array)) {
text = array.join("/");
} else {
text = array;
}
}
return text;
};
const showData = row => {
document.getElementById("title").innerText = row.cells[0].data;
document.getElementById("contributor").innerText = row.cells[1].data;
document.getElementById("temporal").innerText = row.cells[2].data;
document.getElementById("image").innerHTML = "";
if (row.cells[3].data != undefined) {
const img = document.createElement("img");
img.src = row.cells[3].data;
img.onerror = () => {
document.getElementById("image").innerHTML = "";
};
document.getElementById("image").appendChild(img);
}
let desc = "";
if (row.cells[4].data != undefined) {
desc = row.cells[4].data.replaceAll("\n", "<br>");
}
document.getElementById("description").innerHTML = desc;
document.getElementById("dialog").showModal();
document.getElementById("dialog").scrollTo(0, 0);
};
const closeDialog = () => {
document.getElementById("dialog").close();
};
</script>
<style>
dialog {
width: 680px;
max-height: 680px;
}
img {
width: 640px;
height: 640px;
object-fit: contain;
border: thin solid #000000;
background-color: #000000;
}
#contributor, #temporal {font-size: small;}
</style>
</head>
<body onload="init()">
<p>絵画アーカイブ検索</p>
検索キーワード: <input type="text" id="keyword">
最大件数: <input type="number" id="size" value="20" min="20" max="500" step="10">
<input type="button" value="検索" onclick="query()">
<hr>
<div id="table"></div>
<dialog id="dialog">
<div id="title"></div>
<div id="image"></div>
<div id="contributor"></div>
<div id="temporal"></div>
<br>
<div id="description"></div>
<hr>
<input type="button" value="閉じる" onclick="closeDialog()">
</dialog>
</body>
</html>
エディターソフトなどにコピーしてHTMLとして保存します。
ブラウザで開きます。