背景介绍
前两天研究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:
水经微图:
分享到此结束。