0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ArcGIS Proに対応したcoesite4レポジトリに改良する

Posted at

はじめに

本記事では、ArcGIS Proでのベースマップ表示に対応したcoesite4レポジトリの改良にあたっての変更点についてまとめます。

coesite4のクローン

まずは、coesite4をローカルにクローンします。20250711coesiter4forArcGISProという名前のフォルダにクローンしました。

git clone https://github.com/unvt/coesite4.git 20250711coesiter4forArcGISPro

routes/esriIF.jsの修正

ArcGIS Proからリクエストされているtileマップのリクエストに対応するコードに修正しました。
こちらの記事にも関連事項を記載しています。

routes/esriIF.jsの一部
} else if (maxTiles[t] && width == 8 && height == 8) {
  var tmapdataFull = {
    0: [
      1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    ],
    1: [
      1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    ],
    2: [
      1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0,
      0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
      0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    ],
    3: [
      1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
      1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
      1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    ],
  };
  var tmapdata = {};
  if (z < 3) {
    tmapdata = tmapdataFull[z];
  } else {
    tmapdata = tmapdataFull[3];
  }
  res.json({
    adjusted: false,
    location: { left: column, top: row, width: width, height: height },
    data: tmapdata,
  });
  busy = false;
}

package.jsonファイルの修正

以下の2つのモジュールを追加します。

  • "axios":
    HTTPリクエストを送るためのクライアントライブラリ。GET, POST などのリクエストを簡単に送信でき、レスポンスの処理もシンプルに書けます。

  • "node-cache":
    Node.js アプリケーション内で使える 一時的なメモリキャッシュ。APIのレスポンスや重い計算結果を一時保存して、再計算や再取得を防ぐのに便利。

変更後のpacakge.json

package.json
{
  "name": "coesite4",
  "version": "0.0.0",
  "description": "A vector tile server",
  "main": "app.js",
  "scripts": {
    "start": "node app.js"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/unvt/coesite4.git"
  },
  "dependencies": {
    "@azure/msal-node": "^3.3.0",
    "@mapbox/mbtiles": "^0.12.1",
    "@microsoft/microsoft-graph-client": "^3.0.0",
    "axios": "^1.10.0",
    "config": "^3.3.6",
    "connect-flash": "^0.1.1",
    "cookie-parser": "~1.4.4",
    "cors": "^2.8.5",
    "dotenv": "^16.4.7",
    "express": "^4.17.1",
    "express-mysql-session": "^2.1.7",
    "express-promise-router": "^4.1.0",
    "express-session": "^1.17.2",
    "hbs": "^4.1.2",
    "hh-mm-ss": "^1.2.0",
    "hjson": "^3.2.2",
    "http-errors": "^2.0.0",
    "isomorphic-fetch": "^3.0.0",
    "morgan": "^1.10.0",
    "node-cache": "^5.1.2",
    "spdy": "^4.0.2",
    "winston": "^3.3.3",
    "winston-daily-rotate-file": "^5.0.0"
  }
}

routes/VT-r.jsの修正

トークンが存在する場合の処理を記載しています。また、トークンが存在しない場合には、トークが存在した時のIPアドレスを取得しておいて、同様のIPアドレスである場合には認可します。

routes/VT-r.jsの一部
const axios = require('axios');
const NodeCache = require('node-cache');

// Creating a token cache.
// stdTTL is the cache expiration time (in seconds).
// Expired data is cleaned up every 120 seconds.
const tokenCache = new NodeCache({ stdTTL: 30, checkperiod: 120 });
const tokenIPSet = new Set(); // To record IP
const ipTimeoutMap = new Map(); // IP → Timeout ID

function addOrRefreshIP(ip, ttlMs = 60 * 1000) {
  // If the IP is registered, remove it.
  if (ipTimeoutMap.has(ip)) {
    // console.log(`🔄 IP renew: ${ip}, ID: ${ipTimeoutMap.get(ip)}`);
    clearTimeout(ipTimeoutMap.get(ip));
  }

  // Adding IP to Set
  tokenIPSet.add(ip);
  // console.log(`🌟 IP登録・更新: ${ip}`);

  const timeoutId = setTimeout(() => {
    tokenIPSet.delete(ip);
    ipTimeoutMap.delete(ip);
    // console.log(`💨 TTL切れでIP削除: ${ip}`);
  }, ttlMs);

  // Mapに保存(更新)
  ipTimeoutMap.set(ip, timeoutId);
}

