基于PostGIS(Postgres)+Node.js实现的xyz瓦片地图服务器

avatar
作者
猴君
阅读量:0

背景介绍

前两天研究GeoServer发布存储在PostGIS中栅格数据,最终目的是想在PostGIS中存储金字塔瓦片,用GeoServer发布,但是最后经过研究不改GeoServer源码的情况下,好像只支持将大图tif存在PostGIS数据库中进行发布,金字塔存入数据库后由于PostGIS的raster类型只存了瓦片的scale没有存类似层级的东西,导致发布后所有的金字塔层级一起显示了(没有层级控制),导致了很多影像叠加到一起了,后来又想到在数据库新增一个level字段,然后使用sld来进行控制显示,最后发现sld读不到字段表,这个路径也就放弃了(也许是我没弄对,有大佬点拨一下的话万分感谢)。

今天灵感来了(自己弄着玩),想到直接把瓦片的编号以及原始数据存到数据库,然后写个网络接口按照xyz数据源的格式请求,接口里查询数据库返回一张image给客户端,是不是就相当于一个xyz的瓦片服务器了?最终经过测试是可行的,先上两张效果图。

QGIS加载效果:

c432545dfa224e2983d1b205f59a75ab.png

水经微图加载效果:

8e7123471c8a47f580e4696bcdefe3cf.png

下面就说一下实现的具体流程

数据准备

数据是wgs84的瓦片,xyz都是从0开始,从左上角开始逆时针编号,以下是瓦片本地存储示例:

组织结构最外层为z值,第二层为x值,图片名称为y值。

数据库表示例:

b9a129b65fe84e6190d35f0748fb899c.png

然后使用Qt将瓦片读入并写入数据库,相关代码如下:

#pragma once  #include <QString> #include <QSqlDatabase>  class CImageUploader { public: 	CImageUploader(); 	~CImageUploader();  	void Init();  	void CreateTable();  	void UploadTileImage(const QString& strTileDir);  private:  	QSqlDatabase m_db; };  
#include "ImageUploader.h" #include <QDir> #include <QFile> #include <QDebug> #include <QFileInfo> #include <QSqlQuery> #include <QSqlError> #include <QByteArray> #include <QBuffer>  QString strHostName = "192.168.1.7"; QString strDatabaseName = "Tile"; QString strUserName = "postgres"; QString strPassword = "root"; QString strPort = "4321";  CImageUploader::CImageUploader() { 	Init(); 	CreateTable(); }  CImageUploader::~CImageUploader() {  }  void CImageUploader::Init() { 	m_db = QSqlDatabase::addDatabase("QPSQL"); 	m_db.setHostName(strHostName); 	m_db.setDatabaseName(strDatabaseName); 	m_db.setUserName(strUserName); 	m_db.setPassword(strPassword); 	m_db.setPort(strPort.toInt());  	if (!m_db.open()) 	{ 		qDebug() << "Failed to open database connection!" << m_db.lastError().text(); 	} }  void CImageUploader::CreateTable() { 	// 使用 IF NOT EXISTS 判断表是否存在 	QString strCreateTableQuery = QString(R"(         CREATE TABLE IF NOT EXISTS Tile (             id SERIAL PRIMARY KEY,             x BIGINT NOT NULL,             y BIGINT NOT NULL,             z INT NOT NULL,             data BYTEA NOT NULL         );     )");  	QSqlQuery query(m_db); 	if (!query.exec(strCreateTableQuery)) 	{ 		qDebug() << "Exec failed:" << query.lastError().text().toLocal8Bit(); 	} }  void CImageUploader::UploadTileImage(const QString& strTileDir) { 	QDir zDir(strTileDir);  	if (!zDir.exists())  	{ 		qDebug() << "Directory does not exist:" << strTileDir; 		return; 	}  	// 遍历 z 值文件夹 	QStringList zFolders = zDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); 	foreach(const QString &zFolder, zFolders)  	{ 		bool zOk; 		int z = zFolder.toInt(&zOk); 		if (!zOk)  		{ 			qDebug() << "Invalid z folder:" << zFolder; 			continue; 		}  		QDir xDir(zDir.filePath(zFolder));  		// 遍历 x 值文件夹 		QStringList xFolders = xDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); 		foreach(const QString &xFolder, xFolders)  		{ 			bool xOk; 			qint64 x = xFolder.toLongLong(&xOk); 			if (!xOk)  			{ 				qDebug() << "Invalid x folder:" << xFolder; 				continue; 			}  			QDir yDir(xDir.filePath(xFolder));  			// 遍历 y 值的图片文件 			QStringList imageFiles = yDir.entryList(QDir::Files); 			foreach(const QString &imageFile, imageFiles)  			{ 				QString yValueStr = QFileInfo(imageFile).baseName(); 				bool yOk; 				qint64 y = yValueStr.toLongLong(&yOk); 				if (!yOk)  				{ 					qDebug() << "Invalid y file name:" << imageFile; 					continue; 				}  				QString imagePath = yDir.filePath(imageFile); 				QFile file(imagePath); 				if (!file.open(QIODevice::ReadOnly))  				{ 					qDebug() << "Failed to open image file:" << imagePath; 					continue; 				}  				QByteArray imageData = file.readAll(); 				file.close();  				// 上传图片数据到数据库 				QSqlQuery query(m_db); 				query.prepare("INSERT INTO Tile (x, y, z, data) VALUES (:x, :y, :z, :data)"); 				query.bindValue(":x", x); 				query.bindValue(":y", y); 				query.bindValue(":z", z); 				query.bindValue(":data", imageData);  				if (!query.exec())  				{ 					qDebug() << "Failed to upload tile image:" << query.lastError().text(); 				} 				else  				{ 					qDebug() << "Successfully uploaded tile image:" 						<< "z=" << z << ", x=" << x << ", y=" << y; 				} 			} 		} 	} }
#include <QCoreApplication> #include "ImageUploader.h"  int main(int argc, char *argv[]) { 	QCoreApplication a(argc, argv);  	CImageUploader imageUploader; 	imageUploader.UploadTileImage(QString::fromLocal8Bit(R"(H:\data)"));  	return a.exec(); }

