はじめに
PostGISからベクトルタイルを作成するという記事で、小さく簡単なデータからベクトルタイルを作成しました。今回はもう少し進んで、PostgreSQL/PostGISデータベースが入っているサーバからデータを取ってきて、UN Clear Mapのベクトルタイルを作成してみようと思います。GitHubのこちらのページにあるコード等を参考にさせていただきます。
作成したコードはこちらにあります。
使用するコード
参考サイトのindex.jsと比べて、自身の設定に合わせるため、modifyファイルの読み込みをmodify2.jsと変更しています。
// libraries
const config = require('config');
const { spawn } = require('child_process');
const fs = require('fs');
const Queue = require('better-queue');
const pretty = require('prettysize');
const TimeFormat = require('hh-mm-ss');
const { Pool, Query } = require('pg');
const Spinner = require('cli-spinner').Spinner;
const winston = require('winston');
const DailyRotateFile = require('winston-daily-rotate-file');
const modify = require('./modify2.js');
// config constants
const relations = config.get('relations');
const pmtilesDir = config.get('pmtilesDir');
const logDir = config.get('logDir');
const spinnerString = config.get('spinnerString');
const fetchSize = config.get('fetchSize');
const tippecanoePath = config.get('tippecanoePath');
// global configurations
Spinner.setDefaultSpinnerString(spinnerString);
winston.configure({
level: 'silly',
format: winston.format.simple(),
transports: [
new DailyRotateFile({
filename: `${logDir}/produce-clearmap-%DATE%.log`,
datePattern: 'YYYY-MM-DD',
maxSize: '20m',
maxFiles: '14d',
}),
],
});
// global variable
let idle = true;
let modules = {};
let pools = {};
let productionSpinner = new Spinner();
let moduleKeysInProgress = [];
const isIdle = () => {
return idle;
};
const iso = () => {
return new Date().toISOString();
};
const noPressureWrite = (downstream, f) => {
return new Promise(res => {
if (downstream.write(`\x1e${JSON.stringify(f)}\n`)) {
res();
} else {
downstream.once('drain', () => {
res();
});
}
});
};
const fetch = (client, database, schema, table, downstream) => {
return new Promise((resolve, reject) => {
let count = 0;
let features = [];
client
.query(new Query(`FETCH ${fetchSize} FROM cur`))
.on('row', row => {
let f = {
type: 'Feature',
properties: row,
geometry: JSON.parse(row.st_asgeojson),
};
delete f.properties.st_asgeojson;
f.properties._database = database;
f.properties._schema = schema;
f.properties._table = table;
count++;
f = modify(f);
if (f) features.push(f);
})
.on('error', err => {
console.error(err.stack);
reject();
})
.on('end', async () => {
for (f of features) {
try {
await noPressureWrite(downstream, f);
} catch (e) {
reject(e);
}
}
resolve(count);
});
});
};
const dumpAndModify = async (relation, downstream, moduleKey) => {
return new Promise((resolve, reject) => {
const [database, schema, table] = relation.split('::');
if (!pools[database]) {
pools[database] = new Pool({
host: config.get(`connection.${database}.host`),
user: config.get(`connection.${database}.dbUser`),
port: config.get(`connection.${database}.port`),
password: config.get(`connection.${database}.dbPassword`),
database: database,
});
}
pools[database].connect(async (err, client, release) => {
if (err) throw err;
let sql = `SELECT column_name FROM information_schema.columns WHERE table_name='${table}' AND table_schema='${schema}' ORDER BY ordinal_position`;
let cols = await client.query(sql);
cols = cols.rows.map(r => r.column_name).filter(r => r !== 'geom');
//cols = cols.filter(v => !propertyBlacklist.includes(v))
//test--------------------------
if (table == 'unmap_wbya10_a') {
cols.push(`ST_Area(${schema}.${table}.geom) AS areacalc`);
cols.push(`ST_Length(${schema}.${table}.geom) AS lengthcalc`);
}
if (table == 'unmap_dral10_l') {
cols.push(`ST_Length(${schema}.${table}.geom) AS lengthcalc`);
}
//until here--------------------
cols.push(`ST_AsGeoJSON(${schema}.${table}.geom)`);
await client.query(`BEGIN`);
sql = `
DECLARE cur CURSOR FOR
SELECT ${cols.toString()} FROM ${schema}.${table}`;
cols = await client.query(sql);
try {
while (
(await fetch(client, database, schema, table, downstream)) !== 0
) {}
} catch (e) {
reject(e);
}
await client.query(`COMMIT`);
console.log(` ${iso()}: finished ${relation} of Area ${moduleKey}`);
winston.info(`${iso()}: finished ${relation} of ${moduleKey}`);
release();
resolve();
});
});
};
const sleep = wait => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, wait);
});
};
const queue = new Queue(
async (t, cb) => {
const startTime = new Date();
const moduleKey = t.moduleKey; //0-0-0
const tmpPath = `${pmtilesDir}/part-${moduleKey}.pmtiles`;
const dstPath = `${pmtilesDir}/${moduleKey}.pmtiles`;
moduleKeysInProgress.push(moduleKey);
productionSpinner.setSpinnerTitle(moduleKeysInProgress.join(', '));
const tippecanoe = spawn(
tippecanoePath,
[
'--quiet',
'--no-feature-limit',
'--no-tile-size-limit',
'--force',
'--simplification=2',
'--drop-rate=1',
'--minimum-zoom=0',
'--maximum-zoom=5',
'--base-zoom=5',
'--hilbert',
`--output=${tmpPath}`,
],
{ stdio: ['pipe', 'inherit', 'inherit'] }
);
tippecanoe.on('exit', () => {
fs.renameSync(tmpPath, dstPath);
moduleKeysInProgress = moduleKeysInProgress.filter(
v => !(v === moduleKey)
);
productionSpinner.stop();
process.stdout.write('\n');
const logString = `${iso()}: process ${moduleKey} (${pretty(
fs.statSync(dstPath).size
)}) took ${TimeFormat.fromMs(new Date() - startTime)} .`;
winston.info(logString);
console.log(logString);
if (moduleKeysInProgress.length !== 0) {
productionSpinner.setSpinnerTitle('0-0-0');
productionSpinner.start();
}
return cb();
});
productionSpinner.start();
for (relation of relations) {
while (!isIdle()) {
winston.info(`${iso()}: short break due to heavy disk writes.`);
await sleep(5000);
}
try {
await dumpAndModify(relation, tippecanoe.stdin, moduleKey);
} catch (e) {
winston.error(e);
cb(true);
}
}
tippecanoe.stdin.end();
},
{
concurrent: 1,
maxRetries: 3,
retryDelay: 1000,
}
);
// push queue
const queueTasks = () => {
for (let moduleKey of ['0-0-0']) {
// For global, only one push!
queue.push({
moduleKey: moduleKey,
});
}
};
// shutdown system
const shutdown = () => {
winston.info(`${iso()}: production system shutdown.`);
console.log('** production system for clearmap shutdown! **');
};
// main
const main = async () => {
winston.info(`${iso()}: clearmap production started.`);
queueTasks();
queue.on('drain', () => {
shutdown();
});
};
main();
コードの解説
以前の記事で説明済みのコードについては、今回は割愛します。また使用しているライブラリについては、一番下のReferenceにリンクをつけており、そちらで説明をしています。
winston.configure({
level: 'silly',
format: winston.format.simple(),
transports: [
new DailyRotateFile({
filename: `${logDir}/produce-clearmap-%DATE%.log`,
datePattern: 'YYYY-MM-DD',
maxSize: '20m',
maxFiles: '14d',
}),
],
});
winston.configure()はグローバルロガーを設定するためのメソッドで、アプリケーション全体で一貫したロギングを行うには有効ですが、個別のロガーを作成して管理する場合にはwinston.createLogger()の方が柔軟です。こちらにwinstonモジュールの記事があります。
let idle = true
const isIdle = () => {
return idle
}
このコードでは、idleというグローバル変数が定義されているものの、その値がfalseになる条件が定義されていません。現在はデフォルトでtrueに設定されており、isIdle()関数も常にtrueを返します。これにより、コード内の「アイドル状態を監視する機能」が正しく機能していないことになります。
if (table == 'unmap_wbya10_a') {
cols.push(`ST_Area(${schema}.${table}.geom) AS areacalc`);
cols.push(`ST_Length(${schema}.${table}.geom) AS lengthcalc`);
}
if (table == 'unmap_dral10_l') {
cols.push(`ST_Length(${schema}.${table}.geom) AS lengthcalc`);
}
特定のテーブルに対して追加のジオメトリ計算(ST_AreaやST_Length)を実行し、それをSQLクエリに組み込む処理をしています。
こちらの記事でClear Mapベクトルタイルの各レイヤについて説明しました。名称は少し違うのですが、「unmap_wbya10_a」は河川域や湖沼域のポリゴンを表していそうです。「unmap_dral10_l」は河川のラインを表しているものと思います。
・cols.push(ST_Area(${schema}.${table}.geom) AS areacalc
);
ジオメトリ列(geom)の多角形の面積を計算します。ここでは、ST_Area(\${schema}.${table}.geom)で、指定されたスキーマとテーブルのジオメトリの面積を取得し、その結果をareacalcというエイリアス(別名)としてクエリに含めています。
・cols.push(ST_Length(${schema}.${table}.geom) AS lengthcalc
);
ジオメトリ列(geom)の周長(ポリゴンの周りの長さ)や、ラインストリング(線)に対してその全体の長さを計算します。unmap_wbya10_aがポリゴンデータだとすると、ポリゴンの周長を計算しています。この結果はlengthcalcというエイリアスでクエリに含まれます。
・cols.push(ST_Length(${schema}.${table}.geom) AS lengthcalc
);
ラインストリング(線)のジオメトリの全体の長さを計算して、lengthcalcというエイリアスとしてクエリに含めています。
const sleep = (wait) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, wait);
});
};
指定した時間(ミリ秒)だけ待機する非同期関数 sleep を実装したものです。具体的には、setTimeout を使って一定時間が経過した後に Promise を解決(resolve)させることで、他の非同期処理の中で一定時間待つことができるようにしています。
const queue = new Queue(
async (t, cb) => {
引数tにはオブジェクトが入ってきます。例としては、t = { moduleKey: '0-0-0' } のようなものです。
const moduleKey = t.moduleKey; //0-0-0
moduleKeysInProgress.push(moduleKey);
productionSpinner.setSpinnerTitle(moduleKeysInProgress.join(', '));
t.moduleKeyは'0-0-0'のような文字列が入ってきます。そのため、配列moduleKeysInProgressは['0-0-0']のようになり、moduleKeysInProgress.join(', ')は'0-0-0, 0-0-1, 0-0-2'のような文字列になるはずです。しかし、このコードでは、「concurrent: 1」と設定されており、一度にキューに入る処理は一つであるため、このような3つのモジュールが表示されるということはなさそうです。
このコードがあることで、スピナーが回転しながら、ユーザーにこれらのモジュールが処理されていることを示します。
const tippecanoe = spawn(
tippecanoePath,
[
'--quiet',
'--no-feature-limit',
'--no-tile-size-limit',
'--force',
'--simplification=2',
'--drop-rate=1',
'--minimum-zoom=0',
'--maximum-zoom=5',
'--base-zoom=5',
'--hilbert',
`--output=${tmpPath}`,
],
以下はtippecanoeのオプションです。
'--quiet':
コマンド実行中の詳細な出力を抑えるオプションです。tippecanoe が行う処理のログを抑制し、出力を最小限にします。
'--no-feature-limit',:
タイルに含めるフィーチャの数に制限を設けません。デフォルトでは、tippecanoe はパフォーマンスのためにフィーチャ数を制限しますが、このオプションを使うと制限が解除されます。
'--no-tile-size-limit',:
タイルのサイズに制限を設けません。デフォルトでは、各タイルのサイズは制限されていますが、このオプションでその制限を解除します。
'--force',:
出力ファイルが既に存在しても上書きします。通常は既存のファイルがあると警告されますが、このオプションを使うと自動的に上書きします。
'--simplification=2',:
形状の簡略化のレベルを指定します。これは、ジオメトリの詳細度をどの程度落とすかを決めます。値が低いほど詳細なタイルが生成されます。
'--drop-rate=1',:
データを間引く割合を設定します。これは、特にズームアウトしたときに、詳細なフィーチャをどの程度省略するかを決定します。大きな値となるほど間引いて、1だとあまり間引かないような気がしていますが、詳細不明です。今後実験してみる必要があるかもしれません。デフォルトは2.5です。
'--minimum-zoom=0':
作成するタイルの最小ズームレベルを 0 に設定します。
'--maximum-zoom=5':
作成するタイルの最大ズームレベルを 5 に設定します。
'--base-zoom=5',:
ベースとなるズームレベルを 5 に設定します。このレベルでのタイルをベースにし、そこから他のズームレベル用のタイルを生成します。つまり、このズームレベルには全てのフィーチャーが含まれているということだと思います。
'--hilbert',:
ヒルベルト曲線を使った空間インデックスを作成します。これは、タイルの順序付けを改善し、データをより効率的に格納するための技術です。
--output=${tmpPath}
:
出力ファイルのパスを指定します。tmpPath には、一時的な PMTiles ファイルのパスが指定されており、tippecanoe はこのパスにベクトルタイルを出力します。
tippecanoe.on('exit', () => {
fs.renameSync(tmpPath, dstPath);
moduleKeysInProgress = moduleKeysInProgress.filter(
v => !(v === moduleKey)
);
productionSpinner.stop();
process.stdout.write('\n');
const logString = `${iso()}: process ${moduleKey} (${pretty(
fs.statSync(dstPath).size
)}) took ${TimeFormat.fromMs(new Date() - startTime)} .`;
winston.info(logString);
console.log(logString);
if (moduleKeysInProgress.length !== 0) {
productionSpinner.setSpinnerTitle('0-0-0');
productionSpinner.start();
}
return cb();
});
・tippecanoe.on('exit', () => {
Tippecanoeプロセスが終了したときに実行されるイベントリスナーを設定します。'exit' イベントは、プロセスが正常に終了した際に発火します。
・fs.renameSync(tmpPath, dstPath);
tmpPath(一時ファイルのパス)を dstPath(最終的な保存先のパス)に名前を変更します。この操作により、一時ファイルが最終的なファイルとして保存されます。
・moduleKeysInProgress = moduleKeysInProgress.filter(
(v) => !(v === moduleKey)
);
この部分は、現在進行中のモジュールキーのリストから、現在の moduleKey を削除します。filter メソッドを使用して、moduleKeysInProgress 配列から moduleKey と一致しない値だけを残しています。
・productionSpinner.stop();
進行状況を表示するスピナーを停止します。これは、処理が完了したことを示すためです。
・process.stdout.write('\n');
標準出力に改行を出力します。これにより、次の出力が新しい行から始まるようになります。
・const logString = ${iso()}: process ${moduleKey} (${pretty( fs.statSync(dstPath).size )}) took ${TimeFormat.fromMs(new Date() - startTime)} .
;
fs.statSync(dstPath).size:最終的なファイルのサイズをバイト単位で取得し、pretty() で人間が読みやすい形式に変換しています。fs.statSync().sizeについては、こちらのサイトに説明があります。
・winston.info(logString);
console.log(logString);
生成した logString を winston ロガーを使用してログに記録し、コンソールにも出力します。これにより、処理の結果がログとして記録され、開発者が確認できるようになります。
・ if (moduleKeysInProgress.length !== 0) {
productionSpinner.setSpinnerTitle('0-0-0');
productionSpinner.start();
}
進行中のモジュールキーが残っている場合、スピナーのタイトルを '0-0-0'に設定し、再びスピナーを開始します。これは、まだ他の処理が続いていることを示します。
・return cb();
コールバック関数 cb() を呼び出して、処理の終了を通知します。これにより、次の処理が開始できるようになります。
while (!isIdle()) {
winston.info(`${iso()}: short break due to heavy disk writes.`);
await sleep(5000);
}
システムがディスクへの書き込みを行っている間、処理を一時停止させるためのループですが、isIdle()がfalseとなることはないため、関数sleepが実行されることはありません。
tippecanoe.stdin.end();
Tippecanoeプロセスに対して、標準入力ストリームを終了するよう指示するもので、入力データの送信が完了したことを示します。
{
concurrent: 1,
maxRetries: 3,
retryDelay: 1000,
}
・concurrent: 1,
同時に実行できるプロセスの数を指定します。この場合、1 と設定されているため、同時に1つのプロセスのみが実行されることを意味します。これにより、リソースの競合や過負荷を防ぎ、システムが安定して動作するようにします。
・maxRetries: 3,
失敗した場合に再試行する最大回数を指定します。
・retryDelay: 1000,
リトライを行う間隔をミリ秒単位で指定します。ここでは、再試行を行う際に1000ミリ秒(1秒)の遅延を設ける設定です。
const main = async () => {
winston.info(`${iso()}: clearmap production started.`);
queueTasks();
queue.on("drain", () => {
shutdown();
});
};
・queue.on("drain", () => {
queue というオブジェクトに対して、drain イベントのリスナーを設定しています。drain イベントは、キューが空になったとき(すべてのタスクが処理されたとき)に発生します。
コードの実行順番
tippecanoe.on('exit', () => { ... }) が発生するタイミングは、 全ての relations が処理された後です。
具体的には、for (relation of relations) ループ内で、各リレーション (relation) に対して dumpAndModify 関数を実行し、リレーションからデータを取得して tippecanoe.stdin に書き込んでいきます。このループがすべてのリレーションを処理し終わると、tippecanoe.stdin.end();の行で標準入力 (tippecanoe.stdin) が閉じられます。
tippecanoe.stdin.end() が呼ばれると、Tippecanoeプロセスが入力の完了を認識し、タイル生成処理を終了します。すると、tippecanoe.on("exit", ...) がトリガーされ、Tippecanoeプロセスの終了後の処理(ファイルのリネーム、スピナーの停止、ログの記録など)が実行されます。
その他のファイルのコード
modify2.js
まずは、UN Clear Mapのデータではなく、自身が持っているデータで試してみます。index.jsではviewではなく、tableという文字列が使用されているため、それに合わせて記述を変更しています。
const preProcess = f => {
f.tippecanoe = {
layer: 'other',
minzoom: 15,
maxzoom: 15,
};
return f;
};
const postProcess = f => {
delete f.properties['_database'];
// delete f.properties["_view"];
delete f.properties['_table'];
return f;
};
const layerEdit = {
baea_nests: f => {
f.tippecanoe = {
layer: 'testLayer1-point',
minzoom: 3,
maxzoom: 6,
};
//write someting to adjust properties, if needed
return f;
},
linear_projects: f => {
f.tippecanoe = {
layer: 'testLayer2-line',
minzoom: 4,
maxzoom: 5,
};
//write someting to adjust properties, if needed
return f;
},
};
module.exports = f => {
// return postProcess(layerEdit[f.properties._view](preProcess(f)));
return postProcess(layerEdit[f.properties._table](preProcess(f)));
};
config/default.hjson
自身の設定に合わせて記述を変更しています。
{
relations: [
sdb_course::public::baea_nests
sdb_course::public::linear_projects
]
connection:{
sdb_course:{
host: localhost
port: 5432
dbUser: postgres
dbPassword: postgres
}
}
spinnerString: 1
fetchSize: 10
pmtilesDir: pmtiles
logDir: log
tippecanoePath: tippecanoe
}
実行結果
コンソール
一番最後の「0-0-0」が、どこから表示されたのか不明ですが、きちんと実行されていそうです。
ログファイル
info: 2024-09-25T20:48:18.074Z: clearmap production started.
info: 2024-09-25T20:48:18.202Z: finished sdb_course::public::baea_nests of 0-0-0
info: 2024-09-25T20:48:18.238Z: finished sdb_course::public::linear_projects of 0-0-0
info: 2024-09-25T20:48:18.353Z: process 0-0-0 (42.6 kB) took 00:00.272 .
info: 2024-09-25T20:48:18.355Z: production system shutdown.
こちらもきちんと出力されています。
作成されたpmtilesファイル
作成したPMTilesファイルは以下のURLのものです。
https://k96mz.github.io/20240924Postgis2vector-ClearMap-/pmtiles/0-0-0.pmtiles
PMTiles Viewerのページで上記URLを使用するとデータが以下のように見られます。
コード改良版
こちらの記事のtest006-2.jsから必要なコードを加えたものがこちらです。必要なライブラリ、変数、設定などを追記しました。逆にindex.jsに存在した使用されていない、変数idle, modules, 関数isIdle, 関数sleepなどは記載していません。変数名について、 viewをtableに変更、streamをdownstreamに変更しています。
// Libraries
const config = require('config');
const { Pool } = require('pg');
const Cursor = require('pg-cursor');
const modify = require('./modify2.js');
const { spawn } = require('child_process');
const fs = require('fs');
const Queue = require('better-queue');
const pretty = require('prettysize');
const TimeFormat = require('hh-mm-ss');
const Spinner = require('cli-spinner').Spinner;
const winston = require('winston');
const DailyRotateFile = require('winston-daily-rotate-file');
// config constants
const relations = config.get('relations');
const fetchSize = config.get('fetchSize');
const tippecanoePath = config.get('tippecanoePath');
const pmtilesDir = config.get('pmtilesDir');
const logDir = config.get('logDir');
const spinnerString = config.get('spinnerString');
// global configurations
Spinner.setDefaultSpinnerString(spinnerString);
winston.configure({
level: 'silly',
format: winston.format.simple(),
transports: [
new DailyRotateFile({
filename: `${logDir}/produce-clearmap-%DATE%.log`,
datePattern: 'YYYY-MM-DD',
maxSize: '20m',
maxFiles: '14d',
}),
],
});
// global variable
const pools = {};
const productionSpinner = new Spinner();
let moduleKeysInProgress = [];
const iso = () => {
return new Date().toISOString();
};
const noPressureWrite = (downstream, f) => {
return new Promise(res => {
if (downstream.write(`\x1e${JSON.stringify(f)}\n`)) {
res();
} else {
downstream.once('drain', () => {
res();
});
}
});
};
// viewをtableに変更、streamをdownstreamに変更
const fetch = async (database, schema, table, cursor, downstream) => {
try {
const rows = await cursor.read(fetchSize);
if (rows.length === 0) {
// 終了条件
return 0;
}
const features = rows.map(row => {
let f = {
type: 'Feature',
properties: row,
geometry: JSON.parse(row.st_asgeojson),
};
delete f.properties.st_asgeojson;
f.properties._database = database;
// schemaの記載を追記
f.properties._schema = schema;
f.properties._table = table;
f = modify(f);
return f;
});
for (const f of features) {
try {
// console.log(f);
await noPressureWrite(downstream, f);
} catch (err) {
throw err;
}
}
return rows.length;
} catch (err) {
// schemaの記載を追記
console.error(
`Error in fetch function for ${schema}.${table} in ${database}:`,
err
);
throw err;
}
};
const dumpAndModify = async (relation, downstream, moduleKey) => {
const [database, schema, table] = relation.split('::');
if (!pools[database]) {
pools[database] = new Pool({
host: config.get(`connection.${database}.host`),
user: config.get(`connection.${database}.dbUser`),
port: config.get(`connection.${database}.port`),
password: config.get(`connection.${database}.dbPassword`),
database: database,
});
}
let client, cursor;
try {
// プールへの接続
client = await pools[database].connect();
// プレースホルダを使用してカラムの取得
let sql = `
SELECT column_name
FROM information_schema.columns
WHERE table_schema = $1
AND table_name = $2
ORDER BY ordinal_position`;
let cols = await client.query(sql, [schema, table]);
// geomカラムの削除
cols = cols.rows.map(r => r.column_name).filter(r => r !== 'geom');
//cols = cols.filter(v => !propertyBlacklist.includes(v))
//test--------------------------
if (table == 'unmap_wbya10_a') {
cols.push(`ST_Area(${schema}.${table}.geom) AS areacalc`);
cols.push(`ST_Length(${schema}.${table}.geom) AS lengthcalc`);
}
if (table == 'unmap_dral10_l') {
cols.push(`ST_Length(${schema}.${table}.geom) AS lengthcalc`);
}
//until here--------------------
// カラムの最後にGeoJSON化したgeomを追加
cols.push(`ST_AsGeoJSON(${schema}.${table}.geom)`);
await client.query('BEGIN');
// カラムの文字列化
sql = `SELECT ${cols.toString()} FROM ${schema}.${table}`;
cursor = await client.query(new Cursor(sql));
// 全てのデータが読み込まれるまで繰り返し
while ((await fetch(database, schema, table, cursor, downstream)) !== 0) {}
await client.query(`COMMIT`);
console.log(` ${iso()}: finished ${relation} of Area ${moduleKey}`);
winston.info(`${iso()}: finished ${relation} of ${moduleKey}`);
} catch (err) {
console.error(
`Error executing query for ${schema}.${table} in ${database}:`,
err
);
// エラーが発生した場合はロールバック
if (client) {
await client.query('ROLLBACK');
}
throw err;
} finally {
if (cursor) {
await cursor.close();
}
if (client) {
client.release();
}
}
};
const queue = new Queue(
async (t, cb) => {
const startTime = new Date();
const moduleKey = t.moduleKey; //0-0-0
const tmpPath = `${pmtilesDir}/part-${moduleKey}.pmtiles`;
const dstPath = `${pmtilesDir}/${moduleKey}.pmtiles`;
moduleKeysInProgress.push(moduleKey);
productionSpinner.setSpinnerTitle(moduleKeysInProgress.join(', '));
const tippecanoe = spawn(
tippecanoePath,
[
'--quiet',
'--no-feature-limit',
'--no-tile-size-limit',
'--force',
'--simplification=2',
'--drop-rate=1',
'--minimum-zoom=0',
'--maximum-zoom=5',
'--base-zoom=5',
'--hilbert',
`--output=${tmpPath}`,
],
{ stdio: ['pipe', 'inherit', 'inherit'] }
);
tippecanoe.on('exit', () => {
fs.renameSync(tmpPath, dstPath);
moduleKeysInProgress = moduleKeysInProgress.filter(
v => !(v === moduleKey)
);
productionSpinner.stop();
process.stdout.write('\n');
const logString = `${iso()}: process ${moduleKey} (${pretty(
fs.statSync(dstPath).size
)}) took ${TimeFormat.fromMs(new Date() - startTime)} .`;
winston.info(logString);
console.log(logString);
if (moduleKeysInProgress.length !== 0) {
productionSpinner.setSpinnerTitle('0-0-0');
productionSpinner.start();
}
return cb();
});
productionSpinner.start();
for (const relation of relations) {
try {
await dumpAndModify(relation, tippecanoe.stdin, moduleKey);
} catch (err) {
winston.error(err);
cb(true);
}
}
tippecanoe.stdin.end();
},
{
concurrent: 1,
maxRetries: 3,
retryDelay: 1000,
}
);
// push queue
const queueTasks = () => {
for (const moduleKey of ['0-0-0']) {
// For global, only one push!
queue.push({
moduleKey: moduleKey,
});
}
};
// disconnect pools
const closePools = async () => {
for (const db in pools) {
try {
await pools[db].end();
winston.info(`${iso()}: Closed pool for ${db}.`);
} catch (err) {
winston.error(`Error closing pool for ${db}: ${err}`);
}
}
};
// shutdown system
const shutdown = async () => {
await closePools();
winston.info(`${iso()}: production system shutdown.`);
console.log('** production system for clearmap shutdown! **');
};
// main
const main = async () => {
winston.info(`${iso()}: clearmap production started.`);
queueTasks();
queue.on('drain', async () => {
await shutdown();
});
};
main();
関数dumpAndModifyについて、index.jsではPromiseでのラップを行っていましたが、改良版コードでは行っていません。これは、関数dumpAndModifyの中に含まれる非同期処理関数がpromiseを返すため、Promiseでラップする必要がないからです。await が使用できる関数は Promise を返す関数ですので、awaitがついている関数の場合はPromiseでラップする必要はありません。
// disconnect pools
const closePools = async () => {
for (const db in pools) {
try {
await pools[db].end();
winston.info(`${iso()}: Closed pool for ${db}.`);
} catch (err) {
winston.error(`Error closing pool for ${db}: ${err}`);
}
}
};
今までpool接続していた関数について、接続を切る操作をしていなかったので、その操作を加えました。このコードに伴い、後続の関数shutdown、関数mainにawait closePools(), async, awaitなどの記載を加えました。
index-2.jsでも正常にpmtilesファイルを作成することが出来ました。
UN Clear Mapのベクトルタイルを作成する
冒頭でも記載したとおり、GitHubのこちらのページのmodify.jsと、config/default-sample.hjsonのコードを参照して、UN Clear Mapのベクトルタイルを作成します。コード自身が長いので、コード内に解説を記載していきます。
const preProcess = f => {
f.tippecanoe = {
layer: 'other',
minzoom: 5,
maxzoom: 5,
};
// name
// 複数の名前フィールドの中から優先度を持って名前を選択・統一する処理を行っている
if (
f.properties.hasOwnProperty('en_name') ||
f.properties.hasOwnProperty('int_name') ||
f.properties.hasOwnProperty('name') ||
f.properties.hasOwnProperty('ar_name')
// hasOwnProperty() は、オブジェクトの自分自身が持っているプロパティをチェックし、booleanを返す
) {
let name = '';
// 'en_name'と'int_name'があった場合には、'en_name'が優先される
if (f.properties['en_name']) {
name = f.properties['en_name'];
} else if (f.properties['int_name']) {
name = f.properties['int_name'];
} else if (f.properties['name']) {
name = f.properties['name'];
} else {
name = f.properties['ar_name'];
}
// 元のプロパティを削除し、選んだ名前を f.properties.name に統一して保存
delete f.properties['en_name'];
delete f.properties['ar_name'];
delete f.properties['int_name'];
delete f.properties['name'];
f.properties.name = name;
}
return f;
};
const postProcess = f => {
if (f !== null) {
delete f.properties['_database'];
delete f.properties['_table'];
}
return f;
};
const lut = {
custom_planet_land_08_a: f => {
f.tippecanoe = {
layer: 'landmass',
minzoom: 0,
maxzoom: 5,
};
delete f.properties['objectid'];
delete f.properties['fid_1'];
return f;
},
custom_planet_ocean_08_a: f => {
f.tippecanoe = {
layer: 'ocean',
minzoom: 0,
maxzoom: 5,
};
delete f.properties['objectid'];
delete f.properties['fid_1'];
return f;
},
custom_ne_10m_bathymetry_a: f => {
f.tippecanoe = {
layer: 'bathymetry',
minzoom: 2,
maxzoom: 5,
};
delete f.properties['objectid'];
delete f.properties['fid_1'];
return f;
},
un_glc30_global_lc_ss_a: f => {
f.tippecanoe = {
layer: 'landcover',
minzoom: 4,
maxzoom: 5,
};
delete f.properties['id'];
delete f.properties['objectid'];
delete f.properties['objectid_1'];
if (f.properties.gridcode == 80) {
//only urban
// if (f.properties.gridcode == 20 || f.properties.gridcode == 30 || f.properties.gridcode == 80){
return f;
} else {
return null;
}
},
unmap_bndl_l: f => {
f.tippecanoe = {
layer: 'bndl',
minzoom: 5,
maxzoom: 5,
};
delete f.properties['objectid'];
delete f.properties['bdytyp_code'];
delete f.properties['iso3cd'];
delete f.properties['globalid'];
//no need admin 1 and 2 for ZL5
if (f.properties.bdytyp == '6' || f.properties.bdytyp == '7') {
return null;
} else {
return f;
}
},
unmap_bndl05_l: f => {
f.tippecanoe = {
layer: 'bndl',
minzoom: 3,
maxzoom: 4,
};
delete f.properties['objectid'];
delete f.properties['bdytyp_code'];
delete f.properties['iso3cd'];
delete f.properties['globalid'];
//no need admin 1 and 2 for small scale
if (
f.properties.bdytyp == '6' ||
f.properties.bdytyp == '7' ||
f.properties.bdytyp == '8'
) {
return null;
} else {
return f;
}
},
unmap_bndl25_l: f => {
f.tippecanoe = {
layer: 'bndl',
minzoom: 0,
maxzoom: 2,
};
delete f.properties['objectid'];
delete f.properties['bdytyp_code'];
delete f.properties['iso3cd'];
delete f.properties['globalid'];
//no need admin 1 and 2 for small scale
if (
f.properties.bdytyp == '6' ||
f.properties.bdytyp == '7' ||
f.properties.bdytyp == '8'
) {
return null;
} else {
return f;
}
},
custom_ne_rivers_lakecentrelines_l: f => {
f.tippecanoe = {
layer: 'un_water',
maxzoom: 5,
};
if (
f.properties.scalerank == 1 ||
f.properties.scalerank == 2 ||
f.properties.scalerank == 3 ||
f.properties.scalerank == 4
) {
f.tippecanoe.minzoom = 3;
} else if (
f.properties.scalerank == 5 ||
f.properties.scalerank == 6 ||
f.properties.scalerank == 7
) {
f.tippecanoe.minzoom = 4;
} else {
f.tippecanoe.minzoom = 5;
}
delete f.properties['objectid'];
delete f.properties['strokeweig'];
delete f.properties['dissolve'];
delete f.properties['note'];
return f;
},
unmap_bnda_cty_anno_03_p: f => {
f.tippecanoe = {
layer: 'lab_cty',
minzoom: 1,
maxzoom: 2,
};
if (f.properties.annotationclassid == 2) {
f.properties.labtyp = 5; // NSG
} else if (f.properties.annotationclassid == 5) {
f.properties.labtyp = 2; //SG
} else {
f.properties.labtyp = f.properties.annotationclassid;
}
if (f.properties.status == 1) {
return null;
} else {
return f;
}
},
unmap_bnda_cty_anno_04_p: f => {
f.tippecanoe = {
layer: 'lab_cty',
minzoom: 3,
maxzoom: 3,
};
f.properties.labtyp = f.properties.annotationclassid;
if (f.properties.status == 1) {
return null;
} else {
return f;
}
},
unmap_bnda_cty_anno_05_p: f => {
f.tippecanoe = {
layer: 'lab_cty',
minzoom: 4,
maxzoom: 4,
};
f.properties.labtyp = f.properties.annotationclassid;
if (f.properties.status == 1) {
return null;
} else {
return f;
}
},
unmap_bnda_cty_anno_06_p: f => {
f.tippecanoe = {
layer: 'lab_cty',
minzoom: 5,
maxzoom: 5,
};
f.properties.labtyp = f.properties.annotationclassid;
if (f.properties.status == 1) {
return null;
} else {
return f;
}
},
unmap_phyp_label_04_p: f => {
f.tippecanoe = {
layer: 'lab_water',
minzoom: 3,
maxzoom: 3,
};
//Ocean minz 1, Bay Sea minz3
if (
f.properties.annotationclassid == 0 ||
f.properties.annotationclassid == 1
) {
f.tippecanoe.minzoom = 1;
} else if (
f.properties.annotationclassid == 2 ||
f.properties.annotationclassid == 3 ||
f.properties.annotationclassid == 4 ||
f.properties.annotationclassid == 5
) {
f.tippecanoe.minzoom = 2;
} else {
f.tippecanoe.minzoom = 4;
}
delete f.properties['zorder'];
delete f.properties['element'];
delete f.properties['bold'];
delete f.properties['bold_resolved'];
delete f.properties['italic'];
delete f.properties['italic_resolved'];
delete f.properties['underline'];
delete f.properties['underline_resolved'];
delete f.properties['verticalalignment'];
delete f.properties['horizontalalignment'];
delete f.properties['verticalalignment_resolved'];
delete f.properties['horizontalalignment_resolved'];
delete f.properties['xoffset'];
delete f.properties['yoffset'];
delete f.properties['angle'];
delete f.properties['fontleading'];
delete f.properties['wordspacing'];
delete f.properties['characterwidth'];
delete f.properties['characterspacing'];
delete f.properties['flipangle'];
delete f.properties['orid_fid'];
delete f.properties['override'];
if (f.properties.status == 1) {
return null;
} else {
return f;
}
},
unmap_phyp_label_06_p: f => {
f.tippecanoe = {
layer: 'lab_water',
minzoom: 4,
maxzoom: 5,
};
// if (f.properties.annotationclassid == 6) {
// f.tippecanoe.minzoom = 5
// }
delete f.properties['zorder'];
delete f.properties['element'];
delete f.properties['bold'];
delete f.properties['bold_resolved'];
delete f.properties['italic'];
delete f.properties['italic_resolved'];
delete f.properties['underline'];
delete f.properties['underline_resolved'];
delete f.properties['verticalalignment'];
delete f.properties['horizontalalignment'];
delete f.properties['verticalalignment_resolved'];
delete f.properties['horizontalalignment_resolved'];
delete f.properties['xoffset'];
delete f.properties['yoffset'];
delete f.properties['angle'];
delete f.properties['fontleading'];
delete f.properties['wordspacing'];
delete f.properties['characterwidth'];
delete f.properties['characterspacing'];
delete f.properties['flipangle'];
delete f.properties['orid_fid'];
delete f.properties['override'];
if (f.properties.status == 1) {
return null;
} else {
return f;
}
},
unmap_phyp_p: f => {
f.tippecanoe = {
layer: 'phyp_label',
minzoom: 5,
maxzoom: 5,
};
if (f.properties.type == 4 && !/Sea|Ocean|Gulf/.test(f.properties.name)) {
// type が 4 で、かつ名前が “Sea”、“Ocean”、“Gulf” を含まない場合は条件を満たす
// /は正規表現リテラルを示す開始と終了のスラッシュです。これで正規表現を囲む
// test()は指定された文字列が正規表現にマッチするかどうかを判定するメソッド
return f;
} else {
return null;
}
},
unmap_wbya10_a: f => {
f.tippecanoe = {
layer: 'wbya10',
minzoom: 2,
maxzoom: 5,
};
// delete f.properties['objectid']
if (f.properties.areacalc > 2) {
//L1
f.tippecanoe.minzoom = 0;
} else if (
f.properties.areacalc > 1 &&
f.properties.lengthcalc > 5 &&
f.properties.name != 'Mackenzie River'
) {
//L2-L3
f.tippecanoe.minzoom = 1;
} else if (/Ontario/.test(f.properties.name)) {
//L2-L3
f.tippecanoe.minzoom = 1;
} else if (f.properties.name == 'Lake Erie') {
//L2-L3
f.tippecanoe.minzoom = 1;
} else if ([479, 161].includes(f.properties.objectid)) {
// f.properties.objectidの値が479 or 161かどうかを確認している
f.tippecanoe.minzoom = 1;
} else if (f.properties.areacalc > 0.13 && f.properties.cnty != 'CAN') {
//L4
f.tippecanoe.minzoom = 3; //L5 is the same with L4
} else if ([165, 479, 229, 231, 349].includes(f.properties.objectid)) {
f.tippecanoe.minzoom = 3;
} else if (
f.properties.areacalc > 0.05 &&
f.properties.type == 1 &&
f.properties.perenniality != 2
) {
//ZL6
f.tippecanoe.minzoom = 5;
} else if (
f.properties.areacalc > 0.15 &&
f.properties.lengthcalc > 4.5 &&
f.properties.perenniality != 2
) {
//ZL6
f.tippecanoe.minzoom = 5;
} else if (
[2, 3].includes(f.properties.type) &&
f.properties.lengthcalc > 10 &&
f.properties.perenniality != 2
) {
//ZL6
f.tippecanoe.minzoom = 5;
} else if (
/Huai He|Nile River|Sor Mertvyy Kultu|Bahret Assad|Lake Nasser|Nun River|Sanaga|Lac Debo|Tagus River|Loire|Wisla|Waal|Rijn|Danube|Dnepr|Kiyevskoye Vodokhranilishche|Pura|Portnyagino|Kungasalakh|Suolama|Bykovskaya|Lulonga|Fimi|Busira|Ruki|Kainji Reservoir|Zambeze|Lago De Nicaragua|Missouri River|Niger|Pechora|Don|Rio Ica|Rock River|Tennessee River|Conowingo Reservoir/.test(
f.properties.name
) &&
!/Rio Guaviare/.test(f.properties.name)
) {
//ZL6
f.tippecanoe.minzoom = 5;
} else if (
[164745, 212173, 165788, 272373, 303258, 302487].includes(
f.properties.objectid
)
) {
f.tippecanoe.minzoom = 5;
} else {
//L7
f.tippecanoe.minzoom = 6;
}
delete f.properties['fid_1'];
delete f.properties['lengthcalc'];
return f;
},
unmap_wbya_label_05_p: f => {
f.tippecanoe = {
layer: 'lab_inwater',
minzoom: 4,
maxzoom: 4,
};
if (f.properties.status == 1) {
return null;
} else {
return f;
}
},
unmap_wbya_label_06_p: f => {
f.tippecanoe = {
layer: 'lab_inwater',
minzoom: 5,
maxzoom: 5,
};
if (f.properties.status == 1) {
return null;
} else {
return f;
}
},
unmap_dral10_l: f => {
f.tippecanoe = {
layer: 'dral10',
minzoom: 5,
maxzoom: 5,
};
if (f.properties.lengthcalc >= 13.6 && f.properties.type == 1) {
//L4
f.tippecanoe.minzoom = 3;
} else if (
/Yukon|Saskatchewan|Missouri|Mississip|Parana|Araguaia|Xingu|Maranon|Amazonas|Oka|Duna|Don|Volga|Odra|Wisla|Rijn|Rhein|Loire|Indus|Aldan|Lena|Ket|Ob|Gang|Nil|Al Bahr|Abay Wenz|Congo|Niger|Benue|Benoue|Uele|Lualaba|Zambeze|Zambezi|Orange|Brahmaputra|Yangtze|Huang He/.test(
f.properties.name
)
) {
//L5
f.tippecanoe.minzoom = 3; //may need to add a filter with objectid
} else if (
[
2437, 2954, 2955, 3034, 3060, 3090, 3103, 3300, 3311, 3494, 2270, 2990,
1079,
].includes(f.properties.objectid)
) {
//L4
f.tippecanoe.minzoom = 3;
} else if (f.properties.lengthcalc >= 9.2 && f.properties.type == 1) {
//L5
f.tippecanoe.minzoom = 4;
} else if (
/Slave|Ob|Malaya Ob|Kuonamka|Odra|Weser|Pryp|Wisla|Nemunas|Daugava|Oka|Dnipro|Prut|Duna|Rhin|Rijn|Seine|Loire|Don|Volga|Sirdar|Indus|Argun|Kolyma|Alazeya|Indigirka|Aldan|Lena|Khatanga|Kheta|Pyasina|Heilong|Amur|Yangtze|Huang|Brahmaputra|Gang|Ayeyarwady|Menam|Mekong|Murray|Rhein|Parana|Maranon|Araguaia|Xingu|Mamore|Japura|Colorado|Hudson|Missouri|Columbia|Snake|Ohio|Mississip|Yukon|Red|Peace|Kasai|Saskatchewan|Cuanza|Wenz|Chari|Benue|Orange|Benoue|Logone|Senegal|Webi|Zambe|Rovuma|Bahr Aouk|Chobe|Kwando/.test(
f.properties.name
)
) {
//L5
f.tippecanoe.minzoom = 4;
} else if (
[
'Al Bahr al Azraq',
'Mbomou',
'Zaire/Congo',
'Oubangui',
'Nile',
'Nahr an Nil',
'Congo',
'Rio Tapajos',
'Rio Madeira',
' Rio Tapajos',
'Rio Amazonas',
'Al Bahr al Abyad',
'Niger',
].includes(f.tippecanoe.name)
) {
f.tippecanoe.minzoom = 4;
} else if (
[
1100, 3205, 3196, 3185, 3128, 3052, 3494, 3065, 3058, 2516, 3069, 3074,
3076, 3077, 3078, 3034, 3060, 3090, 3103, 2957, 2955, 2954, 2212, 3464,
2939, 3311, 3300, 2054, 2324, 286, 297, 1457, 2496, 2349, 2208, 2339,
1837, 2437, 2270, 2990, 1079,
].includes(f.properties.objectid)
) {
//L5
f.tippecanoe.minzoom = 4;
} else if (
/Zambeze|Esil|Odra|Weser|Colorado|Okavango|Zambezi|Salado|Kwando|Chobe|Grande|Brazos/.test(
f.properties.name
)
) {
//L6
f.tippecanoe.minzoom = 5;
} else if (
[
2846, 3482, 1100, 3060, 3456, 3102, 3022, 1093, 3101, 2325, 2302, 2310,
3445, 2445, 3363, 3332, 3335, 3337, 2442, 2435, 2054, 2436, 2188, 2207,
2794, 2539, 2520, 2679, 2538, 2511, 1654, 1175, 592, 432, 967, 552, 558,
2511, 2266, 2339, 2334, 2142, 2230, 3526, 3572, 2349, 2208, 2235, 2978,
].includes(f.properties.objectid)
) {
//L6
f.tippecanoe.minzoom = 5;
} else {
//L7 -over
f.tippecanoe.minzoom = 6; //other --> no data
}
// delete f.properties['objectid']
delete f.properties['fid_1'];
return f;
},
unmap_popp_p: f => {
f.tippecanoe = {
layer: 'un_popp',
minzoom: 3,
maxzoom: 5,
};
if (f.properties.cartolb === 'Alofi' || f.properties.cartolb === 'Avarua') {
// if (f.properties.cartolb === 'Alofi' ||f.properties.cartolb === 'Avarua' ||f.properties.cartolb === 'Sri Jayewardenepura Kotte' ) {
return null;
} else if (f.properties.poptyp == 1 || f.properties.poptyp == 2) {
return f;
} else if (f.properties.poptyp == 3 && f.properties.scl_id == 10) {
return f;
} else {
return null;
}
},
};
module.exports = f => {
return postProcess(lut[f.properties._table](preProcess(f)));
};
データベース確認
psqlのコマンドを用いて、上記modify.jsで定義されている全てのtable(view)名が、使用するデータベース(PostgreSQL/PostGIS)において存在することを確認しました。例えば、データベースは「un_base」、スキーマは「vectortile」、ビューは「custom_planet_land_08_a」というビューが存在します。
設定ファイルの編集
config/default.jsonを以下の通り修正します。
{
relations: [
un_base::vectortile::custom_planet_land_08_a
un_base::vectortile::custom_planet_ocean_08_a
un_base::vectortile::custom_ne_10m_bathymetry_a
un_base::vectortile::un_glc30_global_lc_ss_a
un_base::vectortile::unmap_bndl_l
un_base::vectortile::unmap_bndl05_l
un_base::vectortile::unmap_bndl25_l
un_base::vectortile::custom_ne_rivers_lakecentrelines_l
un_base::vectortile::unmap_bnda_cty_anno_03_p
un_base::vectortile::unmap_bnda_cty_anno_04_p
un_base::vectortile::unmap_bnda_cty_anno_05_p
un_base::vectortile::unmap_bnda_cty_anno_06_p
un_base::vectortile::unmap_phyp_label_04_p
un_base::vectortile::unmap_phyp_label_06_p
un_base::vectortile::unmap_phyp_p
un_base::vectortile::unmap_wbya10_a
un_base::vectortile::unmap_wbya_label_05_p
un_base::vectortile::unmap_wbya_label_06_p
un_base::vectortile::unmap_dral10_l
un_base::vectortile::unmap_popp_p
]
connection:{
un_base:{
host: // ホスト名を記載
port: 5432
dbUser: // ユーザ名を記載
dbPassword: // パスワードを記載
}
}
spinnerString: 1
fetchSize: 30000
pmtilesDir: pmtiles
logDir: log
tippecanoePath: /usr/local/bin/tippecanoe
}
default.hjsonファイルについて、接続するLinuxのtippecanoeのパスに合わせて、以下の通り変更しました。
tippecanoePath: /usr/local/bin/tippecanoe
ファイルの移動と編集
今まではコード編集について自身のPCで作業していましたが、そのファイルをPostgreSQL/PostGISに接続されているLinuxに移動させます。VMwareを用いていますが、自身のPCと接続することで、ローカルにあるフォルダなどが見られます。
以下は、Linux上での画像です。
私の環境だとFileZillaを用いて自身のローカルPCにつなげようとすると、不安定になりVMwareの接続が切れてしまいました。
そのため、まずは一旦ローカルPCからLinuxPCにファイルを移動させて、そこからさらにサーバに移動させました。
上記のようにデータを移動させてもいいですが、「sudo git clone レポジトリURL」として、LinuxPC上でレポジトリをクローンする方が簡単かもしれません。その後、「npm install」とすると必要なモジュールもインストールされます。エラーが出るときは、sudoをつけると解決することがあります。
作成したmodify.jsに合わせるために、index-2.jsの以下のmodifyモジュールの読み込みの部分をmodify2.jsからmodify.jsに修正しています。
const modify = require('./modify.js');
上記の修正をFileZilla上で行っても、なぜかその修正が反映されていないことがあるので、viで修正した方が確実なようです。
実行結果
実行すると1分524秒で処理が終わりました。
作成したpmtilesファイルをローカルPCに持ってきて、そこからGithubにアップロードしました。今回は上記の方法を取りましたが、LinuxPCから直接アップした方が早そうです。
作成したPMTilesファイルは以下のURLのものです。
https://k96mz.github.io/20240924Postgis2vector-ClearMap-/pmtiles/20241008clear-map.pmtiles
PMTiles Viewerのページで上記URLを使用するとデータが以下のように見られます。スタイルファイルがないので、海の色などおかしいですが、無事に見ることが出来ました。
まとめ
VMwareを用いてLinuxPCに入り、そこからPostgreSQL/PostGISデータベースが入っているサーバからデータを取ってきて、UN Clear Mapのベクトルタイルを作成しました。LinuxPCに入ってから、権限の問題などで若干時間はかかりましたが、ベクトルタイルが作成出来ました。これまでの記事でClear Mapについて、ベクトルタイルの作成、ベクトルタイルの構造理解、地図表示などを記載してきました。次は、総まとめとして自分で作成したベクトルタイルを用いてClear Mapを表示してみたいと思います。
Reference