背景介绍
前两天研究GeoServer发布存储在PostGIS中栅格数据,最终目的是想在PostGIS中存储金字塔瓦片,用GeoServer发布,但是最后经过研究不改GeoServer源码的情况下,好像只支持将大图tif存在PostGIS数据库中进行发布,金字塔存入数据库后由于PostGIS的raster类型只存了瓦片的scale没有存类似层级的东西,导致发布后所有的金字塔层级一起显示了(没有层级控制),导致了很多影像叠加到一起了,后来又想到在数据库新增一个level字段,然后使用sld来进行控制显示,最后发现sld读不到字段表,这个路径也就放弃了(也许是我没弄对,有大佬点拨一下的话万分感谢)。
今天灵感来了(自己弄着玩),想到直接把瓦片的编号以及原始数据存到数据库,然后写个网络接口按照xyz数据源的格式请求,接口里查询数据库返回一张image给客户端,是不是就相当于一个xyz的瓦片服务器了?最终经过测试是可行的,先上两张效果图。
QGIS加载效果:
水经微图加载效果:
下面就说一下实现的具体流程
数据准备
数据是wgs84的瓦片,xyz都是从0开始,从左上角开始逆时针编号,以下是瓦片本地存储示例:
组织结构最外层为z值,第二层为x值,图片名称为y值。
数据库表示例:
然后使用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
效果如下:
然后就是QGIS加载测试,在XYZ Tiles新建链接,输入名称和网址,网址直接输入http://localhost:3000/tiles/{z}/{x}/{y}.jpg即可,Node做了z-1处理),图块分辨率可以不管也可以设为256*256,如下:
在水经微图里点击在线地图(自定义),在弹出的页面中,输入前面所说的网址即可,如下:
最后给两张放大的效果图(数据只有6级):
QGIS:
水经微图:
分享到此结束。