QGIS 导出 mbtiles 作为离线地图,并在 Laravel 中使用的实践

Written by

in

,

将 QGIS 可见图层导出为离线地图、并与 Laravel 这样的 Web 后端结合使用,MBTiles 格式配合前端的 Leaflet.js 或 OpenLayers 是目前比较常见的一套组合。

如果地图包含栅格数据(DEM/地形晕渲)和矢量数据(河流),直接导出为 Web 切片(瓦片)是个可行的方向——QGIS 的切片工具只会渲染当前处于勾选(可见)状态的图层,省去了手动筛选的麻烦。

以下是完整的工作流记录:


第一阶段:在 QGIS 中导出数据 (MBTiles)

为什么选 MBTiles?
相比于导出成千上万个碎小的 .png 图片文件夹(XYZ 目录结构),MBTiles 本质上是一个单文件的 SQLite 数据库,里面存储了所有层级的瓦片图片。它极大地简化了文件的传输、存储,并且 Laravel(PHP)原生支持极速读取 SQLite。

具体步骤:

  1. 检查图层与投影: 确保左侧图层树中,只有你需要导出的图层被勾选。建议将 QGIS 右下角的项目坐标系(CRS)设置为 Web 地图通用的 EPSG:3857 (WGS 84 / Pseudo-Mercator)。在 Web 离线地图开发中,使用非 EPSG:3857 作为切片坐标系,容易打破 Web 地图的”默认规则”,引发一系列兼容性问题。条件允许的话,尽量统一采用 EPSG:3857。
  2. 打开切片工具: 在顶部菜单栏选择 处理 (Processing) -> 工具箱 (Toolbox)
  3. 搜索工具: 在右侧弹出的工具箱中搜索 XYZ,找到 栅格工具 (Raster tools) 下的 生成 XYZ 瓦片 (MBTiles) / Generate XYZ tiles (MBTiles)
  4. 配置参数:
    • 范围 (Extent): 点击右侧的 ...,选择”使用当前画布范围 (Use Map Canvas Extent)”,或者选择某个图层的范围。
    • 最小缩放级别 (Minimum zoom): 通常设为 03
    • 最大缩放级别 (Maximum zoom): ⚠️ 注意: 如果地图范围较大(比如覆盖整个东亚这样的区域),级别设得太高会导致生成文件呈指数级暴增且耗时极长。建议先从较低的缩放级别(如 78)开始测试,确认文件大小和生成时间可接受后,再酌情提高到 10 左右。
    • 输出文件: 选择一个保存路径,命名为 offline_map.mbtiles
  5. 点击 运行 (Run),等待生成完毕。

第二阶段:在 Laravel 中提供服务

现在你有了一个 offline_map.mbtiles(SQLite 数据库文件)。你需要让 Laravel 读取它并输出图片流。

  1. 放置文件: 将生成的 offline_map.mbtiles 放入 Laravel 项目中,例如 storage/app/maps/offline_map.mbtiles
  2. 配置数据库连接:config/database.php 中添加一个新的 SQLite 连接:

    'connections' => [
        // ... 其他连接
        'mbtiles' => [
            'driver'   => 'sqlite',
            'database' => storage_path('app/maps/offline_map.mbtiles'),
            'prefix'   => '',
        ],
    ],
  3. 创建路由与控制器: MBTiles 数据库里通常有一张名为 tiles 的表,包含 zoom_leveltile_columntile_rowtile_data(BLOB 格式的图片)。在 routes/web.php 中添加一个路由:

    use Illuminate\Support\Facades\Route;
    use Illuminate\Support\Facades\DB;
    use Illuminate\Http\Response;
    
    Route::get('/map-tiles/{z}/{x}/{y}', function ($z, $x, $y) {
        // MBTiles 使用的是 TMS 坐标系,Y 轴与标准的 XYZ 相反,需要转换
        $tms_y = (pow(2, $z) - 1) - $y;
    
        $tile = DB::connection('mbtiles')
            ->table('tiles')
            ->where('zoom_level', $z)
            ->where('tile_column', $x)
            ->where('tile_row', $tms_y)
            ->first();
    
        if (!$tile) {
            abort(404); // 或者返回一张透明的占位图片
        }
    
        // 假设导出的格式是 PNG
        return response($tile->tile_data)->header('Content-Type', 'image/png');
    });

第三阶段:前端渲染 (Leaflet.js)

在你的 Laravel Blade 视图(或 Vue/React 组件)中,使用常用的开源地图库 Leaflet.js 来加载这些离线瓦片。

  1. 引入 Leaflet 的 CSS 和 JS:

    <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css" />
    <script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
    <style>
        #map { height: 600px; width: 100%; }
    </style>
  2. 初始化地图并接入 Laravel 接口:

    <div id="map"></div>
    
    <script>
        // 根据实际地图区域调整中心点坐标和初始缩放级别
        var map = L.map('map').setView([35.0, 105.0], 4);
    
        // 添加离线瓦片图层,指向 Laravel 的路由
        L.tileLayer('/map-tiles/{z}/{x}/{y}', {
            minZoom: 3,
            maxZoom: 8, // 必须与你在 QGIS 中导出的层级匹配
            attribution: 'Offline Map Data'
        }).addTo(map);
    </script>

替代方案(最简单,但文件多):导出目录结构 (Directory)

如果你的服务器对 SQLite 拓展支持有问题,或者你想直接用 Nginx 代理静态文件来追求极致性能:

  • 在 QGIS 中选择 生成 XYZ 瓦片 (目录)
  • 将生成的包含层级数字(如 012…)的文件夹整个复制到 Laravel 的 public/tiles/ 目录下。
  • 前端直接写静态路径:L.tileLayer('/tiles/{z}/{x}/{y}.png', {...})
  • 缺点: 高层级地图会产生几十万个碎片文件,非常难移动和删除。