coesite3レポジトリを理解する

# はじめに
本記事では、[cosite3レポジトリ](https://github.com/unvt/coesite3)を解説します。
mysqlがセッション情報の保存に使用されていますが、それ以外は今までの記事解説で記載してきたものと同様の機能です。

# コード解説

## default-exmple.hjson

```hjson: config/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の基本のズームレベルだと思います。基本のズームレベルを６として、地物数が少ない地域では、ズームレベル５とズームレベル４のmbtilesファイルも存在しても良いということだと思います。
 
 tRz:{
  srtm: 6 
 }
 どこにも使用されていないように思われます。
 
 sTileName:{
  UniteStreetMapVector: small-scale
 }
 VT.jsで使用されています。sTileNameは、スタートタイル名の略だと思われます。詳しい説明は[こちらの記事](https://qiita.com/k96mz/items/6e80215e1b50f752248f#z%E3%81%8C5%E6%9C%AA%E6%BA%80%E3%81%A4%E3%81%BE%E3%82%8A%E5%B0%8F%E7%B8%AE%E5%B0%BA%E3%81%AE%E5%A0%B4%E5%90%88)でしています。
 

 esri-tilemap-min:{
  UniteStreetMapVector: 0
 }
esriIF.jsにおけるタイルの表示範囲の最小ズームレベルに使用されています。
 
 esri-tilemap-max:{
  UniteStreetMapVector: 15
 }
esriIF.jsにおけるタイルの表示範囲の最大ズームレベルに使用されています。


## sample.env
```.env: 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

```json: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",
本家の説明は[こちら](https://www.npmjs.com/package/express-mysql-session)にあります。
express-mysql-session は、Node.js の Express.js でセッションを管理するために、MySQLをセッションストアとして使用する モジュールです。

通常、express-session ではデフォルトで メモリ（RAM） にセッションを保存しますが、これは アプリを再起動するとセッションが消える という問題があります。
そこで、express-mysql-session を使うと、MySQL にセッションを保存することで、アプリ再起動後もセッションを維持 できます。

## app.js

```javascript: 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 ...")
//})
```

以下が新しい記載です。

```javascript: app.js
const MySQLStore = require('express-mysql-session')(session) //Added for session store in mysql
```

express-sessionをsessionという変数としてインポートしている前提で、その session を引数として渡します。これにより、MySQL を使ったセッションストア（ユーザーのセッション情報を保存するためのデータストレージ）を Express で利用できるようになります。


```javascript: app.js
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 をミドルウェアとして適用。
•すべてのリクエストで セッション管理 が行われる。

    
```javascript: app.js
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](https://github.com/unvt/coesite3/blob/main/routes/index.js)参照
index.hbsをレンダリングしています。

### app.use('/unvt/auth', authRouter) //after app.use('/', indexRouter)　[./routes/auth.js](https://github.com/unvt/coesite3/blob/main/routes/auth.js)参照
認証のためのコードです。

### app.use('/unvt/users', usersRouter) [./routes/users.js](https://github.com/unvt/coesite3/blob/main/routes/users.js)参照
存在はしていますが、使用されていないように思います。
router.get('/', function(req, res, next) {
  res.send('respond with a resource');
});
というコードなので、テストのためのコードなのかもしれません。


### app.use('/unvt/map', mapRouter) [./routes/map.js](https://github.com/unvt/coesite3/blob/main/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](https://github.com/unvt/coesite3/blob/main/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](https://github.com/unvt/coesite3/blob/main/routes/webmap.js)参照
accessToken が正常に取得できた場合に webmap.hbs をレンダリングします。
webmap.hbsはテスト用のようで、使用されていないように思われます。
さらに、
```
<div id='map'"></div>
```
と、間違いもあります。


### app.use('/unvt/webmap-l', webmapLRouter) [./routes/webmap-l.js](https://github.com/unvt/coesite3/blob/main/routes/webmap-l.js)参照
accessToken が正常に取得できた場合に webmap-l.hbs をレンダリングします。
webmap-l.hbsでは、maplibre、watergisを使用しています。

サインイン後のページから、
Web Map APP (3 styles) - with several MapLibre plugins
として、ページに入ったあとの地図表示に使用されています。

使用しているスタイルファイルは３つに分かれているようです。
「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

![スクリーンショット 2025-02-14 16.17.06.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3684675/3a9dd082-b3bd-4be4-9d12-d80e8994d1ed.jpeg)


「Contour Map」は、以下のスタイルファイルが使用されているようです。
./styles/style-el.json
こちらのスタイルファイルが参照しているベクトルタイルは以下の３つです。下の２つで高さを表現しているのだと思います。一番下は、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

山が凹凸を持って見えますが、実際には盛り上がっていません。陰影図のようなものです。

![スクリーンショット 2025-02-14 16.17.43.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3684675/29f8c77a-bf28-4306-9b07-1b6594c1fb47.jpeg)



### app.use('/unvt/webmap-3d', webmap3DRouter) [./routes/webmap-3d.js](https://github.com/unvt/coesite3/blob/main/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
として、ページに入ったあとの地図だと思います。山が実際には盛り上がって見えます。

![スクリーンショット 2025-02-14 16.23.35.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3684675/962e1da9-d87f-46a2-ade4-3e0ae2e14e95.jpeg)


### app.use('/unvt/VT', VTRouter) [./routes/VT.js](https://github.com/unvt/coesite3/blob/main/routes/VT.js)参照
mbtilesファイルからベクトルタイル情報を取得して、レスポンスとして返します。アプリケーションの核となるコードです。


### //app.use('/unvt/VT-open', VTORouter) [./routes/VT-open.js](https://github.com/unvt/coesite3/blob/main/routes/VT-open.js)参照
VTRouterと比較して、認証がないバージョンです。

VT-open.jsにはなくて、VT.jsにはあるものは以下の記述のみです。
```js: VT.js
if (!req.session.userId) {
  res.status(401).send(`Please log in to get: /zxy/${t}/${z}/${x}/${y}.pbf`)
  } else {
);
```
•この部分があるため、「ログインしていないユーザー」は 401 Unauthorized を返されます。
•req.session.userId によって、ログインセッションが存在するか確認しています。


### app.use('/unvt/esriIF', esriIFRouter) //esri interface [./routes/esriIF.js](https://github.com/unvt/coesite3/blob/main/routes/esriIF.js)参照
ArcGIS Onlineで地図を表示するために必要な、index.json、root.json、タイルマップデータを返すためのコードです。
詳しい説明は[こちらの記事](https://qiita.com/k96mz/items/f9900dc7a15cc99ac2fd#routesesriifjs)でしています。



### app.use('/unvt/rgb-elev', rgbElevRouter) [./routes/rgbElev.js](https://github.com/unvt/coesite3/blob/main/routes/rgbElev.js)参照
mbtilesファイルから標高タイル情報(pngファイル)を取得して、レスポンスとして返します。原理的には、VTRouterと同じです。



# maplibre-gl.cssとmaplibre-gl-r.cssの違い
[maplibre-gl.css](https://github.com/unvt/coesite3/blob/main/public/maplibre-gl%402.4.0/maplibre-gl.css)と[maplibre-gl-r.css](https://github.com/unvt/coesite3/blob/main/public/maplibre-gl%402.4.0/maplibre-gl-r.css)の違いは、著作権表記の背景が半透明の白がデフォルトですが、それを完全に透明にしている点です。coesite3ではmaplibre-gl-r.cssを使用しており著作権表記の背景が透明となっています。


### maplibre-gl.css
![スクリーンショット 2025-03-25 12.01.44.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3684675/d40d9a51-672e-413a-b127-1444a829db94.jpeg)


### maplibre-gl-r.css
![スクリーンショット 2025-03-25 11.58.52.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3684675/3adcba83-df77-49c1-90b6-c031a32e616e.jpeg)


# まとめ
本記事では、cosite3レポジトリについてまとめました。

# Reference
https://github.com/unvt/coesite3

https://www.npmjs.com/package/express-mysql-session