はじめに
本記事では、ArcGIS Proでのベースマップ表示に対応したcoesite4レポジトリの改良にあたっての変更点についてまとめます。
coesite4のクローン
まずは、coesite4をローカルにクローンします。20250711coesiter4forArcGISProという名前のフォルダにクローンしました。
git clone https://github.com/unvt/coesite4.git 20250711coesiter4forArcGISPro
routes/esriIF.jsの修正
ArcGIS Proからリクエストされているtileマップのリクエストに対応するコードに修正しました。
こちらの記事にも関連事項を記載しています。
} 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
{
"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アドレスである場合には認可します。
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