💨

GeoTIFFをOpenLayers上で投影変換して表示する

2022/01/22に公開

OpenlayersでGeoTIFFの表示ができますが、リリース時点でサンプルはGeoTIFFの投影座標系のまま表示するものばかりでした。

そのため複数のGeoTIFFを一度に同じ地図上に表示することができなかったのですが、Issueに投影して表示する方法が示されていたので、参考に実装してみました。

<!DOCTYPE HTML>
<html>

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="icon" type="image/x-icon" href="https://github.githubassets.com/favicon.ico">
    <title>sample cog reprojection</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.12.0/css/ol.css" type="text/css">
    <style>
      .map {
        height: 600px;
        width: 100%;
      }
    </style>

    <script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.12.0/build/ol.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/proj4@2.7.5/dist/proj4-src.min.js"></script>
</head>

<body>
    <h1>GeoTIFFをEPSG:3857に投影変換して表示する</h1>

    <div id="map" class="map"></div>
    <script type="text/javascript">
        (async ()=>{
          const map = new ol.Map({
            target: "map",
            layers: [
              new ol.layer.Tile({
                source: new ol.source.OSM()
              }),
            ],
            view: new ol.View({
              center: [0,0],
              zoom: 0,
            }),
          });


          const proj4Def = await fetch("./proj4def.json")
            .then(response => response.json());

          const setLayer = async (
              map,
              source
            ) => {
              /*
                refer to https://github.com/openlayers/openlayers/issues/13197
              */
              const sourceView = await source.getView();
              const sourceProjection = sourceView.projection;
              if (!(sourceProjection instanceof ol.proj.Projection)) {
                return;
              }
              const code = sourceProjection.getCode();

              // console.log(code); // EPSG:32636
              // codeがprojで未定義の場合ベースマップが崩れてしまうので登録する
              if(!ol.proj.get(code)) {
                // https://www.npmjs.com/package/epsg-index の利用を推奨
                const def = proj4Def[code];

                if(def) {
                  proj4.defs(
                    code,
                    def
                  );
                  ol.proj.proj4.register(proj4);
                }
              }
              const tileGrid = source.getTileGrid();
              const imageSource = new ol.source.XYZ({
                tileGrid: tileGrid,
                url: "{z},{x},{y}",
                interpolate: false,
                tileLoadFunction: function (imageTile, coordString) {
                  const coord = coordString.split(",").map(Number);
                  const tile = source.getTile(
                    coord[0],
                    coord[1],
                    coord[2],
                    1,
                    sourceProjection
                  );

                  const setImage = function () {
                    const tilesize = ol.size.toSize(tileGrid.getTileSize(coord[0]));
                    const canvas = document.createElement("canvas");
                    canvas.width = tilesize[0];
                    canvas.height = tilesize[1];
                    const context = canvas.getContext("2d");
                    if (!context) return;
                    const imgData = context.getImageData(
                      0,
                      0,
                      canvas.width,
                      canvas.height
                    );

                    const pixels = imgData.data;
                    const data = tile.getData();
                    if (data instanceof DataView) return;
                    const l = data.length;
                    for (let i = 0; i < l; i++) {
                      pixels[i] = data[i];
                    }
                    context.putImageData(imgData, 0, 0);
                    imageTile.getImage().src = canvas.toDataURL();
                  };

                  const tileState = tile.getState();
                  if (tileState === 2) {
                    setImage();
                  } else {
                    const tileListener = () => {
                      const tileState = tile.getState();
                      if (tileState !== 1) {
                        tile.removeEventListener("change", tileListener);

                        if (tileState === 2) {
                          setImage();
                        }
                      }
                    };
                    tile.addEventListener("change", tileListener);
                    tile.load();
                  }
                },
                projection: sourceProjection,
              });

              const layer = new ol.layer.WebGLTile({
                source: imageSource,
              });
              map.addLayer(layer);

              const originExtent = sourceView.extent;
              if (originExtent) {
                const transformed = ol.proj.transformExtent(originExtent, code, "EPSG:3857");

                if (transformed[0] > transformed[2])
                  transformed[2] += proj4("EPSG:4326", "EPSG:3857", [180, 0])[0] * 2;

                map.getView().fit(transformed, {
                  padding: [40, 20, 40, 20],
                  maxZoom: 20,
                });
              }
            };

          const source = new ol.source.GeoTIFF({
            sources: [
              {
                url: "https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/2020/S2A_36QWD_20200701_0_L2A/TCI.tif",
                nodata: 0,
              },
            ],
          });
          const sourceState = source.getState();
          if (sourceState === "ready") {
            setLayer(map, source);
          } else {
            const sourceListener = () => {
              const sourceState = source.getState();
              if (sourceState === "error") {
                source.removeEventListener("change", sourceListener);
              }
              if (sourceState === "ready") {
                source.removeEventListener("change", sourceListener);
                setLayer(map, source);
              }
            };
            source.addEventListener("change", sourceListener);
          }
        })();
    </script>
</body>
</html>

ロードしたImageTileをさらにXYZのタイルに変換してから表示するんですね。
これにより投影変換が可能となり、複数のGeoTIFFを共通の座標系で表示することできます。
サンプルコードではデフォルトのEPSG:3857で表示していますが、他の投影系も選択できます[1]

上記のサンプルコードを元にもっと丁寧に実装したのが次のページです。
サンプルページ

読み込むURLを変えることができるので遊んでみてください。
メジャーなEPSGコードで投影されているGeoTIFFであれば読み込めるはずです。
「SAMPLE2」はOverviewsを持っていないGeoTIFFを3つも同時に読み込むため表示されるまでかなり時間がかかります。

ローカルのファイルを読み込むこともできます。
こちらはまだPullRequestの段階(22/01時点)のソースを拝借しています。早くマージされますように[2]

脚注
  1. extentの計算でEPSG:3857に合わせてワープを考慮した処理をしています。他の座標系の場合ここの処理を省いたり、その座標系特有の考慮が必要になります。 ↩︎

  2. マージされてもimportして使う場合はこのあたりの問題でエラーが出るかも。webworker関連と思われる。 ↩︎

Discussion