mirror of
https://github.com/tumic0/QtPBFImagePlugin.git
synced 2024-11-23 19:25:55 +01:00
Initial version
This commit is contained in:
parent
7b79a06fd6
commit
88295f0ca7
4
pbfplugin.json
Normal file
4
pbfplugin.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"Keys": [ "pbf" ],
|
||||||
|
"MimeTypes": [ "image/pbf" ]
|
||||||
|
}
|
35
pbfplugin.pro
Normal file
35
pbfplugin.pro
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
TARGET = pbf
|
||||||
|
TEMPLATE = lib
|
||||||
|
CONFIG += plugin
|
||||||
|
QT += gui widgets
|
||||||
|
|
||||||
|
VERSION = 1.0.0
|
||||||
|
|
||||||
|
PROTOS = protobuf/vector_tile.proto
|
||||||
|
include(protobuf/vector_tile.pri)
|
||||||
|
|
||||||
|
INCLUDEPATH += ./protobuf
|
||||||
|
HEADERS += src/pbfhandler.h \
|
||||||
|
src/pbfplugin.h \
|
||||||
|
src/gzip.h \
|
||||||
|
src/pbf.h \
|
||||||
|
src/style.h \
|
||||||
|
src/color.h \
|
||||||
|
src/text.h \
|
||||||
|
src/tile.h \
|
||||||
|
src/function.h \
|
||||||
|
src/textpathitem.h
|
||||||
|
SOURCES += src/pbfplugin.cpp \
|
||||||
|
src/pbfhandler.cpp \
|
||||||
|
src/gzip.cpp \
|
||||||
|
src/pbf.cpp \
|
||||||
|
src/style.cpp \
|
||||||
|
src/color.cpp \
|
||||||
|
src/text.cpp \
|
||||||
|
src/function.cpp \
|
||||||
|
src/textpathitem.cpp
|
||||||
|
|
||||||
|
LIBS += -lprotobuf-lite
|
||||||
|
|
||||||
|
target.path += $$[QT_INSTALL_PLUGINS]/imageformats
|
||||||
|
INSTALLS += target
|
17
protobuf/vector_tile.pri
Normal file
17
protobuf/vector_tile.pri
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
INCLUDEPATH += $$PWD
|
||||||
|
DEPENDPATH += $$PWD
|
||||||
|
|
||||||
|
protobuf_decl.name = protobuf headers
|
||||||
|
protobuf_decl.input = PROTOS
|
||||||
|
protobuf_decl.output = ${QMAKE_FILE_IN_PATH}/${QMAKE_FILE_BASE}.pb.h
|
||||||
|
protobuf_decl.commands = protoc --cpp_out=${QMAKE_FILE_IN_PATH} --proto_path=${QMAKE_FILE_IN_PATH} ${QMAKE_FILE_NAME}
|
||||||
|
protobuf_decl.variable_out = HEADERS
|
||||||
|
QMAKE_EXTRA_COMPILERS += protobuf_decl
|
||||||
|
|
||||||
|
protobuf_impl.name = protobuf sources
|
||||||
|
protobuf_impl.input = PROTOS
|
||||||
|
protobuf_impl.output = ${QMAKE_FILE_IN_PATH}/${QMAKE_FILE_BASE}.pb.cc
|
||||||
|
protobuf_impl.depends = ${QMAKE_FILE_IN_PATH}/${QMAKE_FILE_BASE}.pb.h
|
||||||
|
protobuf_impl.commands = $$escape_expand(\n)
|
||||||
|
protobuf_impl.variable_out = SOURCES
|
||||||
|
QMAKE_EXTRA_COMPILERS += protobuf_impl
|
78
protobuf/vector_tile.proto
Normal file
78
protobuf/vector_tile.proto
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
package vector_tile;
|
||||||
|
|
||||||
|
option optimize_for = LITE_RUNTIME;
|
||||||
|
|
||||||
|
message Tile {
|
||||||
|
|
||||||
|
// GeomType is described in section 4.3.4 of the specification
|
||||||
|
enum GeomType {
|
||||||
|
UNKNOWN = 0;
|
||||||
|
POINT = 1;
|
||||||
|
LINESTRING = 2;
|
||||||
|
POLYGON = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Variant type encoding
|
||||||
|
// The use of values is described in section 4.1 of the specification
|
||||||
|
message Value {
|
||||||
|
// Exactly one of these values must be present in a valid message
|
||||||
|
optional string string_value = 1;
|
||||||
|
optional float float_value = 2;
|
||||||
|
optional double double_value = 3;
|
||||||
|
optional int64 int_value = 4;
|
||||||
|
optional uint64 uint_value = 5;
|
||||||
|
optional sint64 sint_value = 6;
|
||||||
|
optional bool bool_value = 7;
|
||||||
|
|
||||||
|
extensions 8 to max;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Features are described in section 4.2 of the specification
|
||||||
|
message Feature {
|
||||||
|
optional uint64 id = 1 [ default = 0 ];
|
||||||
|
|
||||||
|
// Tags of this feature are encoded as repeated pairs of
|
||||||
|
// integers.
|
||||||
|
// A detailed description of tags is located in sections
|
||||||
|
// 4.2 and 4.4 of the specification
|
||||||
|
repeated uint32 tags = 2 [ packed = true ];
|
||||||
|
|
||||||
|
// The type of geometry stored in this feature.
|
||||||
|
optional GeomType type = 3 [ default = UNKNOWN ];
|
||||||
|
|
||||||
|
// Contains a stream of commands and parameters (vertices).
|
||||||
|
// A detailed description on geometry encoding is located in
|
||||||
|
// section 4.3 of the specification.
|
||||||
|
repeated uint32 geometry = 4 [ packed = true ];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Layers are described in section 4.1 of the specification
|
||||||
|
message Layer {
|
||||||
|
// Any compliant implementation must first read the version
|
||||||
|
// number encoded in this message and choose the correct
|
||||||
|
// implementation for this version number before proceeding to
|
||||||
|
// decode other parts of this message.
|
||||||
|
required uint32 version = 15 [ default = 1 ];
|
||||||
|
|
||||||
|
required string name = 1;
|
||||||
|
|
||||||
|
// The actual features in this tile.
|
||||||
|
repeated Feature features = 2;
|
||||||
|
|
||||||
|
// Dictionary encoding for keys
|
||||||
|
repeated string keys = 3;
|
||||||
|
|
||||||
|
// Dictionary encoding for values
|
||||||
|
repeated Value values = 4;
|
||||||
|
|
||||||
|
// Although this is an "optional" field it is required by the specification.
|
||||||
|
// See https://github.com/mapbox/vector-tile-spec/issues/47
|
||||||
|
optional uint32 extent = 5 [ default = 4096 ];
|
||||||
|
|
||||||
|
extensions 16 to max;
|
||||||
|
}
|
||||||
|
|
||||||
|
repeated Layer layers = 3;
|
||||||
|
|
||||||
|
extensions 16 to 8191;
|
||||||
|
}
|
48
src/color.cpp
Normal file
48
src/color.cpp
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
#include <QDebug>
|
||||||
|
#include "color.h"
|
||||||
|
|
||||||
|
|
||||||
|
static qreal pval(const QString &str)
|
||||||
|
{
|
||||||
|
QString ts(str.trimmed());
|
||||||
|
ts.chop(1);
|
||||||
|
return ts.toFloat() / 100.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
QColor Color::fromJsonString(const QString &str)
|
||||||
|
{
|
||||||
|
QColor ret;
|
||||||
|
|
||||||
|
if (str.startsWith('#'))
|
||||||
|
return QColor(str);
|
||||||
|
else if (str.startsWith("rgb(")) {
|
||||||
|
QStringList comp(str.mid(4, str.size() - 5).split(','));
|
||||||
|
if (comp.size() != 3)
|
||||||
|
return QColor();
|
||||||
|
ret = QColor(comp.at(0).toInt(), comp.at(1).toInt(),
|
||||||
|
comp.at(2).toInt());
|
||||||
|
} else if (str.startsWith("rgba(")) {
|
||||||
|
QStringList comp(str.mid(5, str.size() - 6).split(','));
|
||||||
|
if (comp.size() != 4)
|
||||||
|
return QColor();
|
||||||
|
ret = QColor(comp.at(0).toInt(), comp.at(1).toInt(),
|
||||||
|
comp.at(2).toInt(), (int)(comp.at(3).toFloat() * 255));
|
||||||
|
} else if (str.startsWith("hsl(")) {
|
||||||
|
QStringList comp(str.mid(4, str.size() - 5).split(','));
|
||||||
|
if (comp.size() != 3)
|
||||||
|
return QColor();
|
||||||
|
ret = QColor::fromHslF(comp.at(0).toFloat() / 360.0, pval(comp.at(1)),
|
||||||
|
pval(comp.at(2)));
|
||||||
|
} else if (str.startsWith("hsla(")) {
|
||||||
|
QStringList comp(str.mid(5, str.size() - 6).split(','));
|
||||||
|
if (comp.size() != 4)
|
||||||
|
return QColor();
|
||||||
|
ret = QColor::fromHslF(comp.at(0).toFloat() / 360.0, pval(comp.at(1)),
|
||||||
|
pval(comp.at(2)), comp.at(3).toFloat());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ret.isValid())
|
||||||
|
qWarning() << str << ": invalid color";
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
11
src/color.h
Normal file
11
src/color.h
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
#ifndef COLOR_H
|
||||||
|
#define COLOR_H
|
||||||
|
|
||||||
|
#include <QColor>
|
||||||
|
|
||||||
|
namespace Color
|
||||||
|
{
|
||||||
|
QColor fromJsonString(const QString &str);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // COLOR_H
|
119
src/function.cpp
Normal file
119
src/function.cpp
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
#include <cmath>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include "color.h"
|
||||||
|
#include "function.h"
|
||||||
|
|
||||||
|
|
||||||
|
#define f(y0, y1, ratio) (y0 * (1.0 - ratio)) + (y1 * ratio)
|
||||||
|
|
||||||
|
static qreal interpolate(const QPointF &p0, const QPointF &p1, qreal base,
|
||||||
|
qreal x)
|
||||||
|
{
|
||||||
|
qreal difference = p1.x() - p0.x();
|
||||||
|
if (difference < 1e-6)
|
||||||
|
return p0.y();
|
||||||
|
|
||||||
|
qreal progress = x - p0.x();
|
||||||
|
qreal ratio = (base == 1.0)
|
||||||
|
? progress / difference
|
||||||
|
: (pow(base, progress) - 1) / (pow(base, difference) - 1);
|
||||||
|
|
||||||
|
return f(p0.y(), p1.y(), ratio);
|
||||||
|
}
|
||||||
|
|
||||||
|
static QColor interpolate(const QPair<qreal, QColor> &p0,
|
||||||
|
const QPair<qreal, QColor> &p1, qreal base, qreal x)
|
||||||
|
{
|
||||||
|
qreal difference = p1.first - p0.first;
|
||||||
|
if (difference < 1e-6)
|
||||||
|
return p0.second;
|
||||||
|
|
||||||
|
qreal progress = x - p0.first;
|
||||||
|
qreal ratio = (base == 1.0)
|
||||||
|
? progress / difference
|
||||||
|
: (pow(base, progress) - 1) / (pow(base, difference) - 1);
|
||||||
|
|
||||||
|
|
||||||
|
qreal p0h, p0s, p0l, p0a;
|
||||||
|
p0.second.getHslF(&p0h, &p0s, &p0l, &p0a);
|
||||||
|
qreal p1h, p1s, p1l, p1a;
|
||||||
|
p1.second.getHslF(&p1h, &p1s, &p1l, &p1a);
|
||||||
|
|
||||||
|
return QColor::fromHslF(f(p0h, p1h, ratio), f(p0s, p1s, ratio),
|
||||||
|
f(p0l, p1l, ratio), f(p0a, p1a, ratio));
|
||||||
|
}
|
||||||
|
|
||||||
|
FunctionF::FunctionF(const QJsonObject &json, qreal dflt)
|
||||||
|
: _default(dflt), _base(1.0)
|
||||||
|
{
|
||||||
|
if (!(json.contains("stops") && json["stops"].isArray()))
|
||||||
|
return;
|
||||||
|
|
||||||
|
QJsonArray stops = json["stops"].toArray();
|
||||||
|
for (int i = 0; i < stops.size(); i++) {
|
||||||
|
if (!stops.at(i).isArray())
|
||||||
|
return;
|
||||||
|
QJsonArray stop = stops.at(i).toArray();
|
||||||
|
if (stop.size() != 2)
|
||||||
|
return;
|
||||||
|
_stops.append(QPointF(stop.at(0).toDouble(), stop.at(1).toDouble()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json.contains("base") && json["base"].isDouble())
|
||||||
|
_base = json["base"].toDouble();
|
||||||
|
}
|
||||||
|
|
||||||
|
qreal FunctionF::value(qreal x) const
|
||||||
|
{
|
||||||
|
if (_stops.isEmpty())
|
||||||
|
return _default;
|
||||||
|
|
||||||
|
QPointF v0(_stops.first());
|
||||||
|
for (int i = 0; i < _stops.size(); i++) {
|
||||||
|
if (x < _stops.at(i).x())
|
||||||
|
return interpolate(v0, _stops.at(i), _base, x);
|
||||||
|
else
|
||||||
|
v0 = _stops.at(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _stops.last().y();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
FunctionC::FunctionC(const QJsonObject &json, const QColor &dflt)
|
||||||
|
: _default(dflt), _base(1.0)
|
||||||
|
{
|
||||||
|
if (!(json.contains("stops") && json["stops"].isArray()))
|
||||||
|
return;
|
||||||
|
|
||||||
|
QJsonArray stops = json["stops"].toArray();
|
||||||
|
for (int i = 0; i < stops.size(); i++) {
|
||||||
|
if (!stops.at(i).isArray())
|
||||||
|
return;
|
||||||
|
QJsonArray stop = stops.at(i).toArray();
|
||||||
|
if (stop.size() != 2)
|
||||||
|
return;
|
||||||
|
_stops.append(QPair<qreal, QColor>(stop.at(0).toDouble(),
|
||||||
|
Color::fromJsonString(stop.at(1).toString())));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json.contains("base") && json["base"].isDouble())
|
||||||
|
_base = json["base"].toDouble();
|
||||||
|
}
|
||||||
|
|
||||||
|
QColor FunctionC::value(qreal x) const
|
||||||
|
{
|
||||||
|
if (_stops.isEmpty())
|
||||||
|
return _default;
|
||||||
|
|
||||||
|
QPair<qreal, QColor> v0(_stops.first());
|
||||||
|
for (int i = 0; i < _stops.size(); i++) {
|
||||||
|
if (x < _stops.at(i).first)
|
||||||
|
return interpolate(v0, _stops.at(i), _base, x);
|
||||||
|
else
|
||||||
|
v0 = _stops.at(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _stops.last().second;
|
||||||
|
}
|
37
src/function.h
Normal file
37
src/function.h
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
#ifndef FUNCTION_H
|
||||||
|
#define FUNCTION_H
|
||||||
|
|
||||||
|
#include <QList>
|
||||||
|
#include <QPointF>
|
||||||
|
#include <QColor>
|
||||||
|
|
||||||
|
class QJsonObject;
|
||||||
|
|
||||||
|
class FunctionF {
|
||||||
|
public:
|
||||||
|
FunctionF(qreal deflt = 0) : _default(deflt), _base(1.0) {}
|
||||||
|
FunctionF(const QJsonObject &json, qreal dflt = 0);
|
||||||
|
|
||||||
|
qreal value(qreal x) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QList<QPointF> _stops;
|
||||||
|
qreal _default;
|
||||||
|
qreal _base;
|
||||||
|
};
|
||||||
|
|
||||||
|
class FunctionC {
|
||||||
|
public:
|
||||||
|
FunctionC(const QColor &deflt = QColor())
|
||||||
|
: _default(deflt), _base(1.0) {}
|
||||||
|
FunctionC(const QJsonObject &json, const QColor &dflt = QColor());
|
||||||
|
|
||||||
|
QColor value(qreal x) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QList<QPair<qreal, QColor> > _stops;
|
||||||
|
QColor _default;
|
||||||
|
qreal _base;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // FUNCTION_H
|
35
src/gzip.cpp
Normal file
35
src/gzip.cpp
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
#include <QtEndian>
|
||||||
|
#include <QDebug>
|
||||||
|
#include <zlib.h>
|
||||||
|
#include "gzip.h"
|
||||||
|
|
||||||
|
|
||||||
|
QByteArray Gzip::uncompress(const QByteArray &data)
|
||||||
|
{
|
||||||
|
QByteArray uba;
|
||||||
|
z_stream stream;
|
||||||
|
|
||||||
|
quint32 *size = (quint32*)(data.constData() + data.size() - sizeof(quint32));
|
||||||
|
uba.resize(qFromLittleEndian(*size));
|
||||||
|
|
||||||
|
stream.zalloc = Z_NULL;
|
||||||
|
stream.zfree = Z_NULL;
|
||||||
|
stream.opaque = Z_NULL;
|
||||||
|
|
||||||
|
stream.next_in = (Bytef*)data.constData();
|
||||||
|
stream.avail_in = data.size();
|
||||||
|
stream.next_out = (Bytef*)uba.data();
|
||||||
|
stream.avail_out = uba.size();
|
||||||
|
|
||||||
|
if (inflateInit2(&stream, MAX_WBITS + 16) != Z_OK)
|
||||||
|
return uba;
|
||||||
|
|
||||||
|
if (inflate(&stream, Z_NO_FLUSH) != Z_STREAM_END) {
|
||||||
|
qCritical() << "Invalid gzip data";
|
||||||
|
uba = QByteArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
inflateEnd(&stream);
|
||||||
|
|
||||||
|
return uba;
|
||||||
|
}
|
11
src/gzip.h
Normal file
11
src/gzip.h
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
#ifndef GZIP_H
|
||||||
|
#define GZIP_H
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
|
|
||||||
|
namespace Gzip
|
||||||
|
{
|
||||||
|
QByteArray uncompress(const QByteArray &data);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // GZIP_H
|
168
src/pbf.cpp
Normal file
168
src/pbf.cpp
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
#include <QByteArray>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QVariantMap>
|
||||||
|
#include "vector_tile.pb.h"
|
||||||
|
#include "style.h"
|
||||||
|
#include "tile.h"
|
||||||
|
#include "pbf.h"
|
||||||
|
|
||||||
|
using namespace google::protobuf;
|
||||||
|
|
||||||
|
#define MOVE_TO 1
|
||||||
|
#define LINE_TO 2
|
||||||
|
#define CLOSE_PATH 7
|
||||||
|
|
||||||
|
#define POLYGON vector_tile::Tile_GeomType::Tile_GeomType_POLYGON
|
||||||
|
#define LINESTRING vector_tile::Tile_GeomType::Tile_GeomType_LINESTRING
|
||||||
|
#define POINT vector_tile::Tile_GeomType::Tile_GeomType_POINT
|
||||||
|
|
||||||
|
struct Layer
|
||||||
|
{
|
||||||
|
Layer(const vector_tile::Tile_Layer *data) : data(data) {}
|
||||||
|
|
||||||
|
const vector_tile::Tile_Layer *data;
|
||||||
|
QVector<QString> keys;
|
||||||
|
QVector<QVariant> values;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Feature
|
||||||
|
{
|
||||||
|
Feature(const vector_tile::Tile_Feature *data, const QVector<QString> *keys,
|
||||||
|
const QVector<QVariant> *values) : data(data), keys(keys),
|
||||||
|
values(values) {}
|
||||||
|
|
||||||
|
const vector_tile::Tile_Feature *data;
|
||||||
|
const QVector<QString> *keys;
|
||||||
|
const QVector<QVariant> *values;
|
||||||
|
};
|
||||||
|
|
||||||
|
static QVariant value(const vector_tile::Tile_Value &val)
|
||||||
|
{
|
||||||
|
if (val.has_bool_value())
|
||||||
|
return QVariant(val.bool_value());
|
||||||
|
else if (val.has_int_value())
|
||||||
|
return QVariant((qlonglong)val.int_value());
|
||||||
|
else if (val.has_sint_value())
|
||||||
|
return QVariant((qlonglong)val.sint_value());
|
||||||
|
else if (val.has_uint_value())
|
||||||
|
return QVariant((qulonglong)val.uint_value());
|
||||||
|
else if (val.has_float_value())
|
||||||
|
return QVariant(val.float_value());
|
||||||
|
else if (val.has_double_value())
|
||||||
|
return QVariant(val.double_value());
|
||||||
|
else if (val.has_string_value())
|
||||||
|
return QVariant(QString::fromStdString(val.string_value()));
|
||||||
|
else
|
||||||
|
return QVariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
static QPoint parameters(quint32 v1, quint32 v2)
|
||||||
|
{
|
||||||
|
return QPoint((v1 >> 1) ^ (-(v1 & 1)), ((v2 >> 1) ^ (-(v2 & 1))));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void feature(const Feature &feature, Style *style, int styleLayer,
|
||||||
|
qreal factor, Tile &tile)
|
||||||
|
{
|
||||||
|
QVariantMap tags;
|
||||||
|
for (int i = 0; i < feature.data->tags_size(); i = i + 2)
|
||||||
|
tags.insert(feature.keys->at(feature.data->tags(i)),
|
||||||
|
feature.values->at(feature.data->tags(i+1)));
|
||||||
|
switch (feature.data->type()) {
|
||||||
|
case POLYGON:
|
||||||
|
tags.insert("$type", QVariant("Polygon"));
|
||||||
|
break;
|
||||||
|
case LINESTRING:
|
||||||
|
tags.insert("$type", QVariant("LineString"));
|
||||||
|
break;
|
||||||
|
case POINT:
|
||||||
|
tags.insert("$type", QVariant("Point"));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!style->match(styleLayer, tags))
|
||||||
|
return;
|
||||||
|
|
||||||
|
QPoint cursor;
|
||||||
|
QPainterPath path;
|
||||||
|
|
||||||
|
for (int i = 0; i < feature.data->geometry_size(); i++) {
|
||||||
|
quint32 g = feature.data->geometry(i);
|
||||||
|
unsigned cmdId = g & 0x7;
|
||||||
|
unsigned cmdCount = g >> 3;
|
||||||
|
|
||||||
|
if (cmdId == MOVE_TO) {
|
||||||
|
for (unsigned j = 0; j < cmdCount; j++) {
|
||||||
|
QPoint offset = parameters(feature.data->geometry(i+1),
|
||||||
|
feature.data->geometry(i+2));
|
||||||
|
i += 2;
|
||||||
|
cursor += offset;
|
||||||
|
path.moveTo(QPointF(cursor) / factor);
|
||||||
|
}
|
||||||
|
} else if (cmdId == LINE_TO) {
|
||||||
|
for (unsigned j = 0; j < cmdCount; j++) {
|
||||||
|
QPoint offset = parameters(feature.data->geometry(i+1),
|
||||||
|
feature.data->geometry(i+2));
|
||||||
|
i += 2;
|
||||||
|
cursor += offset;
|
||||||
|
path.lineTo(QPointF(cursor) / factor);
|
||||||
|
}
|
||||||
|
} else if (cmdId == CLOSE_PATH) {
|
||||||
|
path.closeSubpath();
|
||||||
|
path.moveTo(cursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
style->drawFeature(styleLayer, path, tags, tile);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void layer(const Layer &layer, Style *style, int styleLayer, Tile &tile)
|
||||||
|
{
|
||||||
|
qreal factor = layer.data->extent() / 256.0;
|
||||||
|
|
||||||
|
for (int i = 0; i < layer.data->features_size(); i++)
|
||||||
|
feature(Feature(&(layer.data->features(i)), &(layer.keys),
|
||||||
|
&(layer.values)), style, styleLayer, factor, tile);
|
||||||
|
}
|
||||||
|
|
||||||
|
QImage PBF::image(const QByteArray &data, int zoom, Style *style)
|
||||||
|
{
|
||||||
|
vector_tile::Tile tile;
|
||||||
|
if (!tile.ParseFromArray(data.constData(), data.size())) {
|
||||||
|
qCritical() << "Invalid tile protocol buffer data";
|
||||||
|
return QImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
Tile t;
|
||||||
|
|
||||||
|
style->setZoom(zoom);
|
||||||
|
style->drawBackground(t);
|
||||||
|
|
||||||
|
QMap<QString, Layer> layers;
|
||||||
|
for (int i = 0; i < tile.layers_size(); i++) {
|
||||||
|
const vector_tile::Tile_Layer &layer = tile.layers(i);
|
||||||
|
Layer l(&layer);
|
||||||
|
|
||||||
|
for (int j = 0; j < layer.keys_size(); j++)
|
||||||
|
l.keys.append(QString::fromStdString(layer.keys(j)));
|
||||||
|
for (int j = 0; j < layer.values_size(); j++)
|
||||||
|
l.values.append(value(layer.values(j)));
|
||||||
|
|
||||||
|
layers.insert(QString::fromStdString(tile.layers(i).name()), l);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process data in order of style layers
|
||||||
|
for (int i = 0; i < style->sourceLayers().size(); i++) {
|
||||||
|
QMap<QString, Layer>::const_iterator it = layers.find(
|
||||||
|
style->sourceLayers().at(i));
|
||||||
|
if (it == layers.constEnd())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
layer(*it, style, i, t);
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.render();
|
||||||
|
}
|
14
src/pbf.h
Normal file
14
src/pbf.h
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
#ifndef PBF_H
|
||||||
|
#define PBF_H
|
||||||
|
|
||||||
|
#include <QImage>
|
||||||
|
|
||||||
|
class QByteArray;
|
||||||
|
class Style;
|
||||||
|
|
||||||
|
namespace PBF
|
||||||
|
{
|
||||||
|
QImage image(const QByteArray &data, int zoom, Style *style);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // PBF_H
|
50
src/pbfhandler.cpp
Normal file
50
src/pbfhandler.cpp
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
#include <QImage>
|
||||||
|
#include <QIODevice>
|
||||||
|
#include <QtEndian>
|
||||||
|
#include "gzip.h"
|
||||||
|
#include "pbf.h"
|
||||||
|
#include "pbfhandler.h"
|
||||||
|
|
||||||
|
|
||||||
|
#define GZIP_MAGIC 0x1F8B0800
|
||||||
|
|
||||||
|
bool PBFHandler::canRead() const
|
||||||
|
{
|
||||||
|
return canRead(device());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PBFHandler::canRead(QIODevice *device)
|
||||||
|
{
|
||||||
|
quint32 magic;
|
||||||
|
qint64 size = device->peek((char*)&magic, sizeof(magic));
|
||||||
|
return (size == sizeof(magic)
|
||||||
|
&& (qFromBigEndian(magic) & 0xFFFFFF00) == GZIP_MAGIC);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PBFHandler::read(QImage *image)
|
||||||
|
{
|
||||||
|
QByteArray ba = Gzip::uncompress(device()->readAll());
|
||||||
|
if (ba.isNull())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
bool ok;
|
||||||
|
int zoom = format().toInt(&ok);
|
||||||
|
*image = PBF::image(ba, ok ? zoom : -1, _style);
|
||||||
|
|
||||||
|
return !image->isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PBFHandler::supportsOption(ImageOption option) const
|
||||||
|
{
|
||||||
|
return (option == Size);
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant PBFHandler::option(ImageOption option) const
|
||||||
|
{
|
||||||
|
return (option == Size) ? QSize(256, 256) : QVariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray PBFHandler::name() const
|
||||||
|
{
|
||||||
|
return "pbf";
|
||||||
|
}
|
29
src/pbfhandler.h
Normal file
29
src/pbfhandler.h
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
#ifndef PBFHANDLER_H
|
||||||
|
#define PBFHANDLER_H
|
||||||
|
|
||||||
|
#include <QImageIOHandler>
|
||||||
|
#include <QImage>
|
||||||
|
#include <QVariant>
|
||||||
|
|
||||||
|
class Style;
|
||||||
|
|
||||||
|
class PBFHandler : public QImageIOHandler
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
PBFHandler(Style *style) : _style(style) {}
|
||||||
|
~PBFHandler() {}
|
||||||
|
|
||||||
|
bool canRead() const;
|
||||||
|
bool read(QImage *image);
|
||||||
|
|
||||||
|
QByteArray name() const;
|
||||||
|
QVariant option(ImageOption option) const;
|
||||||
|
bool supportsOption(ImageOption option) const;
|
||||||
|
|
||||||
|
static bool canRead(QIODevice *device);
|
||||||
|
|
||||||
|
private:
|
||||||
|
Style *_style;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // PBFHANDLER_H
|
37
src/pbfplugin.cpp
Normal file
37
src/pbfplugin.cpp
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
#include <QDir>
|
||||||
|
#include <QDebug>
|
||||||
|
#include "pbfplugin.h"
|
||||||
|
#include "pbfhandler.h"
|
||||||
|
#include "style.h"
|
||||||
|
|
||||||
|
|
||||||
|
#define GLOBAL_CONFIG "/usr/share/pbf/style.json"
|
||||||
|
#define USER_CONFIG QDir::homePath() + "/.pbf/style.json"
|
||||||
|
|
||||||
|
PBFPlugin::PBFPlugin()
|
||||||
|
{
|
||||||
|
_style = new Style();
|
||||||
|
|
||||||
|
if (!_style->load(USER_CONFIG))
|
||||||
|
if (!_style->load(GLOBAL_CONFIG))
|
||||||
|
qCritical() << "Map style not found";
|
||||||
|
}
|
||||||
|
|
||||||
|
QImageIOPlugin::Capabilities PBFPlugin::capabilities(QIODevice *device,
|
||||||
|
const QByteArray &format) const
|
||||||
|
{
|
||||||
|
if (device == 0)
|
||||||
|
return (format == "pbf") ? Capabilities(CanRead) : Capabilities();
|
||||||
|
else
|
||||||
|
return (device->isReadable() && PBFHandler::canRead(device))
|
||||||
|
? Capabilities(CanRead) : Capabilities();
|
||||||
|
}
|
||||||
|
|
||||||
|
QImageIOHandler *PBFPlugin::create(QIODevice *device,
|
||||||
|
const QByteArray &format) const
|
||||||
|
{
|
||||||
|
QImageIOHandler *handler = new PBFHandler(_style);
|
||||||
|
handler->setDevice(device);
|
||||||
|
handler->setFormat(format);
|
||||||
|
return handler;
|
||||||
|
}
|
22
src/pbfplugin.h
Normal file
22
src/pbfplugin.h
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
#include <QImageIOPlugin>
|
||||||
|
#include "style.h"
|
||||||
|
|
||||||
|
class Style;
|
||||||
|
|
||||||
|
class PBFPlugin : public QImageIOPlugin
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QImageIOHandlerFactoryInterface"
|
||||||
|
FILE "pbfplugin.json")
|
||||||
|
|
||||||
|
public:
|
||||||
|
PBFPlugin();
|
||||||
|
~PBFPlugin() {}
|
||||||
|
|
||||||
|
Capabilities capabilities(QIODevice *device, const QByteArray &format) const;
|
||||||
|
QImageIOHandler *create(QIODevice *device,
|
||||||
|
const QByteArray &format = QByteArray()) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Style *_style;
|
||||||
|
};
|
433
src/style.cpp
Normal file
433
src/style.cpp
Normal file
@ -0,0 +1,433 @@
|
|||||||
|
#include <cmath>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QDebug>
|
||||||
|
#include "text.h"
|
||||||
|
#include "color.h"
|
||||||
|
#include "tile.h"
|
||||||
|
#include "style.h"
|
||||||
|
|
||||||
|
|
||||||
|
Style::Layer::Filter::Filter(const QJsonArray &json)
|
||||||
|
: _type(Unknown), _not(false)
|
||||||
|
{
|
||||||
|
if (json.isEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
QString type = json.at(0).toString();
|
||||||
|
|
||||||
|
if (type == "==") {
|
||||||
|
_type = EQ;
|
||||||
|
_kv = QPair<QString, QVariant>(json.at(1).toString(),
|
||||||
|
json.at(2).toVariant());
|
||||||
|
} else if (type == "!=") {
|
||||||
|
_type = NE;
|
||||||
|
_kv = QPair<QString, QVariant>(json.at(1).toString(),
|
||||||
|
json.at(2).toVariant());
|
||||||
|
} else if (type == "<") {
|
||||||
|
_type = LT;
|
||||||
|
_kv = QPair<QString, QVariant>(json.at(1).toString(),
|
||||||
|
json.at(2).toVariant());
|
||||||
|
} else if (type == "<=") {
|
||||||
|
_type = LE;
|
||||||
|
_kv = QPair<QString, QVariant>(json.at(1).toString(),
|
||||||
|
json.at(2).toVariant());
|
||||||
|
} else if (type == ">") {
|
||||||
|
_type = GT;
|
||||||
|
_kv = QPair<QString, QVariant>(json.at(1).toString(),
|
||||||
|
json.at(2).toVariant());
|
||||||
|
} else if (type == ">=") {
|
||||||
|
_type = GE;
|
||||||
|
_kv = QPair<QString, QVariant>(json.at(1).toString(),
|
||||||
|
json.at(2).toVariant());
|
||||||
|
} else if (type == "all") {
|
||||||
|
_type = All;
|
||||||
|
for (int i = 1; i < json.size(); i++)
|
||||||
|
_filters.append(Filter(json.at(i).toArray()));
|
||||||
|
} else if (type == "any") {
|
||||||
|
_type = Any;
|
||||||
|
for (int i = 1; i < json.size(); i++)
|
||||||
|
_filters.append(Filter(json.at(i).toArray()));
|
||||||
|
} else if (type == "in") {
|
||||||
|
_type = In;
|
||||||
|
_kv = QPair<QString, QVariant>(json.at(1).toString(), QVariant());
|
||||||
|
for (int i = 2; i < json.size(); i++)
|
||||||
|
_set.insert(json.at(i).toString());
|
||||||
|
} else if (type == "!in") {
|
||||||
|
_type = In;
|
||||||
|
_not = true;
|
||||||
|
_kv = QPair<QString, QVariant>(json.at(1).toString(), QVariant());
|
||||||
|
for (int i = 2; i < json.size(); i++)
|
||||||
|
_set.insert(json.at(i).toString());
|
||||||
|
} else if (type == "has") {
|
||||||
|
_type = Has;
|
||||||
|
_kv = QPair<QString, QVariant>(json.at(1).toString(), QVariant());
|
||||||
|
} else if (type == "!has") {
|
||||||
|
_type = Has;
|
||||||
|
_not = true;
|
||||||
|
_kv = QPair<QString, QVariant>(json.at(1).toString(), QVariant());
|
||||||
|
} else
|
||||||
|
qWarning() << json << ": invalid filter";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Style::Layer::Filter::match(const QVariantMap &tags) const
|
||||||
|
{
|
||||||
|
switch (_type) {
|
||||||
|
case None:
|
||||||
|
return true;
|
||||||
|
case EQ:
|
||||||
|
return tags.value(_kv.first) == _kv.second;
|
||||||
|
case NE:
|
||||||
|
return tags.value(_kv.first) != _kv.second;
|
||||||
|
case GT:
|
||||||
|
return tags.value(_kv.first) > _kv.second;
|
||||||
|
case GE:
|
||||||
|
return tags.value(_kv.first) >= _kv.second;
|
||||||
|
case LT:
|
||||||
|
return tags.value(_kv.first) < _kv.second;
|
||||||
|
case LE:
|
||||||
|
return tags.value(_kv.first) <= _kv.second;
|
||||||
|
case In:
|
||||||
|
return _set.contains(tags.value(_kv.first).toString()) ^ _not;
|
||||||
|
case Has:
|
||||||
|
return tags.contains(_kv.first) ^ _not;
|
||||||
|
case All:
|
||||||
|
for (int i = 0; i < _filters.size(); i++) {
|
||||||
|
if (!_filters.at(i).match(tags))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
case Any:
|
||||||
|
for (int i = 0; i < _filters.size(); i++) {
|
||||||
|
if (_filters.at(i).match(tags))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Style::Layer::Paint::Paint(const QJsonObject &json)
|
||||||
|
: _fillOpacity(1.0), _lineOpacity(1.0), _fillAntialias(true)
|
||||||
|
{
|
||||||
|
if (json.contains("fill-opacity") && json["fill-opacity"].isDouble())
|
||||||
|
_fillOpacity = FunctionF(json["fill-opacity"].toDouble());
|
||||||
|
else if (json.contains("fill-opacity") && json["fill-opacity"].isObject())
|
||||||
|
_fillOpacity = FunctionF(json["fill-opacity"].toObject());
|
||||||
|
if (json.contains("fill-color") && json["fill-color"].isString())
|
||||||
|
_fillColor = FunctionC(Color::fromJsonString(
|
||||||
|
json["fill-color"].toString()));
|
||||||
|
if (json.contains("fill-color") && json["fill-color"].isObject())
|
||||||
|
_fillColor = FunctionC(json["fill-color"].toObject());
|
||||||
|
if (json.contains("fill-outline-color")
|
||||||
|
&& json["fill-outline-color"].isString())
|
||||||
|
_fillOutlineColor = FunctionC(Color::fromJsonString(
|
||||||
|
json["fill-outline-color"].toString()));
|
||||||
|
if (json.contains("fill-outline-color")
|
||||||
|
&& json["fill-outline-color"].isObject())
|
||||||
|
_fillOutlineColor = FunctionC(json["fill-outline-color"].toObject());
|
||||||
|
if (json.contains("fill-antialias") && json["fill-antialias"].isBool())
|
||||||
|
_fillAntialias = json["fill-antialias"].toBool();
|
||||||
|
|
||||||
|
if (json.contains("line-color") && json["line-color"].isString())
|
||||||
|
_lineColor = FunctionC(Color::fromJsonString(json["line-color"]
|
||||||
|
.toString()));
|
||||||
|
if (json.contains("line-color") && json["line-color"].isObject())
|
||||||
|
_lineColor = FunctionC(json["line-color"].toObject());
|
||||||
|
if (json.contains("line-width") && json["line-width"].isObject())
|
||||||
|
_lineWidth = FunctionF(json["line-width"].toObject());
|
||||||
|
else if (json.contains("line-width") && json["line-width"].isDouble())
|
||||||
|
_lineWidth = FunctionF(json["line-width"].toDouble());
|
||||||
|
if (json.contains("line-opacity") && json["line-opacity"].isDouble())
|
||||||
|
_lineOpacity = FunctionF(json["line-opacity"].toDouble());
|
||||||
|
else if (json.contains("line-opacity") && json["line-opacity"].isObject())
|
||||||
|
_lineOpacity = FunctionF(json["line-opacity"].toObject());
|
||||||
|
if (json.contains("line-dasharray") && json["line-dasharray"].isArray()) {
|
||||||
|
QJsonArray array = json["line-dasharray"].toArray();
|
||||||
|
for (int i = 0; i < array.size(); i++)
|
||||||
|
_lineDasharray.append(array.at(i).toDouble());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json.contains("background-color") && json["background-color"].isString())
|
||||||
|
_backgroundColor = FunctionC(Color::fromJsonString(
|
||||||
|
json["background-color"].toString()));
|
||||||
|
if (json.contains("background-color") && json["background-color"].isObject())
|
||||||
|
_backgroundColor = FunctionC(json["background-color"].toObject());
|
||||||
|
|
||||||
|
if (json.contains("text-color") && json["text-color"].isString())
|
||||||
|
_textColor = FunctionC(Color::fromJsonString(json["text-color"]
|
||||||
|
.toString()));
|
||||||
|
if (json.contains("text-color") && json["text-color"].isObject())
|
||||||
|
_textColor = FunctionC(json["text-color"].toObject());
|
||||||
|
}
|
||||||
|
|
||||||
|
QPen Style::Layer::Paint::pen(Type type, int zoom) const
|
||||||
|
{
|
||||||
|
QPen pen(Qt::NoPen);
|
||||||
|
qreal width;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case Line:
|
||||||
|
width = _lineWidth.value(zoom);
|
||||||
|
if (_lineColor.value(zoom).isValid() && width > 0) {
|
||||||
|
pen = QPen(_lineColor.value(zoom), width);
|
||||||
|
if (!_lineDasharray.isEmpty())
|
||||||
|
pen.setDashPattern(_lineDasharray);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Fill:
|
||||||
|
if (_fillOutlineColor.value(zoom).isValid())
|
||||||
|
pen = QPen(_fillOutlineColor.value(zoom));
|
||||||
|
else if (_fillColor.value(zoom).isValid())
|
||||||
|
pen = QPen(_fillColor.value(zoom));
|
||||||
|
break;
|
||||||
|
case Symbol:
|
||||||
|
if (_textColor.value(zoom).isValid())
|
||||||
|
pen = QPen(_textColor.value(zoom));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pen;
|
||||||
|
}
|
||||||
|
|
||||||
|
QBrush Style::Layer::Paint::brush(Type type, int zoom) const
|
||||||
|
{
|
||||||
|
switch (type) {
|
||||||
|
case Fill:
|
||||||
|
return _fillColor.value(zoom).isValid()
|
||||||
|
? QBrush(_fillColor.value(zoom)) : QBrush(Qt::NoBrush);
|
||||||
|
case Background:
|
||||||
|
return _backgroundColor.value(zoom).isValid()
|
||||||
|
? QBrush(_backgroundColor.value(zoom)) : QBrush(Qt::NoBrush);
|
||||||
|
default:
|
||||||
|
return QBrush(Qt::NoBrush);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
qreal Style::Layer::Paint::opacity(Type type, int zoom) const
|
||||||
|
{
|
||||||
|
switch (type) {
|
||||||
|
case Fill:
|
||||||
|
return _fillOpacity.value(zoom);
|
||||||
|
case Line:
|
||||||
|
return _lineOpacity.value(zoom);
|
||||||
|
default:
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Style::Layer::Paint::antialias(Layer::Type type) const
|
||||||
|
{
|
||||||
|
switch (type) {
|
||||||
|
case Fill:
|
||||||
|
return _fillAntialias;
|
||||||
|
case Line:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Style::Layer::Layout::Layout(const QJsonObject &json)
|
||||||
|
: _textSize(16), _textMaxWidth(10), _lineCap(Qt::FlatCap),
|
||||||
|
_lineJoin(Qt::MiterJoin)
|
||||||
|
{
|
||||||
|
if (!(json.contains("text-field") && json["text-field"].isString()))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_textField = json["text-field"].toString();
|
||||||
|
|
||||||
|
QRegExp rx("\\{[^\\}]*\\}");
|
||||||
|
int pos = 0;
|
||||||
|
while ((pos = rx.indexIn(_textField, pos)) != -1) {
|
||||||
|
QString match = rx.capturedTexts().first();
|
||||||
|
_keys.append(match.mid(1, match.size() - 2));
|
||||||
|
pos += rx.matchedLength();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json.contains("text-size") && json["text-size"].isObject())
|
||||||
|
_textSize = FunctionF(json["text-size"].toObject());
|
||||||
|
else if (json.contains("text-size") && json["text-size"].isDouble())
|
||||||
|
_textSize = json["text-size"].toDouble();
|
||||||
|
|
||||||
|
if (json.contains("text-max-width") && json["text-max-width"].isObject())
|
||||||
|
_textMaxWidth = FunctionF(json["text-max-width"].toObject());
|
||||||
|
if (json.contains("text-max-width") && json["text-max-width"].isDouble())
|
||||||
|
_textMaxWidth = json["text-max-width"].toDouble();
|
||||||
|
|
||||||
|
if (json.contains("line-cap") && json["line-cap"].isString()) {
|
||||||
|
if (json["line-cap"].toString() == "round")
|
||||||
|
_lineCap = Qt::RoundCap;
|
||||||
|
else if (json["line-cap"].toString() == "square")
|
||||||
|
_lineCap = Qt::SquareCap;
|
||||||
|
}
|
||||||
|
if (json.contains("line-join") && json["line-join"].isString()) {
|
||||||
|
if (json["line-join"].toString() == "bevel")
|
||||||
|
_lineJoin = Qt::BevelJoin;
|
||||||
|
else if (json["line-join"].toString() == "round")
|
||||||
|
_lineJoin = Qt::RoundJoin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QFont Style::Layer::Layout::font(int zoom) const
|
||||||
|
{
|
||||||
|
QFont font;
|
||||||
|
font.setPixelSize(_textSize.value(zoom));
|
||||||
|
|
||||||
|
return font;
|
||||||
|
}
|
||||||
|
|
||||||
|
Style::Layer::Layer(const QJsonObject &json)
|
||||||
|
: _type(Unknown), _minZoom(-1), _maxZoom(-1)
|
||||||
|
{
|
||||||
|
// type
|
||||||
|
QString type = json["type"].toString();
|
||||||
|
if (type == "fill")
|
||||||
|
_type = Fill;
|
||||||
|
else if (type == "line")
|
||||||
|
_type = Line;
|
||||||
|
else if (type == "background")
|
||||||
|
_type = Background;
|
||||||
|
else if (type == "vector")
|
||||||
|
_type = Vector;
|
||||||
|
else if (type == "symbol")
|
||||||
|
_type = Symbol;
|
||||||
|
|
||||||
|
// source-layer
|
||||||
|
_sourceLayer = json["source-layer"].toString();
|
||||||
|
|
||||||
|
// zooms
|
||||||
|
if (json.contains("minzoom") && json["minzoom"].isDouble())
|
||||||
|
_minZoom = json["minzoom"].toInt();
|
||||||
|
if (json.contains("maxzoom") && json["maxzoom"].isDouble())
|
||||||
|
_maxZoom = json["maxzoom"].toInt();
|
||||||
|
|
||||||
|
// filter
|
||||||
|
if (json.contains("filter") && json["filter"].isArray())
|
||||||
|
_filter = Filter(json["filter"].toArray());
|
||||||
|
|
||||||
|
// layout
|
||||||
|
if (json.contains("layout") && json["layout"].isObject())
|
||||||
|
_layout = Layout(json["layout"].toObject());
|
||||||
|
|
||||||
|
// paint
|
||||||
|
if (json.contains("paint") && json["paint"].isObject())
|
||||||
|
_paint = Paint(json["paint"].toObject());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Style::Layer::match(int zoom, const QVariantMap &tags) const
|
||||||
|
{
|
||||||
|
if (zoom >= 0) {
|
||||||
|
if (_minZoom > 0 && zoom < _minZoom)
|
||||||
|
return false;
|
||||||
|
if (_maxZoom > 0 && zoom > _maxZoom)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_type == Line && _paint.pen(_type, zoom).style() == Qt::NoPen)
|
||||||
|
return false;
|
||||||
|
if (_type == Fill && _paint.brush(_type, zoom) == Qt::NoBrush)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return _filter.match(tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Style::Layer::drawPath(int zoom, const QPainterPath &path,
|
||||||
|
Tile &tile) const
|
||||||
|
{
|
||||||
|
QPainter p(&(tile.background()));
|
||||||
|
|
||||||
|
QPen pen(_paint.pen(_type, zoom));
|
||||||
|
pen.setJoinStyle(_layout.lineJoin());
|
||||||
|
pen.setCapStyle(_layout.lineCap());
|
||||||
|
|
||||||
|
p.setRenderHint(QPainter::Antialiasing,
|
||||||
|
_paint.antialias(_type));
|
||||||
|
p.setPen(pen);
|
||||||
|
p.setBrush(_paint.brush(_type, zoom));
|
||||||
|
p.setOpacity(_paint.opacity(_type, zoom));
|
||||||
|
p.drawPath(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Style::Layer::drawSymbol(int zoom, const QPainterPath &path,
|
||||||
|
const QVariantMap &tags, Tile &tile) const
|
||||||
|
{
|
||||||
|
if (!_layout.showText(zoom))
|
||||||
|
return;
|
||||||
|
|
||||||
|
QString text(_layout.field());
|
||||||
|
for (int i = 0; i < _layout.keys().size(); i++) {
|
||||||
|
const QString &key = _layout.keys().at(i);
|
||||||
|
const QVariant val = tags.value(key);
|
||||||
|
text.replace(QString("{%1}").arg(key), val.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
const QPainterPath::Element &e = path.elementAt(0);
|
||||||
|
QPen pen(_paint.pen(_type, zoom));
|
||||||
|
QFont font(_layout.font(zoom));
|
||||||
|
|
||||||
|
if (path.elementCount() == 1)
|
||||||
|
tile.text().addLabel(text.trimmed(), QPointF(e.x, e.y), font, pen,
|
||||||
|
_layout.maxTextWidth(zoom));
|
||||||
|
else
|
||||||
|
tile.text().addLabel(text.trimmed(), path, font, pen);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Style::load(const QString &fileName)
|
||||||
|
{
|
||||||
|
QFile file(fileName);
|
||||||
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||||
|
return false;
|
||||||
|
QByteArray ba(file.readAll());
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
QJsonObject json(QJsonDocument::fromJson(ba).object());
|
||||||
|
if (json.contains("layers") && json["layers"].isArray()) {
|
||||||
|
QJsonArray layers = json["layers"].toArray();
|
||||||
|
for (int i = 0; i < layers.size(); i++)
|
||||||
|
if (layers[i].isObject())
|
||||||
|
_styles.append(Layer(layers[i].toObject()));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < _styles.size(); i++)
|
||||||
|
_sourceLayers.append(_styles.at(i).sourceLayer());
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Style::match(int layer, const QVariantMap &tags)
|
||||||
|
{
|
||||||
|
return _styles.at(layer).match(_zoom, tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Style::drawFeature(int layer, const QPainterPath &path,
|
||||||
|
const QVariantMap &tags, Tile &tile)
|
||||||
|
{
|
||||||
|
const Layer &sl = _styles.at(layer);
|
||||||
|
|
||||||
|
if (sl.isPath())
|
||||||
|
sl.drawPath(_zoom, path, tile);
|
||||||
|
else if (sl.isSymbol())
|
||||||
|
sl.drawSymbol(_zoom, path, tags, tile);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Style::drawBackground(Tile &tile)
|
||||||
|
{
|
||||||
|
QPainterPath p;
|
||||||
|
p.addRect(tile.background().rect());
|
||||||
|
|
||||||
|
if (_styles.isEmpty()) {
|
||||||
|
tile.background().fill(Qt::lightGray);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < _styles.size(); i++)
|
||||||
|
if (_styles.at(i).isBackground())
|
||||||
|
_styles.at(i).drawPath(_zoom, p, tile);
|
||||||
|
}
|
137
src/style.h
Normal file
137
src/style.h
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
#ifndef STYLE_H
|
||||||
|
#define STYLE_H
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
#include <QVariantMap>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QSet>
|
||||||
|
#include <QPen>
|
||||||
|
#include <QBrush>
|
||||||
|
#include "function.h"
|
||||||
|
|
||||||
|
|
||||||
|
class QPainter;
|
||||||
|
class QPainterPath;
|
||||||
|
class Tile;
|
||||||
|
|
||||||
|
class Style
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
bool load(const QString &fileName);
|
||||||
|
|
||||||
|
void setZoom(int zoom) {_zoom = zoom;}
|
||||||
|
|
||||||
|
const QStringList &sourceLayers() const {return _sourceLayers;}
|
||||||
|
bool match(int layer, const QVariantMap &tags);
|
||||||
|
|
||||||
|
void drawBackground(Tile &tile);
|
||||||
|
void drawFeature(int layer, const QPainterPath &path,
|
||||||
|
const QVariantMap &tags, Tile &tile);
|
||||||
|
|
||||||
|
private:
|
||||||
|
class Layer {
|
||||||
|
public:
|
||||||
|
Layer(const QJsonObject &json);
|
||||||
|
|
||||||
|
const QString &sourceLayer() const {return _sourceLayer;}
|
||||||
|
bool isPath() const {return (_type == Line || _type == Fill);}
|
||||||
|
bool isBackground() const {return (_type == Background);}
|
||||||
|
bool isSymbol() const {return (_type == Symbol);}
|
||||||
|
|
||||||
|
bool match(int zoom, const QVariantMap &tags) const;
|
||||||
|
void drawPath(int zoom, const QPainterPath &path, Tile &tile) const;
|
||||||
|
void drawSymbol(int zoom, const QPainterPath &path,
|
||||||
|
const QVariantMap &tags, Tile &tile) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
enum Type {
|
||||||
|
Unknown,
|
||||||
|
Fill,
|
||||||
|
Line,
|
||||||
|
Background,
|
||||||
|
Vector,
|
||||||
|
Symbol
|
||||||
|
};
|
||||||
|
|
||||||
|
class Filter {
|
||||||
|
public:
|
||||||
|
Filter() : _type(None) {}
|
||||||
|
Filter(const QJsonArray &json);
|
||||||
|
|
||||||
|
bool match(const QVariantMap &tags) const;
|
||||||
|
private:
|
||||||
|
enum Type {
|
||||||
|
None, Unknown,
|
||||||
|
EQ, NE, GE, GT, LE, LT,
|
||||||
|
All, Any,
|
||||||
|
In, Has
|
||||||
|
};
|
||||||
|
|
||||||
|
Type _type;
|
||||||
|
bool _not;
|
||||||
|
QSet<QString> _set;
|
||||||
|
QPair<QString, QVariant> _kv;
|
||||||
|
QVector<Filter> _filters;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Layout {
|
||||||
|
public:
|
||||||
|
Layout() : _textSize(16), _textMaxWidth(10), _lineCap(Qt::FlatCap),
|
||||||
|
_lineJoin(Qt::MiterJoin) {}
|
||||||
|
Layout(const QJsonObject &json);
|
||||||
|
|
||||||
|
bool showText(int zoom) const {return _textSize.value(zoom) > 0;}
|
||||||
|
qreal maxTextWidth(int zoom) const {return _textMaxWidth.value(zoom);}
|
||||||
|
const QString &field() const {return _textField;}
|
||||||
|
const QStringList &keys() const {return _keys;}
|
||||||
|
QFont font(int zoom) const;
|
||||||
|
Qt::PenCapStyle lineCap() const {return _lineCap;}
|
||||||
|
Qt::PenJoinStyle lineJoin() const {return _lineJoin;}
|
||||||
|
|
||||||
|
private:
|
||||||
|
QStringList _keys;
|
||||||
|
QString _textField;
|
||||||
|
FunctionF _textSize;
|
||||||
|
FunctionF _textMaxWidth;
|
||||||
|
Qt::PenCapStyle _lineCap;
|
||||||
|
Qt::PenJoinStyle _lineJoin;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Paint {
|
||||||
|
public:
|
||||||
|
Paint() : _fillOpacity(1.0), _lineOpacity(1.0), _fillAntialias(true)
|
||||||
|
{}
|
||||||
|
Paint(const QJsonObject &json);
|
||||||
|
|
||||||
|
QPen pen(Layer::Type type, int zoom) const;
|
||||||
|
QBrush brush(Layer::Type type, int zoom) const;
|
||||||
|
qreal opacity(Layer::Type type, int zoom) const;
|
||||||
|
bool antialias(Layer::Type type) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
FunctionC _textColor;
|
||||||
|
FunctionC _lineColor;
|
||||||
|
FunctionC _fillColor;
|
||||||
|
FunctionC _fillOutlineColor;
|
||||||
|
FunctionC _backgroundColor;
|
||||||
|
FunctionF _fillOpacity;
|
||||||
|
FunctionF _lineOpacity;
|
||||||
|
FunctionF _lineWidth;
|
||||||
|
bool _fillAntialias;
|
||||||
|
QVector<qreal> _lineDasharray;
|
||||||
|
};
|
||||||
|
|
||||||
|
Type _type;
|
||||||
|
QString _sourceLayer;
|
||||||
|
int _minZoom, _maxZoom;
|
||||||
|
Filter _filter;
|
||||||
|
Layout _layout;
|
||||||
|
Paint _paint;
|
||||||
|
};
|
||||||
|
|
||||||
|
int _zoom;
|
||||||
|
QList<Layer> _styles;
|
||||||
|
QStringList _sourceLayers;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // STYLE_H
|
110
src/text.cpp
Normal file
110
src/text.cpp
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
#include <QGraphicsPixmapItem>
|
||||||
|
#include <QFontMetrics>
|
||||||
|
#include <QPainter>
|
||||||
|
#include "text.h"
|
||||||
|
#include "textpathitem.h"
|
||||||
|
|
||||||
|
|
||||||
|
static QPainterPath subpath(const QPainterPath &path, int start, int end)
|
||||||
|
{
|
||||||
|
QPainterPath p(path.elementAt(start));
|
||||||
|
for (int i = start + 1; i <= end; i++)
|
||||||
|
p.lineTo(path.elementAt(i));
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
static QList<QPainterPath> segments(const QPainterPath &path, qreal segmentLimit,
|
||||||
|
qreal pathLimit)
|
||||||
|
{
|
||||||
|
QList<QPainterPath> list;
|
||||||
|
int start = 0;
|
||||||
|
qreal length = 0;
|
||||||
|
|
||||||
|
for (int i = 1; i < path.elementCount(); i++) {
|
||||||
|
QLineF l(path.elementAt(i-1), path.elementAt(i));
|
||||||
|
qreal sl = l.length();
|
||||||
|
if (sl < segmentLimit || length > pathLimit) {
|
||||||
|
if (length > pathLimit)
|
||||||
|
list.append(subpath(path, start, i - 1));
|
||||||
|
start = i;
|
||||||
|
length = 0;
|
||||||
|
} else
|
||||||
|
length += sl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (length > pathLimit)
|
||||||
|
list.append(subpath(path, start, path.elementCount() - 1));
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool reverse(const QPainterPath &path)
|
||||||
|
{
|
||||||
|
QLineF l(path.elementAt(0), path.elementAt(1));
|
||||||
|
qreal angle = l.angle();
|
||||||
|
return (angle > 90 && angle < 270) ? true : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Text::addLabel(const QString &text, const QPointF &pos, const QFont &font,
|
||||||
|
const QPen &pen, qreal maxTextWidth)
|
||||||
|
{
|
||||||
|
if (text.isEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
QFontMetrics fm(font);
|
||||||
|
int limit = fm.width('M') * maxTextWidth;
|
||||||
|
int flags = Qt::AlignCenter | Qt::TextWordWrap | Qt::TextDontClip;
|
||||||
|
|
||||||
|
QRect br = fm.boundingRect(QRect(0, 0, limit, 0), flags, text);
|
||||||
|
if (!br.isValid())
|
||||||
|
return;
|
||||||
|
br.moveTo((pos - QPointF(br.width() / 2.0, br.height() / 2.0)).toPoint());
|
||||||
|
if (!sceneRect().contains(br))
|
||||||
|
return;
|
||||||
|
QPixmap pm(br.size());
|
||||||
|
pm.fill(Qt::transparent);
|
||||||
|
QPainter p(&pm);
|
||||||
|
p.setFont(font);
|
||||||
|
p.setPen(pen);
|
||||||
|
p.drawText(pm.rect(), flags, text);
|
||||||
|
|
||||||
|
QGraphicsPixmapItem *pi = addPixmap(pm);
|
||||||
|
pi->setPos(br.topLeft());
|
||||||
|
|
||||||
|
QList<QGraphicsItem*> ci = collidingItems(pi);
|
||||||
|
for (int i = 0; i < ci.size(); i++)
|
||||||
|
ci[i]->setVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Text::addLabel(const QString &text, const QPainterPath &path,
|
||||||
|
const QFont &font, const QPen &pen)
|
||||||
|
{
|
||||||
|
if (path.elementCount() < 2 || !path.elementAt(0).isMoveTo())
|
||||||
|
return;
|
||||||
|
if (text.isEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
QFontMetrics fm(font);
|
||||||
|
int textWidth = fm.width(text);
|
||||||
|
|
||||||
|
if (textWidth > path.length())
|
||||||
|
return;
|
||||||
|
|
||||||
|
QList<QPainterPath> list(segments(path, fm.width('M'), textWidth));
|
||||||
|
for (int i = 0; i < list.size(); i++) {
|
||||||
|
const QPainterPath &segment = list.at(i);
|
||||||
|
TextPathItem *pi = new TextPathItem(text, reverse(segment)
|
||||||
|
? segment.toReversed() : segment, font);
|
||||||
|
pi->setPen(pen);
|
||||||
|
addItem(pi);
|
||||||
|
|
||||||
|
if (!sceneRect().contains(pi->sceneBoundingRect())) {
|
||||||
|
delete pi;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<QGraphicsItem*> ci = collidingItems(pi);
|
||||||
|
for (int j = 0; j < ci.size(); j++)
|
||||||
|
ci[j]->setVisible(false);
|
||||||
|
}
|
||||||
|
}
|
18
src/text.h
Normal file
18
src/text.h
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
#ifndef TEXT_H
|
||||||
|
#define TEXT_H
|
||||||
|
|
||||||
|
#include <QGraphicsScene>
|
||||||
|
|
||||||
|
class Text : public QGraphicsScene
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Text(QObject *parent = 0) : QGraphicsScene(parent)
|
||||||
|
{setSceneRect(0, 0, 256, 256);}
|
||||||
|
|
||||||
|
void addLabel(const QString &text, const QPointF &pos, const QFont &font,
|
||||||
|
const QPen &pen, qreal maxTextWidth);
|
||||||
|
void addLabel(const QString &text, const QPainterPath &path,
|
||||||
|
const QFont &font, const QPen &pen);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // TEXT_H
|
45
src/textpathitem.cpp
Normal file
45
src/textpathitem.cpp
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
#include <QFontMetrics>
|
||||||
|
#include <QPainter>
|
||||||
|
#include "textpathitem.h"
|
||||||
|
|
||||||
|
|
||||||
|
TextPathItem::TextPathItem(const QString &text, const QPainterPath &path,
|
||||||
|
const QFont &font, QGraphicsItem *parent) : QGraphicsItem(parent),
|
||||||
|
_text(text), _path(path), _font(font)
|
||||||
|
{
|
||||||
|
QFontMetrics fm(font);
|
||||||
|
QPainterPathStroker s;
|
||||||
|
s.setWidth(fm.height());
|
||||||
|
_shape = s.createStroke(path).simplified();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextPathItem::paint(QPainter *painter,
|
||||||
|
const QStyleOptionGraphicsItem *option, QWidget *widget)
|
||||||
|
{
|
||||||
|
Q_UNUSED(option);
|
||||||
|
Q_UNUSED(widget);
|
||||||
|
|
||||||
|
QFontMetrics fm(_font);
|
||||||
|
int textWidth = fm.width(_text);
|
||||||
|
|
||||||
|
qreal factor = (textWidth) / _path.length();
|
||||||
|
qreal percent = (1.0 - factor) / 2.0;
|
||||||
|
|
||||||
|
painter->setFont(_font);
|
||||||
|
painter->setPen(_pen);
|
||||||
|
|
||||||
|
for (int i = 0; i < _text.size(); i++) {
|
||||||
|
Q_ASSERT(percent <= 1.0);
|
||||||
|
|
||||||
|
QPointF point = _path.pointAtPercent(percent);
|
||||||
|
qreal angle = _path.angleAtPercent(percent);
|
||||||
|
|
||||||
|
painter->translate(point);
|
||||||
|
painter->rotate(-angle);
|
||||||
|
painter->drawText(QPoint(0, 0), _text.at(i));
|
||||||
|
painter->resetTransform();
|
||||||
|
|
||||||
|
int width = fm.charWidth(_text, i);
|
||||||
|
percent += ((qreal)width / (qreal)textWidth) * factor;
|
||||||
|
}
|
||||||
|
}
|
30
src/textpathitem.h
Normal file
30
src/textpathitem.h
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
#ifndef TEXTPATHITEM_H
|
||||||
|
#define TEXTPATHITEM_H
|
||||||
|
|
||||||
|
#include <QGraphicsItem>
|
||||||
|
#include <QPainterPath>
|
||||||
|
#include <QFont>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
class TextPathItem : public QGraphicsItem
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
TextPathItem(const QString &text, const QPainterPath &path,
|
||||||
|
const QFont &font, QGraphicsItem *parent = 0);
|
||||||
|
|
||||||
|
QPainterPath shape() const {return _shape;}
|
||||||
|
QRectF boundingRect() const {return _shape.boundingRect();}
|
||||||
|
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option,
|
||||||
|
QWidget *widget);
|
||||||
|
|
||||||
|
void setPen(const QPen &pen) {_pen = pen;}
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString _text;
|
||||||
|
QPainterPath _path;
|
||||||
|
QPainterPath _shape;
|
||||||
|
QFont _font;
|
||||||
|
QPen _pen;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // TEXTPATHITEM_H
|
26
src/tile.h
Normal file
26
src/tile.h
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
#ifndef TILE_H
|
||||||
|
#define TILE_H
|
||||||
|
|
||||||
|
#include <QImage>
|
||||||
|
#include <QPainter>
|
||||||
|
#include "text.h"
|
||||||
|
|
||||||
|
class Tile {
|
||||||
|
public:
|
||||||
|
Tile() : _background(256, 256, QImage::Format_ARGB32) {}
|
||||||
|
|
||||||
|
Text &text() {return _text;}
|
||||||
|
QImage &background() {return _background;}
|
||||||
|
|
||||||
|
QImage &render() {
|
||||||
|
QPainter p(&_background);
|
||||||
|
_text.render(&p);
|
||||||
|
return _background;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Text _text;
|
||||||
|
QImage _background;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // TILE_H
|
Loading…
Reference in New Issue
Block a user