diff --git a/autotests/data/mapcss/parser-test.mapcss b/autotests/data/mapcss/parser-test.mapcss index d54407d..a5b4fc4 100644 --- a/autotests/data/mapcss/parser-test.mapcss +++ b/autotests/data/mapcss/parser-test.mapcss @@ -1,68 +1,75 @@ @import url("included.mapcss"); // union selector area[railway=platform], relation[type=multipolygon][railway=platform] { color: #ff550022; fill-color: #80f0e0d0; } // chained selector area[railway=platform] node[sign] { unsupportedproperty: todo; fill-color: #ff0000; opacity: 0.5; } // condition with colon-separated keys node[building:part][building:part=elevator] { opacity: 1; } // text properties * { text: ref; text-color: #ff00ff; } // line properties line { dashes: 3,5; linecap: round; linejoin: bevel; casing-width: 1; casing-color: #444444; casing-dashes: 1,1; } // zoom ranges // TODO: some of this doesn't really work yet // node|z12-13 {} node|z10 {} node|z-10 {} node|z10- {} //node|z12-13[name] {} node|z14-[name] {} // font properties * { font-family: Arial; font-size: 16; font-weight: bold; font-style: italic; text-decoration: underline; } // numeric comparison conditions * [layer>1], * [layer<2], * [layer>=3], * [layer<=4] {} // object types as tag or propery values area[indoor=area] { text-position: line; } + +// units +* { + font-size: 16pt; + width: 42px; + casing-width: 2m; +} diff --git a/src/map/assets/css/breeze-common.mapcss b/src/map/assets/css/breeze-common.mapcss index 5d05157..f74bf1f 100644 --- a/src/map/assets/css/breeze-common.mapcss +++ b/src/map/assets/css/breeze-common.mapcss @@ -1,317 +1,326 @@ /* 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 . */ /* common definitions between light and dark style */ /** Various outdoor stuff. */ relation[type=multipolygon][water], relation[type=multipolygon][waterway], relation[type=multipolygon][natural=water], area[water], area[waterway], area[natural=water] { fill-color: #103daee9; z-index: -1000; } *[leisure=park], *[landuse=farmland], *[landuse=forest], *[landuse=grass], *[landuse=village_green], *[landuse=allotments], *[landuse=cemetery], *[natural=wood] { fill-color: #1027ae60; z-index: -999; } area|z17-[amenity=parking] { text: "P"; text-color: #3daee9; font-weight: bold; } area|z17-[amenity=parking][access=private] { text: "🚫"; } *|z17-[amenity=bicycle_parking] { text: "🚲"; } /** Roads */ line[highway=motorway], line[highway=motorway_link], line[highway=trunk], line[highway=trunk_link], line[highway=primary], line[highway=primary_link], line[highway=secondary], line[highway=secondary_link], line[highway=tertiary], line[highway=tertiary_link], line[highway=unclassified], line[highway=residential], line[highway=service], line[highway=living_street], line[highway=pedestrian], line[highway=road] { casing-width: 1; z-index: -10; linecap: round; casing-linecap: round; } line[highway=motorway] { width: 9; } line[highway=trunk] { width: 7; } line[highway=primary] { width: 6.5; } line[highway=secondary] { width: 6; } line[highway=motorway_link], line[highway=tertiary] { width: 5; } line[highway=trunk_link], line[highway=primary_link], line[highway=secondary_link], line[highway=tertiary_link], line[highway=unclassified], line[highway=living_street], line[highway=pedestrian] { width: 4; } line[highway=residential], line[highway=road] { width: 3; } line[highway=service] { width: 2; } line[highway][layer<0] { casing-dashes: 0.25,0.25; casing-linecap: none; } line[highway=steps][layer<0] { casing-dashes: none; } line|z18-[highway=motorway], line|z20-[highway=motorway_link] { text-position: line; text-opacity: 0.75; text: ref; } line|z19-[highway=trunk], line|z20-[highway=trunk_link], line|z19-[highway=primary], line|z20-[highway=primary_link], line|z19-[highway=secondary], line|z20-[highway=secondary_link], line|z20-[highway=tertiary], line|z20-[highway=tertiary_link], line|z20-[highway=unclassified], line|z20-[highway=residential], line|z21-[highway=service], line|z20-[highway=living_street], line|z20-[highway=pedestrian], line|z20-[highway=road] { text-position: line; text-opacity: 0.75; text: name; } /** Railway tracks */ way[railway=rail], way[railway=light_rail], way[railway=subway], -way[railway=tram] +way[railway=tram], +way[railway=monorail] { color: #eff0f1; dashes: 2,2; width: 1.5; // meter casing-width: 3; casing-color: #31363b; linecap: none; } way[railway:traffic_mode=freight], way[railway=rail][service=yard] { casing-color: #7f8c8d; } /** Buildings */ area[building], area[building:part=yes], area[indoor=area] relation[type=multipolygon][indoor=area] relation[type=multipolygon][building], relation[type=multipolygon][building:part=yes] { z-index: -1; } area[building=roof], relation[type=multipolygon][building=roof] { fill-opacity: 0; } /** Airports */ way|z15-[aeroway=runway] { color: #eff0f1; - width: 1; + width: 2px; dashes: 4, 4; - casing-width: 20; // m TODO + casing-width: 20m; casing-color: #95a5a6; text: ref; text-color: #232629; shield-color: #fdbc4b; z-index: -10; } +way|z15-[aeroway=runway][width] +{ + casing-width: width; +} area|z15-[aeroway=helipad] { text: "H"; fill-color: #95a5a6; } way|z15-[aeroway=taxiway] { color: #fdbc4b; - width: 0.5; - casing-width: 10; // m TODO + width: 1px; + casing-width: 10m; casing-color: #95a5a6; z-index: -20; } +way|z15-[aeroway=taxiway][width] +{ + casing-width: width; +} way|z19-[aeroway=taxiway] { text-color: #fdbc4b; text-position: line; font-size: 8; text: ref; text-offset: 12; } way|z19-[aeroway=parking_position] { color: #fdbc4b; - width: 0.3; // TODO 1px + width: 1px; dashes: 8,8; text-color: #fdbc4b; text-position: line; font-size: 8; text: ref; text-offset: 12; } node|z19-[aeroway=parking_position] { text-color: #fdbc4b; font-size: 8; text: ref; } area|z16-[aeroway=terminal], relation|z16-[type=multipolygon][aeroway=terminal] { text: name; } node|z18-[aeroway=gate] { text: ref; text-color: #232629; shield-color: #fdbc4b; } /** Platforms */ area|z17-[railway=platform], area|z17-[public_transport=platform], relation|z17-[type=multipolygon][railway=platform], way|z17-[public_transport=platform] { text: name; text-color: #fcfcfc; shield-color: #1d99f3; shield-casing-color: #fcfcfc; shield-casing-width: 1; } area|z19-[railway=platform], area|z19-[public_transport=platform], relation|z19-[type=multipolygon][railway=platform], way|z19-[public_transport=platform] { font-size: 16; } area|z17-[railway=platform][ref], area|z17-[public_transport=platform][ref], relation|z17-[type=multipolygon][railway=platform][ref], way|z17-[public_transport=platform][ref] { text: ref; } area|z17-[railway=platform][local_ref], area|z17-[public_transport=platform][local_ref], relation|z17-[type=multipolygon][railway=platform][local_ref], way|z17-[public_transport=platform][local_ref] { text: local_ref; } node|z18-[public_transport=platform_section_sign] { text: platform_section_sign_value; text-color: #fcfcfc; shield-color: #1d99f3; shield-casing-color: #fcfcfc; shield-casing-width: 1; } /** Shops/etc */ *|z19- [shop], *|z19- [amenity=cafe], *|z19- [amenity=fast_food], *|z19- [amenity=restaurant], *|z19- [tourism=information] { text: name; } /** icons */ node|z18-[vending=public_transport_tickets] { text: "🎫"; } area|z18-[room=toilets], relation|z16-[type=multipolygon][room=toilets], node|z18-[amenity=toilets] { text: "🚻"; } node|z18-[amenity=atm] { text: "🏧"; } *|z18-[amenity=pharmacy] { text: "⚕"; } diff --git a/src/map/renderer/painterrenderer.cpp b/src/map/renderer/painterrenderer.cpp index 94a7d6e..fb4b008 100644 --- a/src/map/renderer/painterrenderer.cpp +++ b/src/map/renderer/painterrenderer.cpp @@ -1,215 +1,227 @@ /* 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 "painterrenderer.h" #include "view.h" #include "../scene/scenegraph.h" #include #include #include #include #include using namespace KOSMIndoorMap; PainterRenderer::PainterRenderer() = default; PainterRenderer::~PainterRenderer() = default; void PainterRenderer::setPainter(QPainter *painter) { m_painter = painter; } void PainterRenderer::render(const SceneGraph &sg, View *view) { QElapsedTimer frameTimer; frameTimer.start(); m_view = view; beginRender(); renderBackground(sg.backgroundColor()); for (const auto &layerOffsets : sg.layerOffsets()) { const auto layerBegin = sg.itemsBegin(layerOffsets); const auto layerEnd = sg.itemsEnd(layerOffsets); //qDebug() << "rendering layer" << (*layerBegin)->layer; // select elements currently in view m_renderBatch.clear(); m_renderBatch.reserve(layerOffsets.second - layerOffsets.first); const QRectF screenRect(QPointF(0, 0), QSizeF(m_view->screenWidth(), m_view->screenHeight())); for (auto it = layerBegin; it != layerEnd; ++it) { if ((*it).payload->inSceneSpace() && m_view->viewport().intersects((*it).payload->boundingRect())) { m_renderBatch.push_back((*it).payload.get()); } if ((*it).payload->inHUDSpace()) { auto bbox = (*it).payload->boundingRect(); bbox.moveCenter(m_view->mapSceneToScreen(bbox.center())); if (screenRect.intersects(bbox)) { m_renderBatch.push_back((*it).payload.get()); } } } for (auto phase : {SceneGraphItemPayload::FillPhase, SceneGraphItemPayload::CasingPhase, SceneGraphItemPayload::StrokePhase, SceneGraphItemPayload::LabelPhase}) { beginPhase(phase); for (const auto item : m_renderBatch) { if ((item->renderPhases() & phase) == 0) { continue; } if (auto i = dynamic_cast(item)) { renderPolygon(i, phase); } else if (auto i = dynamic_cast(item)) { renderMultiPolygon(i, phase); } else if (auto i = dynamic_cast(item)) { renderPolyline(i, phase); } else if (auto i = dynamic_cast(item)) { renderLabel(i); } else { qCritical() << "Unsupported scene graph item!"; } } } } endRender(); m_view = nullptr; qDebug() << "rendering took:" << frameTimer.elapsed() << "ms for" << sg.items().size() << "items on" << sg.layerOffsets().size() << "layers"; } void PainterRenderer::beginRender() { m_painter->save(); } void PainterRenderer::renderBackground(const QColor &bgColor) { m_painter->fillRect(0, 0, m_view->screenWidth(), m_view->screenHeight(), bgColor); } void PainterRenderer::beginPhase(SceneGraphItemPayload::RenderPhase phase) { switch (phase) { case SceneGraphItemPayload::NoPhase: Q_UNREACHABLE(); case SceneGraphItemPayload::FillPhase: m_painter->setPen(Qt::NoPen); m_painter->setTransform(m_view->sceneToScreenTransform()); m_painter->setClipRect(m_view->viewport()); m_painter->setRenderHint(QPainter::Antialiasing, false); break; case SceneGraphItemPayload::CasingPhase: case SceneGraphItemPayload::StrokePhase: m_painter->setBrush(Qt::NoBrush); m_painter->setTransform(m_view->sceneToScreenTransform()); m_painter->setClipRect(m_view->viewport()); m_painter->setRenderHint(QPainter::Antialiasing, true); break; case SceneGraphItemPayload::LabelPhase: m_painter->setTransform({}); m_painter->setRenderHint(QPainter::Antialiasing, true); break; } } void PainterRenderer::renderPolygon(PolygonItem *item, SceneGraphItemPayload::RenderPhase phase) { if (phase == SceneGraphItemPayload::FillPhase) { m_painter->setBrush(item->brush); m_painter->drawPolygon(item->polygon); } else { auto p = item->pen; - p.setWidthF(m_view->mapScreenDistanceToSceneDistance(item->pen.widthF())); + p.setWidthF(mapToSceneWidth(item->pen.widthF(), item->penWidthUnit)); m_painter->setPen(p); m_painter->drawPolygon(item->polygon); } } void PainterRenderer::renderMultiPolygon(MultiPolygonItem *item, SceneGraphItemPayload::RenderPhase phase) { if (phase == SceneGraphItemPayload::FillPhase) { m_painter->setBrush(item->brush); m_painter->drawPath(item->path); } else { auto p = item->pen; - p.setWidthF(m_view->mapScreenDistanceToSceneDistance(item->pen.widthF())); + p.setWidthF(mapToSceneWidth(item->pen.widthF(), item->penWidthUnit)); m_painter->setPen(p); m_painter->drawPath(item->path); } } void PainterRenderer::renderPolyline(PolylineItem *item, SceneGraphItemPayload::RenderPhase phase) { if (phase == SceneGraphItemPayload::StrokePhase) { auto p = item->pen; - p.setWidthF(m_view->mapMetersToScene(item->pen.widthF())); + p.setWidthF(mapToSceneWidth(item->pen.widthF(), item->penWidthUnit)); m_painter->setPen(p); m_painter->drawPolyline(item->path); } else { auto p = item->casingPen; - p.setWidthF(m_view->mapMetersToScene(item->pen.widthF()) + m_view->mapScreenDistanceToSceneDistance(item->casingPen.widthF())); + p.setWidthF(mapToSceneWidth(item->pen.widthF(), item->penWidthUnit) + mapToSceneWidth(item->casingPen.widthF(), item->casingPenWidthUnit)); m_painter->setPen(p); m_painter->drawPolyline(item->path); } } void PainterRenderer::renderLabel(LabelItem *item) { if (!item->hasFineBbox) { QFontMetricsF fm(item->font); item->bbox = fm.boundingRect(item->text); item->bbox.moveCenter(item->pos); item->hasFineBbox = true; } m_painter->save(); m_painter->translate(m_view->mapSceneToScreen(item->pos)); m_painter->rotate(item->angle); auto box = item->bbox; box.moveCenter({0.0, item->offset}); // draw shield // @see https://wiki.openstreetmap.org/wiki/MapCSS/0.2#Shield_properties auto w = item->casingWidth + item->frameWidth + 2.0; if (item->casingWidth > 0.0 && item->casingColor.alpha() > 0) { m_painter->fillRect(box.adjusted(-w, -w, w, w), item->casingColor); } w -= item->casingWidth; if (item->frameWidth > 0.0 && item->frameColor.alpha() > 0) { m_painter->fillRect(box.adjusted(-w, -w, w, w), item->frameColor); } w -= item->frameWidth; if (item->shieldColor.alpha() > 0) { m_painter->fillRect(box.adjusted(-w, -w, w, w), item->shieldColor); } // draw text m_painter->setPen(item->color); m_painter->setFont(item->font); m_painter->drawText(box.bottomLeft() - QPointF(0, QFontMetricsF(item->font).descent()), item->text); m_painter->restore(); } void PainterRenderer::endRender() { m_painter->restore(); } + +double PainterRenderer::mapToSceneWidth(double width, Unit unit) const +{ + switch (unit) { + case Unit::Pixel: + return m_view->mapScreenDistanceToSceneDistance(width); + case Unit::Meter: + return m_view->mapMetersToScene(width); + } + + return width; +} diff --git a/src/map/renderer/painterrenderer.h b/src/map/renderer/painterrenderer.h index 2805204..c1be1bf 100644 --- a/src/map/renderer/painterrenderer.h +++ b/src/map/renderer/painterrenderer.h @@ -1,65 +1,67 @@ /* 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_PAINTERRENDERER_H #define KOSMINDOORMAP_PAINTERRENDERER_H #include "../scene/scenegraphitem.h" #include class QPainter; namespace KOSMIndoorMap { class LabelItem; class PolygonItem; class PolylineItem; class SceneGraph; class View; /** QPainter-based renderer of a SceneGraph. * Trying to keep this somewhat backend-agnostic to possibly implement a 3D renderer in the future. */ class PainterRenderer { public: explicit PainterRenderer(); ~PainterRenderer(); void setPainter(QPainter *painter); void render(const SceneGraph &sg, View *view); private: void beginRender(); void beginPhase(SceneGraphItemPayload::RenderPhase phase); void renderBackground(const QColor &bgColor); void renderPolygon(PolygonItem *item, SceneGraphItemPayload::RenderPhase phase); void renderMultiPolygon(MultiPolygonItem *item, SceneGraphItemPayload::RenderPhase phase); void renderPolyline(PolylineItem *item, SceneGraphItemPayload::RenderPhase phase); void renderLabel(LabelItem *item); void endRender(); + double mapToSceneWidth(double width, Unit unit) const; + QPainter *m_painter = nullptr; View *m_view = nullptr; std::vector m_renderBatch; // member rather than function-local to preserve allocations }; } #endif // KOSMINDOORMAP_PAINTERRENDERER_H diff --git a/src/map/scene/scenecontroller.cpp b/src/map/scene/scenecontroller.cpp index d58a468..1775e2a 100644 --- a/src/map/scene/scenecontroller.cpp +++ b/src/map/scene/scenecontroller.cpp @@ -1,490 +1,509 @@ /* 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 #include using namespace KOSMIndoorMap; SceneController::SceneController() = default; SceneController::~SceneController() = default; void SceneController::setDataSet(const MapData *data) { m_data = data; m_layerTag = data->dataSet().tagKey("layer"); m_typeTag = data->dataSet().tagKey("type"); m_dirty = true; } void SceneController::setStyleSheet(const MapCSSStyle *styleSheet) { m_styleSheet = styleSheet; m_dirty = true; } void SceneController::setView(const View *view) { m_view = view; m_dirty = true; } void SceneController::updateScene(SceneGraph &sg) const { QElapsedTimer sgUpdateTimer; sgUpdateTimer.start(); // check if we are set up completely yet (we can't rely on a defined order with QML) if (!m_data || !m_view || !m_styleSheet) { return; } // check if the scene is dirty at all if (sg.zoomLevel() == (int)m_view->zoomLevel() && sg.currentFloorLevel() == m_view->level() && !m_dirty) { return; } sg.setZoomLevel(m_view->zoomLevel()); sg.setCurrentFloorLevel(m_view->level()); m_dirty = false; sg.beginSwap(); 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 each level, update or create scene graph elements for (auto it = beginIt; it != endIt; ++it) { for (auto e : (*it).second) { updateElement(e, (*it).first.numericLevel(), sg); } } sg.zSort(); sg.endSwap(); qDebug() << "updated scenegraph took" << sgUpdateTimer.elapsed() << "ms"; } 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; } } } 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); if (m_styleResult.hasAreaProperties()) { PolygonBaseItem *item = nullptr; std::unique_ptr baseItem; if (e.type() == OSM::Type::Relation && e.tagValue(m_typeTag) == QLatin1String("multipolygon")) { baseItem = sg.findOrCreatePayload(e, level); auto i = static_cast(baseItem.get()); if (i->path.isEmpty()) { i->path = createPath(e, m_labelPlacementPath); } else if (m_styleResult.hasLabelProperties()) { SceneGeometry::outerPolygonFromPath(i->path, m_labelPlacementPath); } item = i; } else { baseItem = sg.findOrCreatePayload(e, level); auto i = static_cast(baseItem.get()); if (i->polygon.isEmpty()) { i->polygon = createPolygon(e); } m_labelPlacementPath = i->polygon; 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); + applyPenStyle(e, decl, item->pen, lineOpacity, item->penWidthUnit); 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, std::move(baseItem)); } else if (m_styleResult.hasLineProperties()) { auto baseItem = sg.findOrCreatePayload(e, level); auto item = static_cast(baseItem.get()); if (item->path.isEmpty()) { 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); + applyPenStyle(e, decl, item->pen, lineOpacity, item->penWidthUnit); + applyCasingPenStyle(e, decl, item->casingPen, casingOpacity, item->casingPenWidthUnit); } finalizePen(item->pen, lineOpacity); finalizePen(item->casingPen, casingOpacity); m_labelPlacementPath = item->path; addItem(sg, e, level, std::move(baseItem)); } 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 baseItem = sg.findOrCreatePayload(e, level); auto item = static_cast(baseItem.get()); item->text = text; item->hasFineBbox = false; item->bbox = {}; item->font = m_defaultFont; item->color = m_defaultTextColor; if (item->pos.isNull()) { if (m_styleResult.hasAreaProperties()) { item->pos = SceneGeometry::polygonCentroid(m_labelPlacementPath); } else if (m_styleResult.hasLineProperties()) { item->pos = SceneGeometry::polylineMidPoint(m_labelPlacementPath); } if (item->pos.isNull()) { item->pos = m_view->mapGeoToScene(e.center()); // node or something failed above } } 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() && m_labelPlacementPath.size() > 1) { item->angle = SceneGeometry::polylineMidPointAngle(m_labelPlacementPath); } break; case MapCSSDeclaration::TextOffset: item->offset = decl->doubleValue(); 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, std::move(baseItem)); } } } 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, QPolygonF &outerPath) const { assert(e.type() == OSM::Type::Relation); outerPath = createPolygon(e); QPainterPath path; path.addPolygon(outerPath); // 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, SceneGraphItemPayload *item) const { if (decl->property() == MapCSSDeclaration::ZIndex) { item->z = decl->intValue(); } } -void SceneController::applyPenStyle(const MapCSSDeclaration *decl, QPen &pen, double &opacity) const +void SceneController::applyPenStyle(OSM::Element e, const MapCSSDeclaration *decl, QPen &pen, double &opacity, Unit &unit) const { switch (decl->property()) { case MapCSSDeclaration::Color: pen.setColor(decl->colorValue()); break; case MapCSSDeclaration::Width: + if (!decl->keyValue().isEmpty()) { + pen.setWidthF(e.tagValue(decl->keyValue().constData()).toDouble()); + unit = Unit::Meter; + break; + } pen.setWidthF(decl->doubleValue()); + if (decl->unit() != MapCSSDeclaration::NoUnit) { + unit = decl->unit() == MapCSSDeclaration::Meters ? Unit::Meter : Unit::Pixel; + } 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 +void SceneController::applyCasingPenStyle(OSM::Element e, const MapCSSDeclaration *decl, QPen &pen, double &opacity, Unit &unit) const { switch (decl->property()) { case MapCSSDeclaration::CasingColor: pen.setColor(decl->colorValue()); break; case MapCSSDeclaration::CasingWidth: + if (!decl->keyValue().isEmpty()) { + pen.setWidthF(e.tagValue(decl->keyValue().constData()).toDouble()); + unit = Unit::Meter; + break; + } pen.setWidthF(decl->doubleValue()); + if (decl->unit() != MapCSSDeclaration::NoUnit) { + unit = decl->unit() == MapCSSDeclaration::Meters ? Unit::Meter : Unit::Pixel; + } 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 + if (decl->unit() == MapCSSDeclaration::Pixels) { + font.setPixelSize(decl->doubleValue()); + } else { + font.setPointSizeF(decl->doubleValue()); + } 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, std::unique_ptr &&payload) const { SceneGraphItem item; item.element = e; item.level = level; item.payload = std::move(payload); // get the OSM layer, if set const auto layerStr = e.tagValue(m_layerTag); 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(std::move(item)); } diff --git a/src/map/scene/scenecontroller.h b/src/map/scene/scenecontroller.h index bd6b3b7..2a234ff 100644 --- a/src/map/scene/scenecontroller.h +++ b/src/map/scene/scenecontroller.h @@ -1,94 +1,93 @@ /* 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_SCENECONTROLLER_H #define KOSMINDOORMAP_SCENECONTROLLER_H +#include "scenegraphitem.h" +#include "../style/mapcssresult.h" + #include #include #include -#include "../style/mapcssresult.h" - class QPolygonF; class QString; namespace OSM { class Element; } namespace KOSMIndoorMap { class MapData; class MapCSSStyle; class SceneGraph; -class SceneGraphItem; -class SceneGraphItemPayload; class View; /** Creates/updates the scene graph based on a given style sheet and view. */ class SceneController { public: explicit SceneController(); ~SceneController(); void setDataSet(const MapData *data); void setStyleSheet(const MapCSSStyle *styleSheet); void setView(const View *view); /** Creates or updates @p sg based on the currently set style and view settings. * When possible, provide the scene graph of the previous run to re-use scene graph elements that didn't change. */ void updateScene(SceneGraph &sg) const; private: void updateCanvas(SceneGraph &sg) const; void updateElement(OSM::Element e, int level, SceneGraph &sg) const; QPolygonF createPolygon(OSM::Element e) const; QPainterPath createPath(OSM::Element e, QPolygonF &outerPath) const; void applyGenericStyle(const MapCSSDeclaration *decl, SceneGraphItemPayload *item) const; - void applyPenStyle(const MapCSSDeclaration *decl, QPen &pen, double &opacity) const; - void applyCasingPenStyle(const MapCSSDeclaration *decl, QPen &pen, double &opacity) const; + void applyPenStyle(OSM::Element e, const MapCSSDeclaration *decl, QPen &pen, double &opacity, Unit &unit) const; + void applyCasingPenStyle(OSM::Element e, const MapCSSDeclaration *decl, QPen &pen, double &opacity, Unit &unit) const; void applyFontStyle(const MapCSSDeclaration *decl, QFont &font) const; void initializePen(QPen &pen) const; void finalizePen(QPen &pen, double opacity) const; void addItem(SceneGraph &sg, OSM::Element e, int level, std::unique_ptr &&payload) const; const MapData *m_data = nullptr; const MapCSSStyle *m_styleSheet = nullptr; const View *m_view = nullptr; mutable MapCSSResult m_styleResult; mutable QColor m_defaultTextColor; mutable QFont m_defaultFont; mutable QPolygonF m_labelPlacementPath; OSM::TagKey m_layerTag; OSM::TagKey m_typeTag; mutable bool m_dirty = true; }; } #endif // KOSMINDOORMAP_SCENECONTROLLER_H diff --git a/src/map/scene/scenegraphitem.h b/src/map/scene/scenegraphitem.h index 0448da0..aacfac4 100644 --- a/src/map/scene/scenegraphitem.h +++ b/src/map/scene/scenegraphitem.h @@ -1,157 +1,166 @@ /* 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_SCENEGRAPHITEM_H #define KOSMINDOORMAP_SCENEGRAPHITEM_H #include #include #include #include #include #include #include #include #include namespace KOSMIndoorMap { class SceneGraphItemPayload; +/** Unit for geometry sizes. */ +enum class Unit : uint8_t { + Pixel, + Meter, +}; + /** Scene graph item description and handle for its content. * This is a minimal and cheap part that can be used allocation-free, * and it holds the expensive polymorphic parts (geometry, materials) depending on the * type of this is item. * This split allows to use this part for searching/sorting/indexing. */ class SceneGraphItem { public: /** The OSM::Element this item refers to. */ OSM::Element element; int level = 0; int layer = 0; std::unique_ptr payload; }; /** Payload base class for scene graph items. */ class SceneGraphItemPayload { public: virtual ~SceneGraphItemPayload(); /** See MapCSS spec: "Within a layer, first all fills are rendered, then all casings, then all strokes, then all icons and labels." .*/ enum RenderPhase : uint8_t { NoPhase = 0, FillPhase = 1, CasingPhase = 2, StrokePhase = 4, LabelPhase = 8, }; /** Returns in which phase this item needs to be rendered (can be multiple). */ virtual uint8_t renderPhases() const = 0; /** Bounding box of this item in scene coordinates. * Performance trumps precision here, so estimating this slightly larger rather than computing it expensively makes sense. */ virtual QRectF boundingRect() const = 0; /** Is this item drawn in scene coordinates (as oposed to HUD coordinates)? */ bool inSceneSpace() const; /** Is this item drawn in HUD coordinates (as oposed to scene coordinates)? */ bool inHUDSpace() const; int z = 0; }; /** A path/way/line item in the scenegraph. */ class PolylineItem : public SceneGraphItemPayload { public: uint8_t renderPhases() const override; QRectF boundingRect() const override; QPolygonF path; QPen pen; QPen casingPen; + Unit penWidthUnit = Unit::Meter; + Unit casingPenWidthUnit = Unit::Pixel; }; /** Base item for filled polygons. */ class PolygonBaseItem : public SceneGraphItemPayload { public: uint8_t renderPhases() const override; QBrush brush = Qt::NoBrush; QPen pen; + Unit penWidthUnit = Unit::Pixel; }; /** A single filled polygon. */ class PolygonItem : public PolygonBaseItem { public: QRectF boundingRect() const override; QPolygonF polygon; }; /** Multi-polygon item, used for polygons with "holes" in them. */ class MultiPolygonItem : public PolygonBaseItem { public: QRectF boundingRect() const override; QPainterPath path; }; /** A text or item label */ class LabelItem : public SceneGraphItemPayload { public: uint8_t renderPhases() const override; QRectF boundingRect() const override; QPointF pos; QString text; QColor color; QFont font; double casingWidth = 0.0; QColor casingColor = Qt::transparent; double frameWidth = 0.0; QColor frameColor = Qt::transparent; QColor shieldColor = Qt::transparent; mutable QRectF bbox; bool hasFineBbox = false; double angle = 0.0; double offset = 0.0; }; } #endif // KOSMINDOORMAP_SCENEGRAPHITEM_H diff --git a/src/map/style/mapcssdeclaration.cpp b/src/map/style/mapcssdeclaration.cpp index df2852c..3d5e2a3 100644 --- a/src/map/style/mapcssdeclaration.cpp +++ b/src/map/style/mapcssdeclaration.cpp @@ -1,279 +1,314 @@ /* 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 "mapcssdeclaration.h" #include #include #include using namespace KOSMIndoorMap; // keep this sorted by property name! struct { const char* name; MapCSSDeclaration::Property property; int flags; } static constexpr const property_types[] = { // only those properties have their corresonding flag set that actually trigger emission of a scene graph item // e.g. for a label we either need a text or an icon, the visual properties for those on their own would be a no-op { "casing-color", MapCSSDeclaration::CasingColor, MapCSSDeclaration::LabelProperty }, { "casing-dashes", MapCSSDeclaration::CasingDashes, MapCSSDeclaration::NoFlag }, { "casing-linecap", MapCSSDeclaration::CasingLineCap, MapCSSDeclaration::NoFlag }, { "casing-linejoin", MapCSSDeclaration::CasingLineJoin, MapCSSDeclaration::NoFlag }, { "casing-opacity", MapCSSDeclaration::CasingOpacity, MapCSSDeclaration::NoFlag }, { "casing-width", MapCSSDeclaration::CasingWidth, MapCSSDeclaration::LineProperty }, { "color", MapCSSDeclaration::Color, MapCSSDeclaration::LineProperty }, { "dashes", MapCSSDeclaration::Dashes, MapCSSDeclaration::NoFlag }, { "fill-color", MapCSSDeclaration::FillColor, MapCSSDeclaration::AreaProperty | MapCSSDeclaration::CanvasProperty }, // TODO this also applies to lines { "fill-image", MapCSSDeclaration::FillImage, MapCSSDeclaration::AreaProperty | MapCSSDeclaration::CanvasProperty }, { "fill-opacity", MapCSSDeclaration::FillOpacity, MapCSSDeclaration::AreaProperty }, { "font-family", MapCSSDeclaration::FontFamily, MapCSSDeclaration::NoFlag }, { "font-size", MapCSSDeclaration::FontSize, MapCSSDeclaration::NoFlag }, { "font-style", MapCSSDeclaration::FontStyle, MapCSSDeclaration::NoFlag }, { "font-variant", MapCSSDeclaration::FontVariant, MapCSSDeclaration::NoFlag }, { "font-weight", MapCSSDeclaration::FontWeight, MapCSSDeclaration::NoFlag }, { "icon-height", MapCSSDeclaration::IconWidth, MapCSSDeclaration::NoFlag }, { "icon-image", MapCSSDeclaration::IconImage, MapCSSDeclaration::LabelProperty }, { "icon-opacity", MapCSSDeclaration::IconOpacity, MapCSSDeclaration::NoFlag }, { "icon-width", MapCSSDeclaration::IconWidth, MapCSSDeclaration::NoFlag }, { "image", MapCSSDeclaration::Image, MapCSSDeclaration::LineProperty }, { "linecap", MapCSSDeclaration::LineCap, MapCSSDeclaration::NoFlag }, { "linejoin", MapCSSDeclaration::LineJoin, MapCSSDeclaration::NoFlag }, { "max-width", MapCSSDeclaration::MaxWidth, MapCSSDeclaration::NoFlag }, { "opacity", MapCSSDeclaration::Opacity, MapCSSDeclaration::NoFlag }, { "shield-casing-color", MapCSSDeclaration::ShieldCasingColor, MapCSSDeclaration::LabelProperty }, { "shield-casing-width", MapCSSDeclaration::ShieldCasingWidth, MapCSSDeclaration::NoFlag }, { "shield-color", MapCSSDeclaration::ShieldColor, MapCSSDeclaration::LabelProperty }, { "shield-frame-color", MapCSSDeclaration::ShieldFrameColor, MapCSSDeclaration::LabelProperty }, { "shield-frame-width", MapCSSDeclaration::ShieldFrameWidth, MapCSSDeclaration::NoFlag }, { "shield-image", MapCSSDeclaration::ShieldImage, MapCSSDeclaration::LabelProperty }, { "shield-opacity", MapCSSDeclaration::ShieldOpacity, MapCSSDeclaration::NoFlag }, { "shield-shape", MapCSSDeclaration::ShieldShape, MapCSSDeclaration::NoFlag }, { "shield-text", MapCSSDeclaration::ShieldText, MapCSSDeclaration::LabelProperty }, { "text", MapCSSDeclaration::Text, MapCSSDeclaration::LabelProperty }, { "text-color", MapCSSDeclaration::TextColor, MapCSSDeclaration::CanvasProperty }, { "text-decoration", MapCSSDeclaration::TextDecoration, MapCSSDeclaration::NoFlag }, { "text-halo-color", MapCSSDeclaration::TextHaloColor, MapCSSDeclaration::NoFlag }, { "text-halo-radius", MapCSSDeclaration::TextHaloRadius, MapCSSDeclaration::NoFlag }, { "text-offset", MapCSSDeclaration::TextOffset, MapCSSDeclaration::NoFlag }, { "text-opacity", MapCSSDeclaration::TextOpacity, MapCSSDeclaration::NoFlag }, { "text-position", MapCSSDeclaration::TextPosition, MapCSSDeclaration::NoFlag }, { "text-transform", MapCSSDeclaration::TextTransform, MapCSSDeclaration::NoFlag }, { "width", MapCSSDeclaration::Width, MapCSSDeclaration::LineProperty }, { "z-index", MapCSSDeclaration::ZIndex, MapCSSDeclaration::NoFlag }, }; struct { const char *name; Qt::PenCapStyle capStyle; } static constexpr const capstyle_map[] = { { "none", Qt::FlatCap }, { "round", Qt::RoundCap }, { "square", Qt::SquareCap }, }; struct { const char *name; Qt::PenJoinStyle joinStyle; } static constexpr const joinstyle_map[] = { { "bevel", Qt::BevelJoin }, { "miter", Qt::MiterJoin }, { "round", Qt::RoundJoin }, }; struct { const char *name; QFont::Capitalization capitalizationStyle; } static constexpr const capitalizationstyle_map[] = { { "capitalize", QFont::Capitalize }, { "lowercase", QFont::AllLowercase }, { "none", QFont::MixedCase }, { "normal", QFont::MixedCase }, { "small-caps", QFont::SmallCaps }, { "uppercase", QFont::AllUppercase }, }; +struct { + const char *name; + MapCSSDeclaration::Unit unit; +} static constexpr const unit_map[] = { + { "m", MapCSSDeclaration::Meters }, + { "pt", MapCSSDeclaration::Point }, + { "px", MapCSSDeclaration::Pixels }, +}; + MapCSSDeclaration::MapCSSDeclaration() = default; MapCSSDeclaration::~MapCSSDeclaration() = default; MapCSSDeclaration::Property MapCSSDeclaration::property() const { return m_property; } int MapCSSDeclaration::propertyFlags() const { return m_flags; } int MapCSSDeclaration::intValue() const { return m_doubleValue; } double MapCSSDeclaration::doubleValue() const { return m_doubleValue; } QString MapCSSDeclaration::stringValue() const { return m_stringValue; } QColor MapCSSDeclaration::colorValue() const { return m_colorValue; } QByteArray MapCSSDeclaration::keyValue() const { return m_identValue; } QVector MapCSSDeclaration::dashesValue() const { return m_dashValue; } void MapCSSDeclaration::setDoubleValue(double val) { m_doubleValue = val; } void MapCSSDeclaration::setPropertyName(const char *name, std::size_t len) { const auto it = std::lower_bound(std::begin(property_types), std::end(property_types), name, [len](const auto &lhs, const char *rhs) { const auto lhsLen = std::strlen(lhs.name); const auto cmp = std::strncmp(lhs.name, rhs, std::min(lhsLen, len)); return cmp < 0 || (cmp == 0 && lhsLen < len); }); if (it == std::end(property_types) || std::strncmp((*it).name, name, std::max(len, std::strlen((*it).name))) != 0) { qWarning() << "Unknown property declaration:" << QByteArray::fromRawData(name, len); m_property = Unknown; return; } m_property = (*it).property; m_flags = (*it).flags; } void MapCSSDeclaration::setIdentifierValue(const char *val, int len) { m_identValue = QByteArray(val, len); } void MapCSSDeclaration::setStringValue(char *str) { m_stringValue = QString::fromUtf8(str); free(str); } void MapCSSDeclaration::setColorRgba(uint32_t argb) { m_colorValue = QColor::fromRgba(argb); //qDebug() << m_colorValue << argb; } void MapCSSDeclaration::setDashesValue(const QVector &dashes) { m_dashValue = dashes; } Qt::PenCapStyle MapCSSDeclaration::capStyle() const { for (const auto &c : capstyle_map) { if (std::strcmp(c.name, m_identValue.constData()) == 0) { return c.capStyle; } } qDebug() << "unknown line cap style:" << m_identValue; return Qt::FlatCap; } Qt::PenJoinStyle MapCSSDeclaration::joinStyle() const { for (const auto &j : joinstyle_map) { if (std::strcmp(j.name, m_identValue.constData()) == 0) { return j.joinStyle; } } return Qt::RoundJoin; } QFont::Capitalization MapCSSDeclaration::capitalizationStyle() const { for (const auto &c : capitalizationstyle_map) { if (std::strcmp(c.name, m_identValue.constData()) == 0) { return c.capitalizationStyle; } } return QFont::MixedCase; } bool MapCSSDeclaration::isBoldStyle() const { return m_identValue == "bold"; } bool MapCSSDeclaration::isItalicStyle() const { return m_identValue == "italic"; } bool MapCSSDeclaration::isUnderlineStyle() const { return m_identValue == "underline"; } bool MapCSSDeclaration::textFollowsLine() const { return m_identValue == "line"; } +MapCSSDeclaration::Unit MapCSSDeclaration::unit() const +{ + return m_unit; +} + +void MapCSSDeclaration::setUnit(const char *val, int len) +{ + for (const auto &u : unit_map) { + if (std::strncmp(u.name, val, std::max(std::strlen(u.name), len)) == 0) { + m_unit = u.unit; + return; + } + } + qWarning() << "unknown unit:" << QByteArray(val, len); + m_unit = NoUnit; +} + void MapCSSDeclaration::compile(const OSM::DataSet &dataSet) { - // TODO + Q_UNUSED(dataSet); + // TODO resolve tag key if m_identValue is one } void MapCSSDeclaration::write(QIODevice *out) const { out->write(" "); for (const auto &p : property_types) { if (p.property == m_property) { out->write(p.name); break; } } out->write(": "); if (!std::isnan(m_doubleValue)) { out->write(QByteArray::number(m_doubleValue)); } else if (m_colorValue.isValid()) { out->write(m_colorValue.name(QColor::HexArgb).toUtf8()); } else if (!m_dashValue.isEmpty()) { for (const auto &d : m_dashValue) { out->write(QByteArray::number(d)); out->write(", "); } } else { out->write(m_identValue); } + + for (const auto &u : unit_map) { + if (u.unit == m_unit) { + out->write(u.name); + break; + } + } + out->write(";\n"); } diff --git a/src/map/style/mapcssdeclaration.h b/src/map/style/mapcssdeclaration.h index 0295350..5679179 100644 --- a/src/map/style/mapcssdeclaration.h +++ b/src/map/style/mapcssdeclaration.h @@ -1,172 +1,183 @@ /* 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_MAPCSSDECLARATION_H #define KOSMINDOORMAP_MAPCSSDECLARATION_H #include #include #include #include #include namespace OSM { class DataSet; } class QIODevice; namespace KOSMIndoorMap { class MapCSSParser; } int yyparse(KOSMIndoorMap::MapCSSParser*, void*); namespace KOSMIndoorMap { /** Property/value declaration of a MapCSS rule. * @see https://wiki.openstreetmap.org/wiki/MapCSS/0.2#Vocabulary */ class MapCSSDeclaration { public: explicit MapCSSDeclaration(); ~MapCSSDeclaration(); /** The property set by this declaration. */ enum Property { Unknown, // general properites ZIndex, /// z-order // line properties Width, /// line width Color, /// line color Opacity, /// line opacity Dashes, /// line dash pattern Image, /// fill image for the line LineCap, /// line end cap style: none (default), round, square LineJoin, /// line join style: round (default), miter, bevel // line casing properties CasingWidth, /// line casing width CasingColor, /// line casing color CasingOpacity, /// line casing opacity CasingDashes, /// line casing dash pattern CasingLineCap, /// line casing end cap CasingLineJoin, /// line casing join style // missing here: extrude properites // polygon (and canvas) properties FillColor, /// area fill color FillOpacity, /// area fill opacity FillImage, /// image to fill the area with // icon properties IconImage, /// URL to the icon image IconWidth, /// icon width IconHeight, /// icon height IconOpacity, /// icon opacity // label properties FontFamily, /// font name FontSize, /// font size FontWeight, /// font weight: bold or normal (default) FontStyle, /// italic or normal (default) FontVariant, /// small-caps or normal (default) TextDecoration, /// none (default) or underline TextTransform, /// none (default), uppercase, lowercase or capitalize TextColor, /// text color used for the label TextOpacity, /// text opacity TextPosition, /// @p line or @p center TextOffset, /// vertical offset from the center of the way or point MaxWidth, /// maximum width before wrapping Text, /// label content TextHaloColor, /// text halo color TextHaloRadius, /// text halo radius // shield properites (casing > frame > shield > text) ShieldColor, /// shield color ShieldOpacity, /// shield opacity ShieldFrameColor, /// shield frame color ShieldFrameWidth, /// shield frame width (0 to disable) ShieldCasingColor, /// schield casing color ShieldCasingWidth, /// shield casing width ShieldText, /// text to render on the shield ShieldImage, /// background image of the shield ShieldShape, /// @p rounded or @p rectangular }; Property property() const; /** The type of property. Helps to determine which kind of geometry we need to emit for a rule. */ enum PropertyFlag { NoFlag = 0, AreaProperty = 1, LineProperty = 2, LabelProperty = 4, CanvasProperty = 8, }; int propertyFlags() const; /** Numeric value for this property. */ int intValue() const; double doubleValue() const; /** Quoted string value. */ QString stringValue() const; /** Color value for this property. */ QColor colorValue() const; /** Tag key name value. */ QByteArray keyValue() const; /** Line dashes. */ QVector dashesValue() const; Qt::PenCapStyle capStyle() const; Qt::PenJoinStyle joinStyle() const; QFont::Capitalization capitalizationStyle() const; bool isBoldStyle() const; bool isItalicStyle() const; bool isUnderlineStyle() const; bool textFollowsLine() const; + /** Unit type for numeric value. */ + enum Unit { + NoUnit, + Pixels, + Point, + Meters, + }; + Unit unit() const; + void compile(const OSM::DataSet &dataSet); void write(QIODevice *out) const; private: friend int ::yyparse(KOSMIndoorMap::MapCSSParser*, void*); /** @internal, for use by the parser. */ void setPropertyName(const char *name, std::size_t len); void setIdentifierValue(const char *val, int len); void setDoubleValue(double val); void setStringValue(char *str); void setColorRgba(uint32_t argb); void setDashesValue(const QVector &dashes); + void setUnit(const char *val, int len); Property m_property = Unknown; int m_flags = NoFlag; // ### merge all of this into a QVariant? QByteArray m_identValue; QColor m_colorValue; double m_doubleValue = NAN; QVector m_dashValue; QString m_stringValue; + Unit m_unit = NoUnit; }; } #endif // KOSMINDOORMAP_MAPCSSDECLARATION_H diff --git a/src/map/style/mapcssparser.y b/src/map/style/mapcssparser.y index ab206e3..9735c3f 100644 --- a/src/map/style/mapcssparser.y +++ b/src/map/style/mapcssparser.y @@ -1,254 +1,255 @@ %{ /* 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 "mapcssparser_p.h" #include "mapcssscanner.h" #include "style/mapcssparser.h" #include "style/mapcssrule.h" #include "style/mapcssselector.h" #include "style/mapcssstyle.h" #include void yyerror(YYLTYPE *loc, KOSMIndoorMap::MapCSSParser *parser, yyscan_t scanner, char const* msg) { (void)scanner; printf("PARSER ERROR: %s at %s:%d:%d\n", msg, qPrintable(parser->fileName()), loc->first_line, loc->first_column); } using namespace KOSMIndoorMap; %} %code requires { #include "style/mapcsscondition.h" #include "style/mapcssselector.h" namespace KOSMIndoorMap { class MapCSSDeclaration; class MapCSSParser; class MapCSSRule; class MapCSSStyle; struct StringRef { const char *str; int len; }; struct ZoomRange { int low; int high; }; } #ifndef YY_TYPEDEF_YY_SCANNER_T #define YY_TYPEDEF_YY_SCANNER_T typedef void* yyscan_t; #endif using namespace KOSMIndoorMap; } %define api.pure %define parse.error verbose %locations %lex-param { yyscan_t scanner } %parse-param { KOSMIndoorMap::MapCSSParser *parser } %parse-param { yyscan_t scanner } %union { uint32_t uintVal; double doubleVal; ZoomRange zoomRange; char *str; StringRef strRef; MapCSSRule *rule; MapCSSSelector *selector; MapCSSBasicSelector *basicSelector; MapCSSCondition *condition; MapCSSConditionHolder *conditionHolder; MapCSSCondition::Operator binaryOp; MapCSSDeclaration *declaration; } %token T_LBRACKET %token T_RBRACKET %token T_LBRACE %token T_RBRACE %token T_LPAREN %token T_RPAREN %token T_COLON %token T_SEMICOLON %token T_COMMA %token T_SPACE %token T_DASH %token T_STAR %token T_ZOOM %token T_UNARY_OP %token T_BINARY_OP %token T_KEYWORD_IMPORT %token T_KEYWORD_URL %token T_IDENT %token T_HEX_COLOR %token T_STRING %token T_DOUBLE %type Rule %type Selectors %type Selector %type BasicSelector %type Tests %type Test %type ZoomRange %type Condition %type Key %type Declarations %type Declaration %type PropertyName %type PropertyValue %destructor { free($$); } %destructor { delete $$; } %destructor { delete $$; } %destructor { delete $$; } %destructor { delete $$; } %destructor { delete $$; } %verbose %% // see https://wiki.openstreetmap.org/wiki/MapCSS/0.2/BNF Ruleset: Rule | Ruleset Rule ; Rule: Selectors T_LBRACE Declarations T_RBRACE { $3->setSelector($1); parser->addRule($3); $$ = nullptr; } | Import { $$ = nullptr; } ; // TODO propagate errors Import: T_KEYWORD_IMPORT T_KEYWORD_URL T_LPAREN T_STRING T_RPAREN T_SEMICOLON { parser->addImport($4); } | T_KEYWORD_IMPORT T_KEYWORD_URL T_LPAREN T_STRING T_RPAREN T_IDENT T_SEMICOLON { parser->addImport($4); } ; Selectors: Selector { $$ = $1; } | Selectors T_COMMA Selector { if (auto u = dynamic_cast($1)) { u->selectors.push_back(std::unique_ptr($3)); $$ = $1; } else { auto s = new MapCSSUnionSelector; s->selectors.push_back(std::unique_ptr($1)); s->selectors.push_back(std::unique_ptr($3)); $$ = s; }} ; Selector: BasicSelector { $$ = $1; } | Selector BasicSelector { if (auto chain = dynamic_cast($1)) { chain->selectors.push_back(std::unique_ptr($2)); $$ = $1; } else { auto s = new MapCSSChainedSelector; s->selectors.push_back(std::unique_ptr(static_cast($1))); s->selectors.push_back(std::unique_ptr($2)); $$ = s; }} ; // TODO incomplete: missing class BasicSelector: T_IDENT ZoomRange Tests { $$ = new MapCSSBasicSelector; $$->setObjectType($1.str, $1.len); $$->setZoomRange($2.low, $2.high); $$->setConditions($3); } | T_STAR ZoomRange Tests { $$ = new MapCSSBasicSelector; $$->objectType = MapCSSBasicSelector::Any; $$->setZoomRange($2.low, $2.high); $$->setConditions($3); } ; ZoomRange: %empty { $$.low = 0; $$.high = 0; } | T_ZOOM T_DOUBLE[Low] T_DASH T_DOUBLE[High] { $$.low = $Low; $$.high = $High; } | T_ZOOM T_DOUBLE[Low] T_DASH { $$.low = $Low; $$.high = 0; } | T_ZOOM T_DOUBLE { $$.low = $2; $$.high = $2; } | T_ZOOM T_DASH T_DOUBLE[High] { $$.low = 0; $$.high = $High; } Tests: %empty { $$ = nullptr; } | Tests Test { if ($1) { $1->addCondition($2); $$ = $1; } else { auto holder = new MapCSSConditionHolder; holder->addCondition($2); $$ = holder; }} ; Test: T_LBRACKET Condition T_RBRACKET { $$ = $2; }; // TODO incomplete: unary ops, quoted names, regexps Condition: Key T_BINARY_OP T_IDENT { $$ = new MapCSSCondition; $$->setKey($1.str, $1.len); $$->setOperation($2); $$->setValue($3.str, $3.len); } | Key T_BINARY_OP T_DOUBLE { $$ = new MapCSSCondition; $$->setKey($1.str, $1.len); $$->setOperation($2); $$->setValue($3); } | Key { $$ = new MapCSSCondition; $$->setKey($1.str, $1.len); } ; Key: T_IDENT { $$ = $1; } | Key T_COLON T_IDENT { $$.str = $1.str; $$.len = $3.str - $1.str + $3.len; } ; Declarations: %empty { $$ = new MapCSSRule; } | Declarations Declaration { $$ = $1; $$->addDeclaration($2); } ; Declaration: PropertyName T_COLON PropertyValue T_SEMICOLON { $$ = $3; $$->setPropertyName($1.str, $1.len); } ; PropertyName: T_IDENT { $$ = $1; } | T_IDENT T_DASH PropertyName { $$.str = $1.str; $$.len = $3.str - $1.str + $3.len; } ; // TODO incomplete: missing size, url, eval, color is simplified PropertyValue: T_IDENT { $$ = new MapCSSDeclaration; $$->setIdentifierValue($1.str, $1.len); } | T_HEX_COLOR { $$ = new MapCSSDeclaration; $$->setColorRgba($1); } +| T_DOUBLE T_IDENT { $$ = new MapCSSDeclaration; $$->setDoubleValue($1); $$->setUnit($2.str, $2.len); } | T_DOUBLE { $$ = new MapCSSDeclaration; $$->setDoubleValue($1); } | T_DOUBLE T_COMMA T_DOUBLE { $$ = new MapCSSDeclaration; $$->setDashesValue({$1, $3}); } // generalize to n dash distances | T_STRING { $$ = new MapCSSDeclaration; $$->setStringValue($1); } ; %%