見出し画像

MIERUNEインターン #001 武内樹治さん

MIERUNEでは、弊社への就業に興味がある方向けに、1週間程度のインターンを実施しております。このたび、2023年6月5日から9日までの5日間、武内さんにインターンへご参加いただきました。
本記事は武内さんご自身によるレポートになります。

自己紹介

  • 所属:立命館大学大学院 文学研究科

  • 経歴:愛媛県出身→徳島大学総合科学部→立命館大学大学院

奈良文化財研究所企画調整部文化財情報研究室にて学生アシスタントとして、文化財に関するWebGIS開発の際にMIERUNEという会社があることを知る。その後、文化財総覧WebGISなどでMIERUNEと関わるうちに、興味を持った。2023年3月の自然言語処理学会にて実際に久本さんとお会いし、インターンを実施していることを知り、応募。

研究のなかで分析にRを使ってプログラムを書いたことはあるが、これまでのエンジニアとしての経験はほぼ0。

インターンでやったこと

インターンの5日間でWebGISを作成する
「自身の研究で用いてるオープンデータの可視化や条件検索機能などができるもの」

成果物

以下、日程順に取り組んだことを列挙。

1日目

環境構築

〇各種インストール・準備

  • VS.Code

  • Node.JS

  • GitHub Desktop

  • GitHubアカウント

〇準備

GitHubにてrepository(heiankyo_webgis)作成
GitHubでNew Repository
VS.code Windows PowerShellにてGitHubのレポジトリへ移動。

# 以下を実行
npm create vite@latest
npm install
npm run dev # 開発サーバーを起動


GitHub上でCollaborators、井口さんを追加。

エクスプローラー上で作成されている「heiankyo-webgis」のフォルダごとVS.Codeへドラッグアンドドロップ。以下のようにVS.Codeで読み込まれる(画面左)。
起動している開発サーバーにアクセスすると、アプリケーション画面が表示される(画面右)。

GitHub DesktopからGitHubへpush

〇Vercel環境

作成したものを公開するサービス。

GitHubアカウントを認証

〇修正・更新する

branchを作成(mainを修正する前にmainの複製を作りそれに変更を加える)

Pull Request

Vercelを連携しているので、コードをプッシュしたら自動的にデプロイされるし、Pull Requestを作成したら、プレビューをデプロイしてくれる。

データ用意

〇用いるデータ

発掘調査地点のGISデータ。すべての発掘調査を網羅しているわけではないが、1000件以上の発掘調査データがある。平安京に関連するものであれば、平安京外の発掘調査も登録されている。

属性として、

「内容」:発掘調査成果の要約
「出典」:発掘調査成果がまとめられた発掘調査報告書名
「公開PDF_」:発掘調査報告書へのURLリンク(ウェブでPDF公開されているものに限る)
「西暦年」:発掘調査が行われた西暦年、数値列

などがある。

データの参考

https://ritsumei.repo.nii.ac.jp/?action=repository_uri&item_id=14498

また、発掘調査で出土した遺構や層の時代を示す列を追加

旧石器・縄文、、江戸、明治、大正の13列
「内容」列でそれぞれの時代名が含まれるものがTrueとなるboolean型で作成
そのほか、平安京後の条坊(区画)と調査平面図(Line,1件)も利用する。

〇データをGitHub上に追加する場合
ローカルのマシン上で差分を作る VS.Code上で追加
差分をアップロード GitHubDesktopでcommit
GitHub上で差分を取り込む。Fetch origin

マップを作成する

位置エン本を参考にしながら、MapLibre GL JSをインストール、Mapインスタンスの初期化(index.html)を行い、main.jsの内容を記述していく。

初期画面のzoomレベルや中央の位置座標などを設定し、OSM、平安京のデータを表示する。

