🗺️

位置エン本のサンプルを TypeScript で行うためのメモ

2024/10/19に公開

位置エン本サンプルコードはすべて JavaScript で記述されている。
TypeScript でこれらのサンプルを写経する際、.js ファイルを .ts に置き換え、型情報を変数や関数の引数に追加するだけでは動かない部分があるため、この記事では動かない部分と対策をまとめた。

4章

サンプル全般

位置エン本のサンプルでは HTML 内に JavaScript を直接記述する形になっている。TypeScript を使う都合上、<script> タグを使って TypeScript を変換した JavaScript ファイルを呼び出す形にする。
使用するライブラリは、MapLibre GL JS に関する部分はバージョンアップで TypeScript 対応が良くなっているため、できるだけ新しいバージョン(v4.7.0)を使用するように心がけた。

Leafletを使うサンプル

GeoJSON ファイルを扱うサンプル全般

L.GeoJSON 型や L.Marker 型はジェネリクスなので、GeoJSON のプロパティの型定義を事前に行い、その型定義をジェネリクスに代入すれば、適切な型定義が得られる。

geojson.d.ts
type n02_21_geojson_prop = {
  ['N02_001']: string,
  ['N02_002']: '1'|'2'|'3'|'4'|'5',
  ['N02_003']: string,
  ['N02_004']: string,
}
L.geoJSON<n02_21_geojson_prop>(json, {
  style: (feature) => {
    // feature には undefined の可能性がある(後述)
    const lineType = feature?.properties.N02_002;
    if (lineType) {
      // lineType の型は '1'|'2'|'3'|'4'|'5' になる
      return {
        weight: weightDict[lineType], // 事業者種別コードから線の太さを得る
        color: colorDict[lineType], // 事業者種別コードから線の色を得る
      }
    }
    return {};
  },
})

bindPopup() 関数全般

  1. bindPopup() の第一引数に関数を代入する場合、代入する関数の第一引数@types/leaflet では L.Layer になっているため、GeoJSON ファイルから文字列を得たい場合、L.Marker などの型アサーションがないとエラーになる
  2. L.Content の型の関係上、代入する関数の戻り値は文字列か HTMLElement のどちらかにする必要がある

06_1_manyfigure

  • レイヤー切り替えコントロールの L.control.layers() の第二引数は、JavaScript だと空配列で問題なかったが、TypeScript では空オブジェクトでないとエラーになる
  • L.getJSON()style オプションに関数を代入する場合、関数の型は StyleFunction<P> になるが、この関数の第一引数の features は Optional になっているため、それを想定したコードにする必要がある(下記参照)
      const polygon = L.geoJSON<a16_15_geojson_prop>(json, {
        style: (feature) => {
          // TypeScript 向け: undefined の可能性を取り除かないとエラーになる
          if (feature) {
            return {
              color: 'red',
              stroke: false,
              // 人口を面積で割った値でポリゴンの濃さを変える
              fillOpacity:
                feature.properties['人口'] /
                feature.properties['面積'] /
                20000,
            }
          }
          return {}
        }
      })

MapLibre GL JS を使うサンプル

サンプル全般

2024/10 時点では不要となった処理
  • 公式の型定義は ESM を想定しているが、サンプルでは UMD 形式のファイルを使用しているため、下記のような型定義がないとグローバル変数で使用できない
    • 5章では Vite で自動変換するため、この型定義は不要となる
declare global {
  const maplibregl: typeof import('maplibre-gl')
}

export type {}

08_2_image_maplibre

位置エン本が発売されたときとは異なり、maplibre.Map() のオプションに customAttribution で直接設定することが出来なくなった。attributionControl に変更して下記のように customAttribution を設定する。

const map = new maplibregl.Map({
  // 省略
  zoom: 9,
  attributionControl: {
    customAttribution:
      '<a href="https://maps.gsi.go.jp/development/ichiran.html">地理院タイル</a>',
  },
  // 省略
})

5章

MapLibre GL JS のバージョン

4章と同様に、MapLibre GL JS v4.7.0 を使用するように心がけた。

MapLibre GL Opacity のバージョン

  • v1.7.0 から TypeScript の型定義が付属しているのでそれ以降のバージョンを使用する

MapGeoJSONFeature 型からの経度・緯度の取り出し

サンプルコードでは、何度か MapGeoJSONFeature 型から経度・緯度を取り出す処理がある。feature.geometoryGeometory 型であるため、そのままだと型定義上では coordinate プロパティが存在せず、エラーになる。Point 型ならば、coordinate プロパティは存在するので型アサーションを行う。