/* GET Tile. */
//router.get('/',
// async function(req, res, next) {
router.get(`/zxy/:t/:z/:x/:y.pbf`, async function (req, res) {
  busy = true;
  const t = req.params.t;
  const z = parseInt(req.params.z);
  const x = parseInt(req.params.x);
  const y = parseInt(req.params.y);

  const rawIP = req.socket?.remoteAddress;
  const clientIP =
    typeof rawIP === "string" && rawIP.startsWith("::ffff:")
      ? rawIP.replace(/^::ffff:/, "")
      : rawIP;
  // console.log("✅rawIP: ", rawIP);
  // console.log("✅clientIP: ", clientIP);

  let token = null;
  if (req.query && req.query.token) {
    token = req.query.token;
  }

  let isTokenValid = false;

  // Only if a token exists, the following code checks its validity.
  if (token) {
    let cleanedToken = token;
    // 1. First, check if there is valid token information in the cache.
    if (tokenCache.has(token)) {
      isTokenValid = tokenCache.get(token);
      // console.log(`CACHE HIT! ⚡️ Retrieved token (${token.substring(0, 10)}...) validation result from cache! Valid: ${isTokenValid}`);
    } else {
      // 2. If not found in cache, check via API as usual
      // console.log(`CACHE MISS... 😢 Validating token (${token.substring(0, 10)}...) via API`);

      const parts = token.split("?token=");
      if (parts.length > 1 && parts[0] === parts[1]) {
        cleanedToken = parts[0];
      }
      // console.log('✅ Clean token:', token);
      const generateTokenUrl = `https://dev-geoportal.dfs.un.org/arcgis/sharing/rest/generateToken`;
      const params = new URLSearchParams({
        token: cleanedToken,
        serverUrl: "https://dev-geoportal.dfs.un.org/unvt/rest/services",
        f: "json",
      }).toString();
      const fullUrl = `${generateTokenUrl}?${params}`;
      // console.log('✅fullUrl: ', fullUrl);
      try {
        const response = await axios.get(fullUrl);
        // console.log('✅ generateToken response:', response.data);
        if (response.data && !response.data.error) {
          isTokenValid = true;
          // tokenIPSet.add(clientIP);
          addOrRefreshIP(clientIP); // ★ここでIP登録・更新
        }
      } catch (error) {
        isTokenValid = false;
      }

      // 3. Save the validation result from the API to the cache
      // console.log('✅ Cached token:', token);
      tokenCache.set(token, isTokenValid);
      // console.log(
      //   `Stored API validation result in cache! Valid: ${isTokenValid}`
      // );
    }
    // console.log('✅isTokenValid: ', isTokenValid);
  }

  //if (!req.session.userId) {
  var whiteList = [
    "https://ubukawa.github.io/cors-cookie",
    "https://dev-geoportal.dfs.un.org/",
  ];
  const noSession = !req.session?.userId;
  const invalidReferer = !(
    req.headers.referer &&
    whiteList.some((value) => req.headers.referer.includes(value))
  );
  const invalidToken = !(token && isTokenValid);
  const ipAllowed = !token && tokenIPSet.has(clientIP);
  // console.log("✅tokenIPSet.has(clientIP): ", tokenIPSet.has(clientIP));

  if (noSession && invalidReferer && invalidToken && !ipAllowed) {
    // Redirect unauthenticated requests to home page
    // res.redirect('/')
    res.status(401).send(`Please log in to get: /zxy/${t}/${z}/${x}/${y}.pbf`);
    // busy = false
  } else {

実際にcoesite4レポジトリを更新する

上述した変更を実際のレポジトリに反映させました。

反映したコードで正常に動くか再確認

次に反映したコードで正常に動くか再確認するために、VDI上で実行してみます。
ArcGIS Proについて正常に地図が表示されました。

まとめ

ArcGIS Proでのベースマップ表示に対応したcoesite4レポジトリの改良点についてまとめました。

Reference

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?