1 // SPDX-License-Identifier: LGPL-2.1-or-later
2 //
3 // SPDX-FileCopyrightText: 2012 Torsten Rahn <rahn@kde.org>
4 // SPDX-FileCopyrightText: 2012 Cezar Mocan <mocancezar@gmail.com>
5 // SPDX-FileCopyrightText: 2014 Abhinav Gangwar <abhgang@gmail.com>
6 //
7 // For the Natural Earth Layer providing the Default data set at 0.5 arcminute resolution should be enough.
8 // This fileformat allows for even better packed data than the PNT format. For detailed polygons at arcminute
9 // scale on average it should use only 33% of the amount used by PNT.
10 //
11 // Description of the file format
12 //
13 // In the fileformat initially a file header is provided that provides the file format version and the number
14 // of polygons stored inside the file. A Polygon starts with the Polygon Header which provides the feature id
15 // and the number of so called "absolute nodes" that are about to follow. Absolute nodes always contain
16 // absolute geodetic coordinates. The Polygon Header also provides a flag that allows to specify whether the
17 // polygon is supposed to represent a line string ("0") or a linear ring ("1"). Each absolute node can be followed
18 // by relative nodes: These relative nodes are always nodes that follow in correct order inside the polygon after
19 // "their" absolute node. Each absolute node specifies the number of relative nodes which contain relative
20 // coordinates in reference to their absolute node. So an absolute node provides the absolute reference for
21 // relative nodes across a theoretical area of 2x2 squaredegree-area (which in practice frequently might rather
22 // amount to 1x1 square degrees).
23 //
24 // So much of the compression works by just referencing lat/lon diffs to special "absolute nodes". Hence the
25 // compression will especially work well for polygons with many nodes with a high node density.
26 //
27 // The parser has to convert these relative coordinates to absolute coordinates.
28 //
29 
30 #include "Pn2Runner.h"
31 
32 #include "GeoDataDocument.h"
33 #include "GeoDataPlacemark.h"
34 #include "GeoDataStyle.h"
35 #include "GeoDataPolyStyle.h"
36 #include "GeoDataLinearRing.h"
37 #include "GeoDataPolygon.h"
38 #include "GeoDataMultiGeometry.h"
39 #include "MarbleDebug.h"
40 
41 #include <QFile>
42 #include <QFileInfo>
43 
44 namespace Marble
45 {
46 // Polygon header flags, representing the type of polygon
47 enum polygonFlagType { LINESTRING = 0, LINEARRING = 1, OUTERBOUNDARY = 2, INNERBOUNDARY = 3, MULTIGEOMETRY = 4 };
48 
49 
Pn2Runner(QObject * parent)50 Pn2Runner::Pn2Runner(QObject *parent) :
51     ParsingRunner(parent)
52 {
53 }
54 
~Pn2Runner()55 Pn2Runner::~Pn2Runner()
56 {
57 }
58 
errorCheckLat(qint16 lat)59 bool Pn2Runner::errorCheckLat( qint16 lat )
60 {
61     return !(lat >= -10800 && lat <= +10800);
62 }
63 
errorCheckLon(qint16 lon)64 bool Pn2Runner::errorCheckLon( qint16 lon )
65 {
66     return !(lon >= -21600 && lon <= +21600);
67 }
68 
importPolygon(QDataStream & stream,GeoDataLineString * linestring,quint32 nrAbsoluteNodes)69 bool Pn2Runner::importPolygon( QDataStream &stream, GeoDataLineString* linestring, quint32 nrAbsoluteNodes )
70 {
71     qint16 lat, lon, nrRelativeNodes;
72     qint8 relativeLat, relativeLon;
73     bool error = false;
74 
75 
76     for ( quint32 absoluteNode = 1; absoluteNode <= nrAbsoluteNodes; absoluteNode++ ) {
77         stream >> lat >> lon >> nrRelativeNodes;
78 
79         error = error | errorCheckLat( lat ) | errorCheckLon( lon );
80 
81         qreal degLat = ( 1.0 * lat / 120.0 );
82         qreal degLon = ( 1.0 * lon / 120.0 );
83 
84         GeoDataCoordinates coord( degLon / 180 * M_PI, degLat / 180 * M_PI );
85         linestring->append( coord );
86 
87         for ( qint16 relativeNode = 1; relativeNode <= nrRelativeNodes; ++relativeNode ) {
88             stream >> relativeLat >> relativeLon;
89 
90             qint16 currLat = relativeLat + lat;
91             qint16 currLon = relativeLon + lon;
92 
93 
94             error = error | errorCheckLat( currLat ) | errorCheckLon( currLon );
95 
96             qreal currDegLat = ( 1.0 * currLat / 120.0 );
97             qreal currDegLon = ( 1.0 * currLon / 120.0 );
98 
99 
100             GeoDataCoordinates currCoord( currDegLon / 180 * M_PI, currDegLat / 180 * M_PI );
101             linestring->append( currCoord );
102         }
103     }
104 
105     *linestring = linestring->optimized();
106 
107     return error;
108 }
109 
parseFile(const QString & fileName,DocumentRole role,QString & error)110 GeoDataDocument *Pn2Runner::parseFile(const QString &fileName, DocumentRole role, QString &error)
111 {
112     QFileInfo fileinfo( fileName );
113     if (fileinfo.suffix().compare(QLatin1String("pn2"), Qt::CaseInsensitive) != 0) {
114         error = QStringLiteral("File %1 does not have a pn2 suffix").arg(fileName);
115         mDebug() << error;
116         return nullptr;
117     }
118 
119     QFile  file( fileName );
120     if ( !file.exists() ) {
121         error = QStringLiteral("File %1 does not exist").arg(fileName);
122         mDebug() << error;
123         return nullptr;
124     }
125 
126     file.open( QIODevice::ReadOnly );
127     m_stream.setDevice( &file );  // read the data serialized from the file
128 
129     m_stream >> m_fileHeaderVersion >> m_fileHeaderPolygons >> m_isMapColorField;
130 
131     switch( m_fileHeaderVersion ) {
132         case 1: return parseForVersion1( fileName, role );
133         case 2: return parseForVersion2( fileName, role );
134         default: qDebug() << "File can't be parsed. We don't have parser for file header version:" << m_fileHeaderVersion;
135                 break;
136     }
137 
138     return nullptr;
139 }
140 
parseForVersion1(const QString & fileName,DocumentRole role)141 GeoDataDocument* Pn2Runner::parseForVersion1(const QString& fileName, DocumentRole role)
142 {
143     GeoDataDocument *document = new GeoDataDocument();
144     document->setDocumentRole( role );
145 
146     bool error = false;
147 
148     quint32 ID, nrAbsoluteNodes;
149     quint8 flag, prevFlag = -1;
150 
151     GeoDataStyle::Ptr style;
152     GeoDataPolygon *polygon = new GeoDataPolygon;
153 
154     for ( quint32 currentPoly = 1; ( currentPoly <= m_fileHeaderPolygons ) && ( !error ) && ( !m_stream.atEnd() ); currentPoly++ ) {
155 
156         m_stream >> ID >> nrAbsoluteNodes >> flag;
157 
158         if ( flag != INNERBOUNDARY && ( prevFlag == INNERBOUNDARY || prevFlag == OUTERBOUNDARY ) ) {
159 
160             GeoDataPlacemark *placemark = new GeoDataPlacemark;
161             placemark->setGeometry( polygon );
162             if ( m_isMapColorField ) {
163                 if ( style ) {
164                     placemark->setStyle( style );
165                 }
166             }
167             document->append( placemark );
168         }
169 
170         if ( flag == LINESTRING ) {
171             GeoDataLineString *linestring = new GeoDataLineString;
172             error = error | importPolygon( m_stream, linestring, nrAbsoluteNodes );
173 
174             GeoDataPlacemark *placemark = new GeoDataPlacemark;
175             placemark->setGeometry( linestring );
176             document->append( placemark );
177         }
178 
179         if ( ( flag == LINEARRING ) || ( flag == OUTERBOUNDARY ) || ( flag == INNERBOUNDARY ) ) {
180             if ( flag == OUTERBOUNDARY && m_isMapColorField ) {
181                 quint8 colorIndex;
182                 m_stream >> colorIndex;
183                 style = GeoDataStyle::Ptr(new GeoDataStyle);
184                 GeoDataPolyStyle polyStyle;
185                 polyStyle.setColorIndex( colorIndex );
186                 style->setPolyStyle( polyStyle );
187             }
188 
189             GeoDataLinearRing* linearring = new GeoDataLinearRing;
190             error = error | importPolygon( m_stream, linearring, nrAbsoluteNodes );
191 
192             if ( flag == LINEARRING ) {
193                 GeoDataPlacemark *placemark = new GeoDataPlacemark;
194                 placemark->setGeometry( linearring );
195                 document->append( placemark );
196             }
197 
198             if ( flag == OUTERBOUNDARY ) {
199                 polygon = new GeoDataPolygon;
200                 polygon->setOuterBoundary( *linearring );
201             }
202 
203             if ( flag == INNERBOUNDARY ) {
204                 polygon->appendInnerBoundary( *linearring );
205             }
206         }
207 
208         if ( flag == MULTIGEOMETRY ) {
209             // not implemented yet, for now elements inside a multigeometry are separated as individual geometries
210         }
211 
212         prevFlag = flag;
213     }
214 
215     if ( prevFlag == INNERBOUNDARY || prevFlag == OUTERBOUNDARY ) {
216         GeoDataPlacemark *placemark = new GeoDataPlacemark;
217         if ( m_isMapColorField ) {
218             if ( style ) {
219                 placemark->setStyle( style );
220             }
221         }
222         placemark->setGeometry( polygon );
223         document->append( placemark );
224     }
225 
226     if ( error ) {
227         delete document;
228         document = nullptr;
229         return nullptr;
230     }
231     document->setFileName( fileName );
232     return document;
233 }
234 
parseForVersion2(const QString & fileName,DocumentRole role)235 GeoDataDocument* Pn2Runner::parseForVersion2( const QString &fileName, DocumentRole role )
236 {
237     GeoDataDocument *document = new GeoDataDocument();
238     document->setDocumentRole( role );
239 
240     bool error = false;
241 
242     quint32 nrAbsoluteNodes;
243     quint32 placemarkCurrentID = 1;
244     quint32 placemarkPrevID = 0;
245     quint8 flag, prevFlag = -1;
246 
247     GeoDataPolygon *polygon = new GeoDataPolygon;
248     GeoDataStyle::Ptr style;
249     GeoDataPlacemark *placemark =nullptr; // new GeoDataPlacemark;
250 
251     quint32 currentPoly;
252     for ( currentPoly = 1; ( currentPoly <= m_fileHeaderPolygons ) && ( !error ) && ( !m_stream.atEnd() ); currentPoly++ ) {
253         m_stream >> flag >> placemarkCurrentID;
254 
255         if ( flag == MULTIGEOMETRY && ( prevFlag == INNERBOUNDARY || prevFlag == OUTERBOUNDARY ) ) {
256             if ( placemark ) {
257                 placemark->setGeometry( polygon );
258             }
259         }
260 
261         if ( flag != MULTIGEOMETRY && flag != INNERBOUNDARY && ( prevFlag == INNERBOUNDARY || prevFlag == OUTERBOUNDARY ) ) {
262             if ( placemark ) {
263                 placemark->setGeometry( polygon );
264             }
265         }
266         /**
267          * If the parsed placemark id @p placemarkCurrentID is different
268          * from the id of previous placemark @p placemarkPrevID, it means
269          * we have encountered a new placemark. So, prepare a style @p style
270          * if file has color indices
271          */
272         if ( placemarkCurrentID != placemarkPrevID ) {
273             placemark = new GeoDataPlacemark;
274 
275             // Handle the color index
276             if( m_isMapColorField ) {
277                 quint8 colorIndex;
278                 m_stream >> colorIndex;
279                 style = GeoDataStyle::Ptr(new GeoDataStyle);
280                 GeoDataPolyStyle polyStyle;
281                 polyStyle.setColorIndex( colorIndex );
282                 polyStyle.setFill( true );
283                 style->setPolyStyle( polyStyle );
284                 placemark->setStyle( style );
285             }
286 
287             document->append( placemark );
288         }
289 
290         placemarkPrevID = placemarkCurrentID;
291 
292         if ( flag != MULTIGEOMETRY ) {
293             m_stream >> nrAbsoluteNodes;
294 
295             if ( flag == LINESTRING ) {
296                 GeoDataLineString *linestring = new GeoDataLineString;
297                 error = error | importPolygon( m_stream, linestring, nrAbsoluteNodes );
298                 if ( placemark ) {
299                     placemark->setGeometry( linestring );
300                 }
301             }
302 
303             if ( ( flag == LINEARRING ) || ( flag == OUTERBOUNDARY ) || ( flag == INNERBOUNDARY ) ) {
304                 GeoDataLinearRing* linearring = new GeoDataLinearRing;
305                 error = error || importPolygon( m_stream, linearring, nrAbsoluteNodes );
306 
307                 if ( flag == LINEARRING ) {
308                     if ( placemark ) {
309                         placemark->setGeometry( linearring );
310                     }
311                 } else {
312                     if ( flag == OUTERBOUNDARY ) {
313                         polygon = new GeoDataPolygon;
314                         polygon->setOuterBoundary( *linearring );
315                     }
316 
317                     if ( flag == INNERBOUNDARY ) {
318                         polygon->appendInnerBoundary( *linearring );
319                     }
320 
321                     delete linearring;
322                 }
323             }
324             prevFlag = flag;
325         }
326 
327         else {
328             quint32 placemarkCurrentIDInMulti;
329             quint8 flagInMulti;
330             quint8 prevFlagInMulti = -1;
331             quint8 multiSize = 0;
332 
333             m_stream >> multiSize;
334 
335             GeoDataMultiGeometry *multigeom = new GeoDataMultiGeometry;
336 
337             /**
338              * Read @p multiSize GeoDataGeometry objects
339              */
340             for ( int iter = 0; iter < multiSize; ++iter ) {
341                 m_stream >> flagInMulti >> placemarkCurrentIDInMulti >> nrAbsoluteNodes;
342                 if ( flagInMulti != INNERBOUNDARY && ( prevFlagInMulti == INNERBOUNDARY || prevFlagInMulti == OUTERBOUNDARY ) ) {
343                     multigeom->append( polygon );
344                 }
345 
346                 if ( flagInMulti == LINESTRING ) {
347                     GeoDataLineString *linestring = new GeoDataLineString;
348                     error = error || importPolygon( m_stream, linestring, nrAbsoluteNodes );
349                     multigeom->append( linestring );
350                 }
351 
352                 if ( ( flagInMulti == LINEARRING ) || ( flagInMulti == OUTERBOUNDARY ) || ( flagInMulti == INNERBOUNDARY ) ) {
353                     GeoDataLinearRing* linearring = new GeoDataLinearRing;
354                     error = error | importPolygon( m_stream, linearring, nrAbsoluteNodes );
355 
356                     if ( flagInMulti == LINEARRING ) {
357                         multigeom->append( linearring );
358                     } else {
359                         if ( flagInMulti == OUTERBOUNDARY ) {
360                             polygon = new GeoDataPolygon;
361                             polygon->setOuterBoundary( *linearring );
362                         }
363 
364                         if ( flagInMulti == INNERBOUNDARY ) {
365                             polygon->appendInnerBoundary( *linearring );
366                         }
367 
368                         delete linearring;
369                     }
370                 }
371                 prevFlagInMulti = flagInMulti;
372             }
373 
374             if ( prevFlagInMulti == INNERBOUNDARY || prevFlagInMulti == OUTERBOUNDARY ) {
375                 multigeom->append( polygon );
376             }
377             if ( placemark ) {
378                 placemark->setGeometry( multigeom );
379             }
380             prevFlag = MULTIGEOMETRY;
381         }
382     }
383 
384     if ( (prevFlag == INNERBOUNDARY || prevFlag == OUTERBOUNDARY) && prevFlag != MULTIGEOMETRY ) {
385         placemark->setGeometry( polygon );
386     }
387 
388     if ( error ) {
389         delete document;
390         document = nullptr;
391         return nullptr;
392     }
393     document->setFileName( fileName );
394     return document;
395 }
396 
397 }
398 
399 
400 #include "moc_Pn2Runner.cpp"
401