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

news2024/11/17 14:24:38

背景介绍

前两天研究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

分享到此结束。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2049461.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

XSS漏洞复现(包括xssgame和三个高级xss漏洞)

文章目录 XSS GAME1、Ma Spaghet!2、Jefff3、Ugandan Knuckles4、Ricardo Milos5、Ah Thats Hawt6、Ligma7、Mafia8、Ok, Boomer 全删除属性dom破坏WW3 XSS GAME 地址&#xff1a;https://xss.pwnfunction.com/ 关卡难度层层递进&#xff0c;各个漏洞都有特点&#xff0c;值得…

【最大公约数和最小公倍数】求最大公约数和最小公倍数

输入两个正整数m和n&#xff0c;求其最大公约数和最小公倍数 使用C语言实现&#xff0c;具体代码&#xff1a; 使用辗转相除法&#xff08;也称欧几里得算法&#xff09;计算最大公约数 while(b ! 0){ temp a % b; a b; b temp; } 最小公倍数&#xff08;LCM&…

大模型高效微调工具 Hugging Face PEFT

概述 HF PEFT是一个为大型预训练模型提供高效微调方法的Python库。它通过采用训练少量提示参数或使用低秩适应等重新参数化方法&#xff0c;减少微调时训练参数的数量。本文主要介绍了PEFT库的内容、与Transformers的集成、核心知识点如AutoPeftModels、PeftConfig、PeftType和…

知识改变命运 数据结构【链表面试题】

1. 删除链表中等于给定值 val 的所有节点。 OJ链接 public ListNode removeElements(ListNode head, int val) {if (headnull) {return null;}ListNode curhead.next;ListNode prehead;while(cur!null) {if(cur.valval) {pre.nextcur.next;curcur.next;}else {precur;curcur.ne…

日拱一卒(5)——leetcode学习记录:股票购买

一、任务 给定一个数组 prices &#xff0c;它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。 你只能选择 某一天 买入这只股票&#xff0c;并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。 返回你可以从这笔交易中获取的…

【qt】跳转到另一个界面

如何在一个界面跳转到另一个界面呢&#xff1f; 1.具体步骤 1.先新建一个界面 2.选择qt设计师界面 3.选择W 4.新界面名称 5.界面设计 因为我们要实现通信&#xff0c;需要一个发送信息栏&#xff0c;一个发送按钮&#xff0c;一个清空发送栏按钮 6.实现跳转 我们可以参…

链表反转算法

链表的反转有较多的方法&#xff0c;如原地算法&#xff0c;迭代法、头插法、递归法&#xff0c;本文使用递归法和迭代法两种方式进行演示。 一、定义链表 typedef struct SinglyLinkNode {/**后继节点 */struct SinglyLinkNode* next;/** 节点数据域 */int data; } Lin…

海外代理IP网速快慢取决因素有哪些

在全球化日益加深的今天&#xff0c;海外代理IP成为许多用户进行跨境网络活动的重要工具。然而&#xff0c;用户在使用海外代理IP时&#xff0c;往往会遇到网速快慢不一的问题。本文将从多个角度探讨影响海外代理IP网速快慢的因素&#xff0c;帮助用户更好地理解并选择适合自己…

使用 prefetchComponents 进行组件预取

title: 使用 prefetchComponents 进行组件预取 date: 2024/8/17 updated: 2024/8/17 author: cmdragon excerpt: 摘要&#xff1a;本文介绍Nuxt.js中的prefetchComponents功能&#xff0c;用于预取组件以提高用户体验。通过在客户端后台下载和缓存组件&#xff0c;确保在用户…

普通人如何做文献阅读汇报?

最强小说推文——AI视频生成&#xff1a;小说文案智能分镜智能识别角色和场景批量Ai绘图自动配音添加音乐一键合成视频百万播放量https://aitools.jurilu.com/ 用AI啊&#xff01;以前啃个文献小半天&#xff0c;现在AI 2min搞定&#xff0c;从提取核心论点、参考文献到摘要、翻…

如何将 Windows 11/10/8/7 克隆到另一台计算机

为什么需要将 Windows克隆到新计算机 “我有一台新笔记本电脑来替换我的旧电脑&#xff0c;因为它运行几年后变得越来越慢。我现在面临的问题是如何让 Windows 10、程序和文件与旧 PC 保持相同。我不想重新安装 Windows 和应用程序。有没有快速简便的方法可以做到这一点&#…

检测到目标URL存在http host头攻击漏洞

漏洞描述 修复措施 方法一&#xff1a; nginx 的 default_server 指令可以定义默认的 server 去处理一些没有匹配到 server_name 的请求&#xff0c;如果没有显式定义&#xff0c;则会选取第一个定义的 server 作为 default_server。 server { …

数据结构——关于队列

1.队列的概念及结构 队列&#xff1a;只允许在一端进行插入数据操作&#xff0c;在另一端进行删除数据操作的特殊线性表&#xff0c;队列具有先进先出的特性 入队列&#xff1a;进行插入操作的一端称为队尾 出队列&#xff1a;进行删除操作的一端称为队头 2.队列的…

HiveSQL实战——大厂面试真题

一、字节跳动 最高峰同时直播人数 https://blog.csdn.net/SHWAITME/article/details/135918264 0 问题描述 有如下数据记录直播平台主播上播及下播时间&#xff0c;根据该数据计算出平台最高峰同时直播人数。 ------------------------------------------------------ | us…

NC 用两个栈实现队列

系列文章目录 文章目录 系列文章目录前言 前言 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站&#xff0c;这篇文章男女通用&#xff0c;看懂了就去分享给你的码吧。 描述 用两个栈来实…

计算最简比

原理 c 质数数组长度&#xff0c;除不了就c--&#xff0c;都除不了c0&#xff0c;while(0)退出循环。 mainwindow.h struct WbiH {int width;int height; };WbiH calcWbiH(int w, int h); mainwindow.cpp {......int width player->metaData(SLMD.at(i)).toSize().wi…

JVM上篇:内存与垃圾-回收篇05-本地方法接口和本地方法栈

笔记来源&#xff1a;尚硅谷 JVM 全套教程&#xff0c;百万播放&#xff0c;全网巅峰&#xff08;宋红康详解 java 虚拟机&#xff09; 文章目录 5. 本地方法接口和本地方法栈5.1. 什么是本地方法&#xff1f;5.2. 为什么使用 Native Method&#xff1f;5.2. 本地方法栈 5. 本地…

uniapp去掉页面导航条

在pages.json文件中&#xff0c;globalStyle中添加 ”app-plus“:{"titleNView":false }

C++ 常用STL底层原理

STL 1. std::vector 底层原理&#xff1a;std::vector 是一个动态数组。它在内存中分配一块连续的存储空间来存储元素。随着元素数量的增加&#xff0c;如果存储空间不够用&#xff0c;vector 会分配一块更大的内存&#xff0c;并将现有元素复制到新内存块中。通常&#xff0c…

JavaScript模块化——JS模块化介绍与CommonJS规范

作者&#xff1a;CSDN-PleaSure乐事 欢迎大家阅读我的博客 希望大家喜欢 使用环境&#xff1a;vscode Chrome浏览器 目录 1.模块化概述 1.1什么是模块化 1.2为什么需要模块化 1.2.1全局污染 1.2.2依赖混乱 1.2.3数据安全 2.有哪些模块化规范 3.导入与导出 3.1导入 3.…