律令国(日本の旧国)国境と城の位置を自由に表示したくなったのでやっていきます
プロジェクトの作成
何でもいいのですが何となくVue.jsとTypeScriptを使っていきます
vueは^3.5.18
npm create vue@latest
npm installしてついでにgitコミット
cd shiro-map
npm install
git init && git add -A && git commit -m "initial commit"
実行
npm run dev
http://localhost:5173/にアクセスして動作確認
地図を表示する
地図描画ライブラリのLeafletをインストールします
バージョンは ^1.9.4
npm install leaflet
App.vueを編集
divタグを配置してそこに地図を表示する
地図描画処理はLeafletProc.tsに書くことにしました
App.vue
<script setup lang="ts">
import "leaflet/dist/leaflet.css";
import { onMounted } from 'vue';
import { drawMap } from './LeafletProc';
onMounted( async () => { await drawMap("chizu"); });
</script>
<template>
<div id="chizu"></div>
</template>
<style scoped>
#chizu {
height: 500px;
width: 500px;
}
</style>
LeafletProc.ts
import L from 'leaflet';
export const drawMap = async (elementId: string) => {
const leafletMap = await createMap(elementId);
};
const createMap = async (elementId: string): L.Map => {
// L.Mapオブジェクト生成
const leafletMap = L.map(elementId, {
center: [35.8, 140.2], // 地図の中心座標
zoom: 8, // ズーム
});
// 地図の読み込み
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { // OpenStreetMapのオープンデータ
attribution: "© OpenStreetMap contributors", //地図のデータ提供元を明示するためのクレジット表記
}).addTo(leafletMap);
return leafletMap;
};
国境を描画する
地理データ(GeoJSON)を取ってきて描画します
下総、上総、安房を試しにやってみます
// LeafletProc.ts
import L from 'leaflet';
+ import { countries } from './countries-def';
export const drawMap = async (elementId: string) => {
const leafletMap = await createMap(elementId);
+ // 各国の国境を描画
+ countries.forEach(async (n) => {
+ const layer = await drawCountry(leafletMap, n.source);
+ n.layer = layer;
+ leafletMap.fitBounds(layer.getBounds(), { padding: [20, 20] });
+ });
+ // レイヤ切替設定
+ const config = countries.reduce((acc, cur) => {
+ acc[cur.label] = cur.layer;
+ return acc;
+ }, {});
+
+ // collapsed: false でレイヤー切り替えのUIが展開表示される
+ L.control.layers(null, config, { collapsed: false }).addTo(leafletMap);
};
...
+ /** 指定した国の国境を描画する */
+ const drawCountry = async (leafletMap: L.Map, url: string): Promise<L.GeoJSON> => {
+ const res = await fetch(url);
+ const geo = await res.json();
+ return L.geoJSON(geo).addTo(leafletMap);
+ }
国オブジェクトの定義情報だけ書いておく
countries-def.ts
import { Country } from "./types/country";
export const countries: Country[] = [
{
source: "https://geoshape.ex.nii.ac.jp/kg/geojson/K19.geojson",
label: "下総",
layer: null,
},
{
source: "https://geoshape.ex.nii.ac.jp/kg/geojson/K18.geojson",
label: "上総",
layer: null,
},
{
source: "https://geoshape.ex.nii.ac.jp/kg/geojson/K17.geojson",
label: "安房",
layer: null,
},
];
/src/types/country.d.ts
export interface Country {
source: string;
label: string;
layer: L.GeoJSON | null;
}
城マーカーを表示する
//LeafletProc.ts
import L from 'leaflet';
import { countries } from './countries-def';
+ import { castles } from './castles-def';
+ import { Castle } from './types/castle';
export const drawMap = async (elementId: string) => {
const leafletMap = await createMap(elementId);
// 各国の国境を描画
countries.forEach(async (n) => {
const layer = await drawCountry(leafletMap, n.source);
n.layer = layer;
leafletMap.fitBounds(layer.getBounds(), { padding: [20, 20] });
});
+ // 城マーカーを設定
+ const castleLayer = L.featureGroup().addTo(leafletMap);
+ castles.forEach( n => {
+ drawCastleMarker(castleLayer, n);
+ });
+ config["城"] = castleLayer;
// レイヤ切替設定
const config = countries.reduce((acc, cur) => {
acc[cur.label] = cur.layer;
return acc;
}, {});
L.control.layers(null, config, { collapsed: false }).addTo(leafletMap);
};
const createMap = async (elementId: string): Promise<L.Map> => {
...
// 地図の読み込み
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { // OpenStreetMapのオープンデータ
//地図のデータ提供元を明示するためのクレジット表記
- attribution: "© OpenStreetMap contributors"
+ attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap contributors</a> | ' +
+ '© <a href="https://geoshape.ex.nii.ac.jp/">Geoshape Repository</a>, NII (CC BY 4.0)'
}).addTo(leafletMap);
return leafletMap;
};
+ const drawCastleMarker = (castleLayer, c: Castle) => {
+ const p = L.marker([parseDMS(c.latDms), parseDMS(c.lonDms)]);
+ p.bindPopup(
+ `<b>${c.name}</b>`, {
+ autoClose: false, // 複数ポップアップできるようにする
+ closeOnClick: false, // 複数ポップアップできるようにする
+ }
+ );
+ castleLayer.addLayer(p);
+ }
+ /** DMS形式の文字列を小数度に変換する */
+ const parseDMS = (dmsStr:string): number => {
+ // 方位をチェック
+ const isSouth = dmsStr.includes("南緯");
+ const isWest = dmsStr.includes("西経");
+
+ // 数値部分を抽出
+ const regex = /(\d+)度(\d+)分([\d.]+)秒/;
+ const match = dmsStr.match(regex);
+
+ if (!match) {
+ throw new Error("Invalid DMS string: " + dmsStr);
+ }
+
+ const degrees = parseFloat(match[1]);
+ const minutes = parseFloat(match[2]);
+ const seconds = parseFloat(match[3]);
+
+ // 小数度に変換
+ let decimal = degrees + minutes / 60 + seconds / 3600;
+
+ // 南緯・西経なら負の値に
+ if (isSouth || isWest) {
+ decimal *= -1;
+ }
+
+ return decimal;
+ }
城情報の定義
castles-def.ts
import { Castle } from "./types/castle";
export const castles: Castle[] = [
{
name: "小弓城",
latDms: "北緯35度33分16.0秒",
lonDms: "東経140度09分03.0秒",
},
{
name: "久留里城",
latDms: "北緯35度17分15.2秒",
lonDms: "東経140度5分24秒",
},
{
name: "岡本城",
latDms: "北緯35度03分00.2秒",
lonDms: "東経139度50分07.1秒",
},
];
/src/types/castle.d.ts
export interface Castle {
name: string;
latDms: string;
lonDms: string;
}
Wikipediaの緯度経度は度分秒表示(DMS、文字列)で、
Leafletに渡す緯度経度は十進度(Decimal Degrees、数値型)なので変換しています
GeoJsonのデータ元はGeoshapeリポジトリの旧国一覧から
開発ツール>NetworkからGeoJsonのURLが見れる