diff --git a/src/map/scene/scenecontroller.cpp b/src/map/scene/scenecontroller.cpp index 535bf1c..d360591 100644 --- a/src/map/scene/scenecontroller.cpp +++ b/src/map/scene/scenecontroller.cpp @@ -1,453 +1,433 @@ /* Copyright (C) 2020 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "scenecontroller.h" +#include "scenegeometry.h" #include "scenegraph.h" #include "scenegraphitem.h" #include "../loader/mapdata.h" #include "../renderer/view.h" #include "../style/mapcssdeclaration.h" #include "../style/mapcssstyle.h" #include "../style/mapcssstate.h" #include #include #include #include #include using namespace KOSMIndoorMap; SceneController::SceneController() = default; SceneController::~SceneController() = default; void SceneController::setDataSet(const MapData *data) { m_data = data; } void SceneController::setStyleSheet(const MapCSSStyle *styleSheet) { m_styleSheet = styleSheet; } void SceneController::setView(const View *view) { m_view = view; } void SceneController::updateScene(SceneGraph &sg) const { sg.clear(); // TODO reuse what is still valid updateCanvas(sg); // find all intermediate levels below or above the currently selected "full" level auto it = m_data->m_levelMap.find(MapLevel(m_view->level())); if (it == m_data->m_levelMap.end()) { return; } auto beginIt = it; if (beginIt != m_data->m_levelMap.begin()) { do { --beginIt; } while (!(*beginIt).first.isFullLevel() && beginIt != m_data->m_levelMap.begin()); ++beginIt; } auto endIt = it; for (++endIt; endIt != m_data->m_levelMap.end(); ++endIt) { if ((*endIt).first.isFullLevel()) { break; } } for (auto it = beginIt; it != endIt; ++it) { for (auto e : (*it).second) { updateElement(e, (*it).first.numericLevel(), sg); } } sg.zSort(); } void SceneController::updateCanvas(SceneGraph &sg) const { sg.setBackgroundColor(QGuiApplication::palette().color(QPalette::Base)); m_defaultTextColor = QGuiApplication::palette().color(QPalette::Text); m_defaultFont = QGuiApplication::font(); m_styleSheet->evaluateCanvas(m_styleResult); for (auto decl : m_styleResult.declarations()) { switch (decl->property()) { case MapCSSDeclaration::FillColor: sg.setBackgroundColor(decl->colorValue()); break; case MapCSSDeclaration::TextColor: m_defaultTextColor = decl->colorValue(); break; default: break; } } } -// TODO this is a simplication, assuming equidistant point positions -// this actually needs to be done taking lengths into account -static double angleForPath(const QPolygonF &path) -{ - assert(path.size() >= 2); - - QLineF line; - line.setP1(path.at(path.size() / 2 - 1)); - if (path.size() % 2 == 0) { - line.setP2(path.at(path.size() / 2)); - } else { - line.setP2(path.at(path.size() / 2 + 1)); - } - - auto a = - std::remainder(line.angle(), 360.0); - if (a < -90.0 || a > 90.0) { - a += 180.0; - } - return a; -} - void SceneController::updateElement(OSM::Element e, int level, SceneGraph &sg) const { MapCSSState state; state.element = e; state.zoomLevel = m_view->zoomLevel(); m_styleSheet->evaluate(state, m_styleResult); QPolygonF linePath; if (m_styleResult.hasAreaProperties()) { PolygonBaseItem *item; if (e.type() == OSM::Type::Relation && e.tagValue("type") == QLatin1String("multipolygon")) { auto i = new MultiPolygonItm; i->path = createPath(e); item = i; } else { auto i = new PolygonItem; i->polygon = createPolygon(e); item = i; } double lineOpacity = 1.0; double fillOpacity = 1.0; initializePen(item->pen); for (auto decl : m_styleResult.declarations()) { applyGenericStyle(decl, item); applyPenStyle(decl, item->pen, lineOpacity); switch (decl->property()) { case MapCSSDeclaration::FillColor: item->brush.setColor(decl->colorValue()); item->brush.setStyle(Qt::SolidPattern); break; case MapCSSDeclaration::FillOpacity: fillOpacity = decl->doubleValue(); break; default: break; } } finalizePen(item->pen, lineOpacity); if (item->brush.style() == Qt::SolidPattern && fillOpacity < 1.0) { auto c = item->brush.color(); c.setAlphaF(c.alphaF() * fillOpacity); item->brush.setColor(c); } addItem(sg, e, level, item); } else if (m_styleResult.hasLineProperties()) { auto item = new PolylineItem; item->path = createPolygon(e); double lineOpacity = 1.0; double casingOpacity = 1.0; initializePen(item->pen); initializePen(item->casingPen); for (auto decl : m_styleResult.declarations()) { applyGenericStyle(decl, item); applyPenStyle(decl, item->pen, lineOpacity); applyCasingPenStyle(decl, item->casingPen, casingOpacity); } finalizePen(item->pen, lineOpacity); finalizePen(item->casingPen, casingOpacity); linePath = item->path; addItem(sg, e, level, item); } if (m_styleResult.hasLabelProperties()) { QString text; auto textDecl = m_styleResult.declaration(MapCSSDeclaration::Text); if (!textDecl) { textDecl = m_styleResult.declaration(MapCSSDeclaration::ShieldText); } if (textDecl) { if (!textDecl->keyValue().isEmpty()) { text = e.tagValue(textDecl->keyValue().constData()); } else { text = textDecl->stringValue(); } } if (!text.isEmpty()) { auto item = new LabelItem; item->text = text; item->font = m_defaultFont; item->pos = m_view->mapGeoToScene(e.center()); // TODO center() is too simple for concave polygons item->color = m_defaultTextColor; double textOpacity = 1.0; double shieldOpacity = 1.0; for (auto decl : m_styleResult.declarations()) { applyGenericStyle(decl, item); applyFontStyle(decl, item->font); switch (decl->property()) { case MapCSSDeclaration::TextColor: item->color = decl->colorValue(); break; case MapCSSDeclaration::TextOpacity: textOpacity = decl->doubleValue(); break; case MapCSSDeclaration::ShieldCasingColor: item->casingColor = decl->colorValue(); break; case MapCSSDeclaration::ShieldCasingWidth: item->casingWidth = decl->doubleValue(); break; case MapCSSDeclaration::ShieldColor: item->shieldColor = decl->colorValue(); break; case MapCSSDeclaration::ShieldOpacity: shieldOpacity = decl->doubleValue(); break; case MapCSSDeclaration::ShieldFrameColor: item->frameColor = decl->colorValue(); break; case MapCSSDeclaration::ShieldFrameWidth: item->frameWidth = decl->doubleValue(); break; case MapCSSDeclaration::TextPosition: if (decl->textFollowsLine() && linePath.size() > 1) { - item->angle = angleForPath(linePath); + item->angle = SceneGeometry::angleForPath(linePath); } break; default: break; } } if (item->color.isValid() && textOpacity < 1.0) { auto c = item->color; c.setAlphaF(c.alphaF() * textOpacity); item->color = c; } if (item->shieldColor.isValid() && shieldOpacity < 1.0) { auto c = item->shieldColor; c.setAlphaF(c.alphaF() * shieldOpacity); item->shieldColor = c; } addItem(sg, e, level, item); } } } QPolygonF SceneController::createPolygon(OSM::Element e) const { const auto path = e.outerPath(m_data->dataSet()); QPolygonF poly; poly.reserve(path.size()); for (auto node : path) { poly.push_back(m_view->mapGeoToScene(node->coordinate)); } return poly; } // @see https://wiki.openstreetmap.org/wiki/Relation:multipolygon QPainterPath SceneController::createPath(const OSM::Element e) const { assert(e.type() == OSM::Type::Relation); QPainterPath path; path.addPolygon(createPolygon(e)); // assemble the outer polygon, which can be represented as a set of unsorted lines here even for (const auto &mem : e.relation()->members) { const bool isInner = mem.role == QLatin1String("inner"); if (mem.type != OSM::Type::Way || !isInner) { continue; } auto wayIt = std::lower_bound(m_data->dataSet().ways.begin(), m_data->dataSet().ways.end(), mem.id); if (wayIt == m_data->dataSet().ways.end() || (*wayIt).id != mem.id) { continue; } const auto subPoly = createPolygon(OSM::Element(&(*wayIt))); QPainterPath subPath; subPath.addPolygon(subPoly); path = path.subtracted(subPath); } return path; } void SceneController::applyGenericStyle(const MapCSSDeclaration *decl, SceneGraphItem *item) const { if (decl->property() == MapCSSDeclaration::ZIndex) { item->z = decl->intValue(); } } void SceneController::applyPenStyle(const MapCSSDeclaration *decl, QPen &pen, double &opacity) const { switch (decl->property()) { case MapCSSDeclaration::Color: pen.setColor(decl->colorValue()); break; case MapCSSDeclaration::Width: pen.setWidthF(decl->doubleValue()); break; case MapCSSDeclaration::Dashes: pen.setDashPattern(decl->dashesValue()); break; case MapCSSDeclaration::LineCap: pen.setCapStyle(decl->capStyle()); break; case MapCSSDeclaration::LineJoin: pen.setJoinStyle(decl->joinStyle()); break; case MapCSSDeclaration::Opacity: opacity = decl->doubleValue(); break; default: break; } } void SceneController::applyCasingPenStyle(const MapCSSDeclaration *decl, QPen &pen, double &opacity) const { switch (decl->property()) { case MapCSSDeclaration::CasingColor: pen.setColor(decl->colorValue()); break; case MapCSSDeclaration::CasingWidth: pen.setWidthF(decl->doubleValue()); break; case MapCSSDeclaration::CasingDashes: pen.setDashPattern(decl->dashesValue()); break; case MapCSSDeclaration::CasingLineCap: pen.setCapStyle(decl->capStyle()); break; case MapCSSDeclaration::CasingLineJoin: pen.setJoinStyle(decl->joinStyle()); break; case MapCSSDeclaration::CasingOpacity: opacity = decl->doubleValue(); break; default: break; } } void SceneController::applyFontStyle(const MapCSSDeclaration *decl, QFont &font) const { switch (decl->property()) { case MapCSSDeclaration::FontFamily: font.setFamily(decl->stringValue()); break; case MapCSSDeclaration::FontSize: font.setPointSizeF(decl->doubleValue()); // TODO unit support break; case MapCSSDeclaration::FontWeight: font.setBold(decl->isBoldStyle()); break; case MapCSSDeclaration::FontStyle: font.setItalic(decl->isItalicStyle()); break; case MapCSSDeclaration::FontVariant: font.setCapitalization(decl->capitalizationStyle()); break; case MapCSSDeclaration::TextDecoration: font.setUnderline(decl->isUnderlineStyle()); break; case MapCSSDeclaration::TextTransform: font.setCapitalization(decl->capitalizationStyle()); break; default: break; } } void SceneController::initializePen(QPen &pen) const { pen.setColor(Qt::transparent); // default according to spec pen.setCapStyle(Qt::FlatCap); pen.setJoinStyle(Qt::RoundJoin); pen.setStyle(Qt::SolidLine); } void SceneController::finalizePen(QPen &pen, double opacity) const { if (pen.color().isValid() && opacity < 1.0) { auto c = pen.color(); c.setAlphaF(c.alphaF() * opacity); pen.setColor(c); } if (pen.color().alphaF() == 0.0) { pen.setStyle(Qt::NoPen); // so the renderer can skip this entirely } } void SceneController::addItem(SceneGraph &sg, OSM::Element e, int level, SceneGraphItem *item) const { item->level = level; // get the OSM layer, if set const auto layerStr = e.tagValue(QLatin1String("layer")); if (!layerStr.isEmpty()) { bool success = false; const auto layer = layerStr.toInt(&success); if (success) { // ### Ignore layer information when it matches the level // This is very wrong according to the specification, however it looks that in many places // layer and level tags aren't correctly filled, possibly a side-effect of layer pre-dating // level and layers not having been properly updated when retrofitting level information // Strictly following the MapCSS rendering order yields sub-optimal results in that case, with // relevant elements being hidden. // // Ideally we find a way to detect the presence of that problem, and only then enabling this // workaround, but until we have this, this seems to produce better results in all tests. if (level != layer * 10) { item->layer = layer; } } else { qWarning() << "Invalid layer:" << e.url() << layerStr; } } sg.addItem(item); } diff --git a/src/map/scene/scenegeometry.cpp b/src/map/scene/scenegeometry.cpp index f83aac1..81436b1 100644 --- a/src/map/scene/scenegeometry.cpp +++ b/src/map/scene/scenegeometry.cpp @@ -1,70 +1,94 @@ /* Copyright (C) 2020 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "scenegeometry.h" #include +#include #include +#include + using namespace KOSMIndoorMap; +// TODO this is a simplification, assuming equidistant point positions +// this actually needs to be done taking lengths into account +double SceneGeometry::angleForPath(const QPolygonF &path) +{ + assert(path.size() >= 2); + + QLineF line; + line.setP1(path.at(path.size() / 2 - 1)); + if (path.size() % 2 == 0) { + line.setP2(path.at(path.size() / 2)); + } else { + line.setP2(path.at(path.size() / 2 + 1)); + } + + auto a = - std::remainder(line.angle(), 360.0); + if (a < -90.0 || a > 90.0) { + a += 180.0; + } + return a; +} + /* the algorithm in here would be pretty simple (see https://en.wikipedia.org/wiki/Polygon#Centroid) * if it weren't for numeric stability. We need something that keeps sufficient precision (~7 digits) * in the range of +/- m * n² for n being the largest coordinate value, and m the polygon size. * To help with that we: * - move the polygon bbox center to 0/0. This works as we usually only look at very small areas. * - scale the value by the bbox size, to enable the use of 64bit integers for the intermediate values. * - use 64 bit integers for the intermediate values, as those contain squares of the coordinates * and thus become very large. As we don't use divisions on the intermediate values, integers work for this. */ QPointF SceneGeometry::polygonCentroid(const QPolygonF &poly) { if (poly.size() < 3) { return {}; } const auto bbox = poly.boundingRect(); const auto offset = bbox.center(); const auto scale = 1.0e6 / std::max(bbox.width(), bbox.height()); int64_t a = 0.0; int64_t cx = 0.0; int64_t cy = 0.0; for (int i = 0; i < poly.size(); ++i) { const auto p1 = poly.at(i) - offset; const int64_t x1 = p1.x() * scale; const int64_t y1 = p1.y() * scale; const auto p2 = poly.at((i + 1) % poly.size()) - offset; const int64_t x2 = p2.x() * scale; const int64_t y2 = p2.y() * scale; a += (x1 * y2) - (x2 * y1); cx += (x1 + x2) * (x1 * y2 - x2 * y1); cy += (y1 + y2) * (x1 * y2 - x2 * y1); } if (a == 0) { return {}; } cx /= 3 * a; cy /= 3 * a; return QPointF((double)cx / scale, (double)cy / scale) + offset; } diff --git a/src/map/scene/scenegeometry.h b/src/map/scene/scenegeometry.h index b7fee19..84f3b5d 100644 --- a/src/map/scene/scenegeometry.h +++ b/src/map/scene/scenegeometry.h @@ -1,37 +1,40 @@ /* Copyright (C) 2020 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef KOSMINDOORMAP_SCENEGEOMETRY_H #define KOSMINDOORMAP_SCENEGEOMETRY_H class QPointF; class QPolygonF; namespace KOSMIndoorMap { /** Geometry related functions. */ namespace SceneGeometry { /** Centroid of a polygon. * @see https://en.wikipedia.org/wiki/Polygon#Centroid */ QPointF polygonCentroid(const QPolygonF &poly); + + /** Rotation angle for a label placed alongside @p path. */ + double angleForPath(const QPolygonF &path); } } #endif // KOSMINDOORMAP_SCENEGEOMETRY_H