ℹ️ 实现范围
本文借鉴一个 。NET 8 WinForms RasterLite2 查看器的实际实现。这个查看器不加载 mod_spatialite.dll,也不调用 RL2_LoadRaster() ;它读取已经存在的 RasterLite2 数据库,通过 GDAL 显示完整 Coverage,并通过 SQL + librasterlite2.dll 解码选中的 Tile。这里关注的是"代码可以完成什么"和"每一步如何验证",不是某个项目目录的复制说明。
1. 这类 C# 查看器可以做什么

1_Rasterlite展示
bash
已有 dem.sqlite(Coverage: dem)
│
├─ GDAL:RASTERLITE2:dem.sqlite:dem
│ └─ gdalinfo → 范围;gdal_translate → GeoTIFF → PNG → Mapsui
│
└─ Microsoft.Data.Sqlite:只读 SQL
└─ dem_tiles + dem_tile_data + idx_dem_tiles_geometry
└─ librasterlite2.dll:rl2_raster_decode
└─ float 像元 → PNG → Mapsui
两条路径互相补证:GDAL 证明 Coverage 能被 RasterLite2 驱动完整读取;SQL Tile 路径证明应用能按空间范围定位、读取并解码实际的 Tile BLOB。它们都成功时,才可以说"Coverage 和被选中的 Tile 都可读"。
除 RasterLite2 读取外,示例还完成了这些桌面 GIS 功能:
- 创建 WinForms 地图窗口,使用 Mapsui 显示栅格和矢量图层。
- 将完整 DEM 转成可显示的 PNG,并按 Coverage 范围放入地图。
- 按一个选择多边形筛选相交 Tile,只解码需要的局部数据。
- 将有效浮点像元转成灰度图,并可把像元中心绘制成点图层。
- 读取 Shapefile 作为行政区、地形地貌、土壤类型等叠加图层。
- 通过图层树独立开关完整 DEM、局部 Tile、像元中心点和矢量图层。
- 对数据库缺失、GDAL 子进程失败、Tile 查询为空、native 解码失败等情况抛出明确异常。
⚠️ 不要混淆"读取"和"导入"
当前项目是查看器,不负责创建 Coverage 或把 GeoTIFF 导入 RasterLite2。因此它不能作为 RL2_CreateRasterCoverage、RL2_LoadRaster 或 SpatiaLite 扩展加载的运行证据。
2. 实际依赖与运行条件
要实现上述功能,托管依赖可以按下面的最小组合准备:
bash
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.10" />
<PackageReference Include="Mapsui.WindowsForms" Version="5.1.0" />
<PackageReference Include="Mapsui.Nts" Version="5.1.0" />
</ItemGroup>
</Project>
运行时还需要下列外部资源;可以放在配置文件、应用目录或命令行参数指定的目录中:
| 资源 | 用途 | 本项目的用法 |
|---|---|---|
dem.sqlite |
已存在的 RasterLite2 数据库 | 以只读方式由 Microsoft.Data.Sqlite 打开;GDAL 也从它读取 Coverage |
librasterlite2.dll 及其依赖 |
解码 tile_data_odd / tile_data_even |
SetDllDirectory、PATH 后由 P/Invoke 装载 |
gdalinfo.exe、gdal_translate.exe |
读取 Coverage 元数据、导出预览 GeoTIFF | 子进程方式调用 |
带 RASTERLITE2 driver 的 GDAL 运行时 |
识别 RASTERLITE2: 数据源 |
PATH 中包含 GDAL、SpatiaLite、RasterLite2、vcpkg 目录 |
GDAL_DATA |
GDAL 数据文件(坐标系等) | 在 ProcessStartInfo.Environment 中设置 |
Microsoft.Data.Sqlite 在这里使用其随 NuGet 提供的 e_sqlite3。它只执行普通的 SELECT,不需要数据库连接拥有 RasterLite2 SQL 函数。因此,不能从这个项目推导出"RL2_Version() 可在 SqliteConnection 上执行"。
3. Coverage:用 GDAL 驱动读取完整 DEM
3.1 RASTERLITE2: 数据源的关键细节
RasterLite2 GDAL driver 用冒号分隔数据库和 Coverage:
bash
// 因 ':' 是数据源分隔符,Windows 上使用数据库文件名,
// 并将 GDAL 工作目录设为数据库所在目录。
var source = $"RASTERLITE2:{Path.GetFileName(Database)}:{Coverage}";
不能直接把 Windows 绝对路径拼进数据源,否则盘符中的 : 会破坏数据源解析。正确做法是把 GDAL 工作目录设为数据库所在目录,再传入数据库文件名。
3.2 先读取范围,再生成可显示的 PNG
AddFullDem 的核心流程如下:
bash
using var metadata = JsonDocument.Parse(
RunGdal("gdalinfo.exe", "-json", source));
var corners = metadata.RootElement.GetProperty("cornerCoordinates");
var lowerLeft = corners.GetProperty("lowerLeft");
var upperRight = corners.GetProperty("upperRight");
var extent = new MRect(
lowerLeft[0].GetDouble(), lowerLeft[1].GetDouble(),
upperRight[0].GetDouble(), upperRight[1].GetDouble());
var tifPath = Path.Combine(Path.GetTempPath(), "rasterlite2-full-dem.tif");
RunGdal("gdal_translate.exe",
"-of", "GTiff", "-ot", "Byte", "-outsize", "1200", "0",
"-scale", "0", "145", "0", "255", source, tifPath);
using var image = Image.FromFile(tifPath);
using var png = new MemoryStream();
image.Save(png, ImageFormat.Png);
var feature = new RasterFeature(new MRaster(png.ToArray(), extent));
这里的 0、145 是该 DEM 的显示范围,不是 RasterLite2 的通用值;项目把它们定义为 DemMinimum 和 DemMaximum。实际数据应使用其真实统计范围或显式的渲染策略。
调用外部程序的统一入口还必须同时设置 GDAL_DATA 与动态库搜索路径:
bash
private static string RunGdal(string executable, params string[] arguments)
{
var start = new ProcessStartInfo(Path.Combine(GdalApps, executable))
{
WorkingDirectory = Path.GetDirectoryName(Database)!,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
foreach (var argument in arguments) start.ArgumentList.Add(argument);
start.Environment["GDAL_DATA"] = GdalData;
start.Environment["PATH"] = string.Join(";", new[]
{
GdalApps, GdalBin, SpatiaLiteBin, RasterLiteBin, RasterLiteDirectory,
VcpkgBin, start.Environment["PATH"]
});
using var process = Process.Start(start)
?? throw new InvalidOperationException("Cannot start GDAL.");
var stdout = process.StandardOutput.ReadToEnd();
var stderr = process.StandardError.ReadToEnd();
process.WaitForExit();
if (process.ExitCode != 0)
throw new InvalidOperationException($"{executable} failed: {stderr}");
return stdout;
}
💡 Coverage 验收
gdalinfo -json RASTERLITE2:dem.sqlite:dem 成功并取得正确范围,说明 GDAL 已找到 RASTERLITE2 driver,且名为 dem 的 Coverage 可读取。gdal_translate 进一步成功则证明该 Coverage 可被完整导出。
4. Tile:按空间范围 SQL 查询实际 BLOB
4.1 查询 Tile 表、数据表和空间索引
项目不用 RL2_Get... SQL 函数,而是查询 RasterLite2 生成的 Coverage 专属表:
bash
using var connection = new SqliteConnection(
$"Data Source={Database};Mode=ReadOnly");
connection.Open();
using var command = connection.CreateCommand();
command.CommandText = """
SELECT r.xmin, r.ymax, d.tile_data_odd, d.tile_data_even
FROM dem_tiles t
JOIN dem_tile_data d ON d.tile_id = t.tile_id
JOIN idx_dem_tiles_geometry r ON r.pkid = t.tile_id
WHERE t.pyramid_level = 0
AND r.xmax >= $minX AND r.xmin <= $maxX
AND r.ymax >= $minY AND r.ymin <= $maxY
""";
command.Parameters.AddWithValue("$minX", envelope.MinX);
command.Parameters.AddWithValue("$maxX", envelope.MaxX);
command.Parameters.AddWithValue("$minY", envelope.MinY);
command.Parameters.AddWithValue("$maxY", envelope.MaxY);
这条 SQL 选择原始分辨率(pyramid_level = 0)下、与用户选择多边形外包框相交的 Tile。idx_dem_tiles_geometry 提供空间范围,dem_tile_data 提供奇、偶 BLOB。表名来自当前 Coverage dem;换成 Coverage foo 时,应对应检查 foo_tiles、foo_tile_data、idx_foo_tiles_geometry,而不是照抄 dem_*。
查询到 0 行时,项目会抛出异常:
bash
if (features.Count == 0)
throw new InvalidOperationException("The SQL query returned no RasterLite2 tiles.");
这是一条有效的 Tile 层验收边界:SQL 能打开库却返回 0 行,只能说明数据库可读,不能说明当前空间范围内有栅格数据。
4.2 解码 tile_data_odd / tile_data_even
在打开 SQLite 连接前,项目先设置 RasterLite2 DLL 及其依赖的搜索路径:
bash
NativeKernel32.SetDllDirectory(RasterLiteDirectory);
Environment.SetEnvironmentVariable(
"PATH",
RasterLiteDirectory + ";" + Environment.GetEnvironmentVariable("PATH"));
随后,DecodeTile 用 P/Invoke 解码 BLOB,读出浮点像元,并始终释放 native 资源:
bash
var raster = NativeRasterLite.rl2_raster_decode(
0x31, odd, odd.Length, even, even?.Length ?? 0, IntPtr.Zero);
if (raster == IntPtr.Zero)
throw new InvalidOperationException("Cannot decode a RasterLite2 tile.");
try
{
Ensure(NativeRasterLite.rl2_get_raster_size(raster, out var width, out var height));
Ensure(NativeRasterLite.rl2_raster_data_to_float(raster, out var pointer, out _));
try
{
var values = new float[checked((int)width * (int)height)];
Marshal.Copy(pointer, values, 0, values.Length);
// values -> Bitmap -> PNG -> Mapsui MRaster
}
finally
{
NativeCRuntime.free(pointer);
}
}
finally
{
NativeRasterLite.rl2_destroy_raster(raster);
}
对应声明:
bash
[DllImport(RasterLiteDll, CallingConvention = CallingConvention.Cdecl)]
internal static extern IntPtr rl2_raster_decode(
int scale, byte[] odd, int oddSize, byte[]? even, int evenSize, IntPtr palette);
[DllImport(RasterLiteDll, CallingConvention = CallingConvention.Cdecl)]
internal static extern int rl2_get_raster_size(
IntPtr raster, out uint width, out uint height);
[DllImport(RasterLiteDll, CallingConvention = CallingConvention.Cdecl)]
internal static extern int rl2_raster_data_to_float(
IntPtr raster, out IntPtr buffer, out int bufferSize);
[DllImport(RasterLiteDll, CallingConvention = CallingConvention.Cdecl)]
internal static extern void rl2_destroy_raster(IntPtr raster);
0x31、rl2_raster_data_to_float、NoData 值、像元尺寸和 free 的调用方式都与这份 DEM 和当前 native 构建绑定,不能直接当作所有 Coverage 的模板。特别是 native 内存必须由兼容的运行时释放;更换 RasterLite2 构建或 CRT 后,应以该库的 C API / 头文件为准重新核对分配与释放约定。
4.3 从 float 像元生成局部透明栅格
项目遍历每个像元中心,仅绘制落在选择多边形内、有限且不等于 NoData 的值:
bash
var x = tileMinX + (column + 0.5) * PixelSize;
var y = tileMaxY - (row + 0.5) * PixelSize;
var value = values[row * bitmap.Width + column];
var center = new Point(x, y);
if (!_selection.Covers(center) || !float.IsFinite(value) || value == NoData)
continue;
var gray = (byte)Math.Clamp(
(int)Math.Round((value - DemMinimum) * 255 / (DemMaximum - DemMinimum)),
0, 255);
由此生成的 MRaster 范围是:
bash
var extent = new MRect(
tileMinX,
tileMaxY - height * PixelSize,
tileMinX + width * PixelSize,
tileMaxY);
其中 PixelSize = 10 与库中这一 DEM 的实际分辨率相符;应从 Coverage 元数据或输入数据读取,不应在通用程序中硬编码。
5. Coverage、Section、Tile 分层验证
| 验证层 | 本项目的证据 | 可以证明 | 不能证明 |
|---|---|---|---|
| SQLite 文件 | SqliteConnection.Open() 成功 |
数据库文件可按只读方式打开 | RasterLite2 DLL、Coverage 或 Tile 可读 |
| Coverage | gdalinfo -json RASTERLITE2:dem.sqlite:dem 成功 |
GDAL 可识别并读取 dem Coverage |
某个应用的原生 Tile 解码逻辑正确 |
| 完整影像 | gdal_translate 成功并显示图层 |
Coverage 可导出为完整图像 | 局部 SQL 范围筛选正确 |
| Tile 查询 | dem_tiles、dem_tile_data、空间索引联表返回行 |
目标范围内存在原始层级的 Tile BLOB | BLOB 能成功解码 |
| Tile 解码 | rl2_raster_decode 非空、尺寸与 float 转换成功 |
本机构建可解码所取 Tile | 颜色拉伸、NoData 和坐标范围正确 |
| 局部显示 | Mapsui 叠加的 SQL tile 与完整 DEM 对齐 | SQL 筛选、像元定位、渲染结果相互一致 | 写入/导入过程一定正确 |
dem_sections 虽然是 RasterLite2 的重要对象,但当前查看器不读取它。若需要验收"某份源影像是否已被导入",请在数据库工具中补查 dem_sections 的记录;那属于数据生产链路,不是本项目的显示链路。
6. 当前项目的边界与落地建议
- 示例中的数据库、DLL、GDAL 和 Shapefile 目录均是绝对路径。发布前应移入配置文件或命令行参数,并在启动时逐项校验存在性。
- SQL 参数已用于空间范围,避免了把坐标值直接拼进 SQL;Coverage 名称不能用参数绑定到表名,若要可选 Coverage,应从受信白名单选择并安全地构造标识符。
- 通过
PATH解析 native 依赖是进程级行为。桌面工具可以接受;生产部署应使用固定、版本一致的 native 目录,并记录 GDAL / RasterLite2 的版本。 - 全图路径每次会导出一个临时 TIFF。若反复刷新,建议缓存 PNG 或在退出时清理自己创建的临时文件。
- 这是 Windows x64 的实践。
kernel32!SetDllDirectory、msvcrt!free和 DLL 文件名都不是跨平台方案。
7. 最小验收清单
dotnet build成功。dem.sqlite、librasterlite2.dll、gdalinfo.exe和GDAL_DATA目录均存在。- 启动后,"DEM (full RasterLite2)"图层能显示完整影像。
- 选择区域与 DEM 相交时,"DEM (SQL tile selection)"图层能显示,并可按需打开像元中心点图层。
- 两个 DEM 图层在同一范围内对齐;否则依次核查 Tile 查询范围、
PixelSize、NoData、坐标系和 GDAL 的 Coverage 元数据。
一句话总结:本项目先让 GDAL 验证整个 RasterLite2 Coverage,再直接从 SQLite 读取 Tile BLOB 并交给 librasterlite2.dll 解码;二者叠加一致,才是当前 C# 查看链路可靠的完成标志。