File indexing completed on 2025-01-05 03:59:32
0001 // SPDX-License-Identifier: LGPL-2.1-or-later 0002 // 0003 // SPDX-FileCopyrightText: 2007 Inge Wallin <ingwa@kde.org> 0004 // SPDX-FileCopyrightText: 2007-2012 Torsten Rahn <rahn@kde.org> 0005 // SPDX-FileCopyrightText: 2012 Cezar Mocan <mocancezar@gmail.com> 0006 // 0007 0008 // Local 0009 #include "CylindricalProjection.h" 0010 0011 #include "CylindricalProjection_p.h" 0012 0013 // Marble 0014 #include "GeoDataLinearRing.h" 0015 #include "GeoDataLineString.h" 0016 #include "GeoDataCoordinates.h" 0017 #include "GeoDataLatLonAltBox.h" 0018 #include "ViewportParams.h" 0019 0020 #include <QPainterPath> 0021 0022 // Maximum amount of nodes that are created automatically between actual nodes. 0023 static const int maxTessellationNodes = 200; 0024 0025 namespace Marble { 0026 0027 CylindricalProjection::CylindricalProjection() 0028 : AbstractProjection( new CylindricalProjectionPrivate( this ) ) 0029 { 0030 } 0031 0032 CylindricalProjection::CylindricalProjection( CylindricalProjectionPrivate* dd ) 0033 : AbstractProjection( dd ) 0034 { 0035 } 0036 0037 CylindricalProjection::~CylindricalProjection() 0038 { 0039 } 0040 0041 CylindricalProjectionPrivate::CylindricalProjectionPrivate( CylindricalProjection * parent ) 0042 : AbstractProjectionPrivate( parent ), 0043 q_ptr( parent ) 0044 { 0045 0046 } 0047 0048 0049 QPainterPath CylindricalProjection::mapShape( const ViewportParams *viewport ) const 0050 { 0051 // Convenience variables 0052 int width = viewport->width(); 0053 int height = viewport->height(); 0054 0055 qreal yTop; 0056 qreal yBottom; 0057 qreal xDummy; 0058 0059 // Get the top and bottom coordinates of the projected map. 0060 screenCoordinates( 0.0, maxLat(), viewport, xDummy, yTop ); 0061 screenCoordinates( 0.0, minLat(), viewport, xDummy, yBottom ); 0062 0063 // Don't let the map area be outside the image 0064 if ( yTop < 0 ) 0065 yTop = 0; 0066 if ( yBottom > height ) 0067 yBottom = height; 0068 0069 QPainterPath mapShape; 0070 mapShape.addRect( 0071 0, 0072 yTop, 0073 width, 0074 yBottom - yTop ); 0075 0076 return mapShape; 0077 } 0078 0079 bool CylindricalProjection::screenCoordinates( const GeoDataLineString &lineString, 0080 const ViewportParams *viewport, 0081 QVector<QPolygonF *> &polygons ) const 0082 { 0083 0084 Q_D( const CylindricalProjection ); 0085 // Compare bounding box size of the line string with the angularResolution 0086 // Immediately return if the latLonAltBox is smaller. 0087 if ( !viewport->resolves( lineString.latLonAltBox() ) ) { 0088 // qCDebug(DIGIKAM_MARBLE_LOG) << "Object too small to be resolved"; 0089 return false; 0090 } 0091 0092 QVector<QPolygonF *> subPolygons; 0093 d->lineStringToPolygon( lineString, viewport, subPolygons ); 0094 0095 polygons << subPolygons; 0096 return polygons.isEmpty(); 0097 } 0098 int CylindricalProjectionPrivate::tessellateLineSegment( const GeoDataCoordinates &aCoords, 0099 qreal ax, qreal ay, 0100 const GeoDataCoordinates &bCoords, 0101 qreal bx, qreal by, 0102 QVector<QPolygonF*> &polygons, 0103 const ViewportParams *viewport, 0104 TessellationFlags f, 0105 int mirrorCount, 0106 qreal repeatDistance) const 0107 { 0108 // We take the manhattan length as a distance approximation 0109 // that can be too big by a factor of sqrt(2) 0110 qreal distance = fabs((bx - ax)) + fabs((by - ay)); 0111 #ifdef SAFE_DISTANCE 0112 // Interpolate additional nodes if the line segment that connects the 0113 // current or previous nodes might cross the viewport. 0114 // The latter can pretty safely be excluded for most projections if both points 0115 // are located on the same side relative to the viewport boundaries and if they are 0116 // located more than half the line segment distance away from the viewport. 0117 const qreal safeDistance = - 0.5 * distance; 0118 if ( !( bx < safeDistance && ax < safeDistance ) 0119 || !( by < safeDistance && ay < safeDistance ) 0120 || !( bx + safeDistance > viewport->width() 0121 && ax + safeDistance > viewport->width() ) 0122 || !( by + safeDistance > viewport->height() 0123 && ay + safeDistance > viewport->height() ) 0124 ) 0125 { 0126 #endif 0127 int maxTessellationFactor = viewport->radius() < 20000 ? 10 : 20; 0128 int const finalTessellationPrecision = qBound(2, viewport->radius()/200, maxTessellationFactor) * tessellationPrecision; 0129 0130 // Let the line segment follow the spherical surface 0131 // if the distance between the previous point and the current point 0132 // on screen is too big 0133 if ( distance > finalTessellationPrecision ) { 0134 const int tessellatedNodes = qMin<int>( distance / finalTessellationPrecision, maxTessellationNodes ); 0135 0136 mirrorCount = processTessellation( aCoords, bCoords, 0137 tessellatedNodes, 0138 polygons, 0139 viewport, 0140 f, 0141 mirrorCount, 0142 repeatDistance ); 0143 } 0144 else { 0145 mirrorCount = crossDateLine( aCoords, bCoords, bx, by, polygons, mirrorCount, repeatDistance ); 0146 } 0147 #ifdef SAFE_DISTANCE 0148 } 0149 #endif 0150 return mirrorCount; 0151 } 0152 0153 0154 int CylindricalProjectionPrivate::processTessellation( const GeoDataCoordinates &previousCoords, 0155 const GeoDataCoordinates ¤tCoords, 0156 int tessellatedNodes, 0157 QVector<QPolygonF*> &polygons, 0158 const ViewportParams *viewport, 0159 TessellationFlags f, 0160 int mirrorCount, 0161 qreal repeatDistance) const 0162 { 0163 0164 const bool clampToGround = f.testFlag( FollowGround ); 0165 const bool followLatitudeCircle = f.testFlag( RespectLatitudeCircle ) 0166 && previousCoords.latitude() == currentCoords.latitude(); 0167 0168 // Calculate steps for tessellation: lonDiff and altDiff 0169 qreal lonDiff = 0.0; 0170 if ( followLatitudeCircle ) { 0171 const int previousSign = previousCoords.longitude() > 0 ? 1 : -1; 0172 const int currentSign = currentCoords.longitude() > 0 ? 1 : -1; 0173 0174 lonDiff = currentCoords.longitude() - previousCoords.longitude(); 0175 if ( previousSign != currentSign 0176 && fabs(previousCoords.longitude()) + fabs(currentCoords.longitude()) > M_PI ) { 0177 if ( previousSign > currentSign ) { 0178 // going eastwards -> 0179 lonDiff += 2 * M_PI ; 0180 } else { 0181 // going westwards -> 0182 lonDiff -= 2 * M_PI; 0183 } 0184 } 0185 if ( fabs( lonDiff ) == 2 * M_PI ) { 0186 return mirrorCount; 0187 } 0188 } 0189 0190 // Create the tessellation nodes. 0191 GeoDataCoordinates previousTessellatedCoords = previousCoords; 0192 for ( int i = 1; i <= tessellatedNodes; ++i ) { 0193 const qreal t = (qreal)(i) / (qreal)( tessellatedNodes + 1 ); 0194 0195 GeoDataCoordinates currentTessellatedCoords; 0196 0197 if ( followLatitudeCircle ) { 0198 // To tessellate along latitude circles use the 0199 // linear interpolation of the longitude. 0200 // interpolate the altitude, too 0201 const qreal altDiff = currentCoords.altitude() - previousCoords.altitude(); 0202 const qreal altitude = altDiff * t + previousCoords.altitude(); 0203 const qreal lon = lonDiff * t + previousCoords.longitude(); 0204 const qreal lat = previousTessellatedCoords.latitude(); 0205 0206 currentTessellatedCoords = GeoDataCoordinates(lon, lat, altitude); 0207 } 0208 else { 0209 // To tessellate along great circles use the 0210 // normalized linear interpolation ("NLERP") for latitude and longitude. 0211 currentTessellatedCoords = previousCoords.nlerp(currentCoords, t); 0212 } 0213 0214 if (clampToGround) { 0215 currentTessellatedCoords.setAltitude(0); 0216 } 0217 0218 Q_Q(const CylindricalProjection); 0219 qreal bx, by; 0220 q->screenCoordinates( currentTessellatedCoords, viewport, bx, by ); 0221 mirrorCount = crossDateLine( previousTessellatedCoords, currentTessellatedCoords, bx, by, polygons, 0222 mirrorCount, repeatDistance ); 0223 previousTessellatedCoords = currentTessellatedCoords; 0224 } 0225 0226 // For the clampToGround case add the "current" coordinate after adding all other nodes. 0227 GeoDataCoordinates currentModifiedCoords( currentCoords ); 0228 if ( clampToGround ) { 0229 currentModifiedCoords.setAltitude( 0.0 ); 0230 } 0231 Q_Q(const CylindricalProjection); 0232 qreal bx, by; 0233 q->screenCoordinates( currentModifiedCoords, viewport, bx, by ); 0234 mirrorCount = crossDateLine( previousTessellatedCoords, currentModifiedCoords, bx, by, polygons, 0235 mirrorCount, repeatDistance ); 0236 return mirrorCount; 0237 } 0238 0239 int CylindricalProjectionPrivate::crossDateLine( const GeoDataCoordinates & aCoord, 0240 const GeoDataCoordinates & bCoord, 0241 qreal bx, 0242 qreal by, 0243 QVector<QPolygonF*> &polygons, 0244 int mirrorCount, 0245 qreal repeatDistance ) 0246 { 0247 qreal aLon = aCoord.longitude(); 0248 qreal aSign = aLon > 0 ? 1 : -1; 0249 0250 qreal bLon = bCoord.longitude(); 0251 qreal bSign = bLon > 0 ? 1 : -1; 0252 0253 qreal delta = 0; 0254 if( aSign != bSign && fabs(aLon) + fabs(bLon) > M_PI ) { 0255 int sign = aSign > bSign ? 1 : -1; 0256 mirrorCount += sign; 0257 } 0258 delta = repeatDistance * mirrorCount; 0259 *polygons.last() << QPointF( bx + delta, by ); 0260 0261 return mirrorCount; 0262 } 0263 0264 bool CylindricalProjectionPrivate::lineStringToPolygon( const GeoDataLineString &lineString, 0265 const ViewportParams *viewport, 0266 QVector<QPolygonF *> &polygons ) const 0267 { 0268 const TessellationFlags f = lineString.tessellationFlags(); 0269 bool const tessellate = lineString.tessellate(); 0270 const bool noFilter = f.testFlag(PreventNodeFiltering); 0271 0272 qreal x = 0; 0273 qreal y = 0; 0274 0275 qreal previousX = -1.0; 0276 qreal previousY = -1.0; 0277 0278 int mirrorCount = 0; 0279 qreal distance = repeatDistance( viewport ); 0280 0281 QPolygonF * polygon = new QPolygonF; 0282 if (!tessellate) { 0283 polygon->reserve(lineString.size()); 0284 } 0285 polygons.append( polygon ); 0286 0287 GeoDataLineString::ConstIterator itCoords = lineString.constBegin(); 0288 GeoDataLineString::ConstIterator itPreviousCoords = lineString.constBegin(); 0289 0290 GeoDataLineString::ConstIterator itBegin = lineString.constBegin(); 0291 GeoDataLineString::ConstIterator itEnd = lineString.constEnd(); 0292 0293 bool processingLastNode = false; 0294 0295 // We use a while loop to be able to cover linestrings as well as linear rings: 0296 // Linear rings require to tessellate the path from the last node to the first node 0297 // which isn't really convenient to achieve with a for loop ... 0298 0299 const bool isLong = lineString.size() > 10; 0300 const int maximumDetail = levelForResolution(viewport->angularResolution()); 0301 // The first node of optimized linestrings has a non-zero detail value. 0302 const bool hasDetail = itBegin->detail() != 0; 0303 0304 bool isStraight = lineString.latLonAltBox().height() == 0 || lineString.latLonAltBox().width() == 0; 0305 0306 Q_Q( const CylindricalProjection ); 0307 bool const isClosed = lineString.isClosed(); 0308 while ( itCoords != itEnd ) 0309 { 0310 // Optimization for line strings with a big amount of nodes 0311 bool skipNode = (hasDetail ? itCoords->detail() > maximumDetail 0312 : isLong && !processingLastNode && itCoords != itBegin && 0313 !viewport->resolves( *itPreviousCoords, *itCoords ) ); 0314 0315 if ( !skipNode || noFilter) { 0316 q->screenCoordinates( *itCoords, viewport, x, y ); 0317 0318 // Initializing variables that store the values of the previous iteration 0319 if ( !processingLastNode && itCoords == itBegin ) { 0320 itPreviousCoords = itCoords; 0321 previousX = x; 0322 previousY = y; 0323 } 0324 0325 // This if-clause contains the section that tessellates the line 0326 // segments of a linestring. If you are about to learn how the code of 0327 // this class works you can safely ignore this section for a start. 0328 if ( tessellate && !isStraight) { 0329 mirrorCount = tessellateLineSegment( *itPreviousCoords, previousX, previousY, 0330 *itCoords, x, y, 0331 polygons, viewport, 0332 f, mirrorCount, distance ); 0333 } 0334 0335 else { 0336 // special case for polys which cross dateline but have no Tesselation Flag 0337 // the expected rendering is a screen coordinates straight line between 0338 // points, but in projections with repeatX things are not smooth 0339 mirrorCount = crossDateLine( *itPreviousCoords, *itCoords, x, y, polygons, mirrorCount, distance ); 0340 } 0341 0342 itPreviousCoords = itCoords; 0343 previousX = x; 0344 previousY = y; 0345 } 0346 0347 // Here we modify the condition to be able to process the 0348 // first node after the last node in a LinearRing. 0349 0350 if ( processingLastNode ) { 0351 break; 0352 } 0353 ++itCoords; 0354 0355 if (isClosed && itCoords == itEnd) { 0356 itCoords = itBegin; 0357 processingLastNode = true; 0358 } 0359 } 0360 0361 // Closing e.g. in the Antarctica case. 0362 // This code makes the assumption that 0363 // - the first node is located at 180 E 0364 // - and the last node is located at 180 W 0365 // TODO: add a similar pattern in the crossDateLine() code. 0366 /* 0367 GeoDataLatLonAltBox box = lineString.latLonAltBox(); 0368 if( lineString.isClosed() && box.width() == 2*M_PI ) { 0369 QPolygonF *poly = polygons.last(); 0370 if( box.containsPole( NorthPole ) ) { 0371 qreal topMargin = 0.0; 0372 qreal dummy = 0.0; 0373 q_ptr->screenCoordinates(0.0, q_ptr->maxLat(), viewport, topMargin, dummy ); 0374 poly->push_back( QPointF( poly->last().x(), topMargin ) ); 0375 poly->push_back( QPointF( poly->first().x(), topMargin ) ); 0376 } else { 0377 qreal bottomMargin = 0.0; 0378 qreal dummy = 0.0; 0379 q_ptr->screenCoordinates(0.0, q_ptr->minLat(), viewport, bottomMargin, dummy ); 0380 poly->push_back( QPointF( poly->last().x(), bottomMargin ) ); 0381 poly->push_back( QPointF( poly->first().x(), bottomMargin ) ); 0382 } 0383 } */ 0384 0385 repeatPolygons( viewport, polygons ); 0386 0387 return polygons.isEmpty(); 0388 } 0389 0390 void CylindricalProjectionPrivate::translatePolygons( const QVector<QPolygonF *> &polygons, 0391 QVector<QPolygonF *> &translatedPolygons, 0392 qreal xOffset ) 0393 { 0394 // qCDebug(DIGIKAM_MARBLE_LOG) << "Translation: " << xOffset; 0395 translatedPolygons.reserve(polygons.size()); 0396 0397 QVector<QPolygonF *>::const_iterator itPolygon = polygons.constBegin(); 0398 QVector<QPolygonF *>::const_iterator itEnd = polygons.constEnd(); 0399 0400 for( ; itPolygon != itEnd; ++itPolygon ) { 0401 QPolygonF * polygon = new QPolygonF; 0402 *polygon = **itPolygon; 0403 polygon->translate( xOffset, 0 ); 0404 translatedPolygons.append( polygon ); 0405 } 0406 } 0407 0408 void CylindricalProjectionPrivate::repeatPolygons( const ViewportParams *viewport, 0409 QVector<QPolygonF *> &polygons ) const 0410 { 0411 Q_Q( const CylindricalProjection ); 0412 0413 qreal xEast = 0; 0414 qreal xWest = 0; 0415 qreal y = 0; 0416 0417 // Choose a latitude that is inside the viewport. 0418 const qreal centerLatitude = viewport->viewLatLonAltBox().center().latitude(); 0419 0420 const GeoDataCoordinates westCoords(-M_PI, centerLatitude); 0421 const GeoDataCoordinates eastCoords(+M_PI, centerLatitude); 0422 0423 q->screenCoordinates( westCoords, viewport, xWest, y ); 0424 q->screenCoordinates( eastCoords, viewport, xEast, y ); 0425 0426 if ( xWest <= 0 && xEast >= viewport->width() - 1 ) { 0427 // qCDebug(DIGIKAM_MARBLE_LOG) << "No repeats"; 0428 return; 0429 } 0430 0431 const qreal repeatXInterval = xEast - xWest; 0432 0433 const int repeatsLeft = (xWest > 0 ) ? (int)(xWest / repeatXInterval) + 1 : 0; 0434 const int repeatsRight = (xEast < viewport->width()) ? (int)((viewport->width() - xEast) / repeatXInterval) + 1 : 0; 0435 0436 QVector<QPolygonF *> repeatedPolygons; 0437 0438 for (int it = repeatsLeft; it > 0; --it) { 0439 const qreal xOffset = -it * repeatXInterval; 0440 QVector<QPolygonF *> translatedPolygons; 0441 translatePolygons( polygons, translatedPolygons, xOffset ); 0442 repeatedPolygons << translatedPolygons; 0443 } 0444 0445 repeatedPolygons << polygons; 0446 0447 for (int it = 1; it <= repeatsRight; ++it) { 0448 const qreal xOffset = +it * repeatXInterval; 0449 QVector<QPolygonF *> translatedPolygons; 0450 translatePolygons( polygons, translatedPolygons, xOffset ); 0451 repeatedPolygons << translatedPolygons; 0452 } 0453 0454 polygons = repeatedPolygons; 0455 0456 // qCDebug(DIGIKAM_MARBLE_LOG) << Q_FUNC_INFO << "Coordinates: " << xWest << xEast 0457 // << "Repeats: " << repeatsLeft << repeatsRight; 0458 } 0459 0460 qreal CylindricalProjectionPrivate::repeatDistance( const ViewportParams *viewport ) const 0461 { 0462 // Choose a latitude that is inside the viewport. 0463 qreal centerLatitude = viewport->viewLatLonAltBox().center().latitude(); 0464 0465 GeoDataCoordinates westCoords( -M_PI, centerLatitude ); 0466 GeoDataCoordinates eastCoords( +M_PI, centerLatitude ); 0467 qreal xWest, xEast, dummyY; 0468 0469 Q_Q( const AbstractProjection ); 0470 0471 q->screenCoordinates( westCoords, viewport, xWest, dummyY ); 0472 q->screenCoordinates( eastCoords, viewport, xEast, dummyY ); 0473 0474 return xEast - xWest; 0475 } 0476 0477 } 0478