1 // SPDX-License-Identifier: LGPL-2.1-or-later
2 //
3 // SPDX-FileCopyrightText: 2015 Dennis Nienhüser <nienhueser@kde.org>
4 //
5 
6 #include <OsmRelation.h>
7 #include <MarbleDebug.h>
8 #include <GeoDataPlacemark.h>
9 #include <GeoDataRelation.h>
10 #include <GeoDataDocument.h>
11 #include <GeoDataPolygon.h>
12 #include <GeoDataLatLonAltBox.h>
13 #include <StyleBuilder.h>
14 #include <osm/OsmObjectManager.h>
15 
16 namespace Marble {
17 
OsmMember()18 OsmRelation::OsmMember::OsmMember() :
19     reference(0)
20 {
21     // nothing to do
22 }
23 
osmData()24 OsmPlacemarkData &OsmRelation::osmData()
25 {
26     return m_osmData;
27 }
28 
osmData() const29 const OsmPlacemarkData &OsmRelation::osmData() const
30 {
31     return m_osmData;
32 }
33 
parseMember(const QXmlStreamAttributes & attributes)34 void OsmRelation::parseMember(const QXmlStreamAttributes &attributes)
35 {
36     addMember(attributes.value(QLatin1String("ref")).toLongLong(),
37               attributes.value(QLatin1String("role")).toString(),
38               attributes.value(QLatin1String("type")).toString());
39 }
40 
addMember(qint64 reference,const QString & role,const QString & type)41 void OsmRelation::addMember(qint64 reference, const QString &role, const QString &type)
42 {
43     OsmMember member;
44     member.reference = reference;
45     member.role = role;
46     member.type = type;
47     m_members << member;
48 }
49 
createMultipolygon(GeoDataDocument * document,OsmWays & ways,const OsmNodes & nodes,QSet<qint64> & usedNodes,QSet<qint64> & usedWays) const50 void OsmRelation::createMultipolygon(GeoDataDocument *document, OsmWays &ways, const OsmNodes &nodes, QSet<qint64> &usedNodes, QSet<qint64> &usedWays) const
51 {
52     if (!m_osmData.containsTag(QStringLiteral("type"), QStringLiteral("multipolygon"))) {
53         return;
54     }
55 
56     QStringList const outerRoles = QStringList() << QStringLiteral("outer") << QString();
57     QSet<qint64> outerWays;
58     QSet<qint64> outerNodes;
59     OsmRings const outer = rings(outerRoles, ways, nodes, outerNodes, outerWays);
60 
61     if (outer.isEmpty()) {
62         return;
63     }
64 
65     GeoDataPlacemark::GeoDataVisualCategory outerCategory = StyleBuilder::determineVisualCategory(m_osmData);
66     if (outerCategory == GeoDataPlacemark::None) {
67         // Try to determine the visual category from the relation members
68         GeoDataPlacemark::GeoDataVisualCategory const firstCategory =
69                 StyleBuilder::determineVisualCategory(ways[*outerWays.begin()].osmData());
70 
71         bool categoriesAreSame = true;
72         for (auto wayId: outerWays) {
73             GeoDataPlacemark::GeoDataVisualCategory const category =
74                     StyleBuilder::determineVisualCategory(ways[wayId].osmData());
75             if( category != firstCategory ) {
76                 categoriesAreSame = false;
77                 break;
78             }
79         }
80 
81         if( categoriesAreSame ) {
82             outerCategory = firstCategory;
83         }
84     }
85 
86     for (auto wayId: outerWays) {
87         Q_ASSERT(ways.contains(wayId));
88         const auto &osmData = ways[wayId].osmData();
89         GeoDataPlacemark::GeoDataVisualCategory const category = StyleBuilder::determineVisualCategory(osmData);
90         if ((category == GeoDataPlacemark::None || category == outerCategory) && osmData.isEmpty()) {
91             // Schedule way for removal: It's a non-styled way only used to create the outer boundary in this polygon
92             usedWays << wayId;
93         } // else we keep it
94 
95         for(auto nodeId: ways[wayId].references()) {
96             ways[wayId].osmData().addNodeReference(nodes[nodeId].coordinates(), nodes[nodeId].osmData());
97         }
98     }
99 
100     QStringList const innerRoles = QStringList() << QStringLiteral("inner");
101     QSet<qint64> innerWays;
102     OsmRings const inner = rings(innerRoles, ways, nodes, usedNodes, innerWays);
103 
104     bool const hasMultipleOuterRings = outer.size() > 1;
105     for (int i=0, n=outer.size(); i<n; ++i) {
106         auto const & outerRing = outer[i];
107 
108         GeoDataPolygon *polygon = new GeoDataPolygon;
109         polygon->setOuterBoundary(outerRing.first);
110         OsmPlacemarkData osmData = m_osmData;
111         osmData.addMemberReference(-1, outerRing.second);
112 
113         int index = 0;
114         for (auto const &innerRing: inner) {
115             if (innerRing.first.isEmpty() || !outerRing.first.contains(innerRing.first.first())) {
116                 // Simple check to see if this inner ring is inside the outer ring
117                 continue;
118             }
119 
120             if (StyleBuilder::determineVisualCategory(innerRing.second) == GeoDataPlacemark::None) {
121                 // Schedule way for removal: It's a non-styled way only used to create the inner boundary in this polygon
122                 usedWays << innerRing.second.id();
123             }
124             polygon->appendInnerBoundary(innerRing.first);
125             osmData.addMemberReference(index, innerRing.second);
126             ++index;
127         }
128 
129         if (outerCategory == GeoDataPlacemark::Bathymetry) {
130             // In case of a bathymetry store elevation info since it is required during styling
131             // The ele=* tag is present in the outermost way
132             const QString ele = QStringLiteral("ele");
133             const OsmPlacemarkData &outerWayData = outerRing.second;
134             auto tagIter = outerWayData.findTag(ele);
135             if (tagIter != outerWayData.tagsEnd()) {
136                 osmData.addTag(ele, tagIter.value());
137             }
138         }
139 
140         GeoDataPlacemark *placemark = new GeoDataPlacemark;
141         placemark->setName(m_osmData.tagValue(QStringLiteral("name")));
142         placemark->setVisualCategory(outerCategory);
143         placemark->setOsmData(osmData);
144         placemark->setZoomLevel(StyleBuilder::minimumZoomLevel(outerCategory));
145         placemark->setPopularity(StyleBuilder::popularity(placemark));
146         placemark->setVisible(outerCategory != GeoDataPlacemark::None);
147         placemark->setGeometry(polygon);
148         if (hasMultipleOuterRings) {
149             /** @todo Use a GeoDataMultiGeometry to keep the ID? */
150             osmData.setId(0);
151             OsmObjectManager::initializeOsmData(placemark);
152         } else {
153             OsmObjectManager::registerId(osmData.id());
154         }
155         usedNodes |= outerNodes;
156 
157         document->append(placemark);
158     }
159 }
160 
stringToType(const QString & str)161 static OsmType stringToType(const QString &str)
162 {
163     if (str == "relation") {
164         return OsmType::Relation;
165     }
166     if (str == "node") {
167         return OsmType::Node;
168     }
169     return OsmType::Way;
170 }
171 
createRelation(GeoDataDocument * document,const QHash<qint64,GeoDataPlacemark * > & placemarks) const172 void OsmRelation::createRelation(GeoDataDocument *document, const QHash<qint64, GeoDataPlacemark*>& placemarks) const
173 {
174     if (m_osmData.containsTag(QStringLiteral("type"), QStringLiteral("multipolygon"))) {
175         return;
176     }
177 
178     OsmPlacemarkData osmData = m_osmData;
179     GeoDataRelation *relation = new GeoDataRelation;
180 
181     relation->setName(osmData.tagValue(QStringLiteral("name")));
182     if (relation->name().isEmpty()) {
183         relation->setName(osmData.tagValue(QStringLiteral("ref")));
184     }
185     relation->osmData() = osmData;
186 
187     for (auto const &member: m_members) {
188         auto const iter = placemarks.find(member.reference);
189         if (iter != placemarks.constEnd()) {
190             relation->addMember(*iter, member.reference, stringToType(member.type), member.role);
191         }
192     }
193 
194     if (relation->members().isEmpty()) {
195         delete relation;
196         return;
197     }
198 
199     OsmObjectManager::registerId(osmData.id());
200     relation->setVisible(false);
201     document->append(relation);
202 }
203 
rings(const QStringList & roles,const OsmWays & ways,const OsmNodes & nodes,QSet<qint64> & usedNodes,QSet<qint64> & usedWays) const204 OsmRelation::OsmRings OsmRelation::rings(const QStringList &roles, const OsmWays &ways, const OsmNodes &nodes, QSet<qint64> &usedNodes, QSet<qint64> &usedWays) const
205 {
206     QSet<qint64> currentWays;
207     QSet<qint64> currentNodes;
208     QList<qint64> roleMembers;
209     for (auto const &member: m_members) {
210         if (roles.contains(member.role)) {
211             if (!ways.contains(member.reference)) {
212                 // A way is missing. Return nothing.
213                 return OsmRings();
214             }
215             roleMembers << member.reference;
216         }
217     }
218 
219     OsmRings result;
220     QList<OsmWay> unclosedWays;
221     for(auto wayId: roleMembers) {
222         GeoDataLinearRing ring;
223         OsmWay const & way = ways[wayId];
224         if (way.references().isEmpty()) {
225             continue;
226         }
227         if (way.references().first() != way.references().last()) {
228             unclosedWays.append(way);
229             continue;
230         }
231 
232         OsmPlacemarkData placemarkData = way.osmData();
233         for(auto id: way.references()) {
234             if (!nodes.contains(id)) {
235                 // A node is missing. Return nothing.
236                 return OsmRings();
237             }
238             const auto &node = nodes[id];
239             ring << node.coordinates();
240             placemarkData.addNodeReference(node.coordinates(), node.osmData());
241         }
242         Q_ASSERT(ways.contains(wayId));
243         currentWays << wayId;
244         result << OsmRing(GeoDataLinearRing(ring.optimized()), placemarkData);
245     }
246 
247     if( !unclosedWays.isEmpty() ) {
248         //mDebug() << "Trying to merge non-trivial polygon boundary in relation " << m_osmData.id();
249         while( unclosedWays.length() > 0 ) {
250             GeoDataLinearRing ring;
251             qint64 firstReference = unclosedWays.first().references().first();
252             qint64 lastReference = firstReference;
253             OsmPlacemarkData placemarkData;
254             bool ok = true;
255             while( ok ) {
256                 ok = false;
257                 for(int i = 0; i<unclosedWays.length(); ) {
258                     const OsmWay &nextWay = unclosedWays.at(i);
259                     if( nextWay.references().first() == lastReference
260                             || nextWay.references().last() == lastReference ) {
261 
262                         bool isReversed = nextWay.references().last() == lastReference;
263                         QVector<qint64> v = nextWay.references();
264                         while( !v.isEmpty() ) {
265                             qint64 id = isReversed ? v.takeLast() : v.takeFirst();
266                             if (!nodes.contains(id)) {
267                                 // A node is missing. Return nothing.
268                                 return OsmRings();
269                             }
270                             if ( id != lastReference ) {
271                                 const auto &node = nodes[id];
272                                 ring << node.coordinates();
273                                 placemarkData.addNodeReference(node.coordinates(), node.osmData());
274                                 currentNodes << id;
275                             }
276                         }
277                         lastReference = isReversed ? nextWay.references().first()
278                                                    : nextWay.references().last();
279                         Q_ASSERT(ways.contains(nextWay.osmData().id()));
280                         currentWays << nextWay.osmData().id();
281                         unclosedWays.removeAt(i);
282                         ok = true;
283                         break;
284                     } else {
285                         ++i;
286                     }
287                 }
288             }
289 
290             if(lastReference != firstReference) {
291                 return OsmRings();
292             } else {
293                 /** @todo Merge tags common to all rings into the new osm data? */
294                 result << OsmRing(GeoDataLinearRing(ring.optimized()), placemarkData);
295             }
296         }
297     }
298 
299     usedWays |= currentWays;
300     usedNodes |= currentNodes;
301     return result;
302 }
303 
304 }
305