服务实现

数据写入数据库后只需要弄一个http接口了,我这里使用的是Node.js直连PostGIS数据库,然后根据请求去查数据库对应的image值,然后返回给客户端,Node代码如下:

const express = require('express'); const { Pool } = require('pg'); const app = express(); const port = 3000;  // 配置PostgreSQL连接池 const pool = new Pool({     user: 'postgres',     host: '192.168.1.7',     database: 'Tile',     password: 'root',     port: 4321, });  // XYZ瓦片接口 app.get('/tiles/:z/:x/:y', async (req, res) => {     let { z, x, y } = req.params;      // 提取数字部分的 y 值(去除文件扩展名)     y = y.split('.')[0];      try {         // 确保 z, x, y 是有效的数字         const zInt = parseInt(z, 10);         const xInt = parseInt(x, 10);         const yInt = parseInt(y, 10);          if (isNaN(zInt) || isNaN(xInt) || isNaN(yInt)) {             return res.status(400).send('Invalid tile coordinates');         }          // 查询数据库获取瓦片数据         const queryText = `             SELECT data              FROM Tile              WHERE z = $1 AND x = $2 AND y = $3         `;         const result = await pool.query(queryText, [zInt -1, xInt, yInt]);          if (result.rows.length > 0) {             const tileData = result.rows[0].data;             res.setHeader('Content-Type', 'image/jpeg'); // 确保设置正确的图片格式             res.send(tileData);         } else {             res.status(404).send('Tile not found');         }     } catch (err) {         console.error('Error fetching tile:', err);         res.status(500).send('Internal Server Error');     } });  // 启动服务器 app.listen(port, () => {     console.log(`Tile server is running at http://localhost:${port}`); }); 

 安装依赖:

npm init -y npm install express pg 

 启动:

node server.js 

然后就可以在浏览器请求测试(图片格式后缀其实没影响加不加都可):

http://localhost:3000/tiles/{z}/{x}/{y}.jpg 

例如:

localhost:3000/tiles/1/0/0.jpg

 效果如下:

bc78f7cd1cdc4923bae70fd8ee18d0eb.png

然后就是QGIS加载测试,在XYZ Tiles新建链接,输入名称和网址,网址直接输入http://localhost:3000/tiles/{z}/{x}/{y}.jpg即可,Node做了z-1处理),图块分辨率可以不管也可以设为256*256,如下:

8d7dc05ab57c47ed881115df9f901acf.png

在水经微图里点击在线地图(自定义),在弹出的页面中,输入前面所说的网址即可,如下:

3eb740a032664fb3a502a122cafa8b35.png

最后给两张放大的效果图(数据只有6级):

QGIS:

92f813507a384ff38ce3dc2a3d063603.png

水经微图:

7deb52c1be7e4c9abd5e412391980ee1.png

分享到此结束。

    广告一刻

    为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!