diff --git a/src/map-quick/osmelement.cpp b/src/map-quick/osmelement.cpp index d39934a..4a1be56 100644 --- a/src/map-quick/osmelement.cpp +++ b/src/map-quick/osmelement.cpp @@ -1,65 +1,63 @@ /* 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 "osmelement.h" using namespace KOSMIndoorMap; OSMElement::OSMElement() = default; OSMElement::OSMElement(OSM::Element e) : m_element(e) { } OSMElement::~OSMElement() = default; bool OSMElement::isNull() const { return m_element.type() == OSM::Type::Null; } QString OSMElement::name() const { - // TODO read localized value - return m_element.tagValue("name"); + return m_element.tagValue("name", QLocale()); } // TODO generalize for *:wikipedia tags QUrl OSMElement::wikipediaUrl() const { - // TODO read localized value - const auto wp = m_element.tagValue("wikipedia"); + const auto wp = m_element.tagValue("wikipedia", QLocale()); if (wp.isEmpty()) { return {}; } const auto idx = wp.indexOf(QLatin1Char(':')); if (idx < 0) { return {}; } QUrl url; url.setScheme(QStringLiteral("https")); url.setHost(wp.leftRef(idx) + QLatin1String(".wikipedia.org")); url.setPath(QLatin1String("/wiki/") + wp.midRef(idx + 1)); return url; } QString OSMElement::tagValue(const QString &key) const { return m_element.tagValue(key.toUtf8().constData()); } diff --git a/src/map/scene/scenecontroller.cpp b/src/map/scene/scenecontroller.cpp index 1775e2a..8640b7a 100644 --- a/src/map/scene/scenecontroller.cpp +++ b/src/map/scene/scenecontroller.cpp @@ -1,509 +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 "../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(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(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()); + text = e.tagValue(textDecl->keyValue().constData(), QLocale()); } 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(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(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: 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/osm/datatypes.h b/src/osm/datatypes.h index 9d20cca..b38a4b6 100644 --- a/src/osm/datatypes.h +++ b/src/osm/datatypes.h @@ -1,363 +1,395 @@ /* 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 OSM_DATATYPES_H #define OSM_DATATYPES_H #include +#include #include #include #include #include namespace OSM { class DataSet; /** OSM element identifier. */ typedef int64_t Id; /** Coordinate, stored as 1e7 * degree to avoid floating point precision issues, * and offset to unsigned values to make the z-order curve work. * Can be in an invalid state with coordinates out of range, see isValid(). * @see https://en.wikipedia.org/wiki/Z-order_curve for the z-order curve stuff */ class Coordinate { public: Coordinate() = default; explicit constexpr Coordinate(double lat, double lon) : latitude((lat + 90.0) * 10'000'000) , longitude((lon + 180.0) * 10'000'000) {} explicit constexpr Coordinate(uint32_t lat, uint32_t lon) : latitude(lat) , longitude(lon) {} /** Create a coordinate from a z-order curve index. */ explicit constexpr Coordinate(uint64_t z) : latitude(0) , longitude(0) { for (int i = 0; i < 32; ++i) { latitude += (z & (1ull << (i * 2))) >> i; longitude += (z & (1ull << (1 + i * 2))) >> (i + 1); } } constexpr inline bool isValid() const { return latitude != std::numeric_limits::max() && longitude != std::numeric_limits::max(); } constexpr inline bool operator==(Coordinate other) const { return latitude == other.latitude && longitude == other.longitude; } /** Z-order curve value for this coordinate. */ constexpr inline uint64_t z() const { uint64_t z = 0; for (int i = 0; i < 32; ++i) { z += ((uint64_t)latitude & (1 << i)) << i; z += ((uint64_t)longitude & (1 << i)) << (i + 1); } return z; } constexpr inline double latF() const { return (latitude / 10'000'000.0) - 90.0; } constexpr inline double lonF() const { return (longitude / 10'000'000.0) - 180.0; } uint32_t latitude = std::numeric_limits::max(); uint32_t longitude = std::numeric_limits::max(); }; /** Bounding box, ie. a pair of coordinates. */ class BoundingBox { public: constexpr BoundingBox() = default; constexpr inline BoundingBox(Coordinate c1, Coordinate c2) : min(c1) , max(c2) {} constexpr inline bool isValid() const { return min.isValid() && max.isValid(); } constexpr inline bool operator==(BoundingBox other) const { return min == other.min && max == other.max; } constexpr inline uint32_t width() const { return max.longitude - min.longitude; } constexpr inline uint32_t height() const { return max.latitude - min.latitude; } constexpr inline double widthF() const { return width() / 10'000'000.0; } constexpr inline double heightF() const { return height() / 10'000'000.0; } constexpr inline Coordinate center() const { return Coordinate(min.latitude + height() / 2, min.longitude + width() / 2); } Coordinate min; Coordinate max; }; constexpr inline BoundingBox unite(BoundingBox bbox1, BoundingBox bbox2) { if (!bbox1.isValid()) { return bbox2; } if (!bbox2.isValid()) { return bbox1; } BoundingBox ret; ret.min.latitude = std::min(bbox1.min.latitude, bbox2.min.latitude); ret.min.longitude = std::min(bbox1.min.longitude, bbox2.min.longitude); ret.max.latitude = std::max(bbox1.max.latitude, bbox2.max.latitude); ret.max.longitude = std::max(bbox1.max.longitude, bbox2.max.longitude); return ret; } constexpr inline bool intersects(BoundingBox bbox1, BoundingBox bbox2) { return !(bbox2.min.latitude > bbox1.max.latitude || bbox2.max.latitude < bbox1.min.latitude || bbox2.min.longitude > bbox1.max.longitude || bbox2.max.longitude < bbox1.min.longitude); } constexpr inline bool contains(BoundingBox bbox, Coordinate coord) { return bbox.min.latitude <= coord.latitude && bbox.max.latitude >= coord.latitude && bbox.min.longitude <= coord.longitude && bbox.max.longitude >= coord.longitude; } constexpr inline uint32_t latitudeDistance(BoundingBox bbox1, BoundingBox bbox2) { return bbox1.max.latitude < bbox2.min.latitude ? bbox2.min.latitude - bbox1.max.latitude : bbox1.min.latitude - bbox2.max.latitude; } constexpr inline uint32_t longitudeDifference(BoundingBox bbox1, BoundingBox bbox2) { return bbox1.max.longitude < bbox2.min.longitude ? bbox2.min.longitude - bbox1.max.longitude : bbox1.min.longitude - bbox2.max.longitude; } /** A key of an OSM tag. * See DataSet::tagKey(). */ class TagKey { public: constexpr inline TagKey() = default; constexpr inline const char* name() const { return key; } constexpr inline bool isNull() const { return !key; } // yes, pointer compare is enough here inline constexpr bool operator<(TagKey other) const { return key < other.key; } inline constexpr bool operator==(TagKey other) const { return key == other.key; } inline constexpr bool operator!=(TagKey other) const { return key != other.key; } private: friend class DataSet; explicit constexpr inline TagKey(const char *keyData) : key(keyData) {} const char* key = nullptr; }; /** An OSM element tag. */ class Tag { public: inline constexpr bool operator<(const Tag &other) const { return key < other.key; } TagKey key; QString value; }; /** An OSM node. */ class Node { public: constexpr inline bool operator<(const Node &other) const { return id < other.id; } QString url() const; Id id; Coordinate coordinate; std::vector tags; }; /** An OSM way. */ class Way { public: constexpr inline bool operator<(const Way &other) const { return id < other.id; } bool isClosed() const; QString url() const; Id id; mutable BoundingBox bbox; std::vector nodes; std::vector tags; }; /** Element type. */ enum class Type : uint8_t { Null, Node, Way, Relation }; /** A member in a relation. */ // TODO this has 7 byte padding, can we make this more efficient? class Member { public: Id id; QString role; Type type; }; /** An OSM relation. */ class Relation { public: constexpr inline bool operator<(const Relation &other) const { return id < other.id; } QString url() const; Id id; mutable BoundingBox bbox; std::vector members; std::vector tags; }; /** A set of nodes, ways and relations. */ class DataSet { public: explicit DataSet(); DataSet(const DataSet&) = delete; DataSet(DataSet &&other); ~DataSet(); DataSet& operator=(const DataSet&) = delete; DataSet& operator=(DataSet &&); void addNode(Node &&node); void addWay(Way &&way); void addRelation(Relation &&rel); /** Look up a tag key for the given tag name, if it exists. * If no key exists, an empty/invalid/null key is returned. * Use this for tag lookup, not for creating/adding tags. */ TagKey tagKey(const char *keyName) const; enum TagKeyMemory { TagKeyIsPersistent, TagKeyIsTransient }; /** Create a tag key for the given tag name. If none exist yet a new one is created. * Use this for creating tags, not for lookup, prefer tagKey() for that. * @param keyMemOpt specifies whether @p keyName is persisent for the lifetime of this * instance and thus can be used without requiring a copy. If the memory is transient * the string is copied if needed, and released in the DataSet destructor. */ TagKey makeTagKey(const char *keyName, TagKeyMemory keyMemOpt = TagKeyIsTransient); std::vector nodes; std::vector ways; std::vector relations; private: std::vector m_tagKeyRegistry; std::vector m_stringPool; }; /** Returns the tag value for @p key of @p elem. */ template inline QString tagValue(const Elem& elem, TagKey key) { const auto it = std::lower_bound(elem.tags.begin(), elem.tags.end(), key, [](const auto &lhs, const auto &rhs) { return lhs.key < rhs; }); if (it != elem.tags.end() && (*it).key == key) { return (*it).value; } return {}; } /** Returns the tag value for key name @p keyName of @p elem. * @warning This is slow due to doing a linear search and string comparissons. * Where possible avoid this in favor of tagValue(). */ template inline QString tagValue(const Elem& elem, const char *keyName) { const auto it = std::find_if(elem.tags.begin(), elem.tags.end(), [keyName](const auto &tag) { return std::strcmp(tag.key.name(), keyName) == 0; }); if (it != elem.tags.end()) { return (*it).value; } return {}; } +/** Returns the localized version of the tag value for key name @p keyName of @p elem. + * @warning This is slow due to doing a linear search and string comparissons. + */ +template +inline QString tagValue(const Elem& elem, const char *keyName, const QLocale &locale) +{ + QByteArray key(keyName); + key.push_back(':'); + const auto baseLen = key.size(); + for (const auto &lang : locale.uiLanguages()) { + key.resize(baseLen); + key.append(lang.toUtf8()); + const auto it = std::find_if(elem.tags.begin(), elem.tags.end(), [key](const auto &tag) { return std::strcmp(tag.key.name(), key.constData()) == 0; }); + if (it != elem.tags.end()) { + return (*it).value; + } + + const auto idx = lang.indexOf(QLatin1Char('-')); + if (idx > 0) { + key.resize(baseLen); + key.append(lang.leftRef(idx).toUtf8()); + const auto it = std::find_if(elem.tags.begin(), elem.tags.end(), [key](const auto &tag) { return std::strcmp(tag.key.name(), key.constData()) == 0; }); + if (it != elem.tags.end()) { + return (*it).value; + } + } + } + + return tagValue(elem, keyName); +} + /** Inserts a new tag, or replaces an existing one with the same key. */ template inline void setTag(Elem &elem, Tag &&tag) { const auto it = std::lower_bound(elem.tags.begin(), elem.tags.end(), tag); if (it == elem.tags.end() || (*it).key != tag.key) { elem.tags.insert(it, std::move(tag)); } else { (*it) = std::move(tag); } } /** Inserts a new tag, or updates an existing one. */ template inline void setTagValue(Elem &elem, TagKey key, const QString &value) { Tag tag{ key, value }; setTag(elem, std::move(tag)); } template inline bool operator<(const Elem &elem, Id id) { return elem.id < id; } } QDebug operator<<(QDebug debug, OSM::Coordinate coord); QDebug operator<<(QDebug debug, OSM::BoundingBox bbox); #endif // OSM_DATATYPES_H diff --git a/src/osm/element.cpp b/src/osm/element.cpp index d04f9bb..ebb7b5a 100644 --- a/src/osm/element.cpp +++ b/src/osm/element.cpp @@ -1,263 +1,279 @@ /* 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 "element.h" using namespace OSM; Id Element::id() const { switch (type()) { case Type::Null: return {}; case Type::Node: return node()->id; case Type::Way: return way()->id; case Type::Relation: return relation()->id; } return {}; } Coordinate Element::center() const { switch (type()) { case Type::Null: return {}; case Type::Node: return node()->coordinate; case Type::Way: return way()->bbox.center(); case Type::Relation: return relation()->bbox.center(); } return {}; } BoundingBox Element::boundingBox() const { switch (type()) { case Type::Null: return {}; case Type::Node: return BoundingBox(node()->coordinate, node()->coordinate); case Type::Way: return way()->bbox; case Type::Relation: return relation()->bbox; } return {}; } QString Element::tagValue(TagKey key) const { switch (type()) { case Type::Null: return {}; case Type::Node: return OSM::tagValue(*node(), key); case Type::Way: return OSM::tagValue(*way(), key); case Type::Relation: return OSM::tagValue(*relation(), key); } return {}; } QString Element::tagValue(const char *keyName) const { switch (type()) { case Type::Null: return {}; case Type::Node: return OSM::tagValue(*node(), keyName); case Type::Way: return OSM::tagValue(*way(), keyName); case Type::Relation: return OSM::tagValue(*relation(), keyName); } return {}; } +QString Element::tagValue(const char *keyName, const QLocale &locale) const +{ + switch (type()) { + case Type::Null: + return {}; + case Type::Node: + return OSM::tagValue(*node(), keyName, locale); + case Type::Way: + return OSM::tagValue(*way(), keyName, locale); + case Type::Relation: + return OSM::tagValue(*relation(), keyName, locale); + } + + return {}; +} + std::vector::const_iterator OSM::Element::tagsBegin() const { switch (type()) { case Type::Null: Q_UNREACHABLE(); case Type::Node: return node()->tags.begin(); case Type::Way: return way()->tags.begin(); case Type::Relation: return relation()->tags.begin(); } Q_UNREACHABLE(); } std::vector::const_iterator OSM::Element::tagsEnd() const { switch (type()) { case Type::Null: Q_UNREACHABLE(); case Type::Node: return node()->tags.end(); case Type::Way: return way()->tags.end(); case Type::Relation: return relation()->tags.end(); } Q_UNREACHABLE(); } QString Element::url() const { switch (type()) { case Type::Null: return {}; case Type::Node: return node()->url(); case Type::Way: return way()->url(); case Type::Relation: return relation()->url(); } return {}; } template static void appendNodesFromWay(const DataSet &dataSet, std::vector &nodes, const Iter& nodeBegin, const Iter &nodeEnd) { nodes.reserve(nodes.size() + std::distance(nodeBegin, nodeEnd)); for (auto it = nodeBegin; it != nodeEnd; ++it) { const auto nodeIt = std::lower_bound(dataSet.nodes.begin(), dataSet.nodes.end(), (*it)); if (nodeIt == dataSet.nodes.end() || (*nodeIt).id != (*it)) { continue; } nodes.push_back(&(*nodeIt)); } } static OSM::Id appendNextPath(const DataSet &dataSet, std::vector &nodes, OSM::Id startNode, std::vector &ways) { if (ways.empty()) { return {}; } for (auto it = std::next(ways.begin()); it != ways.end(); ++it) { assert(!(*it)->nodes.empty()); // ensured in the caller if ((*it)->nodes.front() == startNode) { appendNodesFromWay(dataSet, nodes, (*it)->nodes.begin(), (*it)->nodes.end()); const auto lastNodeId = (*it)->nodes.back(); ways.erase(it); return lastNodeId; } // path segments can also be backwards if ((*it)->nodes.back() == startNode) { appendNodesFromWay(dataSet, nodes, (*it)->nodes.rbegin(), (*it)->nodes.rend()); const auto lastNodeId = (*it)->nodes.front(); ways.erase(it); return lastNodeId; } } return {}; } std::vector Element::outerPath(const DataSet &dataSet) const { switch (type()) { case Type::Null: return {}; case Type::Node: return {node()}; case Type::Way: { std::vector nodes; appendNodesFromWay(dataSet, nodes, way()->nodes.begin(), way()->nodes.end()); return nodes; } case Type::Relation: { if (tagValue("type") != QLatin1String("multipolygon")) { return {}; } // collect the relevant ways std::vector ways; for (const auto &member : relation()->members) { if (member.role != QLatin1String("outer")) { continue; } const auto it = std::lower_bound(dataSet.ways.begin(), dataSet.ways.end(), member.id); if (it != dataSet.ways.end() && (*it).id == member.id && !(*it).nodes.empty()) { ways.push_back(&(*it)); } } // stitch them together (there is no well-defined order) std::vector nodes; for (auto it = ways.begin(); it != ways.end();) { assert(!(*it)->nodes.empty()); // ensured above appendNodesFromWay(dataSet, nodes, (*it)->nodes.begin(), (*it)->nodes.end()); const auto startNode = (*it)->nodes.front(); auto lastNode = (*it)->nodes.back(); do { lastNode = appendNextPath(dataSet, nodes, lastNode, ways); } while (lastNode && lastNode != startNode); it = ways.erase(it); } return nodes; } } return {}; } void Element::recomputeBoundingBox(const DataSet &dataSet) { switch (type()) { case Type::Null: case Type::Node: break; case Type::Way: way()->bbox = std::accumulate(way()->nodes.begin(), way()->nodes.end(), OSM::BoundingBox(), [&dataSet](auto bbox, auto nodeId) { const auto nodeIt = std::lower_bound(dataSet.nodes.begin(), dataSet.nodes.end(), nodeId); if (nodeIt == dataSet.nodes.end() || (*nodeIt).id != nodeId) { return bbox; } return OSM::unite(bbox, {(*nodeIt).coordinate, (*nodeIt).coordinate}); }); break; case Type::Relation: relation()->bbox = {}; for_each_member(dataSet, *relation(), [this, &dataSet](auto mem) { mem.recomputeBoundingBox(dataSet); relation()->bbox = OSM::unite(relation()->bbox, mem.boundingBox()); }); break; } } diff --git a/src/osm/element.h b/src/osm/element.h index a9ab5d5..dc5072e 100644 --- a/src/osm/element.h +++ b/src/osm/element.h @@ -1,150 +1,151 @@ /* 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 OSM_ELEMENT_H #define OSM_ELEMENT_H #include "datatypes.h" #include namespace OSM { namespace Internal { template class TaggedPointer { public: explicit inline constexpr TaggedPointer(T *ptr, uint8_t tag) : m_data(reinterpret_cast(ptr) | (tag & TagMask)) {} inline T* get() const { return reinterpret_cast(m_data & ~TagMask); } inline uint8_t tag() const { return m_data & TagMask; } inline operator bool() const { return (m_data & ~TagMask); } private: enum { TagMask = 0x3 }; std::uintptr_t m_data; }; } /** A reference to any of OSM::Node/OSM::Way/OSM::Relation. * Lifetime of the referenced object needs to extend beyond the lifetime of this. */ class Element { public: inline constexpr Element() : m_elem(nullptr, static_cast(Type::Null)) {} inline Element(const Node *node) : m_elem(node, static_cast(Type::Node)) {} inline Element(const Way *way) : m_elem(way, static_cast(Type::Way)) {} inline Element(const Relation *relation) : m_elem(relation, static_cast(Type::Relation)) {} inline Type type() const { return static_cast(m_elem.tag()); } inline const Node* node() const { return static_cast(m_elem.get()); } inline const Way* way() const { return static_cast(m_elem.get()); } inline const Relation* relation() const { return static_cast(m_elem.get()); } Id id() const; Coordinate center() const; BoundingBox boundingBox() const; QString tagValue(TagKey key) const; QString tagValue(const char *keyName) const; + QString tagValue(const char *keyName, const QLocale &locale) const; std::vector::const_iterator tagsBegin() const; std::vector::const_iterator tagsEnd() const; QString url() const; /** Returns all nodes belonging to the outer path of this element. * In the simplest case that's a single closed polygon, but it can also be a sequence of multiple * closed loop polygons, or a polyline. */ std::vector outerPath(const DataSet &dataSet) const; /** Recompute the bounding box of this element. * We usually assume those to be provided by Overpass/osmconvert, but there seem to be cases where those * aren't reliable. */ void recomputeBoundingBox(const DataSet &dataSet); private: Internal::TaggedPointer m_elem; }; enum ForeachFlag : uint8_t { IncludeRelations = 1, IncludeWays = 2, IncludeNodes = 4, IterateAll = IncludeRelations | IncludeWays | IncludeNodes, }; template inline void for_each(const DataSet &dataSet, Func func, uint8_t flags = IterateAll) { if (flags & IncludeRelations) { for (const auto &rel : dataSet.relations) { func(Element(&rel)); } } if (flags & IncludeWays) { for (const auto &way : dataSet.ways) { func(Element(&way)); } } if (flags & IncludeNodes) { for (const auto &node : dataSet.nodes) { func(Element(&node)); } } } template inline void for_each_member(const DataSet &dataSet, const Relation &rel, Func func) { for (const auto &mem : rel.members) { switch (mem.type) { case Type::Null: break; case Type::Node: { const auto it = std::lower_bound(dataSet.nodes.begin(), dataSet.nodes.end(), mem.id); if (it != dataSet.nodes.end() && (*it).id == mem.id) { func(Element(&(*it))); } break; } case Type::Way: { const auto it = std::lower_bound(dataSet.ways.begin(), dataSet.ways.end(), mem.id); if (it != dataSet.ways.end() && (*it).id == mem.id) { func(Element(&(*it))); } break; } case Type::Relation: { const auto it = std::lower_bound(dataSet.relations.begin(), dataSet.relations.end(), mem.id); if (it != dataSet.relations.end() && (*it).id == mem.id) { func(Element(&(*it))); } break; } } } } } #endif // OSM_ELEMENT_H