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