はじめに
本記事では、cosite3レポジトリを解説します。
mysqlがセッション情報の保存に使用されていますが、それ以外は今までの記事解説で記載してきたものと同様の機能です。
コード解説
default-exmple.hjson
{
morganFormat: tiny
htdocsPath: public
privkeyPath: (somewhere)/privkey.pem
fullchainPath: (somewhere)/cert.pem
logDirPath: log
port: 443
tz:{
UniteStreetMapVector: 6
contour: 6
}
tRz:{
srtm: 6
}
sTileName:{
UniteStreetMapVector: small-scale
}
defaultZ: 6
mbtilesDir: mbtiles
esriDir: esri
esri-tilemap-min:{
UniteStreetMapVector: 0
}
esri-tilemap-max:{
UniteStreetMapVector: 15
}
}
それぞれの役割については、以下の通りです。
tz:{
UniteStreetMapVector: 6
contour: 6
}
VT.jsで使用されています。mbtilesの基本のズームレベルだと思います。基本のズームレベルを6として、地物数が少ない地域では、ズームレベル5とズームレベル4のmbtilesファイルも存在しても良いということだと思います。
tRz:{
srtm: 6
}
どこにも使用されていないように思われます。
sTileName:{
UniteStreetMapVector: small-scale
}
VT.jsで使用されています。sTileNameは、スタートタイル名の略だと思われます。詳しい説明はこちらの記事でしています。
esri-tilemap-min:{
UniteStreetMapVector: 0
}
esriIF.jsにおけるタイルの表示範囲の最小ズームレベルに使用されています。
esri-tilemap-max:{
UniteStreetMapVector: 15
}
esriIF.jsにおけるタイルの表示範囲の最大ズームレベルに使用されています。
sample.env
OAUTH_CLIENT_ID=(Your APP ID here)
OAUTH_CLIENT_SECRET=(Your secret here)
OAUTH_REDIRECT_URI=http://localhost:3000/auth/callback(Your callback here)
OAUTH_SCOPES='user.read'
OAUTH_AUTHORITY=https://login.microsoftonline.com/(Your tenant here)/
MYSQL_USER=(yours)
MYSQL_PASSWORD=(yours)
MYSQL_DATABASE=(yours)
ここで、OAUTH_CLIENT_IDは、coesiteではOAUTH_APP_ID、
OAUTH_CLIENT_SECRETは、coesiteではOAUTH_APP_SECRETと呼ばれていましたが、書き方の違いだけです。
MYSQLについては、それぞれ、ユーザ名、パスワード、データベース名を指定します。
MYSQL_USER=(yours)
MYSQL_PASSWORD=(yours)
MYSQL_DATABASE=(yours)
package.json
{
"name": "coesite3",
"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/coesite3.git"
},
"dependencies": {
"@azure/msal-node": "^1.3.0",
"@mapbox/mbtiles": "^0.12.1",
"@microsoft/microsoft-graph-client": "^3.0.0",
"config": "^3.3.6",
"connect-flash": "^0.1.1",
"cookie-parser": "~1.4.4",
"cors": "^2.8.5",
"debug": "^4.3.1",
"dotenv": "^10.0.0",
"express": "^4.17.1",
"express-mysql-session": "^2.1.7",
"express-promise-router": "^4.1.0",
"express-session": "^1.17.2",
"express-validator": "^6.12.1",
"fs": "0.0.1-security",
"hbs": "^4.1.2",
"hh-mm-ss": "^1.2.0",
"hjson": "^3.2.2",
"http-errors": "^1.8.0",
"https": "^1.0.0",
"isomorphic-fetch": "^3.0.0",
"moment": "^2.29.1",
"moment-timezone": "^0.5.31",
"morgan": "^1.10.0",
"spdy": "^4.0.2",
"uuid": "^8.3.1",
"windows-iana": "^5.0.2",
"winston": "^3.3.3",
"winston-daily-rotate-file": "^4.5.5"
}
}
新しいものは以下です。
express-mysql-sessionモジュール
"express-mysql-session": "^2.1.7",
本家の説明はこちらにあります。
express-mysql-session は、Node.js の Express.js でセッションを管理するために、MySQLをセッションストアとして使用する モジュールです。
通常、express-session ではデフォルトで メモリ(RAM) にセッションを保存しますが、これは アプリを再起動するとセッションが消える という問題があります。
そこで、express-mysql-session を使うと、MySQL にセッションを保存することで、アプリ再起動後もセッションを維持 できます。
app.js
require('dotenv').config()
//for Server fnction
const session = require('express-session')
const MySQLStore = require('express-mysql-session')(session) //Added for session store in mysql
const flash = require('connect-flash')
const msal = require('@azure/msal-node')
var createError = require('http-errors')
const express = require('express')
const path = require('path')
var cookieParser = require('cookie-parser')
const morgan = require('morgan')
const winston = require('winston')
const DailyRotateFile = require('winston-daily-rotate-file')
const spdy = require('spdy') //for https
//for File processing
const config = require('config')
const fs = require('fs')
const cors = require('cors')
// config constants
const morganFormat = config.get('morganFormat')
const logDirPath = config.get('logDirPath')
const port = config.get('port')
const privkeyPath = config.get('privkeyPath')
const fullchainPath = config.get('fullchainPath')
const htdocsPath = config.get('htdocsPath')
const defaultZ = config.get('defaultZ')
const mbtilesDir = config.get('mbtilesDir')
// logger configuration
const logger = winston.createLogger({
transports: [
new winston.transports.Console(),
new DailyRotateFile({
filename: `${logDirPath}/coesite-%DATE%.log`,
datePattern: 'YYYY-MM-DD'
})
]
})
logger.stream = {
write: (message) => { logger.info(message.trim()) }
}
// logger until here
var authRouter = require('./routes/auth') //before app
const app = express()
// In-memory storage of logged-in users
// For demo purposes only, production apps should store
// this in a reliable storage
app.locals.users = {};
// MSAL config
const msalConfig = {
auth: {
clientId: process.env.OAUTH_CLIENT_ID,
authority: process.env.OAUTH_AUTHORITY,
clientSecret: process.env.OAUTH_CLIENT_SECRET
//Client credential (secret, certificate, or assertion) must not be empty when creating a confidential client.
//An application should at most have one credential
},
system: {
loggerOptions: {
loggerCallback(loglevel, message, containsPii) {
console.log(message);
},
piiLoggingEnabled: false,
logLevel: msal.LogLevel.Verbose,
}
}
};
// Create msal application object
app.locals.msalClient = new msal.ConfidentialClientApplication(msalConfig);
//(before indexRouter) until here
var indexRouter = require('./routes/index')
var usersRouter = require('./routes/users')
var mapRouter = require('./routes/map') //test 0104
var mapLRouter = require('./routes/map-l') //test 0104
var webmapRouter = require('./routes/webmap') //test 0104
var webmapLRouter = require('./routes/webmap-l')
var webmap3DRouter = require('./routes/webmap-3d')
var VTRouter = require('./routes/VT') //test 0308
//var VTRouter = require('./routes/VT-r') //referer test
//var VTORouter = require('./routes/VT-open') //test 0322(only for development env.)
var esriIFRouter = require('./routes/esriIF') //esri interface (tilemap, etc..)
var rgbElevRouter = require('./routes/rgbElev')
/*
// Session middleware
// NOTE: Uses default in-memory session store, which is not
// suitable for production
app.use(session({
secret: process.env.OAUTH_CLIENT_SECRET,
resave: false,
saveUninitialized: false,
unset: 'destroy'
}))
// note: session will be replaceid with mysql
*/
//session with mysql (from here)
const mysqlOptions ={
host: 'localhost',
port: 3306,
user: process.env.MYSQL_USER,
password: process.env.MYSQL_PASSWORD,
database: process.env.MYSQL_DATABASE
}
const sessionStore = new MySQLStore(mysqlOptions)
const sess = {
secret: process.env.OAUTH_CLIENT_SECRET,
cookie: {maxAge: 60000000}, //16h40m
store: sessionStore,
resave: true, //make it true if necessary
saveUninitialized: true //make it true if necessary
}
//sess.cookie.secure = true //for production
app.use(session(sess))
//session with mysql (until here)
// Flash middleware
app.use(flash())
// Set up local vars for template layout
app.use(function (req, res, next) {
// Read any flashed errors and save
// in the response locals
res.locals.error = req.flash('error_msg')
// Check for simple error string and
// convert to layout's expected format
var errs = req.flash('error')
for (var i in errs) {
res.locals.error.push({ message: 'An error occurred', debug: errs[i] })
}
// Check for an authenticated user and load
// into response locals
if (req.session.userId) {
res.locals.user = app.locals.users[req.session.userId]
}
next()
})
// view engine setup
app.set('unvt/views', path.join(__dirname, 'views'))
app.set('view engine', 'hbs')
app.use(morgan(morganFormat, {
stream: logger.stream
}))
app.use(express.json())
app.use(express.urlencoded({ extended: false }))
app.use(cookieParser())
/*
// for credential
const corsOption = {
origin: '*',
credentials: true
}
app.options('*', cors(corsOption)) //test
app.use(cors(corsOption))
// for credential (unitil here)
*/
app.use(cors())
//app.use(express.static(path.join(__dirname, htdocsPath)))
app.use('/unvt', express.static('public'))
app.use('/unvt/', indexRouter)
app.use('/unvt/auth', authRouter) //after app.use('/', indexRouter)
app.use('/unvt/users', usersRouter)
app.use('/unvt/map', mapRouter)
app.use('/unvt/map-l', mapLRouter)
app.use('/unvt/webmap', webmapRouter)
app.use('/unvt/webmap-l', webmapLRouter)
app.use('/unvt/webmap-3d', webmap3DRouter)
app.use('/unvt/VT', VTRouter)
//app.use('/unvt/VT-open', VTORouter)
app.use('/unvt/esriIF', esriIFRouter) //esri interface
app.use('/unvt/rgb-elev', rgbElevRouter)
// error handler
app.use((req, res) => {
res.sendStatus(404)
})
app.use(function (err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message
res.locals.error = req.app.get('env') === 'development' ? err : {}
// render the error page
res.status(err.status || 500)
res.render('error')
})
//for https
spdy.createServer({
key: fs.readFileSync(privkeyPath),
cert: fs.readFileSync(fullchainPath)
}, app).listen(port)
//for http
//app.listen(port, () => {
// console.log(`Running at Port ${port} ...`)
//app.listen(3000, () => {
//console.log("running at port 3000 ...")
//})
以下が新しい記載です。
const MySQLStore = require('express-mysql-session')(session) //Added for session store in mysql
express-sessionをsessionという変数としてインポートしている前提で、その session を引数として渡します。これにより、MySQL を使ったセッションストア(ユーザーのセッション情報を保存するためのデータストレージ)を Express で利用できるようになります。
const mysqlOptions ={
host: 'localhost',
port: 3306,
user: process.env.MYSQL_USER,
password: process.env.MYSQL_PASSWORD,
database: process.env.MYSQL_DATABASE
}
const sessionStore = new MySQLStore(mysqlOptions)
const sess = {
secret: process.env.OAUTH_CLIENT_SECRET,
cookie: {maxAge: 60000000}, //16h40m
store: sessionStore,
resave: true, //make it true if necessary
saveUninitialized: true //make it true if necessary
}
//sess.cookie.secure = true //for production
app.use(session(sess))
•mysqlOptions は MySQL データベースの接続情報を設定するオブジェクトです。
•host: 'localhost' → ローカルの MySQL サーバー に接続。
•port: 3306 → MySQL のデフォルトポート を使用。
const sessionStore = new MySQLStore(mysqlOptions);
•express-mysql-session (= MySQLStore) を使って MySQL を セッションストア としてセットアップ。
•これにより、Express アプリのセッションデータが MySQL のデータベースに保存される。
• セッション情報は sessions というテーブルに保存され、各セッションごとに session_id, expires, data などのカラムが作成される。
sessオブジェクトについて
secret:セッション ID の署名に使用される秘密鍵
cookie.maxAge:クライアントのセッション Cookie の有効期間 (60000000ms = 16時間40分)
store:MySQL ストア を指定 (セッションを MySQL に保存)
resave:true にすると、セッションが変更されなくても毎回保存する
saveUninitialized: true にすると、未変更のセッションも保存する
// sess.cookie.secure = true // for production
•secure: true にすると、HTTPS の場合のみセッション Cookie を送信 する。
•開発環境では HTTP で動作することが多いため、コメントアウト されている。
•本番環境 (production) では有効にすべき(セキュリティ向上のため)。
•app.use(session(sess)) によって、express-session をミドルウェアとして適用。
•すべてのリクエストで セッション管理 が行われる。
app.set('unvt/views', path.join(__dirname, 'views'))
chatGPTに聞いたところ、app.set('unvt/views', ...) を使うなら、app.get('unvt/views') で参照しなければ意味がないらしいです。
app.set('unvt/views', ...) ではなく、app.set('views', ...) に修正するのが良さそうです。
それぞれのルーティングの役割
app.use('/unvt', express.static('public')) 静的ファイルの提供
public/ フォルダ内の静的ファイルを /unvt/ というURLで提供します。
サーバーの処理を通さずに直接ファイルを返します(ミドルウェアなし)。
app.use('/unvt/', indexRouter) ./routes/index.js参照
index.hbsをレンダリングしています。
app.use('/unvt/auth', authRouter) //after app.use('/', indexRouter) ./routes/auth.js参照
認証のためのコードです。
app.use('/unvt/users', usersRouter) ./routes/users.js参照
存在はしていますが、使用されていないように思います。
router.get('/', function(req, res, next) {
res.send('respond with a resource');
});
というコードなので、テストのためのコードなのかもしれません。
app.use('/unvt/map', mapRouter) ./routes/map.js参照
accessToken が正常に取得できた場合に map.hbs をレンダリングします。
map.hbsでは、mapbox-gl.jsと、./styles/style-std.jsonのスタイルファイルを使用しています。
上記スタイルファイルが参照しているベクトルタイルは以下です。
https://dev-geoportal.dfs.un.org/unvt/VT/zxy/UniteStreetMapVector/{z}/{x}/{y}.pbf
app.use('/unvt/map-l', mapLRouter) ./routes/map-l.js参照
accessToken が正常に取得できた場合に map-l.hbs をレンダリングします。
map-l.hbsでは、maplibre-gl.jsと、./styles/style-std.jsonのスタイルファイルを使用しています。スタイルファイル自体は、一つ上のmapRouterと同じなので、mapboxかmaplibreの違いです。
app.use('/unvt/webmap', webmapRouter) ./routes/webmap.js参照
accessToken が正常に取得できた場合に webmap.hbs をレンダリングします。
webmap.hbsはテスト用のようで、使用されていないように思われます。
さらに、
<div id='map'"></div>
と、間違いもあります。
app.use('/unvt/webmap-l', webmapLRouter) ./routes/webmap-l.js参照
accessToken が正常に取得できた場合に webmap-l.hbs をレンダリングします。
webmap-l.hbsでは、maplibre、watergisを使用しています。
サインイン後のページから、
Web Map APP (3 styles) - with several MapLibre plugins
として、ページに入ったあとの地図表示に使用されています。
使用しているスタイルファイルは3つに分かれているようです。
「Standard」は、以下のスタイルがメインで使用されています。
./styles/style-std.json
「Building-3d」は、以下のスタイルファイルが使用されているようです。
./styles/style-prod.json
こちらのスタイルファイルが参照しているベクトルタイルも以下です。
https://dev-geoportal.dfs.un.org/unvt/VT/zxy/UniteStreetMapVector/{z}/{x}/{y}.pbf
「Contour Map」は、以下のスタイルファイルが使用されているようです。
./styles/style-el.json
こちらのスタイルファイルが参照しているベクトルタイルは以下の3つです。下の2つで高さを表現しているのだと思います。一番下は、NASADEMを使用しており、pngファイルであることにも注意が必要です。
・https://dev-geoportal.dfs.un.org/unvt/VT/zxy/UniteStreetMapVector/{z}/{x}/{y}.pbf
・https://dev-geoportal.dfs.un.org/unvt/VT/zxy/contour/{z}/{x}/{y}.pbf
・https://dev-geoportal.dfs.un.org/unvt/rgb-Elev/zxy/{z}/{x}/{y}.png
山が凹凸を持って見えますが、実際には盛り上がっていません。陰影図のようなものです。
app.use('/unvt/webmap-3d', webmap3DRouter) ./routes/webmap-3d.js参照
accessToken が正常に取得できた場合に webmap-3d.hbs をレンダリングします。
webmap-3d.hbsでは、maplibre、watergis、./styles/style-el.jsonのスタイルファイルを使用しています。
サインイン後のページから、
Web Map APP - 3D (Testing) - with MapLibre version 2.4.0
として、ページに入ったあとの地図だと思います。山が実際には盛り上がって見えます。
app.use('/unvt/VT', VTRouter) ./routes/VT.js参照
mbtilesファイルからベクトルタイル情報を取得して、レスポンスとして返します。アプリケーションの核となるコードです。
//app.use('/unvt/VT-open', VTORouter) ./routes/VT-open.js参照
VTRouterと比較して、認証がないバージョンです。
app.use('/unvt/esriIF', esriIFRouter) //esri interface ./routes/esriIF.js参照
ArcGIS Onlineで地図を表示するために必要な、index.json、root.json、タイルマップデータを返すためのコードです。
詳しい説明はこちらの記事でしています。
app.use('/unvt/rgb-elev', rgbElevRouter) ./routes/rgbElev.js参照
mbtilesファイルから標高タイル情報(pngファイル)を取得して、レスポンスとして返します。原理的には、VTRouterと同じです。
maplibre-gl.cssとmaplibre-gl-r.cssの違い
maplibre-gl.cssとmaplibre-gl-r.cssの違いは、著作権表記の背景が半透明の白がデフォルトですが、それを完全に透明にしている点です。
まとめ
本記事では、cosite3レポジトリについてまとめました。
Reference