File indexing completed on 2024-05-05 03:49:52

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     //    mDebug() << "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 &currentCoords,
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     // mDebug() << "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         // mDebug() << "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     // mDebug() << "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