Базовая работа с GDAL: гипсобатиметрическая модель Земли
Содержание
Постановка задачи⌗
Подготовим простенькую C++ модель, возвращающую глубину Мирового океана или высоту над уровнем моря для заданных географических координат (широта, долгота), а также строгую принадлежность данной точки суше либо океану.
Для глубин и высот будем использовать растровые датасеты (полученные CleanTOPO2, GEBCO, SRTM и прочими проектами), а для строгой классификации суша-вода - векторные карты геометрии суши и океанов (например, проект Natural Earth содержит много полезных физических карт Земли). Наличие двух принципиально разных датасетов обусловлено прежде всего факторами точности измерений: разрешения растра, топографической съёмки, грубостью векторных многоугольников и т.д. Для чтения датасетов используем библиотеку абстракции геопространственных данных GDAL, а для тестирования модели - графические возможности Qt.
Датасеты⌗
Для глубин океанов и высот над уровнем моря я воспользуюсь одним комбинированным растром Blue Earth Bathymetry. Это редактированная версия GEBCO (General Bathymetric Chart of the Oceans), дополненная топографией для всего Земного шара.
Для классификации вода-земля я воспользуюсь векторной картой 1:10м массы суши Земли от Natural Earth Data.
Наша модель, однако, сможет поддерживать раздельные растры для суши и океана, а также незаисимую работу двух векторных слоёв.
Интерфейс к модели⌗
#include <filesystem>
class hypsobathymetry_model {
public:
virtual ~hypsobathymetry_model() = default;
/*
* Query interface
*/
virtual bool is_land(double lat, double lon) = 0;
virtual bool is_ocean(double lat, double lon) = 0;
virtual int value(double lat, double lon) = 0;
/*
* Attachments of data files
*/
virtual void set_ocean_geometry(const std::filesystem::path& path) = 0;
virtual void set_land_geometry(const std::filesystem::path& path) = 0;
virtual void set_ocean_values_raster(const std::filesystem::path& path) = 0;
virtual void set_land_values_raster(const std::filesystem::path& path) = 0;
};
Реализация модели⌗
Конкреция использует С++ биндинги к GDAL. Мы владеем несколькими объектами GDALDataset
(тип полностью абстрагирует конкретные растровые/векторные форматы данных).
#include "hypsobathymetry_model.h"
#include <gdal/gdal.h>
#include <gdal/ogrsf_frmts.h>
#include <memory>
class gdal_hypsobathymetry_model : public hypsobathymetry_model {
private:
std::unique_ptr<GDALDataset> m_ocean_geometry_dataset;
std::unique_ptr<GDALDataset> m_land_geometry_dataset;
std::shared_ptr<GDALDataset> m_ocean_values_dataset;
std::shared_ptr<GDALDataset> m_land_values_dataset;
public:
gdal_hypsobathymetry_model();
virtual bool is_land(double lat, double lon) override;
virtual bool is_ocean(double lat, double lon) override;
virtual int value(double lat, double lon) override;
virtual void set_ocean_geometry(const std::filesystem::path& path) override;
virtual void set_land_geometry(const std::filesystem::path& path) override;
virtual void set_ocean_values_raster(const std::filesystem::path& path) override;
virtual void set_land_values_raster(const std::filesystem::path& path) override;
};
Инициализация GDAL⌗
В конструкторе gdal_hypsobathymetry_model()
инициализируем библиотеку GDAL:
GDALAllRegister();
Открытие датасетов⌗
Окрытие векторных и растровых датасетов происходит соответствующими вызовами GDALOpenEx()
и GDALOpen()
:
GDALDataset* dataset;
dataset = static_cast<GDALDataset*>(
GDALOpenEx(path.c_str(), GDAL_OF_VECTOR, nullptr, nullptr, nullptr));
dataset = static_cast<GDALDataset*>(GDALOpen(path.c_str(), GA_ReadOnly));
Извлечение информации из датасетов⌗
В GDAL применяется концепция слоёв (layer) для векторных и растровых областей (raster band) для растровых датасетов.
Вектор: геометрия суши/океанов⌗
Внутри датасета может быть один или более объектов OGRLayer
. Внутри каждого такого слоя содержится коллекция обектов OGRFeature
, каждый со своей геометрией OGRGeometry
(фундаментальный тип геометрии един для всего слоя). Для тестирования
“попадания” или нет заданных широты/долготы в пределы геометрии одного или более feature мы используем конструкцию следующего вида:
OGRPoint point(lon, lat);
for (auto&& layer : m_land_geometry_dataset->GetLayers()) {
for (auto&& feature : layer) {
auto geometry = feature->GetGeometryRef();
if (geometry->Contains(&point) == true) {
return true;
}
}
}
Как видно, мы создаём точку и тестируем её принадлежность каждой из извлекаемых геометрий методом OGRGeometry::Contains()
.
Растр: численные значения глубин океана и рельефа возвышенностей⌗
Растр читать немного сложнее, так как тут уже не уйти от математической трансформации географических координат: если вершины наших векторных геометрий хранятся в файле прямо в географических координатах (первая проекция, т.е. X как широта, Y как долгота), то прямоугольный растровый файл имеет чёткое количество пикселей по горизонтале и по вертикале. Необходимо сопоставлять точку, т.е. строку и столбец растра реальным географическим координатам и обратно.
Геотрансформация⌗
В терминологии GDAL смещение в растре по горизонтали называется “pixel offset”, по вертикали “line offset”. Сначала из загруженного датасета методом GetGeoTransform()
считывается матрица прямого преобразования (маппинг смещений от верхнего левого угла растра в геокоординаты), затем функцией GDALInvGeoTransform()
из этой матрицы строится обратная ей.
double geotransform[6], inverse_geotransform[6];
if (dataset->GetGeoTransform(geotransform) != CE_None) {
throw std::runtime_error("Geo transform not present in the raster");
}
if (!GDALInvGeoTransform(geotransform, inverse_geotransform)) {
throw std::domain_error("Can't invert the geo transform");
}
int raster_pixel_coord = static_cast<int>(
floor(inverse_geotransform[0] + inverse_geotransform[1] * lon +
inverse_geotransform[2] * lat));
int raster_line_coord = static_cast<int>(
floor(inverse_geotransform[3] + inverse_geotransform[4] * lon +
inverse_geotransform[5] * lat));
Считывание блока растра⌗
Внутри одного датасета может быть один или более объектов GDALRasterBand
(аналогия с OGRLayer
выше). Их порядковый счёт идёт с единицы. Извлекаем первый доступный raster band и читаем блок собственно данных методом GetRasterBand()
. GDAL оптимизирует чтение растра прямоугольными блоками, однако нас интересует лишь одна точка растра, поэтому в нашем случае размер блока составит 1 x 1 пиксель:
auto band = dataset->GetRasterBand(1);
double raster_pixel_value[2];
if (band->RasterIO(GF_Read, raster_pixel_coord, raster_line_coord, 1, 1,
raster_pixel_value, 1, 1, GDT_CFloat64, 0, 0) != CE_None) {
throw std::runtime_error("Raster read error");
}
int value = (int)raster_pixel_value[0];
Закрытие датасетов⌗
Объекты GDALDataset
закрывают все открытые файлы и полностью деинициализируются в деструкторе данного класса.
Полный код имплементации модели⌗
Ниже приводится полный исходный код. Модель пытается использовать всю доступную ей информацию: например, при отсутствии датасета геометрии океанов она применяет карту суши (и наоборот).
#include "gdal_hypsobathymetry_model.h"
#include <iostream>
#include <stdexcept>
static void check_lat_lon(double lat, double lon) {
if (lat > 90.0 || lat < -90.0 || lon > 180.0 || lon < -180.0) {
throw std::out_of_range("Invalid lat/lon");
}
}
bool gdal_hypsobathymetry_model::is_land(double lat, double lon) {
check_lat_lon(lat, lon);
if (m_land_geometry_dataset == nullptr &&
m_ocean_geometry_dataset == nullptr) {
throw std::domain_error("No geometry data");
}
if (m_land_geometry_dataset == nullptr) {
return !is_ocean(lat, lon);
}
OGRPoint point(lon, lat);
for (auto&& layer : m_land_geometry_dataset->GetLayers()) {
for (auto&& feature : layer) {
auto geometry = feature->GetGeometryRef();
if (geometry->Contains(&point) == true) {
return true;
}
}
}
return false;
}
bool gdal_hypsobathymetry_model::is_ocean(double lat, double lon) {
check_lat_lon(lat, lon);
if (m_land_geometry_dataset == nullptr &&
m_ocean_geometry_dataset == nullptr) {
throw std::domain_error("No geometry data");
}
if (m_ocean_geometry_dataset == nullptr) {
return !is_land(lat, lon);
}
OGRPoint point(lon, lat);
for (auto&& layer : m_ocean_geometry_dataset->GetLayers()) {
for (auto&& feature : layer) {
auto geometry = feature->GetGeometryRef();
if (geometry->Contains(&point) == true) {
return true;
}
}
}
return false;
}
int gdal_hypsobathymetry_model::value(double lat, double lon) {
check_lat_lon(lat, lon);
if (m_ocean_values_dataset == nullptr && m_land_values_dataset == nullptr) {
throw std::domain_error("No raster values data");
}
// select the appropriate raster dataset
GDALDataset* dataset = nullptr;
if (m_ocean_values_dataset == m_land_values_dataset) {
// combined dataset
dataset = m_ocean_values_dataset.get();
} else {
// both datasets are different; maybe one of them is null
bool use_land = is_land(lat, lon);
dataset =
use_land ? m_land_values_dataset.get() : m_ocean_values_dataset.get();
if (dataset == nullptr) {
std::string msg = use_land ? "Land " : "Ocean ";
msg.append("raster data is null");
throw std::domain_error(msg);
}
}
double geotransform[6], inverse_geotransform[6];
if (dataset->GetGeoTransform(geotransform) != CE_None) {
throw std::runtime_error("Geo transform not present in the raster");
}
if (!GDALInvGeoTransform(geotransform, inverse_geotransform)) {
throw std::domain_error("Can't invert the geo transform");
}
int raster_pixel_coord = static_cast<int>(
floor(inverse_geotransform[0] + inverse_geotransform[1] * lon +
inverse_geotransform[2] * lat));
int raster_line_coord = static_cast<int>(
floor(inverse_geotransform[3] + inverse_geotransform[4] * lon +
inverse_geotransform[5] * lat));
// use the first raster band
auto band = dataset->GetRasterBand(1);
double raster_pixel_value[2];
if (band->RasterIO(GF_Read, raster_pixel_coord, raster_line_coord, 1, 1,
raster_pixel_value, 1, 1, GDT_CFloat64, 0, 0) != CE_None) {
throw std::runtime_error("Raster read error");
}
int value = (int)raster_pixel_value[0];
return value;
}
void gdal_hypsobathymetry_model::set_ocean_geometry(
const std::filesystem::path& path) {
if (path.empty() == true) {
m_ocean_geometry_dataset.reset(nullptr);
return;
}
auto dataset = static_cast<GDALDataset*>(
GDALOpenEx(path.c_str(), GDAL_OF_VECTOR, nullptr, nullptr, nullptr));
if (dataset == nullptr) {
throw std::runtime_error("GDALOpenEx() returned null dataset");
}
std::cout << "Set ocean geometry from " << path.string() << " ("
<< dataset->GetDriverName() << ")\n";
m_ocean_geometry_dataset.reset(dataset);
}
void gdal_hypsobathymetry_model::set_land_geometry(
const std::filesystem::path& path) {
if (path.empty() == true) {
m_land_geometry_dataset.reset(nullptr);
return;
}
auto dataset = static_cast<GDALDataset*>(
GDALOpenEx(path.c_str(), GDAL_OF_VECTOR, nullptr, nullptr, nullptr));
if (dataset == nullptr) {
throw std::runtime_error("GDALOpenEx() returned null dataset");
}
std::cout << "Set land geometry from " << path.string() << " ("
<< dataset->GetDriverName() << ")\n";
m_land_geometry_dataset.reset(dataset);
}
void gdal_hypsobathymetry_model::set_ocean_values_raster(
const std::filesystem::path& path) {
if (path.empty() == true) {
m_ocean_values_dataset.reset();
return;
}
auto dataset = static_cast<GDALDataset*>(GDALOpen(path.c_str(), GA_ReadOnly));
if (dataset == nullptr) {
throw std::runtime_error("GDALOpen() returned null dataset");
}
if (m_land_values_dataset != nullptr) {
auto other_dataset = m_land_values_dataset;
if (other_dataset->GetFileList()[0] == path.string()) {
std::cout << "Reusing land values raster for ocean: " << path.string()
<< "\n";
m_ocean_values_dataset = other_dataset;
return;
}
}
std::cout << "Set ocean values from " << path.string() << " ("
<< dataset->GetDriverName() << ")\n";
m_ocean_values_dataset.reset(dataset);
}
void gdal_hypsobathymetry_model::set_land_values_raster(
const std::filesystem::path& path) {
if (path.empty() == true) {
m_ocean_values_dataset.reset();
return;
}
auto dataset = static_cast<GDALDataset*>(GDALOpen(path.c_str(), GA_ReadOnly));
if (dataset == nullptr) {
throw std::runtime_error("GDALOpen() returned null dataset");
}
if (m_ocean_values_dataset != nullptr) {
auto other_dataset = m_ocean_values_dataset;
if (other_dataset->GetFileList()[0] == path.string()) {
std::cout << "Reusing ocean values raster for land: " << path.string()
<< "\n";
m_land_values_dataset = other_dataset;
return;
}
}
std::cout << "Set land values raster from " << path.string() << " ("
<< dataset->GetDriverName() << ")\n";
m_land_values_dataset.reset(dataset);
}
gdal_hypsobathymetry_model::gdal_hypsobathymetry_model() { GDALAllRegister(); }
Визуализация контуров суши/береговых линий⌗
Воспользуемся графическим фреймворком QGraphicsScene
.
Унаследуем от QGraphicsItemGroup
класс, который при инстанциации конвертирует заданный векторный датасет участков суши в одну большую группу QGraphicsPolygonItem
, а также экспозит простой метод для установки толщины линий, цвета контура и штриховки всех многоугольников коллекции.
#include <QBrush>
#include <QGraphicsItem>
#include <QGraphicsItemGroup>
#include <optional>
class GdalCoastlineGraphicsItem : public QGraphicsItemGroup {
private:
public:
GdalCoastlineGraphicsItem(const char* coastline_shp);
void setPenBrushForAllPolygons(std::optional<qreal> penwidth,
std::optional<QColor> pencolor = std::nullopt,
std::optional<QBrush> brush = std::nullopt);
};
Особый интерес представляют статические функции трансформации вершин геометрий датасета (зеркального отражения точки по вертикали, т.к. в Qt графическая ось Y растёт вниз, а в датасете использованы географические координаты -90…+90) при создании объектов QPolygonF
. В системе GDAL/GEOS геометрия типа OGRMultiPolygon
содержит несколько OGRPolygon
, причём объект OGRPolygon
может описывать не один, а несколько многоугольников сразу: обязательный exterior ring (многоугольник) и опциональные interior rings (многоугольники) для внутренних “дырок”. Геометрии можно итерировать по точкам.
static void transform_qpoint(QPointF& point) {
point.setY(90.f - point.y());
point.setX(point.x() * 60); // 1 minute -> 1 pixel scale
point.setY(point.y() * 60);
}
static QList<QPolygonF> convert_polygon(const OGRPolygon& ogrpoly) {
QPolygonF qextring;
auto extring = ogrpoly.getExteriorRing();
for (auto& pt : extring) {
QPointF qpt{pt.getX(), pt.getY()};
transform_qpoint(qpt);
qextring.push_back(qpt);
}
QList<QPolygonF> qintrings;
if (ogrpoly.getNumInteriorRings() > 0) {
qDebug("Poly has %d interior rings:", ogrpoly.getNumInteriorRings());
for (int i = 0; i < ogrpoly.getNumInteriorRings(); ++i) {
auto intring = ogrpoly.getInteriorRing(i);
qDebug("\tRing %d (%d points)", i, intring->getNumPoints());
QPolygonF qintring;
for (auto& pt : intring) {
QPointF qpt{pt.getX(), pt.getY()};
transform_qpoint(qpt);
qintring.push_back(qpt);
}
qintrings.push_back(qintring);
}
}
qintrings.push_front(qextring);
return qintrings;
}
static QList<QPolygonF> convert_multipolygon(
const OGRMultiPolygon& ogrmultipoly) {
QList<QPolygonF> qpolys;
for (const OGRPolygon* poly : ogrmultipoly) {
qpolys.append(convert_polygon(*poly));
}
return qpolys;
}
Вот полная имплементация:
#include "GdalCoastlineGraphicsItem.h"
#include <gdal/ogrsf_frmts.h>
#include <QGraphicsLineItem>
#include <QGraphicsPathItem>
#include <QPen>
static void transform_qpoint(QPointF& point) {
point.setY(90.f - point.y());
point.setX(point.x() * 60); // 1 minute -> 1 pixel scale
point.setY(point.y() * 60);
}
static QList<QPolygonF> convert_polygon(const OGRPolygon& ogrpoly) {
QPolygonF qextring;
auto extring = ogrpoly.getExteriorRing();
for (auto& pt : extring) {
QPointF qpt{pt.getX(), pt.getY()};
transform_qpoint(qpt);
qextring.push_back(qpt);
}
QList<QPolygonF> qintrings;
if (ogrpoly.getNumInteriorRings() > 0) {
qDebug("Poly has %d interior rings:", ogrpoly.getNumInteriorRings());
for (int i = 0; i < ogrpoly.getNumInteriorRings(); ++i) {
auto intring = ogrpoly.getInteriorRing(i);
qDebug("\tRing %d (%d points)", i, intring->getNumPoints());
QPolygonF qintring;
for (auto& pt : intring) {
QPointF qpt{pt.getX(), pt.getY()};
transform_qpoint(qpt);
qintring.push_back(qpt);
}
qintrings.push_back(qintring);
}
}
qintrings.push_front(qextring);
return qintrings;
}
static QList<QPolygonF> convert_multipolygon(
const OGRMultiPolygon& ogrmultipoly) {
QList<QPolygonF> qpolys;
for (const OGRPolygon* poly : ogrmultipoly) {
qpolys.append(convert_polygon(*poly));
}
return qpolys;
}
GdalCoastlineGraphicsItem::GdalCoastlineGraphicsItem(
const char* coastline_shp) {
GDALAllRegister();
auto dataset = static_cast<GDALDataset*>(
GDALOpenEx(coastline_shp, GDAL_OF_VECTOR, nullptr, nullptr, nullptr));
if (dataset == nullptr) {
throw std::runtime_error("GDALOpenEx() returned null dataset");
}
QList<QPolygonF> total_qpolys;
auto layer = dataset->GetLayer(0);
for (auto& feature : layer) {
auto multipolygon = feature->GetGeometryRef()->toMultiPolygon();
if (multipolygon == nullptr) {
qDebug("Geometry for FID %lld is not MultiPolygon", feature->GetFID());
break;
}
auto qpolys = convert_multipolygon(*multipolygon);
qDebug(
"Constructed list of total %d polys from a multypolygon for FID %lld",
qpolys.size(), feature->GetFID());
total_qpolys.append(qpolys);
}
// create the items
for (auto& qpoly : total_qpolys) {
auto qpolyitem = new QGraphicsPolygonItem{qpoly};
this->addToGroup(qpolyitem);
}
delete dataset;
}
void GdalCoastlineGraphicsItem::setPenBrushForAllPolygons(
std::optional<qreal> penwidth, std::optional<QColor> pencolor,
std::optional<QBrush> brush) {
for (auto item : childItems()) {
auto shapeitem = dynamic_cast<QAbstractGraphicsShapeItem*>(item);
if (shapeitem) {
QPen pen = shapeitem->pen();
if (penwidth.has_value()) {
pen.setWidthF(penwidth.value());
}
if (pencolor.has_value()) {
pen.setColor(pencolor.value());
}
shapeitem->setPen(pen);
if (brush.has_value()) {
shapeitem->setBrush(brush.value());
}
}
}
}
Тестирование модели⌗
Для удобства просмотра возьмём QGraphicsView
и добавим в него испускание сигнала при клике по холсту (трансформируем мышиные координаты в широту/долготу), а также поддержку zoom-in/zoom-out колёсиком мышки с автокорректировкой толщины контура GdalCoastlineGraphicsItem
. Весь остальной функционал вроде скроллинга получается автоматически при подключении объекта-сцены благодаря фреймворку Qt Graphics.
#include <QGraphicsView>
class ZoomableGraphicsView : public QGraphicsView {
Q_OBJECT
public:
ZoomableGraphicsView(QWidget* parent);
signals:
void lat_lon_clicked(double lat, double lon);
protected:
virtual void wheelEvent(QWheelEvent* event) override;
virtual void mousePressEvent(QMouseEvent* event) override;
};
Полная реализация просмотрщика сцены:
#include "ZoomableGraphicsView.h"
#include <QScrollBar>
#include <QWheelEvent>
#include <QtMath>
#include "GdalCoastlineGraphicsItem.h"
void ZoomableGraphicsView::wheelEvent(QWheelEvent* event) {
static float total_zoom = 1.f;
static const qreal ZOOM_STEP = .5;
if (event->modifiers() & Qt::ControlModifier) {
// zoom
const ViewportAnchor anchor = transformationAnchor();
setTransformationAnchor(QGraphicsView::AnchorUnderMouse);
int angle = event->angleDelta().y();
qreal factor;
if (angle > 0) {
factor = 1. + ZOOM_STEP;
} else {
factor = 1. - ZOOM_STEP;
}
scale(factor, factor);
total_zoom *= factor;
// rescale outlines of certain graphics objects in the scene
auto scn = scene();
if (scn != nullptr) {
for (auto item : scn->items()) {
if (auto coastlineitem =
dynamic_cast<GdalCoastlineGraphicsItem*>(item)) {
coastlineitem->setPenBrushForAllPolygons({1.f / total_zoom});
}
}
}
qDebug("Zoom: total zoom %f", total_zoom);
setTransformationAnchor(anchor);
} else {
QGraphicsView::wheelEvent(event);
}
}
ZoomableGraphicsView::ZoomableGraphicsView(QWidget* parent)
: QGraphicsView(parent) {}
void ZoomableGraphicsView::mousePressEvent(QMouseEvent* event) {
QPointF mousepos = event->localPos();
auto scenepos = mapToScene(event->x(), event->y());
double lat = 90. - (scenepos.y() / 60);
double lon = scenepos.x() / 60.;
emit lat_lon_clicked(lat, lon);
QGraphicsView::mousePressEvent(event);
}
В главном окне создаём графику береговых линий, устанавливаем цвет штриховки.
auto coastline = new GdalCoastlineGraphicsItem{land_shp};
coastline->setPenBrushForAllPolygons(
std::nullopt, QColor{0, 128, 0},
QBrush{QColor{15, 15, 15}}); // landmass bg color
Добавляем её в наш просмотрщик, подписываемся на его сигнал о клике по холсту (т.е. широте/долготе).
auto scene = new QGraphicsScene;
scene->addItem(coastline);
// oceans background color
ui->graphicsView->setBackgroundBrush(QBrush{QColor{27, 38, 49}});
ui->graphicsView->setScene(scene);
QObject::connect(ui->graphicsView, &ZoomableGraphicsView::lat_lon_clicked,
this, &MainWindow::lat_lon_requested);
В конструкторе окна мы инициализировали нашу гипсобатиметрическую модель – пришло время её опросить по кликам мышки:
void MainWindow::lat_lon_requested(double lat, double lon) {
auto on_land = model->is_land(lat, lon);
ui->label->setText(QString::asprintf(
"LatLon: %f, %f, %s, %s %d meters", lat, lon, on_land ? "land" : "ocean",
on_land ? "altitude" : "depth", model->value(lat, lon)));
}
Берега Южной Америки:
Где-то в Тихом океане:
Апеннинский полуостров: