File indexing completed on 2025-01-05 03:59:29

0001 // SPDX-License-Identifier: LGPL-2.1-or-later
0002 //
0003 // SPDX-FileCopyrightText: 2015 Dennis Nienhüser <nienhueser@kde.org>
0004 //
0005 
0006 #include "OsmRelation.h"
0007 #include "GeoDataPlacemark.h"
0008 #include "GeoDataRelation.h"
0009 #include "GeoDataDocument.h"
0010 #include "GeoDataPolygon.h"
0011 #include "GeoDataLatLonAltBox.h"
0012 #include "StyleBuilder.h"
0013 #include "OsmObjectManager.h"
0014 
0015 #include "digikam_debug.h"
0016 
0017 namespace Marble {
0018 
0019 OsmRelation::OsmMember::OsmMember() :
0020     reference(0)
0021 {
0022     // nothing to do
0023 }
0024 
0025 OsmPlacemarkData &OsmRelation::osmData()
0026 {
0027     return m_osmData;
0028 }
0029 
0030 const OsmPlacemarkData &OsmRelation::osmData() const
0031 {
0032     return m_osmData;
0033 }
0034 
0035 void OsmRelation::parseMember(const QXmlStreamAttributes &attributes)
0036 {
0037     addMember(attributes.value(QLatin1String("ref")).toLongLong(),
0038               attributes.value(QLatin1String("role")).toString(),
0039               attributes.value(QLatin1String("type")).toString());
0040 }
0041 
0042 void OsmRelation::addMember(qint64 reference, const QString &role, const QString &type)
0043 {
0044     OsmMember member;
0045     member.reference = reference;
0046     member.role = role;
0047     member.type = type;
0048     m_members << member;
0049 }
0050 
0051 void OsmRelation::createMultipolygon(GeoDataDocument *document, OsmWays &ways, const OsmNodes &nodes, QSet<qint64> &usedNodes, QSet<qint64> &usedWays) const
0052 {
0053     if (!m_osmData.containsTag(QStringLiteral("type"), QStringLiteral("multipolygon"))) {
0054         return;
0055     }
0056 
0057     QStringList const outerRoles = QStringList() << QStringLiteral("outer") << QString();
0058     QSet<qint64> outerWays;
0059     QSet<qint64> outerNodes;
0060     OsmRings const outer = rings(outerRoles, ways, nodes, outerNodes, outerWays);
0061 
0062     if (outer.isEmpty()) {
0063         return;
0064     }
0065 
0066     GeoDataPlacemark::GeoDataVisualCategory outerCategory = StyleBuilder::determineVisualCategory(m_osmData);
0067     if (outerCategory == GeoDataPlacemark::None) {
0068         // Try to determine the visual category from the relation members
0069         GeoDataPlacemark::GeoDataVisualCategory const firstCategory =
0070                 StyleBuilder::determineVisualCategory(ways[*outerWays.begin()].osmData());
0071 
0072         bool categoriesAreSame = true;
0073         for (auto wayId: outerWays) {
0074             GeoDataPlacemark::GeoDataVisualCategory const category =
0075                     StyleBuilder::determineVisualCategory(ways[wayId].osmData());
0076             if( category != firstCategory ) {
0077                 categoriesAreSame = false;
0078                 break;
0079             }
0080         }
0081 
0082         if( categoriesAreSame ) {
0083             outerCategory = firstCategory;
0084         }
0085     }
0086 
0087     for (auto wayId: outerWays) {
0088         Q_ASSERT(ways.contains(wayId));
0089         const auto &osmData = ways[wayId].osmData();
0090         GeoDataPlacemark::GeoDataVisualCategory const category = StyleBuilder::determineVisualCategory(osmData);
0091         if ((category == GeoDataPlacemark::None || category == outerCategory) && osmData.isEmpty()) {
0092             // Schedule way for removal: It's a non-styled way only used to create the outer boundary in this polygon
0093             usedWays << wayId;
0094         } // else we keep it
0095 
0096         for(auto nodeId: ways[wayId].references()) {
0097             ways[wayId].osmData().addNodeReference(nodes[nodeId].coordinates(), nodes[nodeId].osmData());
0098         }
0099     }
0100 
0101     QStringList const innerRoles = QStringList() << QStringLiteral("inner");
0102     QSet<qint64> innerWays;
0103     OsmRings const inner = rings(innerRoles, ways, nodes, usedNodes, innerWays);
0104 
0105     bool const hasMultipleOuterRings = outer.size() > 1;
0106     for (int i=0, n=outer.size(); i<n; ++i) {
0107         auto const & outerRing = outer[i];
0108 
0109         GeoDataPolygon *polygon = new GeoDataPolygon;
0110         polygon->setOuterBoundary(outerRing.first);
0111         OsmPlacemarkData osmData = m_osmData;
0112         osmData.addMemberReference(-1, outerRing.second);
0113 
0114         int index = 0;
0115         for (auto const &innerRing: inner) {
0116             if (innerRing.first.isEmpty() || !outerRing.first.contains(innerRing.first.first())) {
0117                 // Simple check to see if this inner ring is inside the outer ring
0118                 continue;
0119             }
0120 
0121             if (StyleBuilder::determineVisualCategory(innerRing.second) == GeoDataPlacemark::None) {
0122                 // Schedule way for removal: It's a non-styled way only used to create the inner boundary in this polygon
0123                 usedWays << innerRing.second.id();
0124             }
0125             polygon->appendInnerBoundary(innerRing.first);
0126             osmData.addMemberReference(index, innerRing.second);
0127             ++index;
0128         }
0129 
0130         if (outerCategory == GeoDataPlacemark::Bathymetry) {
0131             // In case of a bathymetry store elevation info since it is required during styling
0132             // The ele=* tag is present in the outermost way
0133             const QString ele = QStringLiteral("ele");
0134             const OsmPlacemarkData &outerWayData = outerRing.second;
0135             auto tagIter = outerWayData.findTag(ele);
0136             if (tagIter != outerWayData.tagsEnd()) {
0137                 osmData.addTag(ele, tagIter.value());
0138             }
0139         }
0140 
0141         GeoDataPlacemark *placemark = new GeoDataPlacemark;
0142         placemark->setName(m_osmData.tagValue(QStringLiteral("name")));
0143         placemark->setVisualCategory(outerCategory);
0144         placemark->setOsmData(osmData);
0145         placemark->setZoomLevel(StyleBuilder::minimumZoomLevel(outerCategory));
0146         placemark->setPopularity(StyleBuilder::popularity(placemark));
0147         placemark->setVisible(outerCategory != GeoDataPlacemark::None);
0148         placemark->setGeometry(polygon);
0149         if (hasMultipleOuterRings) {
0150             /** @todo Use a GeoDataMultiGeometry to keep the ID? */
0151             osmData.setId(0);
0152             OsmObjectManager::initializeOsmData(placemark);
0153         } else {
0154             OsmObjectManager::registerId(osmData.id());
0155         }
0156         usedNodes |= outerNodes;
0157 
0158         document->append(placemark);
0159     }
0160 }
0161 
0162 static OsmType stringToType(const QString &str)
0163 {
0164     if (str == QLatin1String("relation")) {
0165         return OsmType::Relation;
0166     }
0167     if (str == QLatin1String("node")) {
0168         return OsmType::Node;
0169     }
0170     return OsmType::Way;
0171 }
0172 
0173 void OsmRelation::createRelation(GeoDataDocument *document, const QHash<qint64, GeoDataPlacemark*>& placemarks) const
0174 {
0175     if (m_osmData.containsTag(QStringLiteral("type"), QStringLiteral("multipolygon"))) {
0176         return;
0177     }
0178 
0179     OsmPlacemarkData osmData = m_osmData;
0180     GeoDataRelation *relation = new GeoDataRelation;
0181 
0182     relation->setName(osmData.tagValue(QStringLiteral("name")));
0183     if (relation->name().isEmpty()) {
0184         relation->setName(osmData.tagValue(QStringLiteral("ref")));
0185     }
0186     relation->osmData() = osmData;
0187 
0188     for (auto const &member: m_members) {
0189         auto const iter = placemarks.find(member.reference);
0190         if (iter != placemarks.constEnd()) {
0191             relation->addMember(*iter, member.reference, stringToType(member.type), member.role);
0192         }
0193     }
0194 
0195     if (relation->members().isEmpty()) {
0196         delete relation;
0197         return;
0198     }
0199 
0200     OsmObjectManager::registerId(osmData.id());
0201     relation->setVisible(false);
0202     document->append(relation);
0203 }
0204 
0205 OsmRelation::OsmRings OsmRelation::rings(const QStringList &roles, const OsmWays &ways, const OsmNodes &nodes, QSet<qint64> &usedNodes, QSet<qint64> &usedWays) const
0206 {
0207     QSet<qint64> currentWays;
0208     QSet<qint64> currentNodes;
0209     QList<qint64> roleMembers;
0210     for (auto const &member: m_members) {
0211         if (roles.contains(member.role)) {
0212             if (!ways.contains(member.reference)) {
0213                 // A way is missing. Return nothing.
0214                 return OsmRings();
0215             }
0216             roleMembers << member.reference;
0217         }
0218     }
0219 
0220     OsmRings result;
0221     QList<OsmWay> unclosedWays;
0222     for(auto wayId: roleMembers) {
0223         GeoDataLinearRing ring;
0224         OsmWay const & way = ways[wayId];
0225         if (way.references().isEmpty()) {
0226             continue;
0227         }
0228         if (way.references().first() != way.references().last()) {
0229             unclosedWays.append(way);
0230             continue;
0231         }
0232 
0233         OsmPlacemarkData placemarkData = way.osmData();
0234         for(auto id: way.references()) {
0235             if (!nodes.contains(id)) {
0236                 // A node is missing. Return nothing.
0237                 return OsmRings();
0238             }
0239             const auto &node = nodes[id];
0240             ring << node.coordinates();
0241             placemarkData.addNodeReference(node.coordinates(), node.osmData());
0242         }
0243         Q_ASSERT(ways.contains(wayId));
0244         currentWays << wayId;
0245         result << OsmRing(GeoDataLinearRing(ring.optimized()), placemarkData);
0246     }
0247 
0248     if( !unclosedWays.isEmpty() ) {
0249         //qCDebug(DIGIKAM_MARBLE_LOG) << "Trying to merge non-trivial polygon boundary in relation " << m_osmData.id();
0250         while( unclosedWays.length() > 0 ) {
0251             GeoDataLinearRing ring;
0252             qint64 firstReference = unclosedWays.first().references().first();
0253             qint64 lastReference = firstReference;
0254             OsmPlacemarkData placemarkData;
0255             bool ok = true;
0256             while( ok ) {
0257                 ok = false;
0258                 for(int i = 0; i<unclosedWays.length(); ) {
0259                     const OsmWay &nextWay = unclosedWays.at(i);
0260                     if( nextWay.references().first() == lastReference
0261                             || nextWay.references().last() == lastReference ) {
0262 
0263                         bool isReversed = nextWay.references().last() == lastReference;
0264                         QVector<qint64> v = nextWay.references();
0265                         while( !v.isEmpty() ) {
0266                             qint64 id = isReversed ? v.takeLast() : v.takeFirst();
0267                             if (!nodes.contains(id)) {
0268                                 // A node is missing. Return nothing.
0269                                 return OsmRings();
0270                             }
0271                             if ( id != lastReference ) {
0272                                 const auto &node = nodes[id];
0273                                 ring << node.coordinates();
0274                                 placemarkData.addNodeReference(node.coordinates(), node.osmData());
0275                                 currentNodes << id;
0276                             }
0277                         }
0278                         lastReference = isReversed ? nextWay.references().first()
0279                                                    : nextWay.references().last();
0280                         Q_ASSERT(ways.contains(nextWay.osmData().id()));
0281                         currentWays << nextWay.osmData().id();
0282                         unclosedWays.removeAt(i);
0283                         ok = true;
0284                         break;
0285                     } else {
0286                         ++i;
0287                     }
0288                 }
0289             }
0290 
0291             if(lastReference != firstReference) {
0292                 return OsmRings();
0293             } else {
0294                 /** @todo Merge tags common to all rings into the new osm data? */
0295                 result << OsmRing(GeoDataLinearRing(ring.optimized()), placemarkData);
0296             }
0297         }
0298     }
0299 
0300     usedWays |= currentWays;
0301     usedNodes |= currentNodes;
0302     return result;
0303 }
0304 
0305 }