const nearestFeature = features.reduce((minDistFeature: MapGeoJSONFeature, feature) => {
  const dist = distance(
    [longitude, latitude],
    (<Point>feature.geometry).coordinates,
  );
}

ただし、クリックイベントでポップアップをする処理ではこれだけだとエラーになる。
ポップアップ位置の経度・緯度を指定するが、この値は LngLatLikeになっている必要がある。coordinates プロパティの型は、Positionであるがこれは LngLatLike 型に含まれない型なので LngLatLike 型に型アサーションを行う。

const feature = features[0];
const popup = new maplibregl.Popup()
  .setLngLat((<Point>feature.geometry).coordinates as LngLatLike)

経緯度を渡すと最寄りの指定緊急避難場所を返す部分の実装

現在地に最も近い地物を見つける部分の実装

JavaScript の実装では features.reduce() のコールバック関数の第一引数及び、第二引数の型が異なるため、TypeScript にそのまま持ってくると型の不一致でエラーになる。
コールバック関数について、TypeScript では以下の2点を満たすような実装を行う(下記参照)

  1. features.reduce() は第一引数のコールバック関数のみ使用する
  2. コールバック関数で最小距離の初回更新時は minDistFeature.propertiesdist プロパティが存在しないことを条件とする
// 現在地に最も近い地物を見つける
const nearestFeature = features.reduce((minDistFeature: MapGeoJSONFeature, feature) => {
  const dist = distance(
    [longitude, latitude],
    (<Point>feature.geometry).coordinates,
  );
  if (!minDistFeature.properties.hasOwnProperty('dist') || minDistFeature.properties.dist > dist) {
    const nextFeature = feature;
    nextFeature.properties.dist = dist;
    return nextFeature;
  }
  return minDistFeature;
});

現在表示中の指定緊急避難場所レイヤーを特定する実装

指定緊急避難場所レイヤーは "type": "circle" で実装しているので、skhbから始まるlayerを抽出した後は、CircleLayerSpecification 型の配列が返ってくる。

/*
 * TypeScript 向け: 指定緊急避難場所レイヤーは "type": "circle" なので
 * CircleLayerSpecification 型にする
 */
const skhbLayers = style.layers.filter((layer) =>
  // `skhb`から始まるlayerを抽出
  layer.id.startsWith('skhb'),
) as Array<CircleLayerSpecification>;
const visibleSkhbLayers = skhbLayers.filter(
  // 現在表示中のレイヤーを見つける
  (layer) => layer.layout?.visibility === 'visible',
);
return visibleSkhbLayers[0].filter; // 表示中レイヤーのfilter条件を返す

現在地からの最寄りの指定緊急避難場所取得時の実装

2024/07 時点では不要となった処理

変数 currentSkhbLayerFiltergetCurrentSkhbLayerFilter() から値を代入した後の処理で、map.querySourceFeatures() の filter 条件に currentSkhbLayerFilter を使用するが、位置エン本が発売されてしばらくの間は currentSkhbLayerFilter に含まれる、boolean 型の影響でエラーになっていた。そのため下記の実装が必要だった。

// TypeScript 向け: map.querySourceFeatures で使用する filter は
// any[] 型なので、boolean は除外する
if (Array.isArray(currentSkhbLayerFilter) === false) {
  return;
}
// ここまでが必要だった

const features = map.querySourceFeatures('skhb', {
  sourceLayer: 'skhb',
  filter: currentSkhbLayerFilter,
});

2024/07 時点では不要となった。

現在位置と最寄り施設のラインを引く部分の実装

ラインを示す GeoJSON-Feature の代入時の実装

ラインを示す変数 routeFeature に代入する際、@types/geojson ライブラリ内の Feature 型の定義では properties が必須となっている。 そのため、null または {}properties の値に設定する。

const routeFeature: Feature = {
  type: 'Feature',
  geometry: {
    type: 'LineString',
    coordinates: [
      userLocation,
      (<Point>nearestFeature?._geometry).coordinates,
    ],
  },
  // TypeScript 向け: null または {} の properties が存在しないとエラーになる
  properties: null
};

地図の 'route' レイヤーのデータ設定を行うときの実装

'route' レイヤーのデータ設定を行う際、map.getSource('route') で 'route' レイヤーのソースを取得する。getSource() の型定義はジェネリクスになっておりSource 型を拡張した型の値なら問題なく使用できる。Source 型を拡張した GeoJSONSource 型を型定義に代入すれば、戻り値の型は GeoJSONSource 型または undefined 型となる。

map.getSource<GeoJSONSource>('route')?.setData({
  type: 'FeatureCollection',
  features: [],
});
2024/10 時点では不要となった処理

map.getSource('route') の型は、ジェネリクスのない Source または undefined 型だった。そのため、GeoJSONSource 型として型アサーションを行う必要があった。

(<GeoJSONSource>map.getSource('route')).setData({
  type: 'FeatureCollection',
  features: [],
});

その他の実装

geolocate イベントのリスナー関数の型

イベントのリスナー関数の第一引数は、ライブラリに添付している型定義は any 型になっている。e.coords に関しては、GeolocationCoordinates 型の値が入るためそれは設定しておく。

geolocationControl.on('geolocate', (e: {coords: GeolocationCoordinates}) => {
  // 位置情報が更新されるたびに発火・userLocationを更新
  userLocation = [e.coords.longitude, e.coords.latitude];
});

Discussion