//地図画面を初期化
const map = new maplibregl.Map({
  container: "map", // div要素のid
  zoom: 12, // 初期表示のズーム
  center: [135.73, 35], // 初期表示の中心
  minZoom: 8, // 最小ズーム
  maxZoom: 20, // 最大ズーム
  maxBounds: [134, 34, 137, 36], // 表示可能な範囲
  hash: true,
  style: {
    version: 8,
    sprite: "https://demotiles.maplibre.org/styles/osm-bright-gl-style/sprite", // スプライトは使わないけど、legendのエラーを回避するために定義
    glyphs: "https://mierune.github.io/fonts/{fontstack}/{range}.pbf",
    sources: {
      // 背景地図ソース
			//オープンストリートマップ
      osm: {
        type: "raster",
        tiles: ["https://tile.openstreetmap.org/{z}/{x}/{y}.png"],
        maxzoom: 19,
        tileSize: 256,
        attribution:
          '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
      },
			//平安京のポイントデータ
			excavation: {
        type: "geojson",
        data: "/excavation.geojson",
        attribution:
          '<a href="https://heiankyoexcavationdb-rstgis.hub.arcgis.com/">平安京跡データベース</a>',
      },
},
//sourceで読み込んできたデータをどのように表示するか
		layers: [
      // 背景地図レイヤー
      {
        id: "osm-layer",
        source: "osm",
        type: "raster",
      },
			//平安京のポイントデータ
			{
        id: "excavation",
        source: "excavation",
        type: "circle",
        paint: {
          "circle-color": "#90c",
          "circle-stroke-color": "#fff",
          "circle-stroke-width": 1,
        },
},
});
  • ポイントデータからヒートマップを作成

ポイントデータの視覚化として、ヒートマップを作る。また、データの位置情報の精度として同一地点に複数のポイントがあるときがある(元データが位置情報をジオコーディングで付与している点と、かなり近いところを発掘する場合もある)。

→ヒートマップにより、複数のポイントが重なっている地点はそれがわかるようになる。

先ほど、ポイントデータをsourceに追加しているので、それを利用したheatmapレイヤーを作成する。

const map = new maplibregl.Map({
//中略
			//平安京のポイントデータ
			excavation: {
        type: "geojson",
        data: "/excavation.geojson",
        attribution:
          '<a href="https://heiankyoexcavationdb-rstgis.hub.arcgis.com/">平安京跡データベース</a>',
      },
},
		layers: [
			//平安京のポイントデータ
			{
        id: "excavation",
        source: "excavation",
        type: "circle",
        paint: {
          "circle-color": "#90c",
          "circle-stroke-color": "#fff",
          "circle-stroke-width": 1,
        },
				//heatmap
				{
        id: "excavation-heat",
        source: "excavation",
        type: "heatmap",   //ここをheatmapにする
				},
 //中略   
});

なお、ヒートマップは後日さらに加工修正した

  • ポップアップ表示

目的:対象事物の上にカーソルがいくとカーソルが切り替わり、クリックすることで事物の属性値が表示されるようにする

map.on("load", () => {
//中略
	map.on("click", (e) => {
    // クリックした位置にある地物を取得
    const features = map.queryRenderedFeatures(e.point, {
      layers: [
        "excavation",
      ],
    });
    if (features.length === 0) return;

    //ポップアップのところにスクロール機能をつける
    let popupHtml = `<div style="max-height:400px; overflow-y: scroll;">`;
    //ポップアップで表示させるものを5つにする(その準備)
    const MAX_ITEMS = 8;

    //ポップアップするときに事物が複数あるときの処理
    features.forEach((feature, idx) => {
      if (idx + 1 > MAX_ITEMS) return;
      //ポップアップのスタイル
      popupHtml += `<div style="margin:4px 0; padding:2px; background-color:#00000000;">`;
      // 各地物の情報をHTMLに追加する
			//ここでは「内容」「場所」の列と、「公開PDF」(この列には発掘調査報告書のURLがある)を表示
      popupHtml += `<div>${feature.properties["内容"]}</div>
      <div>${feature.properties["場所"]}</div>`;
      if (feature.properties["公開PDF_"] !== undefined) {
        // URLがある場合はaタグを追加する
        popupHtml += `<a href='${feature.properties["公開PDF_"]}'>報告書link</a>`;
      } else {
				//報告書URLがない場合もあるため、そのときは以下を表示
        popupHtml += `報告書PDFなし`;
      }
      //ポップアップの中で事物と事物の間に横線を入れる
      popupHtml += `<hr/>`;
      popupHtml += `</div>`;
    });

    popupHtml += "</div>";
    const popup = new maplibregl.Popup()
      .setLngLat(e.lngLat)
      .setHTML(popupHtml)
      .addTo(map);
  });

  //地図上でマウスが移動した際のイベント
  map.on("mousemove", (e) => {
    //マウスカーソル以下にレイヤーが存在するかどうかをチェック
    const features = map.queryRenderedFeatures(e.point, {
      layers: [
        "excavation-excavation",
      ],
    });

    if (features.length > 0) {
      //地物が存在する場合はカーソルをpointerに変更
      map.getCanvas().style.cursor = "pointer";
    } else {
      //存在しない場合はデフォルト
      map.getCanvas().style.cursor = "";
    }
  });
  //スライダーが変更されたときに、関数を実行
  slider.on("update", () => {
    updateExcavationFilter();
  });
  ageSlider.on("update", () => {
    updateExcavationFilter();
  });
});

1日目に地図上でデータ表示、ヒートマップ、簡単な属性のポップアップまでできた。

2日目

背景地図の切り替え機能

OSMや地理院地図などの背景地図を用意する。

そして、背景地図の切り替え機能(1つのみ選択可能)を実装。

map.on("load", () => {
//中略
  const opacity = new OpacityControl({
    //背景地図の切り替え機能、baseLayerにすることで複数のなかで一つを選択して表示する機能
    //baseLayersをoverLayersにすると、複数選択可能になる。
		baseLayers: {
      "osm-layer": "OpenStreetMap",
      "gsi-photo-layer": "地理院地図 全国最新写真(シームレス)",
      "gsi-gazo1-layer": "地理院地図 1974年-1978年写真",
      "gsi-elevation-layer": "地理院地図 色別標高図",
    },
  });
  map.addControl(opacity, "top-left");
});

スライダーを作成する

条件に応じた絞り込みをしたい。

条件には、時間軸として、①調査を行った年「西暦年」と、②出土した遺構の時代「旧石器」など14列。

参考にしたサイト,ライブラリ

https://refreshless.com/nouislider/pips/

//発掘年(西暦年)スライダーを初期化,id("slider")でindex.htmlの中に探しに行く
const sliderDiv = document.getElementById("slider");
const minYear = 1890;
const maxYear = 2023;
const slider = noUiSlider.create(sliderDiv, {
  start: [minYear, maxYear],
  connect: true,
  range: {
    min: minYear,
    max: maxYear,
  },
  step: 1,
  tooltips: true,
  pips: {
    mode: "positions",
    values: [1900, 1950, 2000],
  },
  format: {
    to: function (value) {
      return value;
    },
    // スライダーの値を表示:整数値に変換
    from: function (value) {
      return Number(value);
    },
  },
});

//遺構時代スライダー
//旧石器、縄文など列名にある14個の時代を定義
const AGES = [
  "旧石器",
  "縄文",
  "弥生",
  "古墳",
  "飛鳥白鳳",
  "奈良",
  "平安",
  "鎌倉",
  "室町",
  "戦国",
  "安土桃山",
  "江戸",
  "明治",
  "大正",
];
//時代スライダーを初期化
//indexで作成したage-slidarを探してくる
const ageSliderDiv = document.getElementById("age-slider");
const ageSlider = noUiSlider.create(ageSliderDiv, {
  start: [0, 13],
  connect: true,
  range: {
    min: 0,
    max: 13,
  },
  step: 1,
  tooltips: true,
  //目盛りを追加
  pips: {
    mode: "steps",
    format: {
      to: function (value) {
        return AGES[value];
      },
      // スライダーの値を表示:整数値に変換
      from: function (value) {
        return Number(value);
      },
    },
  },
  format: {
    to: function (value) {
      return AGES[value];
    },
    // スライダーの値を表示:整数値に変換
    from: function (value) {
      return Number(value);
    },
  },
});
//関数を作成、スライダーが変更されたときに実行される、スライダーに合わせて地図のフィルターを更新
function updateExcavationFilter() {
  //slider.getでスライダーを動かしたときの値を取得
  const min = slider.get()[0];
  const max = slider.get()[1];

  //絞り込み条件を追加していく配列
  const conditions = [];
  //絞り込み条件に含まれているかどうかのflagを宣言
  let flag = false;
  AGES.forEach((age) => {
    if (age === ageSlider.get()[0]) flag = true; // 開始条件に合致
    if (flag) conditions.push(["get", age]);
    if (age === ageSlider.get()[1]) flag = false; // 終了条件に合致
  });

  //条件絞り込みを定義する
  const filter = [
    "all", //ここでand条件を指定
    ["all", [">=", ["get", "西暦年"], min], ["<=", ["get", "西暦年"], max]],
    ["any", ...conditions], //anyでor条件,時代の範囲で一個でもTrueがあれば表示、...は配列を展開(スプレット構文)
  ];

  //地図にfilterで定義した条件をポイントデータとヒートマップに反映
  map.setFilter("excavation", 
    ...filter,
  );

  map.setFilter("excavation-heat", filter);
}

スライダーの目盛り

時代スライダーでは、pipsの引数modeを「steps」にする

(https://refreshless.com/nouislider/pips/)

2日目に属性値で条件検索(発掘した年と出土した遺構の時代)をスライダーで実装、レイヤー表示・非表示チェックボックス実装までできた。

調査年スライダーによる可視化

発掘調査(本発掘調査)について、発掘調査のほとんどが開発のまえに行われるもの→発掘調査は開発が行われた地点で行われる。発掘調査は重要な遺跡ほど重点的に調査する→遺跡のなかでも重要な遺跡ほど発掘調査が多い。

調査年スライダーをもちいて可視化した、1970年代あたりで実施された発掘調査の分布

左京のほうで南北に多いのは、地下鉄線工事によるもの。中央北側(二条城よりすこし北の千本丸太町あたり)に多いのは、ここがかつての平安宮(皇居)であり、重要な遺跡とされているから。

遺構時代スライダーによる可視化

10世紀後半に書かれた随筆では、平安京の右京が廃れた旨の記述がある。

池亭記 - Wikipedia

  • 旧石器から平安時代でフィルタ

  • 平安時代以降でフィルタ

鎌倉以降では右京の遺構が少ないことから、平安時代よりあとの時代には右京での人の営みなどは少なかったのかもしれない。

3日目

heatmapを微調整

MapLibre GL JSのライブラリのヒートマップには様々な設定ができる。

Create a heatmap layer | MapLibre GL JS Docs | MapLibre

色や重みづけなどを設定。

{
        id: "excavation-heat",
        source: "excavation",
        type: "heatmap",
        paint: {
          "heatmap-weight": 1,
          "heatmap-opacity": 0.7,
          //"heatmap-intensity": 3,
          //"heatmap-radius": [
          //"interpolate",
          //以下のところで、zoomレベルに応じてヒートマップを作るときのpxを指定できる、ある程度の距離(例えば500m範囲)などができたりする
          //["linear"],
          //["zoom"],
          //0, // zoom=0
          //0, // 0px
          //10, // zoom=10
          //10, // 30px
          //15, // zoom=15
          //30,
          //],
          "heatmap-color": [
            "interpolate",
            ["linear"],
            ["heatmap-density"],
            0,
            "rgba(0,0,0,1)", //値0でもちょっとした色を出力(0,0,0,0)でもいい。
            0.5,
            "rgba(96, 244, 2, 0.81)",
            1.0,
            "rgba(255, 231, 0, 0.8)",
          ],
        },
        filter: [
          "all", //and条件
          [">=", ["get", "西暦年"], 1890],
          ["<=", ["get", "西暦年"], 2023],
        ],
        layout: { visibility: "none" },
      },

属性値で色分け・凡例表示

目的:発掘調査地点を属性値にある調査機関別に色分けして表示する・凡例に表示する

参考にしたサイト:

https://maplibre.org/maplibre-style-spec/
https://github.com/watergis/mapbox-gl-legend

調査機関を分類する。調査機関は「a調査機」列で、ここでは4つの主要調査機関と、その他という5つに分類する

//定義
const expressionDict = {
  //4つの値でなければ、その他(-other)に分類
  "excavation-other": [
    "all",
    ["!=", ["get", "a調査機"], "企業"],
    ["!=", ["get", "a調査機"], "京都市埋蔵文化財研究所"],
    ["!=", ["get", "a調査機"], "古代学協会"],
    ["!=", ["get", "a調査機"], "京都府埋蔵文化財調査研究センター"],
  ],
  //それぞれの属性値で分類
  "excavation-company": ["==", ["get", "a調査機"], "企業"],
  "excavation-shimaibun": ["==", ["get", "a調査機"], "京都市埋蔵文化財研究所"],
  "excavation-kodaigaku": ["==", ["get", "a調査機"], "古代学協会"],
  "excavation-humaibun": [
    "==",
    ["get", "a調査機"],
    "京都府埋蔵文化財調査研究センター",
  ],
};
const map = new maplibregl.Map({
//中略
  sprite: "https://demotiles.maplibre.org/styles/osm-bright-gl-style/sprite", // スプライトは使わないけど、legendのエラーを回避するために定義
	sources: {
		//excavaiton用の箱だけ用意する、のちにテキスト検索をするため、geojsonのままではできない
      excavation: {
        type: "geojson",
        data: {
          type: "FeatureCollection",
          features: [],
        },
        attribution:
          '<a href="https://heiankyoexcavationdb-rstgis.hub.arcgis.com/">平安京跡データベース</a>',
      },
//中略
	layers: [
//以下、legendで表示・非表示まで実装できるようにlegendで分類する分、excavationを分類して入れる。
      {
        id: "excavation-other",
        source: "excavation",
        type: "circle",
        paint: {
          "circle-color": "#90c",
          "circle-stroke-color": "#fff",
          "circle-stroke-width": 1,
        },
        filter: [
          "all",
          [">=", ["get", "西暦年"], 1890],
          ["<=", ["get", "西暦年"], 2023],
          expressionDict["excavation-other"],
        ],
        layout: {
          visibility: "visible",
        },
        //以下、分類した分だけ、expressionDictを変えつつLayersに入れる。
      },
		],
});

上記で、ポイントが色分け表示される。次に凡例を表示する。

map.on("load", () => {
  //中略
  const targets = {
    "excavation-other": "調査地点(その他)",
    "excavation-company": "調査地点(企業)",
    "excavation-shimaibun": "調査地点(京都市埋蔵文化財研究所)",
    "excavation-kodaigaku": "調査地点(古代学協会)",
    "excavation-humaibun": "調査地点(京都府埋蔵文化財調査研究センター)",
//heatmapuなどのレイヤーも加える
  };

  //legendを追加する
  map.addControl(
    new MaplibreLegendControl(targets, {
      showDefault: false,//falseで初期の画面では隠す。
      onlyRendered: false,
    }),
    "top-left"
  );

上記のコードで、画面にlegend(凡例)が追加される。「Only rendered」にチェックをいれることで画面上に表示されている事物の凡例のみが「Legend」のバーに表示されるようになる。

4日目

テキスト検索機能実装

目的:発掘調査地点pointの属性値に対してテキスト検索を実装する

テキスト検索に便利なツールFuse.jsを使っていく。

Live Demo | Fuse.js (fusejs.io)

  • index.html

テキスト検索に必要な箱(div, input)を作成

inputのところがメイン、placeholderの値で、テキスト検索のボックスに最初に表示される文字を指定することができる。

  • main.js

//jsonデータをJavaScriptのデータとして読み込む、geojsonではうまくいかないのでjsonで処理
import excavationGeojson from "./public/excavationpoint.json";
import Fuse from "fuse.js";
const map = new maplibregl.Map({
//中略
},
layers: [
//excavaiton用の箱だけ用意する、のちにテキスト検索をするため、geojsonのままではできない
      excavation: {
        type: "geojson",
        data: {
          type: "FeatureCollection",
          features: [],
        },
        attribution:
          '<a href="https://heiankyoexcavationdb-rstgis.hub.arcgis.com/">平安京跡データベース</a>',
      },

optionsに発掘調査地点pointの属性のなかで、テキスト検索の対象となる属性を指定。ここでは内容、場所、書名の3つの属性で行う。

テキスト検索の欄に何も入力していないときに地図に発掘調査地点pointがすべて表示されなくなる現象を回避するため、if文でテキスト検索部分での入力値(e.target.value)がない場合は、すべてを返すように設定。

fuse.searchで検索を実行する。結果(result)は配列のため、geojsonに変換できるように、pushで要素を追加。

実際の画面での検索実行結果(検索キーワード:井戸)

5日目

スマホなどの画面でもある程度見栄えをよくする

目的:画面サイズが小さいと、検索バーやタイトルが被ってしまうので、画面サイズにある程度合わせて配置を変える

参考にしたサイト

【CSS】メディアクエリ(@media)とは何か?使い方を実例で解説|.cssファイルやlinkタグでの画面幅以上・以下の指定(初心者向け、わかりやすい) (prograshi.com)

#wordsearch {
  position: absolute;
  top: 7px;
  left: 90px;
}

@media screen and (max-width: 550px) {
  #wordsearch {
    position: absolute;
    top: 30px;
    right: 5px;
  }
}

man-widthの値以下の画面サイズになると、テキスト検索バーの位置が変わるようになる。

画像は横幅を狭くした場合の画面。

以上が5日間で取り組んだこと。

感想

札幌市電を貸し切って開催した社内イベントでの武内さん
  • フロントエンドに関することをやったことがなかったが、5日間である程度形にできたことは純粋に嬉しく・楽しかった。インターン中はメンターの井口さんにかなり、かなりWebGIS作成を懇切丁寧にサポートいただいて、私の知識・技術の能力が5日間とは思えないほどレベルアップできた。

  • ある程度位置エン本を読んできたので、実際のWebGIS作成も大きく躓くことなく作成することができたのではと思う。

  • 初めての北海道で、市電に乗れてさらにジンギスカンまで食べることができて北海道を満喫できた。そのようなオフィス以外の空間でも社員の皆さんとお話できてよかったです!(あと、北海道埋蔵文化財センターで動物型土製品も見ることができた)

5日間、MIERUNEの皆様に大変お世話になりました。

ありがとうございました!

武内さんのメンターをさせていただきましたCTOの@kanahiro_iguchiです。提案していただいた素敵なデータとユニークなアイデアをもとに、インターンにとどまらない実用性のあるアプリケーションになったと思います。ぜひ今後も機能を追加してみたり、別のデータを使って新たなアプリケーションを作ってみてください。
この期間が弊社の技術や雰囲気を理解する一助になれば幸いです。今回はご参加いただきありがとうございました!


この記事が気に入ったらサポートをしてみませんか?