2020-02-29 13:47:27 +01:00
|
|
|
#include <cmath>
|
2018-03-30 10:25:05 +02:00
|
|
|
#include <QFileInfo>
|
|
|
|
#include <QEventLoop>
|
|
|
|
#include <QXmlStreamReader>
|
2018-04-02 23:33:10 +02:00
|
|
|
#include <QStringList>
|
2018-03-30 10:25:05 +02:00
|
|
|
#include "crs.h"
|
|
|
|
#include "wms.h"
|
|
|
|
|
|
|
|
|
2020-03-03 09:29:16 +01:00
|
|
|
static QString bareFormat(const QString &format)
|
|
|
|
{
|
|
|
|
return format.left(format.indexOf(';')).trimmed();
|
|
|
|
}
|
|
|
|
|
2020-02-29 13:47:27 +01:00
|
|
|
static inline double hint2denominator(double h)
|
|
|
|
{
|
2020-02-29 20:11:49 +01:00
|
|
|
/* Some WMS 1.1.1 servers use a 72dpi resolution by default. Using the usual
|
|
|
|
90dpi (0.28mm) resolution known from later standards (WMS 1.3, WMTS) does
|
|
|
|
make them return emty images in the "max" scale level. */
|
|
|
|
return h / (M_SQRT2 * 0.36e-3);
|
2020-02-29 13:47:27 +01:00
|
|
|
}
|
|
|
|
|
2018-04-02 19:59:52 +02:00
|
|
|
WMS::CTX::CTX(const Setup &setup) : setup(setup), formatSupported(false)
|
|
|
|
{
|
|
|
|
QStringList ll = setup.layer().split(',');
|
|
|
|
|
2018-04-03 22:35:13 +02:00
|
|
|
if (setup.style().isEmpty()) {
|
|
|
|
for (int i = 0; i < ll.size(); i++)
|
|
|
|
layers.append(Layer(ll.at(i)));
|
|
|
|
} else {
|
|
|
|
QStringList sl = setup.style().split(',');
|
|
|
|
if (ll.size() != sl.size())
|
|
|
|
return;
|
2018-04-02 19:59:52 +02:00
|
|
|
|
2018-04-03 22:35:13 +02:00
|
|
|
for (int i = 0; i < ll.size(); i++)
|
|
|
|
layers.append(Layer(ll.at(i), sl.at(i)));
|
|
|
|
}
|
2018-04-02 19:59:52 +02:00
|
|
|
}
|
|
|
|
|
2020-03-04 19:47:23 +01:00
|
|
|
void WMS::get(QXmlStreamReader &reader, CTX &ctx)
|
|
|
|
{
|
|
|
|
while (reader.readNextStartElement()) {
|
2020-12-22 22:09:09 +01:00
|
|
|
if (reader.name() == QLatin1String("OnlineResource")) {
|
2020-03-04 19:47:23 +01:00
|
|
|
QXmlStreamAttributes attr = reader.attributes();
|
|
|
|
ctx.url = attr.value("xlink:href").toString();
|
|
|
|
reader.skipCurrentElement();
|
|
|
|
} else
|
|
|
|
reader.skipCurrentElement();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void WMS::http(QXmlStreamReader &reader, CTX &ctx)
|
|
|
|
{
|
|
|
|
while (reader.readNextStartElement()) {
|
2020-12-22 22:09:09 +01:00
|
|
|
if (reader.name() == QLatin1String("Get"))
|
2020-03-04 19:47:23 +01:00
|
|
|
get(reader, ctx);
|
|
|
|
else
|
|
|
|
reader.skipCurrentElement();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void WMS::dcpType(QXmlStreamReader &reader, CTX &ctx)
|
|
|
|
{
|
|
|
|
while (reader.readNextStartElement()) {
|
2020-12-22 22:09:09 +01:00
|
|
|
if (reader.name() == QLatin1String("HTTP"))
|
2020-03-04 19:47:23 +01:00
|
|
|
http(reader, ctx);
|
|
|
|
else
|
|
|
|
reader.skipCurrentElement();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-30 10:25:05 +02:00
|
|
|
void WMS::getMap(QXmlStreamReader &reader, CTX &ctx)
|
|
|
|
{
|
|
|
|
while (reader.readNextStartElement()) {
|
2020-12-22 22:09:09 +01:00
|
|
|
if (reader.name() == QLatin1String("Format")) {
|
2020-03-01 13:26:19 +01:00
|
|
|
QString format(reader.readElementText());
|
2020-03-03 09:29:16 +01:00
|
|
|
if (bareFormat(format) == bareFormat(ctx.setup.format()))
|
2018-04-02 19:59:52 +02:00
|
|
|
ctx.formatSupported = true;
|
2020-12-22 22:09:09 +01:00
|
|
|
} else if (reader.name() == QLatin1String("DCPType"))
|
2020-03-04 19:47:23 +01:00
|
|
|
dcpType(reader, ctx);
|
|
|
|
else
|
2018-03-30 10:25:05 +02:00
|
|
|
reader.skipCurrentElement();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void WMS::request(QXmlStreamReader &reader, CTX &ctx)
|
|
|
|
{
|
|
|
|
while (reader.readNextStartElement()) {
|
2020-12-22 22:09:09 +01:00
|
|
|
if (reader.name() == QLatin1String("GetMap"))
|
2018-03-30 10:25:05 +02:00
|
|
|
getMap(reader, ctx);
|
|
|
|
else
|
|
|
|
reader.skipCurrentElement();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
QString WMS::style(QXmlStreamReader &reader)
|
|
|
|
{
|
|
|
|
QString name;
|
|
|
|
|
|
|
|
while (reader.readNextStartElement()) {
|
2020-12-22 22:09:09 +01:00
|
|
|
if (reader.name() == QLatin1String("Name"))
|
2018-03-30 10:25:05 +02:00
|
|
|
name = reader.readElementText();
|
|
|
|
else
|
|
|
|
reader.skipCurrentElement();
|
|
|
|
}
|
|
|
|
|
|
|
|
return name;
|
|
|
|
}
|
|
|
|
|
2018-04-01 20:01:25 +02:00
|
|
|
RectC WMS::geographicBoundingBox(QXmlStreamReader &reader)
|
|
|
|
{
|
2018-04-13 21:14:12 +02:00
|
|
|
double left = NAN, top = NAN, right = NAN, bottom = NAN;
|
2018-04-01 20:01:25 +02:00
|
|
|
|
|
|
|
while (reader.readNextStartElement()) {
|
2020-12-22 22:09:09 +01:00
|
|
|
if (reader.name() == QLatin1String("westBoundLongitude"))
|
2018-04-01 20:01:25 +02:00
|
|
|
left = reader.readElementText().toDouble();
|
2020-12-22 22:09:09 +01:00
|
|
|
else if (reader.name() == QLatin1String("eastBoundLongitude"))
|
2018-04-01 20:01:25 +02:00
|
|
|
right = reader.readElementText().toDouble();
|
2020-12-22 22:09:09 +01:00
|
|
|
else if (reader.name() == QLatin1String("northBoundLatitude"))
|
2018-04-01 20:01:25 +02:00
|
|
|
top = reader.readElementText().toDouble();
|
2020-12-22 22:09:09 +01:00
|
|
|
else if (reader.name() == QLatin1String("southBoundLatitude"))
|
2018-04-01 20:01:25 +02:00
|
|
|
bottom = reader.readElementText().toDouble();
|
|
|
|
else
|
|
|
|
reader.skipCurrentElement();
|
|
|
|
}
|
|
|
|
|
|
|
|
return RectC(Coordinates(left, top), Coordinates(right, bottom));
|
|
|
|
}
|
|
|
|
|
2018-03-30 10:25:05 +02:00
|
|
|
void WMS::layer(QXmlStreamReader &reader, CTX &ctx,
|
2018-04-01 20:01:25 +02:00
|
|
|
const QList<QString> &pCRSs, const QList<QString> &pStyles,
|
|
|
|
RangeF &pScaleDenominator, RectC &pBoundingBox)
|
2018-03-30 10:25:05 +02:00
|
|
|
{
|
|
|
|
QString name;
|
|
|
|
QList<QString> CRSs(pCRSs);
|
|
|
|
QList<QString> styles(pStyles);
|
2018-04-01 20:01:25 +02:00
|
|
|
RangeF scaleDenominator(pScaleDenominator);
|
|
|
|
RectC boundingBox(pBoundingBox);
|
2018-04-02 19:59:52 +02:00
|
|
|
int index;
|
2018-03-30 10:25:05 +02:00
|
|
|
|
|
|
|
while (reader.readNextStartElement()) {
|
2020-12-22 22:09:09 +01:00
|
|
|
if (reader.name() == QLatin1String("Name"))
|
2018-03-30 10:25:05 +02:00
|
|
|
name = reader.readElementText();
|
2020-12-22 22:09:09 +01:00
|
|
|
else if (reader.name() == QLatin1String("CRS")
|
|
|
|
|| reader.name() == QLatin1String("SRS"))
|
2018-03-30 10:25:05 +02:00
|
|
|
CRSs.append(reader.readElementText());
|
2020-12-22 22:09:09 +01:00
|
|
|
else if (reader.name() == QLatin1String("Style"))
|
2018-03-30 10:25:05 +02:00
|
|
|
styles.append(style(reader));
|
2020-12-22 22:09:09 +01:00
|
|
|
else if (reader.name() == QLatin1String("ScaleHint")) {
|
2020-02-29 13:47:27 +01:00
|
|
|
QXmlStreamAttributes attr = reader.attributes();
|
|
|
|
double minHint = attr.value("min").toString().toDouble();
|
|
|
|
double maxHint = attr.value("max").toString().toDouble();
|
2020-02-29 21:40:13 +01:00
|
|
|
if (minHint > 0)
|
|
|
|
scaleDenominator.setMin(hint2denominator(minHint));
|
|
|
|
if (maxHint > 0)
|
|
|
|
scaleDenominator.setMax(hint2denominator(maxHint));
|
|
|
|
reader.skipCurrentElement();
|
2020-12-22 22:09:09 +01:00
|
|
|
} else if (reader.name() == QLatin1String("MinScaleDenominator")) {
|
2018-04-02 23:27:42 +02:00
|
|
|
double sd = reader.readElementText().toDouble();
|
2018-04-03 01:14:58 +02:00
|
|
|
if (sd > 0)
|
2018-04-02 23:27:42 +02:00
|
|
|
scaleDenominator.setMin(sd);
|
2020-12-22 22:09:09 +01:00
|
|
|
} else if (reader.name() == QLatin1String("MaxScaleDenominator")) {
|
2018-04-02 23:27:42 +02:00
|
|
|
double sd = reader.readElementText().toDouble();
|
2018-04-03 01:14:58 +02:00
|
|
|
if (sd > 0)
|
2018-04-02 23:27:42 +02:00
|
|
|
scaleDenominator.setMax(sd);
|
2020-12-22 22:09:09 +01:00
|
|
|
} else if (reader.name() == QLatin1String("LatLonBoundingBox")) {
|
2018-03-30 10:25:05 +02:00
|
|
|
QXmlStreamAttributes attr = reader.attributes();
|
2018-04-01 20:01:25 +02:00
|
|
|
boundingBox = RectC(Coordinates(
|
|
|
|
attr.value("minx").toString().toDouble(),
|
|
|
|
attr.value("maxy").toString().toDouble()),
|
|
|
|
Coordinates(attr.value("maxx").toString().toDouble(),
|
|
|
|
attr.value("miny").toString().toDouble()));
|
2018-03-30 10:25:05 +02:00
|
|
|
reader.skipCurrentElement();
|
2020-12-22 22:09:09 +01:00
|
|
|
} else if (reader.name() == QLatin1String("EX_GeographicBoundingBox"))
|
2018-04-01 20:01:25 +02:00
|
|
|
boundingBox = geographicBoundingBox(reader);
|
2020-12-22 22:09:09 +01:00
|
|
|
else if (reader.name() == QLatin1String("Layer"))
|
2018-04-01 20:01:25 +02:00
|
|
|
layer(reader, ctx, CRSs, styles, scaleDenominator, boundingBox);
|
2018-03-30 10:25:05 +02:00
|
|
|
else
|
|
|
|
reader.skipCurrentElement();
|
|
|
|
}
|
|
|
|
|
2018-04-02 19:59:52 +02:00
|
|
|
if ((index = ctx.layers.indexOf(name)) >= 0) {
|
|
|
|
Layer &layer = ctx.layers[index];
|
|
|
|
layer.scaleDenominator = scaleDenominator;
|
|
|
|
layer.boundingBox = boundingBox;
|
|
|
|
layer.isDefined = true;
|
2018-04-03 22:35:13 +02:00
|
|
|
layer.hasStyle = styles.contains(layer.style) || layer.style.isEmpty();
|
2018-04-02 19:59:52 +02:00
|
|
|
layer.hasCRS = CRSs.contains(ctx.setup.crs());
|
2018-03-30 10:25:05 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void WMS::capability(QXmlStreamReader &reader, CTX &ctx)
|
|
|
|
{
|
|
|
|
QList<QString> CRSs;
|
|
|
|
QList<QString> styles;
|
2018-04-03 23:14:47 +02:00
|
|
|
RangeF scaleDenominator(133.295598991, 559082264.0287178);
|
2018-04-01 20:01:25 +02:00
|
|
|
RectC boundingBox;
|
2018-03-30 10:25:05 +02:00
|
|
|
|
|
|
|
while (reader.readNextStartElement()) {
|
2020-12-22 22:09:09 +01:00
|
|
|
if (reader.name() == QLatin1String("Layer"))
|
2018-04-01 20:01:25 +02:00
|
|
|
layer(reader, ctx, CRSs, styles, scaleDenominator, boundingBox);
|
2020-12-22 22:09:09 +01:00
|
|
|
else if (reader.name() == QLatin1String("Request"))
|
2018-03-30 10:25:05 +02:00
|
|
|
request(reader, ctx);
|
|
|
|
else
|
|
|
|
reader.skipCurrentElement();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void WMS::capabilities(QXmlStreamReader &reader, CTX &ctx)
|
|
|
|
{
|
2018-03-31 11:27:01 +02:00
|
|
|
_version = reader.attributes().value("version").toString();
|
|
|
|
|
2018-03-30 10:25:05 +02:00
|
|
|
while (reader.readNextStartElement()) {
|
2020-12-22 22:09:09 +01:00
|
|
|
if (reader.name() == QLatin1String("Capability"))
|
2018-03-30 10:25:05 +02:00
|
|
|
capability(reader, ctx);
|
|
|
|
else
|
|
|
|
reader.skipCurrentElement();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-17 21:06:51 +01:00
|
|
|
bool WMS::parseCapabilities()
|
2018-03-30 10:25:05 +02:00
|
|
|
{
|
2020-03-17 21:06:51 +01:00
|
|
|
QFile file(_path);
|
|
|
|
CTX ctx(_setup);
|
2018-03-30 10:25:05 +02:00
|
|
|
QXmlStreamReader reader;
|
|
|
|
|
2018-04-02 19:59:52 +02:00
|
|
|
|
|
|
|
if (ctx.layers.isEmpty()) {
|
2018-04-03 22:35:13 +02:00
|
|
|
_errorString = "Invalid layers/styles list definition";
|
2018-04-02 19:59:52 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2018-03-30 10:25:05 +02:00
|
|
|
if (!file.open(QFile::ReadOnly | QFile::Text)) {
|
|
|
|
_errorString = file.errorString();
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
reader.setDevice(&file);
|
|
|
|
if (reader.readNextStartElement()) {
|
2020-12-22 22:09:09 +01:00
|
|
|
if (reader.name() == QLatin1String("WMS_Capabilities")
|
|
|
|
|| reader.name() == QLatin1String("WMT_MS_Capabilities"))
|
2018-03-30 10:25:05 +02:00
|
|
|
capabilities(reader, ctx);
|
|
|
|
else
|
2018-04-01 20:01:25 +02:00
|
|
|
reader.raiseError("Not a WMS Capabilities XML file");
|
2018-03-30 10:25:05 +02:00
|
|
|
}
|
|
|
|
if (reader.error()) {
|
2020-03-17 21:06:51 +01:00
|
|
|
_errorString = QString("%1:%2: %3").arg(_path).arg(reader.lineNumber())
|
2018-03-30 10:25:05 +02:00
|
|
|
.arg(reader.errorString());
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2018-04-02 19:59:52 +02:00
|
|
|
if (!ctx.formatSupported) {
|
2018-03-30 10:25:05 +02:00
|
|
|
_errorString = ctx.setup.format() + ": format not provided";
|
|
|
|
return false;
|
|
|
|
}
|
2018-04-02 19:59:52 +02:00
|
|
|
|
|
|
|
for (int i = 0; i < ctx.layers.size(); i++) {
|
|
|
|
const Layer &layer = ctx.layers.at(i);
|
|
|
|
|
|
|
|
if (!layer.isDefined) {
|
2018-04-03 23:05:17 +02:00
|
|
|
_errorString = layer.name + ": layer not provided";
|
2018-04-02 19:59:52 +02:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (!layer.hasStyle) {
|
2018-04-03 23:05:17 +02:00
|
|
|
_errorString = layer.style + ": style not provided for layer "
|
2018-04-02 19:59:52 +02:00
|
|
|
+ layer.name;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (!layer.hasCRS) {
|
|
|
|
_errorString = ctx.setup.crs() + ": CRS not provided for layer "
|
|
|
|
+ layer.name;
|
|
|
|
return false;
|
|
|
|
}
|
2018-04-02 23:27:42 +02:00
|
|
|
if (!layer.scaleDenominator.isValid()
|
|
|
|
|| layer.scaleDenominator.isNull()) {
|
|
|
|
_errorString = "Invalid scale denominator range for layer "
|
2018-04-02 19:59:52 +02:00
|
|
|
+ layer.name;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
if (!layer.boundingBox.isValid()) {
|
2018-04-02 23:27:42 +02:00
|
|
|
_errorString = "Invalid/missing bounding box for layer "
|
2018-04-02 19:59:52 +02:00
|
|
|
+ layer.name;
|
|
|
|
return false;
|
|
|
|
}
|
2018-03-30 10:25:05 +02:00
|
|
|
}
|
2018-04-02 19:59:52 +02:00
|
|
|
|
2018-03-30 10:25:05 +02:00
|
|
|
_projection = CRS::projection(ctx.setup.crs());
|
2018-05-16 18:52:48 +02:00
|
|
|
if (!_projection.isValid()) {
|
2018-03-30 10:25:05 +02:00
|
|
|
_errorString = ctx.setup.crs() + ": unknown CRS";
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2020-03-17 21:06:51 +01:00
|
|
|
_bbox = ctx.layers.first().boundingBox;
|
2018-04-02 23:27:42 +02:00
|
|
|
for (int i = 1; i < ctx.layers.size(); i++)
|
2020-03-17 21:06:51 +01:00
|
|
|
_bbox &= ctx.layers.at(i).boundingBox;
|
|
|
|
if (_bbox.isNull()) {
|
2018-04-02 23:27:42 +02:00
|
|
|
_errorString = "Empty layers bounding box join";
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
_scaleDenominator = ctx.layers.first().scaleDenominator;
|
|
|
|
for (int i = 1; i < ctx.layers.size(); i++)
|
|
|
|
_scaleDenominator &= ctx.layers.at(i).scaleDenominator;
|
|
|
|
if (_scaleDenominator.isNull()) {
|
|
|
|
_errorString = "Empty layers scale denominator range join";
|
|
|
|
return false;
|
|
|
|
}
|
2018-03-30 10:25:05 +02:00
|
|
|
|
2020-03-17 21:06:51 +01:00
|
|
|
if (_version >= "1.3.0") {
|
|
|
|
if (_setup.coordinateSystem().axisOrder() == CoordinateSystem::Unknown)
|
|
|
|
_cs = _projection.coordinateSystem();
|
|
|
|
else
|
|
|
|
_cs = _setup.coordinateSystem();
|
|
|
|
} else
|
|
|
|
_cs = CoordinateSystem::XY;
|
|
|
|
|
|
|
|
_getMapUrl = ctx.url.isEmpty() ? _setup.url() : ctx.url;
|
2020-03-04 19:47:23 +01:00
|
|
|
|
2018-03-30 10:25:05 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-03-17 21:06:51 +01:00
|
|
|
void WMS::capabilitiesReady()
|
|
|
|
{
|
|
|
|
if (!QFileInfo(_path).exists()) {
|
2018-03-30 10:25:05 +02:00
|
|
|
_errorString = "Error downloading capabilities XML file";
|
2020-03-17 21:06:51 +01:00
|
|
|
_valid = false;
|
|
|
|
} else {
|
|
|
|
_ready = true;
|
|
|
|
_valid = parseCapabilities();
|
2018-03-30 10:25:05 +02:00
|
|
|
}
|
2019-03-05 22:34:50 +01:00
|
|
|
|
2020-03-17 21:06:51 +01:00
|
|
|
emit downloadFinished();
|
2018-03-30 10:25:05 +02:00
|
|
|
}
|
|
|
|
|
2020-03-17 21:06:51 +01:00
|
|
|
WMS::WMS(const QString &file, const WMS::Setup &setup, QObject *parent)
|
2021-08-26 22:22:18 +02:00
|
|
|
: QObject(parent), _setup(setup), _path(file), _valid(false), _ready(false)
|
2018-03-30 10:25:05 +02:00
|
|
|
{
|
2020-03-17 21:06:51 +01:00
|
|
|
QString url = QString("%1%2service=WMS&request=GetCapabilities")
|
2018-09-16 12:05:11 +02:00
|
|
|
.arg(setup.url(), setup.url().contains('?') ? "&" : "?");
|
2018-03-30 10:25:05 +02:00
|
|
|
|
2021-08-26 22:22:18 +02:00
|
|
|
if (!QFileInfo(file).exists()) {
|
|
|
|
Downloader *downloader = new Downloader(this);
|
|
|
|
connect(downloader, &Downloader::finished, this,
|
|
|
|
&WMS::capabilitiesReady);
|
|
|
|
|
|
|
|
QList<Download> dl;
|
|
|
|
dl.append(Download(url, _path));
|
2023-05-13 15:01:35 +02:00
|
|
|
_valid = downloader->get(dl, _setup.headers());
|
2021-08-26 22:22:18 +02:00
|
|
|
} else {
|
2020-03-17 21:06:51 +01:00
|
|
|
_ready = true;
|
|
|
|
_valid = parseCapabilities();
|
|
|
|
}
|
2018-03-30 10:25:05 +02:00
|
|
|
}
|