diff --git a/CMakeLists.txt b/CMakeLists.txt index 34cd1d9..c41091d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,7 +6,7 @@ if (${CMAKE_SOURCE_DIR} STREQUAL ${CMAKE_CURRENT_LIST_DIR}) set(test_maplibre_VERSION 1.0.0) set(CMAKE_CXX_STANDARD 20) include(third_party/build_infra/start.cmake) - include(third_party/CPP_Core/main.cmake) + set(test_maplibre_rely QMapLibre_qt_5_15_2) library_is_installed_with_rely(global ok ${test_maplibre_rely}) diff --git a/main.cmake b/main.cmake index 34279d7..903420f 100644 --- a/main.cmake +++ b/main.cmake @@ -7,10 +7,11 @@ find_package(Qt5 REQUIRED COMPONENTS Widgets) find_package(QMapLibre COMPONENTS Widgets REQUIRED) -file(GLOB_RECURSE srcs ${dir}/*.c ${dir}/*.h ${dir}/*.cpp ${dir}/*.hpp) +file(GLOB_RECURSE srcs ${dir}/*.qrc ${dir}/*.c ${dir}/*.h ${dir}/*.cpp ${dir}/*.hpp) add_executable(test_maplibre ${srcs}) target_link_libraries(test_maplibre PRIVATE Qt5::Widgets QMapLibre::Widgets) + target_compile_definitions(test_maplibre PRIVATE QT_MAPLIBRE_STATIC) @@ -20,4 +21,9 @@ if(WIN32) endif() if(MSVC) target_compile_options(test_maplibre PRIVATE /utf-8) -endif() \ No newline at end of file +endif() + + +set_property(TARGET test_maplibre PROPERTY AUTOMOC ON) +set_property(TARGET test_maplibre PROPERTY AUTOUIC ON) +set_property(TARGET test_maplibre PROPERTY AUTORCC ON) \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 3fe21ac..007f072 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,32 +1,266 @@ #include "httplib.h" #include -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include #include +#include +struct MapSource { + const char *id; + const char *name; + const char *tileUrl; + const char *overlayUrl; + int minZoom; + int maxZoom; +}; +const MapSource *find_source(const std::vector& sources, const std::string& id) { + for (const auto& source : sources) { + if (id == source.id) { + return &source; + } + } + return nullptr; +} +std::string replace_all(std::string value, const std::string& from, const std::string& to) { + std::size_t pos = 0; + while ((pos = value.find(from, pos)) != std::string::npos) { + value.replace(pos, from.size(), to); + pos += to.size(); + } + return value; +} +std::string get_content_type(const std::string& path) { + std::string ext = std::filesystem::path(path).extension().string(); + if (ext == ".png") { + return "image/png"; + } + if (ext == ".jpg" || ext == ".jpeg") { + return "image/jpeg"; + } + if (ext == ".webp") { + return "image/webp"; + } + return "application/octet-stream"; +} +std::string read_binary_file(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + return std::string(std::istreambuf_iterator(file), std::istreambuf_iterator()); +} +QJsonObject make_raster_source(const std::string& url, const MapSource& source) { + QJsonObject rasterSource; + QJsonArray tiles; + tiles.append(QString::fromUtf8(url.c_str())); + rasterSource["type"] = "raster"; + rasterSource["tiles"] = tiles; + rasterSource["tileSize"] = 256; + rasterSource["minzoom"] = source.minZoom; + rasterSource["maxzoom"] = source.maxZoom; + rasterSource["scheme"] = "xyz"; + return rasterSource; +} +QJsonObject make_raster_layer(const char *id, const char *source) { + QJsonObject rasterPaint; + rasterPaint["raster-opacity"] = 1.0; + QJsonObject rasterLayer; + rasterLayer["id"] = QString::fromUtf8(id); + rasterLayer["type"] = "raster"; + rasterLayer["source"] = QString::fromUtf8(source); + rasterLayer["paint"] = rasterPaint; + return rasterLayer; +} +QByteArray make_raster_style_json(const MapSource& source, const std::string& tiandituKey) { + QJsonObject sources; + std::string tileUrl = replace_all(source.tileUrl, "{tk}", tiandituKey); + sources["base"] = make_raster_source(tileUrl, source); + QJsonObject backgroundPaint; + backgroundPaint["background-color"] = "#000000"; + QJsonObject backgroundLayer; + backgroundLayer["id"] = "background"; + backgroundLayer["type"] = "background"; + backgroundLayer["paint"] = backgroundPaint; + QJsonArray layers; + layers.append(backgroundLayer); + layers.append(make_raster_layer("base", "base")); + if (source.overlayUrl && source.overlayUrl[0] != '\0') { + std::string overlayUrl = replace_all(source.overlayUrl, "{tk}", tiandituKey); + sources["overlay"] = make_raster_source(overlayUrl, source); + layers.append(make_raster_layer("overlay", "overlay")); + } + QJsonObject root; + root["version"] = 8; + root["name"] = QString::fromUtf8(source.name); + root["sources"] = sources; + root["layers"] = layers; + return QJsonDocument(root).toJson(QJsonDocument::Compact); +} +void register_tile_server(httplib::Server& server, std::string& localTileDir, std::mutex& localTileDirMutex) { + server.Get(R"(/tiles/(.+))", [&localTileDir, &localTileDirMutex](const httplib::Request& req, httplib::Response& res) { + std::string root; + { + std::lock_guard lock(localTileDirMutex); + root = localTileDir; + } + std::string relative = req.matches[1].str(); + std::filesystem::path path = std::filesystem::path(root) / std::filesystem::path(relative); + if (!std::filesystem::exists(path) || !std::filesystem::is_regular_file(path)) { + res.status = 404; + res.set_content("tile not found", "text/plain"); + return; + } + std::string body = read_binary_file(path); + std::string contentType = get_content_type(path.string()); + res.set_content(body, contentType.c_str()); + }); +} int main(int argc, char *argv[]) { QApplication app(argc, argv); - QString stylePath = "C:/Users/wyc/CLionProjects/test_maplibre/src/style.json"; - QFileInfo styleInfo(stylePath); - qDebug() << "style exists:" << styleInfo.exists(); - qDebug() << "style path:" << styleInfo.absoluteFilePath(); + QSettings appSettings("wyc", "test_maplibre"); + std::string tiandituKey = appSettings.value("tianditu/key", "").toString().trimmed().toStdString(); + std::string localTileDir = appSettings.value("local/tile_dir", "D:/tiles").toString().trimmed().toStdString(); + std::mutex tiandituKeyMutex; + std::mutex localTileDirMutex; + std::vector sources = { + {"local_png", "本地瓦片 PNG", "http://127.0.0.1:18080/tiles/{z}/{x}/{y}.png", nullptr, 0, 20}, + {"local_jpg", "本地瓦片 JPG", "http://127.0.0.1:18080/tiles/{z}/{x}/{y}.jpg", nullptr, 0, 20}, + {"osm", "OSM 标准地图", "https://tile.openstreetmap.org/{z}/{x}/{y}.png", nullptr, 0, 19}, + {"google_m", "谷歌街道 m", "https://mt0.google.com/vt/lyrs=m&x={x}&y={y}&z={z}", nullptr, 0, 20}, + {"google_p", "谷歌街道 p", "https://mt0.google.com/vt/lyrs=p&x={x}&y={y}&z={z}", nullptr, 0, 20}, + {"google_r", "谷歌街道 r", "https://mt0.google.com/vt/lyrs=r&x={x}&y={y}&z={z}", nullptr, 0, 20}, + {"google_t", "谷歌地形 t", "https://mt0.google.com/vt/lyrs=t&x={x}&y={y}&z={z}", nullptr, 0, 20}, + {"google_s", "谷歌影像 s", "https://mt0.google.com/vt/lyrs=s&x={x}&y={y}&z={z}", nullptr, 0, 20}, + {"google_y", "谷歌影像含标注 y", "https://mt0.google.com/vt/lyrs=y&x={x}&y={y}&z={z}", nullptr, 0, 20}, + {"google_h", "谷歌透明标注 h", "https://mt0.google.com/vt/lyrs=h&x={x}&y={y}&z={z}", nullptr, 0, 20}, + {"google_cn_s", "谷歌中国影像 s", "https://mt0.google.com/vt/lyrs=s&gl=CN&x={x}&y={y}&z={z}", nullptr, 0, 20}, + {"google_cn_h", "谷歌中国透明标注 h", "https://mt0.google.com/vt/lyrs=h&gl=CN&x={x}&y={y}&z={z}", nullptr, 0, 20}, + {"google_cn_y", "谷歌中国影像含标注 y", "https://mt0.google.com/vt/lyrs=y&gl=CN&x={x}&y={y}&z={z}", nullptr, 0, 20}, + {"amap_road", "高德地图", "https://webrd01.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}", nullptr, 0, 20}, + {"amap_sat", "高德卫星", "https://webst01.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}", nullptr, 0, 20}, + {"amap_sat_label", "高德卫星标注", "https://webst01.is.autonavi.com/appmaptile?style=8&x={x}&y={y}&z={z}", nullptr, 0, 20}, + {"tdt_img", "天地图影像", "https://t0.tianditu.gov.cn/DataServer?T=img_w&x={x}&y={y}&l={z}&tk={tk}", nullptr, 0, 18}, + {"tdt_cia", "天地图影像标注", "https://t0.tianditu.gov.cn/DataServer?T=cia_w&x={x}&y={y}&l={z}&tk={tk}", nullptr, 0, 18}, + {"tdt_img_cia", "天地图影像+标注", "https://t0.tianditu.gov.cn/DataServer?T=img_w&x={x}&y={y}&l={z}&tk={tk}", "https://t0.tianditu.gov.cn/DataServer?T=cia_w&x={x}&y={y}&l={z}&tk={tk}", 0, 18}, + {"tdt_vec", "天地图矢量", "https://t0.tianditu.gov.cn/DataServer?T=vec_w&x={x}&y={y}&l={z}&tk={tk}", nullptr, 0, 18}, + {"tdt_cva", "天地图矢量标注", "https://t0.tianditu.gov.cn/DataServer?T=cva_w&x={x}&y={y}&l={z}&tk={tk}", nullptr, 0, 18}, + {"tdt_vec_cva", "天地图矢量+标注", "https://t0.tianditu.gov.cn/DataServer?T=vec_w&x={x}&y={y}&l={z}&tk={tk}", "https://t0.tianditu.gov.cn/DataServer?T=cva_w&x={x}&y={y}&l={z}&tk={tk}", 0, 18} + }; httplib::Server server; - server.set_mount_point("/", "C:/Users/wyc/CLionProjects/test_maplibre/src"); - server.set_mount_point("/tiles", "D:/tiles"); + server.Get(R"(/style/([A-Za-z0-9_]+)\.json)", [&sources, &tiandituKey, &tiandituKeyMutex](const httplib::Request& req, httplib::Response& res) { + const MapSource *source = find_source(sources, req.matches[1].str()); + if (!source) { + res.status = 404; + res.set_content("style not found", "text/plain"); + return; + } + std::string key; + { + std::lock_guard lock(tiandituKeyMutex); + key = tiandituKey; + } + QByteArray styleJson = make_raster_style_json(*source, key); + res.set_content(styleJson.constData(), styleJson.size(), "application/json"); + }); + register_tile_server(server, localTileDir, localTileDirMutex); std::thread serverThread([&server]() { server.listen("127.0.0.1", 18080); }); server.wait_until_ready(); QMapLibre::Settings settings; - QMapLibre::GLWidget widget(settings); - widget.resize(1200, 800); - widget.setWindowTitle("MapLibre Qt Widgets Demo"); - widget.show(); - //widget.map()->setStyleUrl("https://demotiles.maplibre.org/style.json"); - widget.map()->setStyleUrl("http://127.0.0.1:18080/style.json"); - widget.map()->setCoordinateZoom(QMapLibre::Coordinate(35.0, 105.0), 4.0); + QMainWindow window; + QMapLibre::GLWidget *mapWidget = new QMapLibre::GLWidget(settings); + QToolBar *toolbar = window.addToolBar("Map Source"); + QComboBox *sourceBox = new QComboBox(&window); + QLineEdit *tiandituKeyEdit = new QLineEdit(&window); + QPushButton *applyTiandituKeyButton = new QPushButton("保存天地图Key", &window); + QLineEdit *localTileDirEdit = new QLineEdit(&window); + QPushButton *selectLocalTileDirButton = new QPushButton("选择瓦片目录", &window); + QPushButton *applyLocalTileDirButton = new QPushButton("保存瓦片目录", &window); + int styleVersion = 0; + for (const auto& source : sources) { + sourceBox->addItem(QString::fromUtf8(source.name), QString::fromUtf8(source.id)); + } + tiandituKeyEdit->setText(QString::fromStdString(tiandituKey)); + tiandituKeyEdit->setPlaceholderText("输入天地图Key"); + tiandituKeyEdit->setMinimumWidth(320); + localTileDirEdit->setText(QString::fromStdString(localTileDir)); + localTileDirEdit->setPlaceholderText("输入本地瓦片目录,例如 D:/tiles"); + localTileDirEdit->setMinimumWidth(360); + toolbar->addWidget(sourceBox); + toolbar->addWidget(tiandituKeyEdit); + toolbar->addWidget(applyTiandituKeyButton); + toolbar->addWidget(localTileDirEdit); + toolbar->addWidget(selectLocalTileDirButton); + toolbar->addWidget(applyLocalTileDirButton); + window.setCentralWidget(mapWidget); + window.resize(1400, 800); + window.setWindowTitle("MapLibre Qt Widgets Demo"); + window.show(); + auto reloadCurrentSource = [&]() { + QString id = sourceBox->currentData().toString(); + mapWidget->map()->setStyleUrl(QString("http://127.0.0.1:18080/style/%1.json?v=%2").arg(id).arg(++styleVersion)); + }; + auto saveTiandituKey = [&]() { + QString value = tiandituKeyEdit->text().trimmed(); + { + std::lock_guard lock(tiandituKeyMutex); + tiandituKey = value.toStdString(); + } + appSettings.setValue("tianditu/key", value); + appSettings.sync(); + }; + auto saveLocalTileDir = [&]() { + QString value = localTileDirEdit->text().trimmed(); + { + std::lock_guard lock(localTileDirMutex); + localTileDir = value.toStdString(); + } + appSettings.setValue("local/tile_dir", value); + appSettings.sync(); + }; + QObject::connect(sourceBox, QOverload::of(&QComboBox::currentIndexChanged), [&](int) { + reloadCurrentSource(); + }); + QObject::connect(applyTiandituKeyButton, &QPushButton::clicked, [&]() { + saveTiandituKey(); + reloadCurrentSource(); + }); + QObject::connect(tiandituKeyEdit, &QLineEdit::returnPressed, [&]() { + saveTiandituKey(); + reloadCurrentSource(); + }); + QObject::connect(selectLocalTileDirButton, &QPushButton::clicked, [&]() { + QString dir = QFileDialog::getExistingDirectory(&window, "选择本地瓦片目录", localTileDirEdit->text().trimmed()); + if (!dir.isEmpty()) { + localTileDirEdit->setText(dir); + saveLocalTileDir(); + reloadCurrentSource(); + } + }); + QObject::connect(applyLocalTileDirButton, &QPushButton::clicked, [&]() { + saveLocalTileDir(); + reloadCurrentSource(); + }); + QObject::connect(localTileDirEdit, &QLineEdit::returnPressed, [&]() { + saveLocalTileDir(); + reloadCurrentSource(); + }); + reloadCurrentSource(); + mapWidget->map()->setCoordinateZoom(QMapLibre::Coordinate(35.0, 105.0), 4.0); int ret = app.exec(); server.stop(); serverThread.join(); diff --git a/src/style.qrc b/src/style.qrc new file mode 100644 index 0000000..06ce1ec --- /dev/null +++ b/src/style.qrc @@ -0,0 +1,5 @@ + + + ../style/style.json + + \ No newline at end of file diff --git a/src/style.json b/style/style.json similarity index 100% rename from src/style.json rename to style/style.json