はじめに
本記事では、marbleレポジトリを解説します。このレポジトリでは、ArcGIS online に合わせたベクトルタイルのホスティングを可能にしています。
コードの解説
app.js
const config = require('config')
//onst fs = require('fs')
const express = require('express')
//const spdy = require('spdy') //for https
const cors = require('cors')
const morgan = require('morgan')
//const TimeFormat = require('hh-mm-ss')
const winston = require('winston')
const DailyRotateFile = require('winston-daily-rotate-file')
// config constants
const morganFormat = config.get('morganFormat')
const htdocsPath = config.get('htdocsPath')
//const privkeyPath = config.get('privkeyPath') //for https
//const fullchainPath = config.get('fullchainPath') //for https
const port = config.get('port')
const logDirPath = config.get('logDirPath')
// logger configuration
const logger = winston.createLogger({
transports: [
new winston.transports.Console(),
new DailyRotateFile({
filename: `${logDirPath}/server-%DATE%.log`,
datePattern: 'YYYY-MM-DD'
})
]
})
logger.stream = {
write: (message) => { logger.info(message.trim()) }
}
// app
const app = express()
var VTRouter = require('./routes/VT') //tiling
var esriIFRouter = require('./routes/esriIF') //esri interface (tilemap, etc..)
app.use(cors())
app.use(morgan(morganFormat, {
stream: logger.stream
}))
app.use(express.static(htdocsPath))
app.use('/VT', VTRouter)
app.use('/esriIF', esriIFRouter) //esri interface
//for http
app.listen(port, () => {
console.log(`Running at Port ${port} ...`)
})
/* for https
spdy.createServer({
key: fs.readFileSync(privkeyPath),
cert: fs.readFileSync(fullchainPath)
}, app).listen(port)
*/
上記コードは、今までの記事でほぼ解説が行われています。唯一異なる点が、
var esriIFRouter = require('./routes/esriIF')
です。
/esriIF
にアクセスがあった場合には、'./routes/esriIF.js'で処理されます。
routes/esriIF.js
var express = require('express')
var router = express.Router()
const config = require('config')
const fs = require('fs')
const cors = require('cors')
// config constants
const esriDir = config.get('esriDir')
const maxTiles = config.get('esri-tilemap-max')
const minTiles = config.get('esri-tilemap-min')
// variables
let busy = false
var app = express()
app.use(cors())
//Returing an index of VectorTileServer --> make sure that the index.json is ready
router.get(`/:t/VectorTileServer`,
async function(req, res) {
busy = true
const t = req.params.t
var indexjsonPath = `${esriDir}/${t}/index.json`
if(fs.existsSync( indexjsonPath )){
res.sendFile('index.json', { root: `./${esriDir}/${t}` })
busy = false
} else {
console.log(indexjsonPath)
res.status(404).send(`index.json not found: esriIF/${t}/VectorTileServer`)
busy = false
}
}
)
router.get(`/:t/VectorTileServer/index.json`,
async function(req, res) {
busy = true
const t = req.params.t
var indexjsonPath = `${esriDir}/${t}/index.json`
if(fs.existsSync( indexjsonPath )){
res.sendFile('index.json', { root: `./${esriDir}/${t}` })
busy = false
} else {
console.log(indexjsonPath)
res.status(404).send(`index.json not found: esriIF/${t}/VectorTileServer`)
busy = false
}
}
)
//Returing a style file --> make sure that the style.json is ready
router.get(`/:t/VectorTileServer/resources/styles`, //need to think about other request f=json, index.json etc
async function(req, res) {
busy = true
const t = req.params.t
var stylejsonPath = `${esriDir}/${t}/style.json`
if(fs.existsSync( stylejsonPath )){
res.sendFile('style.json', { root: `./${esriDir}/${t}` })
busy = false
} else {
console.log(stylejsonPath)
res.status(404).send(`style.json (root.json) not found: esriIF/${t}/VectorTileServer/resources/style`)
busy = false
}
}
)
router.get(`/:t/VectorTileServer/resources/styles/root.json`,
async function(req, res) {
busy = true
const t = req.params.t
var stylejsonPath = `${esriDir}/${t}/style.json`
if(fs.existsSync( stylejsonPath )){
res.sendFile('style.json', { root: `./${esriDir}/${t}` })
busy = false
} else {
console.log(stylejsonPath)
res.status(404).send(`style.json (root.json) not found: esriIF/${t}/VectorTileServer/resources/style`)
busy = false
}
}
)
//Tilemap function- t,left,top,m,ny are extracted from the path
router.get(`/:t/VectorTileServer/tilemap/:z/:row/:column/:width/:height`, //need to think about other request f=json, index.json etc
async function(req, res) {
busy = true
const t = req.params.t
const z = parseInt(req.params.z)
const row = parseInt(req.params.row)
const column = parseInt(req.params.column)
const width = parseInt(req.params.width)
const height = parseInt(req.params.height)
var maxTileZ = maxTiles[t]
var minTileZ = minTiles[t]
if(z > maxTileZ || z < minTileZ){ //if Z is larger than the max tile ZL or smaller than the min tile ZL, there is no tile.
res.json({"error":{"code":422,"message":"Tiles not present","details":[]}})
} else {
if( maxTiles[t] && width==32 && height==32){ //in the future, we may add a detailed condition.
// if(fs.existsSync( tmPath )){ // you can think about deliverying pre-existing json if you want.
var tmapdataFull = {
//Please note that at each zoom level, we use the same values for any tilemaps.
//If we have a real tile mapping, we can consider extracting the requested tile map from the whole array.
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。
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,1,1,0,0,0,0,0,0,0,0,.....],
// 1が2個続いた後に0が30個あり、それが2セット。その後はずっと0。
2:[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,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,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,1,1,1,1,0,0,0,0,.....],
// 1が4個続いた後に0が28個あり、それが4セット。その後はずっと0。
3:[1,1,1,1,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,1,1,1,1,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,1,1,1,1,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,1,1,1,1,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,1,1,1,1,1,1,1,1,0,0,0,....],
// 1が8個続いた後に0が24個あり、それが8セット。その後はずっと0。
4:[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,....],
// 1が16個続いた後に0が16個あり、それが16セット。その後はずっと0。
5:[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,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,...],
// 1が32個続く、それが32セット。
// それぞれ32 * 32 = 1024個の数字が存在する。
}
var tmapdata = {}
if (z < 5){
tmapdata = tmapdataFull[z]
}else{
tmapdata = tmapdataFull[5]
}
res.json({ adjusted: false , location: {left: column, top: row, width: width, height: height}, data: tmapdata})
busy = false
} else {
res.status(404).send(`tilemap not found: esriIF/${t}/VectorTileServer/tilemap/${z}/${row}/${column}/${width}/${height}`)
busy = false
}
}
}
)
module.exports = router;
default.hjsonは以下の通りです。
{
morganFormat: tiny
htdocsPath: htdocs
privkeyPath: ./key/privkey.pem
fullchainPath: ./key/cert.pem
logDirPath: log
port: 8836
mbtilesDir: mbtiles
esriDir: esri
esri-tilemap-min:{
ne-test: 0
test: 0
}
esri-tilemap-max:{
ne-test: 5
test: 15
}
}
router.get(`/:t/VectorTileServer`,
async function(req, res) {
busy = true
const t = req.params.t
var indexjsonPath = `${esriDir}/${t}/index.json`
if(fs.existsSync( indexjsonPath )){
res.sendFile('index.json', { root: `./${esriDir}/${t}` })
busy = false
} else {
console.log(indexjsonPath)
res.status(404).send(`index.json not found: esriIF/${t}/VectorTileServer`)
busy = false
}
}
)
このルートハンドラは、/:t/VectorTileServer に対する GET リクエストを処理しており、 index.json ファイルが見つかれば、その内容をクライアントに送信し、存在しない場合は404 エラーを返します。正確には、実際にindex.jsonというファイルがなくても、\${esriDir}と${t}が存在すれば、indexjsonPathは存在することになるので、fs.existsSync( indexjsonPath ) はtrueになります。そのため、index.jsonが存在しなくても、index.jsonを返すので、改良の余地があると思われます。
ここで、tは「ne-test」や「test」となります。
res.sendFile('index.json', { root: `./${esriDir}/${t}` })
res.sendFile: クライアントにファイルを送信します。
•第一引数: クライアントに送信するファイル名。
•第二引数: ファイルのルートディレクトリを指定します。この場合、./\${esriDir}/${t} ディレクトリがルートとして設定されています。
以下のコードは上記のコードがあれば、不要と思われます。なぜなら、1つ目のルート(/:t/VectorTileServer)で、クライアントからのリクエストが :t/VectorTileServer/index.json であっても、適切に処理できるからです。
router.get(`/:t/VectorTileServer/index.json`,
async function(req, res) {
busy = true
const t = req.params.t
var indexjsonPath = `${esriDir}/${t}/index.json`
if(fs.existsSync( indexjsonPath )){
res.sendFile('index.json', { root: `./${esriDir}/${t}` })
busy = false
} else {
console.log(indexjsonPath)
res.status(404).send(`index.json not found: esriIF/${t}/VectorTileServer`)
busy = false
}
}
)
しかし、以下の記事に記載がある通り、2つのコードがないとArcGISではエラーが出るのかもしれないです。
https://qiita.com/T-ubu/items/1f772fd92ec8dfb0c2b2
router.get(`/:t/VectorTileServer/resources/styles`, //need to think about other request f=json, index.json etc
async function(req, res) {
busy = true
const t = req.params.t
var stylejsonPath = `${esriDir}/${t}/style.json`
if(fs.existsSync( stylejsonPath )){
res.sendFile('style.json', { root: `./${esriDir}/${t}` })
busy = false
} else {
console.log(stylejsonPath)
res.status(404).send(`style.json (root.json) not found: esriIF/${t}/VectorTileServer/resources/style`)
busy = false
}
}
)
このコードは、クライアントが /:t/VectorTileServer/resources/styles にリクエストを送ったときに style.json ファイルを返すルートハンドラーを定義しています。
ファイルが存在する場合は、
./\${esriDir}/${t} つまり、「./esri/ne-test/style.json」のようなファイルが返されます。
router.get(`/:t/VectorTileServer/resources/styles/root.json`,
async function(req, res) {
busy = true
const t = req.params.t
var stylejsonPath = `${esriDir}/${t}/style.json`
if(fs.existsSync( stylejsonPath )){
res.sendFile('style.json', { root: `./${esriDir}/${t}` })
busy = false
} else {
console.log(stylejsonPath)
res.status(404).send(`style.json (root.json) not found: esriIF/${t}/VectorTileServer/resources/style`)
busy = false
}
}
)
上記コードは同様に不要だと思われましたが、実際にはないとエラーが出るようです。
router.get(`/:t/VectorTileServer/tilemap/:z/:row/:column/:width/:height`, //need to think about other request f=json, index.json etc
async function(req, res) {
busy = true
const t = req.params.t
const z = parseInt(req.params.z)
const row = parseInt(req.params.row)
const column = parseInt(req.params.column)
const width = parseInt(req.params.width)
const height = parseInt(req.params.height)
var maxTileZ = maxTiles[t]
var minTileZ = minTiles[t]
if(z > maxTileZ || z < minTileZ){ //if Z is larger than the max tile ZL or smaller than the min tile ZL, there is no tile.
res.json({"error":{"code":422,"message":"Tiles not present","details":[]}})
} else {
if( maxTiles[t] && width==32 && height==32){ //in the future, we may add a detailed condition.
// if(fs.existsSync( tmPath )){ // you can think about deliverying pre-existing json if you want.
var tmapdataFull = {
//Please note that at each zoom level, we use the same values for any tilemaps.
//If we have a real tile mapping, we can consider extracting the requested tile map from the whole array.
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。
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,1,1,0,0,0,0,0,0,0,0,.....],
// 1が2個続いた後に0が30個あり、それが2セット。その後はずっと0。
2:[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,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,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,1,1,1,1,0,0,0,0,.....],
// 1が4個続いた後に0が28個あり、それが4セット。その後はずっと0。
3:[1,1,1,1,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,1,1,1,1,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,1,1,1,1,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,1,1,1,1,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,1,1,1,1,1,1,1,1,0,0,0,....],
// 1が8個続いた後に0が24個あり、それが8セット。その後はずっと0。
4:[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,....],
// 1が16個続いた後に0が16個あり、それが16セット。その後はずっと0。
5:[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,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,...],
// 1が32個続く、それが32セット。
// それぞれ32 * 32 = 1024個の数字が存在する。
}
var tmapdata = {}
if (z < 5){
tmapdata = tmapdataFull[z]
}else{
tmapdata = tmapdataFull[5]
}
res.json({ adjusted: false , location: {left: column, top: row, width: width, height: height}, data: tmapdata})
busy = false
} else {
res.status(404).send(`tilemap not found: esriIF/${t}/VectorTileServer/tilemap/${z}/${row}/${column}/${width}/${height}`)
busy = false
}
}
}
)
このコードは、クライアントからのリクエストを処理してタイルマップデータを返すためのルートを定義しています。
• エンドポイント: /:t/VectorTileServer/tilemap/:z/:row/:column/:width/:height
•t: ne-testなど
•z: ズームレベル
•row: タイルの行番号
•column: タイルの列番号
•width: タイルの幅(ピクセル単位)
•height: タイルの高さ(ピクセル単位)
例: リクエストが /ne-test/VectorTileServer/tilemap/5/10/20/32/32 の場合:
• t = 'ne-test'
• z = 5
• row = 10
• column = 20
• width = 32
• height = 32
maxTileZ = maxTiles[t] = esri-tilemap-max[ne-test] = 5 などとなります。
minTileZ = minTiles[t] = esri-tilemap-min[ne-test] = 0 などとなります。
if (z > maxTileZ || z < minTileZ) {
res.json({"error":{"code":422,"message":"Tiles not present","details":[]}});
}
ズームレベルが許容範囲外の場合、エラーメッセージを返します。
tmapdataFull には、ズームレベルごとのタイルマップデータが配列で定義されていて、ズームレベルに合わせて、tmapdataに格納されます。
・z < 5:対応するズームレベルのデータを返す
・z >= 5: ズームレベル5以上の固定データを返す
res.json({
adjusted: false,
location: { left: column, top: row, width: width, height: height },
data: tmapdata
});
タイルの位置情報(column と row)およびタイルデータ(tmapdata)を JSON 形式で返しています。
例としては、以下です。
{
"adjusted": false,
"location": {
"left": 20,
"top": 10,
"width": 32,
"height": 32
},
"data": [1, 1, 1, 1, 1, ...] // ズームレベルに応じたデータ
}
tmapdataFull の構造とパターン
概要
• タイルデータはズームレベルごとに異なる密度を持ちます。
各ズームレベルの例:
• z = 0: [1, 0, 0, 0, 0, ...](1つのタイルが存在)。
• z = 1: [1, 1, 0, 0, 0, ...](2つのタイルが連続、合計のタイル数は4)。
• z = 5: [1, 1, 1, 1, ...](32x32=1024のすべてのタイルが存在)。
つまり1の個数が、そのズームレベルに存在するタイルの数です。
/esri/ne-test/index.json
{
"currentVersion": 10.9,
"name": "Test with Natural Earth",
"copyrightText": "NaturalEarth",
"capabilities": "TilesOnly,Tilemap",
"type": "indexedFlat",
"tileMap":"tilemap",
"defaultStyles": "resources/styles",
"tiles": [
"https://ubukawa.github.io/vt-test/ne-test/{z}/{x}/{y}.pbf"
],
"initialExtent": {
"xmin": -2.0037507842788246E7,
"ymin": -2.0037508342787E7,
"xmax": 2.0037507842788246E7,
"ymax": 2.0037508342787E7,
"spatialReference": {
"wkid": 102100,
"latestWkid": 3857
}
},
"fullExtent": {
"xmin": -2.0037507842788246E7,
"ymin": -2.0037508342787E7,
"xmax": 2.0037507842788246E7,
"ymax": 2.0037508342787E7,
"spatialReference": {
"wkid": 102100,
"latestWkid": 3857
}
},
"minScale": 2.958287637957775E8,
"maxScale": 9244648.868618,
"tileInfo": {
"rows": 512,
"cols": 512,
"dpi": 96,
"format": "pbf",
"origin": {
"x": -2.0037508342787E7,
"y": 2.0037508342787E7
},
"spatialReference": {
"wkid": 102100,
"latestWkid": 3857
},
"lods": [
{
"level": 0,
"resolution": 78271.516964,
"scale": 2.958287637957775E8
},
{
"level": 1,
"resolution": 39135.75848199995,
"scale": 1.479143818978885E8
},
{
"level": 2,
"resolution": 19567.87924100005,
"scale": 7.39571909489445E7
},
{
"level": 3,
"resolution": 9783.93962049995,
"scale": 3.6978595474472E7
},
{
"level": 4,
"resolution": 4891.96981024998,
"scale": 1.8489297737236E7
},
{
"level": 5,
"resolution": 2445.98490512499,
"scale": 9244648.868618
},
{
"level": 6,
"resolution": 1222.992452562495,
"scale": 4622324.434309
},
{
"level": 7,
"resolution": 611.496226281245,
"scale": 2311162.2171545
},
{
"level": 8,
"resolution": 305.74811314069,
"scale": 1155581.1085775
},
{
"level": 9,
"resolution": 152.874056570279,
"scale": 577790.5542885
},
{
"level": 10,
"resolution": 76.4370282852055,
"scale": 288895.2771445
},
{
"level": 11,
"resolution": 38.2185141425366,
"scale": 144447.638572
},
{
"level": 12,
"resolution": 19.1092570712683,
"scale": 72223.819286
},
{
"level": 13,
"resolution": 9.55462853563415,
"scale": 36111.909643
},
{
"level": 14,
"resolution": 4.777314267817075,
"scale": 18055.9548215
},
{
"level": 15,
"resolution": 2.388657133974685,
"scale": 9027.977411
},
{
"level": 16,
"resolution": 1.19432856698734,
"scale": 4513.9887055
}
]
},
"maxzoom": 16,
"resourceInfo": {
"styleVersion": 8,
"tileCompression": "gzip",
"cacheInfo": {
"storageInfo": {
"packetSize": 128,
"storageFormat": "compactV2"
}
}
},
"minLOD": 0,
"maxLOD": 5,
"exportTilesAllowed": false,
"maxExportTilesCount": 100000,
"supportedExtensions": ""
}
このJSONは、ベクトルタイルサービスを利用するクライアントに、タイルデータを効率的に取得し表示するための必要な情報を提供します。特にズームレベルやタイルフォーマット、空間範囲などの情報が記載されているため、クライアント側で正確に地図を描画することができます。
基本情報
• "capabilities": "TilesOnly,Tilemap"
サーバーが提供する機能を記述します。この場合、タイル(TilesOnly)とタイルマップ(Tilemap)の両方をサポートしています。
タイルマップ(Tilemap)
概要:
• 「タイルマップ」は、地図データのカバー範囲やタイルの有無を事前にクライアントに提供する機能です。
• 各ズームレベルで利用可能なタイルがどこにあるのか(タイルの存在情報)を示すデータ構造です。
特性:
• メタ情報の提供: どのタイルが存在するかを事前に確認できるため、効率的にタイルを取得できます。
• 高速化: クライアントが存在しないタイルをリクエストする無駄を省き、サーバーの負荷を軽減します。
• 形式: JSON形式でタイルの存在を表現することが多いです。
例:
{
"0": [1, 0, 0, 0],
"1": [1, 1, 0, 0],
"2": [1, 1, 1, 0]
}
• キー(0, 1, 2): ズームレベル
• 値(配列): タイルの存在を示す(1は存在、0は存在しない)。
利用方法:
• クライアントはタイルマップを事前にダウンロードし、どのタイルをリクエストするべきかを判断します。
• タイルが存在しない場所にはリクエストを送信しないように最適化できます。
• "type": "indexedFlat"
タイルの格納方法を示します。この場合は「indexedFlat」(インデックス化されたフラット構造)。
※「indexedFlat」は、地図データ(特にベクトルタイル)の管理と配信におけるデータ構造の形式を指します。データがインデックス(目次や索引)を持つことで、特定のデータ要素(タイルなど)へのアクセスを効率化します。これは、Tilemapによる効率的なタイル取得のことを指しています。「flat」は/tiles/0/0/0.pbfのようにデータが階層を持たず、フラットに配置されることです。
• "tileMap": "tilemap"
タイルマップを指定するエンドポイント。
• "defaultStyles": "resources/styles"
デフォルトのスタイルが格納されているリソースパス。
esriIF.jsの中で、resources/stylesへのURLアクセスがstyle.jsonにつながることが記載されています。
ArcGIS REST APIには、resources/styleと記載されているが、実際はresources/stylesなので、そちらを採用したとのことが、該当Githubレポジトリに記載されています。
タイル情報
• "tiles": [ "https://ubukawa.github.io/vt-test/ne-test/{z}/{x}/{y}.pbf" ]
タイルが提供されるURL
範囲(Extent)
• "initialExtent" と "fullExtent"
地図の初期表示範囲と、データがカバーする完全な範囲を定義します。
• "xmin" と "xmax": 緯度方向の最小値と最大値(Web Mercator投影での単位はメートル)。
xmin: -2.0037507842788246E7のE7は10の7乗の意味で、 約 -20,037,508 メートル で、これは地図の西端(経度 -180° 付近)に対応します。
• "ymin" と "ymax": 経度方向の最小値と最大値。
ymin: -2.0037508342787E7
Y座標が 約 -20,037,508 メートル で、これは地図の南端(緯度 -85.0511° 付近)に対応します。
• "spatialReference": 空間参照系。ここでは Web Mercator(EPSG:3857)。
WKID は Well-Known ID の略で、wkid:102100はEPSG:3857の別名。
スケールと解像度
• "minScale" と "maxScale"
データが使用可能な最小スケールと最大スケール。スケールは縮尺を表し、値が小さいほど詳細です。
"minScale": 2.958287637957775E8 は、小縮尺を表し、地図上の 1 cm が現実世界で約 2,958 km に相当します。LOD0のscaleと同等です。
"maxScale": 9244648.868618 は、大縮尺を表し、地図上の 1 cm が現実世界で約 92.4 km に相当する。大縮尺と言いつつも、約900万分の1なので、細かくは見られません。LOD5のscaleと同等です(ホントはLOD16と同等に記載すべきなのかもしれません)。
"tileInfo"
タイルの構造情報を提供します。
• "rows" と "cols": 各タイルのサイズ(ピクセル単位)。ここでは 512x512 ピクセル。
• "dpi": 解像度。
• "format": タイルデータの形式(pbf = プロトコルバッファ)。
• "origin": タイルの原点(左上端の座標)。
"origin": {
"x": -2.0037508342787E7,
"y": 2.0037508342787E7
},
は経度-180度、緯度85度あたりを指す。
"lods": Level of Detail (LOD) のリスト。
• "level": LODのレベル。
• "resolution": 各レベルの解像度(1ピクセルあたりの地理空間距離)。
• "scale": 縮尺。
リソースの追加情報
• "styleVersion": 8: スタイルのバージョン。
• "tileCompression": "gzip": タイルデータが gzip 圧縮されていることを示す。
• "cacheInfo": タイルキャッシュの詳細。
• "packetSize": 128: パケットサイズ。
• "storageFormat": "compactV2": ストレージ形式(compactV2形式)。
compactV2 は、Esri(ArcGIS)の地図タイルキャッシュで使用される効率的なストレージ形式。複数のタイルを単一のバンドルファイル(bundle file)に格納することで、ディスクの空間効率が向上。
その他の情報
• "minLOD": 0 と "maxLOD": 5
サポートされるLOD(ズームレベル)の範囲。ここでは0(最小ズーム)から5(最大ズーム)まで。これはクライアント側の設定です。サーバ側の設定は、 "maxzoom": 16で決まります。
• "exportTilesAllowed": false
タイルのエクスポートが許可されていない。
• "maxExportTilesCount": 100000
エクスポート可能な最大タイル数。
/esri/ne-test/style.json
{
"version": 8,
"name": "test-simple",
"sources": {
"ne-test": {
"type": "vector",
"url": "http://localhost:8836/esriIF/ne-test/VectorTileServer/",
"minzoom": 0,
"maxzoom": 5
}
},
"sprite": "https://ubukawa.github.io/vt-test/sprite/sprite",
"glyphs": "https://ubukawa.github.io/vt-test/font/{fontstack}/{range}.pbf",
"layers": [
{
"id": "background",
"type": "background",
"maxzoom": 8,
"paint": {"background-color": "rgba(135, 188, 196, 1)"}
},
{
"id": "landmass",
"type": "fill",
"source": "ne-test",
"source-layer": "landmass",
"maxzoom": 8,
"paint": {"fill-color": "rgba(174, 255, 147, 1)"}
},
{
"id": "coastline",
"type": "line",
"source": "ne-test",
"source-layer": "coastl",
"maxzoom": 8,
"paint": {"line-color": "rgba(58, 0, 160, 1)"}
},
{
"id": "bndl",
"type": "line",
"source": "ne-test",
"source-layer": "bndl",
"maxzoom": 8,
"paint": {"line-color": "rgba(138, 138, 138, 1)"}
},
{
"id": "popp",
"type": "symbol",
"source": "ne-test",
"source-layer": "popp",
"minzoom": 2,
"maxzoom": 8,
"layout": {
"text-font": ["sans"],
"text-field": "{NAME}",
"icon-image": "national_capital",
"text-offset": [1, 1]
}
}
],
"_ssl": true,
"metadata": {
"arcgisStyleUrl": "https://unvt.github.io/marble/esriIF/ne-test/VectorTileServer/resources/styles/",
"arcgisOriginalItemTitle": "test-style",
"arcgisEditorExtents": [
{
"xmin": -20037507.067161843,
"ymin": -20037508.342787,
"xmax": 20037507.067161843,
"ymax": 18440002.895114176,
"spatialReference": {
"wkid": 4326
}
},
{
"xmin": -20037507.067161843,
"ymin": -20037508.342787,
"xmax": 20037507.067161843,
"ymax": 18440002.895114176,
"spatialReference": {
"wkid": 4326
}
},
{
"xmin": -20037507.067161843,
"ymin": -20037508.342787,
"xmax": 20037507.067161843,
"ymax": 18440002.895114176,
"spatialReference": {
"wkid": 4326
}
},
{
"xmin": -20037507.067161843,
"ymin": -20037508.342787,
"xmax": 20037507.067161843,
"ymax": 18440002.895114176,
"spatialReference": {
"wkid": 4326
}
}
],
"arcgisMinimapVisibility": false
}
}
"url": "http://localhost:8836/esriIF/ne-test/VectorTileServer/",
esriIF.jsにおいて、esriIFにルーティングされた後は、
/:t/VectorTileServer = /ne-test/VectorTileServer
のルートでは、
./\${esriDir}/${t}/index.json = ./ersi/ne-test/index.json
が返されます。
・"_ssl": true,
"_ssl": true に設定されている場合、このスタイル設定で参照されるすべてのリソース(タイルデータ、スプライト、フォントなど)は、HTTPSプロトコルを使用してアクセスすることを期待しています。
• スタイルで参照されるURLがすべてHTTPSで提供される必要があります。
• 例: sprite, glyphs, sources に指定されるURLが HTTPS で始まっていることを確認する必要があります。
しかし、"url": "http://localhost:8836/esriIF/ne-test/VectorTileServer/",
はhttpを利用しています。開発時のローカルで環境では通常問題ないそうです。
・メタデータ
"arcgisStyleUrl": "https://unvt.github.io/marble/esriIF/ne-test/VectorTileServer/resources/styles/"
ArcGIS関連のツールやアプリケーションがこのスタイルをロードする際に使用されるURLです。
"arcgisEditorExtents": [
{
"xmin": -20037507.067161843,
"ymin": -20037508.342787,
"xmax": 20037507.067161843,
"ymax": 18440002.895114176,
"spatialReference": {
"wkid": 4326
}
},
...
]
地図エディターで編集や操作を行う際の地理的な範囲(エディターの可視範囲)を定義します。Web Mercator(EPSG:4326) で指定されています。同じ範囲が4回繰り返されているのが謎です。
"arcgisMinimapVisibility": false
最小化されたプレビュー用地図(ミニマップ)を表示するかどうかを指定します。
routes/VT.js
こちらのコードは以前と同様なので、説明は省略します。
その他
ここからの発展などはこちらの記事などにあります。
まとめ
本記事では、marbleレポジトリを解説しました。
Reference