From 88295f0ca7ec027b3949d3a40ffe06caf1c8a0d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20T=C5=AFma?= Date: Mon, 29 Oct 2018 00:11:23 +0100 Subject: [PATCH] Initial version --- pbfplugin.json | 4 + pbfplugin.pro | 35 +++ protobuf/vector_tile.pri | 17 ++ protobuf/vector_tile.proto | 78 +++++++ src/color.cpp | 48 ++++ src/color.h | 11 + src/function.cpp | 119 ++++++++++ src/function.h | 37 ++++ src/gzip.cpp | 35 +++ src/gzip.h | 11 + src/pbf.cpp | 168 ++++++++++++++ src/pbf.h | 14 ++ src/pbfhandler.cpp | 50 +++++ src/pbfhandler.h | 29 +++ src/pbfplugin.cpp | 37 ++++ src/pbfplugin.h | 22 ++ src/style.cpp | 433 +++++++++++++++++++++++++++++++++++++ src/style.h | 137 ++++++++++++ src/text.cpp | 110 ++++++++++ src/text.h | 18 ++ src/textpathitem.cpp | 45 ++++ src/textpathitem.h | 30 +++ src/tile.h | 26 +++ 23 files changed, 1514 insertions(+) create mode 100644 pbfplugin.json create mode 100644 pbfplugin.pro create mode 100644 protobuf/vector_tile.pri create mode 100644 protobuf/vector_tile.proto create mode 100644 src/color.cpp create mode 100644 src/color.h create mode 100644 src/function.cpp create mode 100644 src/function.h create mode 100644 src/gzip.cpp create mode 100644 src/gzip.h create mode 100644 src/pbf.cpp create mode 100644 src/pbf.h create mode 100644 src/pbfhandler.cpp create mode 100644 src/pbfhandler.h create mode 100644 src/pbfplugin.cpp create mode 100644 src/pbfplugin.h create mode 100644 src/style.cpp create mode 100644 src/style.h create mode 100644 src/text.cpp create mode 100644 src/text.h create mode 100644 src/textpathitem.cpp create mode 100644 src/textpathitem.h create mode 100644 src/tile.h diff --git a/pbfplugin.json b/pbfplugin.json new file mode 100644 index 0000000..c5cc32e --- /dev/null +++ b/pbfplugin.json @@ -0,0 +1,4 @@ +{ + "Keys": [ "pbf" ], + "MimeTypes": [ "image/pbf" ] +} diff --git a/pbfplugin.pro b/pbfplugin.pro new file mode 100644 index 0000000..16e475b --- /dev/null +++ b/pbfplugin.pro @@ -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 diff --git a/protobuf/vector_tile.pri b/protobuf/vector_tile.pri new file mode 100644 index 0000000..b92e31d --- /dev/null +++ b/protobuf/vector_tile.pri @@ -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 diff --git a/protobuf/vector_tile.proto b/protobuf/vector_tile.proto new file mode 100644 index 0000000..ef3d870 --- /dev/null +++ b/protobuf/vector_tile.proto @@ -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; +} diff --git a/src/color.cpp b/src/color.cpp new file mode 100644 index 0000000..e334875 --- /dev/null +++ b/src/color.cpp @@ -0,0 +1,48 @@ +#include +#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; +} diff --git a/src/color.h b/src/color.h new file mode 100644 index 0000000..260a110 --- /dev/null +++ b/src/color.h @@ -0,0 +1,11 @@ +#ifndef COLOR_H +#define COLOR_H + +#include + +namespace Color +{ + QColor fromJsonString(const QString &str); +} + +#endif // COLOR_H diff --git a/src/function.cpp b/src/function.cpp new file mode 100644 index 0000000..fc4bdca --- /dev/null +++ b/src/function.cpp @@ -0,0 +1,119 @@ +#include +#include +#include +#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 &p0, + const QPair &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(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 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; +} diff --git a/src/function.h b/src/function.h new file mode 100644 index 0000000..fbd2ec8 --- /dev/null +++ b/src/function.h @@ -0,0 +1,37 @@ +#ifndef FUNCTION_H +#define FUNCTION_H + +#include +#include +#include + +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 _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 > _stops; + QColor _default; + qreal _base; +}; + +#endif // FUNCTION_H diff --git a/src/gzip.cpp b/src/gzip.cpp new file mode 100644 index 0000000..20d79d6 --- /dev/null +++ b/src/gzip.cpp @@ -0,0 +1,35 @@ +#include +#include +#include +#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; +} diff --git a/src/gzip.h b/src/gzip.h new file mode 100644 index 0000000..debf125 --- /dev/null +++ b/src/gzip.h @@ -0,0 +1,11 @@ +#ifndef GZIP_H +#define GZIP_H + +#include + +namespace Gzip +{ + QByteArray uncompress(const QByteArray &data); +} + +#endif // GZIP_H diff --git a/src/pbf.cpp b/src/pbf.cpp new file mode 100644 index 0000000..e47ebc4 --- /dev/null +++ b/src/pbf.cpp @@ -0,0 +1,168 @@ +#include +#include +#include +#include +#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 keys; + QVector values; +}; + +struct Feature +{ + Feature(const vector_tile::Tile_Feature *data, const QVector *keys, + const QVector *values) : data(data), keys(keys), + values(values) {} + + const vector_tile::Tile_Feature *data; + const QVector *keys; + const QVector *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 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::const_iterator it = layers.find( + style->sourceLayers().at(i)); + if (it == layers.constEnd()) + continue; + + layer(*it, style, i, t); + } + + return t.render(); +} diff --git a/src/pbf.h b/src/pbf.h new file mode 100644 index 0000000..d5b2b7a --- /dev/null +++ b/src/pbf.h @@ -0,0 +1,14 @@ +#ifndef PBF_H +#define PBF_H + +#include + +class QByteArray; +class Style; + +namespace PBF +{ + QImage image(const QByteArray &data, int zoom, Style *style); +} + +#endif // PBF_H diff --git a/src/pbfhandler.cpp b/src/pbfhandler.cpp new file mode 100644 index 0000000..13412d7 --- /dev/null +++ b/src/pbfhandler.cpp @@ -0,0 +1,50 @@ +#include +#include +#include +#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"; +} diff --git a/src/pbfhandler.h b/src/pbfhandler.h new file mode 100644 index 0000000..77a4883 --- /dev/null +++ b/src/pbfhandler.h @@ -0,0 +1,29 @@ +#ifndef PBFHANDLER_H +#define PBFHANDLER_H + +#include +#include +#include + +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 diff --git a/src/pbfplugin.cpp b/src/pbfplugin.cpp new file mode 100644 index 0000000..7de12f3 --- /dev/null +++ b/src/pbfplugin.cpp @@ -0,0 +1,37 @@ +#include +#include +#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; +} diff --git a/src/pbfplugin.h b/src/pbfplugin.h new file mode 100644 index 0000000..566b068 --- /dev/null +++ b/src/pbfplugin.h @@ -0,0 +1,22 @@ +#include +#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; +}; diff --git a/src/style.cpp b/src/style.cpp new file mode 100644 index 0000000..e5c6d45 --- /dev/null +++ b/src/style.cpp @@ -0,0 +1,433 @@ +#include +#include +#include +#include +#include +#include +#include +#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(json.at(1).toString(), + json.at(2).toVariant()); + } else if (type == "!=") { + _type = NE; + _kv = QPair(json.at(1).toString(), + json.at(2).toVariant()); + } else if (type == "<") { + _type = LT; + _kv = QPair(json.at(1).toString(), + json.at(2).toVariant()); + } else if (type == "<=") { + _type = LE; + _kv = QPair(json.at(1).toString(), + json.at(2).toVariant()); + } else if (type == ">") { + _type = GT; + _kv = QPair(json.at(1).toString(), + json.at(2).toVariant()); + } else if (type == ">=") { + _type = GE; + _kv = QPair(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(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(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(json.at(1).toString(), QVariant()); + } else if (type == "!has") { + _type = Has; + _not = true; + _kv = QPair(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); +} diff --git a/src/style.h b/src/style.h new file mode 100644 index 0000000..ddcfd08 --- /dev/null +++ b/src/style.h @@ -0,0 +1,137 @@ +#ifndef STYLE_H +#define STYLE_H + +#include +#include +#include +#include +#include +#include +#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 _set; + QPair _kv; + QVector _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 _lineDasharray; + }; + + Type _type; + QString _sourceLayer; + int _minZoom, _maxZoom; + Filter _filter; + Layout _layout; + Paint _paint; + }; + + int _zoom; + QList _styles; + QStringList _sourceLayers; +}; + +#endif // STYLE_H diff --git a/src/text.cpp b/src/text.cpp new file mode 100644 index 0000000..a02accc --- /dev/null +++ b/src/text.cpp @@ -0,0 +1,110 @@ +#include +#include +#include +#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 segments(const QPainterPath &path, qreal segmentLimit, + qreal pathLimit) +{ + QList 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 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 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 ci = collidingItems(pi); + for (int j = 0; j < ci.size(); j++) + ci[j]->setVisible(false); + } +} diff --git a/src/text.h b/src/text.h new file mode 100644 index 0000000..d6efcdc --- /dev/null +++ b/src/text.h @@ -0,0 +1,18 @@ +#ifndef TEXT_H +#define TEXT_H + +#include + +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 diff --git a/src/textpathitem.cpp b/src/textpathitem.cpp new file mode 100644 index 0000000..9349f9e --- /dev/null +++ b/src/textpathitem.cpp @@ -0,0 +1,45 @@ +#include +#include +#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; + } +} diff --git a/src/textpathitem.h b/src/textpathitem.h new file mode 100644 index 0000000..31683fc --- /dev/null +++ b/src/textpathitem.h @@ -0,0 +1,30 @@ +#ifndef TEXTPATHITEM_H +#define TEXTPATHITEM_H + +#include +#include +#include +#include + +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 diff --git a/src/tile.h b/src/tile.h new file mode 100644 index 0000000..8e1728e --- /dev/null +++ b/src/tile.h @@ -0,0 +1,26 @@ +#ifndef TILE_H +#define TILE_H + +#include +#include +#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