diff --git a/src/lib/marble/VectorTileModel.cpp b/src/lib/marble/VectorTileModel.cpp index 3d137e46e..263e16308 100644 --- a/src/lib/marble/VectorTileModel.cpp +++ b/src/lib/marble/VectorTileModel.cpp @@ -1,267 +1,250 @@ /* This file is part of the Marble Virtual Globe. This program is free software licensed under the GNU LGPL. You can find a copy of this license in LICENSE.txt in the top directory of the source code. Copyright 2012 Ander Pijoan Copyright 2013 Bernhard Beschow */ #include "VectorTileModel.h" #include "GeoDataDocument.h" #include "GeoDataLatLonBox.h" #include "GeoDataTreeModel.h" #include "GeoDataTypes.h" #include "GeoSceneVectorTileDataset.h" #include "MarbleGlobal.h" #include "MarbleDebug.h" #include "MathHelper.h" +#include "MarbleMath.h" #include "TileId.h" #include "TileLoader.h" #include #include using namespace Marble; TileRunner::TileRunner( TileLoader *loader, const GeoSceneVectorTileDataset *texture, const TileId &id ) : m_loader( loader ), m_texture( texture ), m_id( id ) { } void TileRunner::run() { GeoDataDocument *const document = m_loader->loadTileVectorData( m_texture, m_id, DownloadBrowse ); emit documentLoaded( m_id, document ); } VectorTileModel::CacheDocument::CacheDocument(GeoDataDocument *doc, VectorTileModel *vectorTileModel, const GeoDataLatLonBox &boundingBox) : m_document( doc ), m_vectorTileModel(vectorTileModel), m_boundingBox(boundingBox) { // nothing to do } VectorTileModel::CacheDocument::~CacheDocument() { m_vectorTileModel->removeTile(m_document); } VectorTileModel::VectorTileModel( TileLoader *loader, const GeoSceneVectorTileDataset *layer, GeoDataTreeModel *treeModel, QThreadPool *threadPool ) : m_loader( loader ), m_layer( layer ), m_treeModel( treeModel ), m_threadPool( threadPool ), m_tileLoadLevel( -1 ), m_tileZoomLevel(-1) { connect(this, SIGNAL(tileAdded(GeoDataDocument*)), treeModel, SLOT(addDocument(GeoDataDocument*)) ); connect(this, SIGNAL(tileRemoved(GeoDataDocument*)), treeModel, SLOT(removeDocument(GeoDataDocument*)) ); connect(treeModel, SIGNAL(removed(GeoDataObject*)), this, SLOT(cleanupTile(GeoDataObject*)) ); } -void VectorTileModel::setViewport( const GeoDataLatLonBox &bbox, int radius ) +void VectorTileModel::setViewport( const GeoDataLatLonBox &latLonBox, int radius ) { // choose the smaller dimension for selecting the tile level, leading to higher-resolution results const int levelZeroWidth = m_layer->tileSize().width() * m_layer->levelZeroColumns(); const int levelZeroHight = m_layer->tileSize().height() * m_layer->levelZeroRows(); const int levelZeroMinDimension = qMin( levelZeroWidth, levelZeroHight ); qreal linearLevel = ( 4.0 * (qreal)( radius ) / (qreal)( levelZeroMinDimension ) ); if ( linearLevel < 1.0 ) linearLevel = 1.0; // Dirty fix for invalid entry linearLevel // As our tile resolution doubles with each level we calculate // the tile level from tilesize and the globe radius via log(2) qreal tileLevelF = qLn( linearLevel ) / qLn( 2.0 ); int tileZoomLevel = (int)( tileLevelF * 1.00001 ); // snap to the sharper tile level a tiny bit earlier // to work around rounding errors when the radius // roughly equals the global texture width m_tileZoomLevel = tileZoomLevel; + // Determine available tile levels in the layer and thereby + // select the tileZoomLevel that is actually used: QVector tileLevels = m_layer->tileLevels(); - if (tileLevels.isEmpty() || tileZoomLevel < tileLevels.first()) { + if (tileLevels.isEmpty() /* || tileZoomLevel < tileLevels.first() */) { + // if there is no (matching) tile level then show nothing + // and bail out. m_documents.clear(); return; } int tileLevel = tileLevels.first(); for (int i=1, n=tileLevels.size(); i tileZoomLevel) { break; } tileLevel = tileLevels[i]; } tileZoomLevel = tileLevel; // if zoom level has changed, empty vectortile cache if ( tileZoomLevel != m_tileLoadLevel ) { m_tileLoadLevel = tileZoomLevel; m_documents.clear(); } const unsigned int maxTileX = ( 1 << tileZoomLevel ) * m_layer->levelZeroColumns(); const unsigned int maxTileY = ( 1 << tileZoomLevel ) * m_layer->levelZeroRows(); /** LOGIC FOR DOWNLOADING ALL THE TILES THAT ARE INSIDE THE SCREEN AT THE CURRENT ZOOM LEVEL **/ // New tiles X and Y for moved screen coordinates // More info: http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Subtiles // More info: http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#C.2FC.2B.2B // Sometimes the formula returns wrong huge values, x and y have to be between 0 and 2^ZoomLevel - unsigned int minX = qMin( maxTileX, - qMax( lon2tileX( bbox.west(GeoDataCoordinates::Degree), maxTileX ), - 0 ) ); - - unsigned int minY = qMin( maxTileY, - qMax( lat2tileY( bbox.north(GeoDataCoordinates::Degree), maxTileY ), - 0 ) ); - - unsigned int maxX = qMax( 0, - qMin( lon2tileX( bbox.east(GeoDataCoordinates::Degree), maxTileX ), - maxTileX ) ); - - unsigned int maxY = qMax( 0, - qMin( lat2tileY( bbox.south(GeoDataCoordinates::Degree), maxTileY ), - maxTileY ) ); - - bool left = minX < maxTileX; - bool right = maxX > 0; - bool up = minY < maxTileY; - bool down = maxY > 0 ; + unsigned int westX = qBound( 0, lon2tileX( latLonBox.west(), maxTileX ), maxTileX); + unsigned int northY = qBound( 0, lat2tileY( latLonBox.north(), maxTileY ), maxTileY); + unsigned int eastX = qBound( 0, lon2tileX( latLonBox.east(), maxTileX ), maxTileX); + unsigned int southY = qBound( 0, lat2tileY( latLonBox.south(), maxTileY ), maxTileY ); // Download tiles and send them to VectorTileLayer // When changing zoom, download everything inside the screen - if ( left && right && up && down ) - - setViewport( tileZoomLevel, minX, minY, maxX, maxY ); - + if ( !latLonBox.crossesDateLine() ) { + queryTiles( tileZoomLevel, westX, northY, eastX, southY ); + } // When only moving screen, just download the new tiles - else if ( left || right || up || down ){ - - if ( left ) - setViewport( tileZoomLevel, minX, maxTileY, maxTileX, 0 ); - if ( right ) - setViewport( tileZoomLevel, 0, maxTileY, maxX, 0 ); - if ( up ) - setViewport( tileZoomLevel, maxTileX, minY, 0, maxTileY ); - if ( down ) - setViewport( tileZoomLevel, maxTileX, 0, 0, maxY ); - - // During testing discovered that this code above does not request the "corner" tiles - + else { + queryTiles( tileZoomLevel, 0, northY, eastX, southY ); + queryTiles( tileZoomLevel, westX, northY, maxTileX, southY ); } - - removeTilesOutOfView(bbox); + removeTilesOutOfView(latLonBox); } void VectorTileModel::removeTilesOutOfView(const GeoDataLatLonBox &boundingBox) { GeoDataLatLonBox const extendedViewport = boundingBox.scaled(2.0, 2.0); for (auto iter = m_documents.begin(); iter != m_documents.end();) { bool const isOutOfView = !extendedViewport.intersects(iter.value()->m_boundingBox); if (isOutOfView) { iter = m_documents.erase(iter); } else { ++iter; } } } QString VectorTileModel::name() const { return m_layer->name(); } void VectorTileModel::removeTile(GeoDataDocument *document) { emit tileRemoved(document); } int VectorTileModel::tileZoomLevel() const { return m_tileZoomLevel; } int VectorTileModel::cachedDocuments() const { return m_documents.size(); } void VectorTileModel::updateTile( const TileId &id, GeoDataDocument *document ) { m_pendingDocuments.removeAll(id); if (!document) { return; } if ( m_tileLoadLevel != id.zoomLevel() ) { delete document; return; } document->setName(QString("%1/%2/%3").arg(id.zoomLevel()).arg(id.x()).arg(id.y())); m_garbageQueue << document; if (m_documents.contains(id)) { m_documents.remove(id); } GeoDataLatLonBox const boundingBox = id.toLatLonBox(m_layer); m_documents[id] = QSharedPointer(new CacheDocument(document, this, boundingBox)); emit tileAdded(document); } void VectorTileModel::clear() { m_documents.clear(); } -void VectorTileModel::setViewport( int tileZoomLevel, +void VectorTileModel::queryTiles( int tileZoomLevel, unsigned int minTileX, unsigned int minTileY, unsigned int maxTileX, unsigned int maxTileY ) { // Download all the tiles inside the given indexes for ( unsigned int x = minTileX; x <= maxTileX; ++x ) { for ( unsigned int y = minTileY; y <= maxTileY; ++y ) { const TileId tileId = TileId( 0, tileZoomLevel, x, y ); if ( !m_documents.contains( tileId ) && !m_pendingDocuments.contains( tileId ) ) { m_pendingDocuments << tileId; TileRunner *job = new TileRunner( m_loader, m_layer, tileId ); connect( job, SIGNAL(documentLoaded(TileId,GeoDataDocument*)), this, SLOT(updateTile(TileId,GeoDataDocument*)) ); m_threadPool->start( job ); } } } } void VectorTileModel::cleanupTile(GeoDataObject *object) { if (object->nodeType() == GeoDataTypes::GeoDataDocumentType) { GeoDataDocument* document = static_cast(object); if (m_garbageQueue.contains(document)) { m_garbageQueue.removeAll(document); delete document; } } } unsigned int VectorTileModel::lon2tileX( qreal lon, unsigned int maxTileX ) { - return (unsigned int)floor((lon + 180.0) / 360.0 * maxTileX); + return (unsigned int)floor(0.5 * (lon / M_PI + 1.0) * maxTileX); } -unsigned int VectorTileModel::lat2tileY( qreal lat, unsigned int maxTileY ) +unsigned int VectorTileModel::lat2tileY( qreal latitude, unsigned int maxTileY ) { - return (unsigned int)floor((1.0 - log( tan(lat * M_PI/180.0) + 1.0 / cos(lat * M_PI/180.0)) / M_PI) / 2.0 * maxTileY); + // We need to calculate the tile position from the latitude + // projected using the Mercator projection. This requires the inverse Gudermannian + // function which is only defined between -85°S and 85°N. Therefore in order to + // prevent undefined results we need to restrict our calculation: + qreal maxAbsLat = 85.0 * DEG2RAD; + qreal lat = (qAbs(latitude) > maxAbsLat) ? latitude/qAbs(latitude) * maxAbsLat : latitude; + return (unsigned int)floor(0.5 * (1.0 - gdInv(lat) / M_PI) * maxTileY); } #include "moc_VectorTileModel.cpp" diff --git a/src/lib/marble/VectorTileModel.h b/src/lib/marble/VectorTileModel.h index de4dc064f..7bf99db0d 100644 --- a/src/lib/marble/VectorTileModel.h +++ b/src/lib/marble/VectorTileModel.h @@ -1,118 +1,118 @@ /* This file is part of the Marble Virtual Globe. This program is free software licensed under the GNU LGPL. You can find a copy of this license in LICENSE.txt in the top directory of the source code. Copyright 2012 Ander Pijoan Copyright 2013 Bernhard Beschow */ #ifndef MARBLE_VECTORTILEMODEL_H #define MARBLE_VECTORTILEMODEL_H #include #include #include #include "TileId.h" class QThreadPool; namespace Marble { class GeoDataDocument; class GeoDataLatLonBox; class GeoDataTreeModel; class GeoSceneVectorTileDataset; class GeoDataObject; class TileLoader; class TileRunner : public QObject, public QRunnable { Q_OBJECT public: TileRunner( TileLoader *loader, const GeoSceneVectorTileDataset *texture, const TileId &id ); void run(); Q_SIGNALS: void documentLoaded( const TileId &id, GeoDataDocument *document ); private: TileLoader *const m_loader; const GeoSceneVectorTileDataset *const m_texture; const TileId m_id; }; class VectorTileModel : public QObject { Q_OBJECT public: explicit VectorTileModel( TileLoader *loader, const GeoSceneVectorTileDataset *layer, GeoDataTreeModel *treeModel, QThreadPool *threadPool ); void setViewport( const GeoDataLatLonBox &bbox, int radius ); QString name() const; void removeTile(GeoDataDocument* document); int tileZoomLevel() const; int cachedDocuments() const; public Q_SLOTS: void updateTile( const TileId &id, GeoDataDocument *document ); void clear(); Q_SIGNALS: void tileCompleted( const TileId &tileId ); void tileAdded(GeoDataDocument *document); void tileRemoved(GeoDataDocument *document); private Q_SLOTS: void cleanupTile(GeoDataObject* feature); private: void removeTilesOutOfView(const GeoDataLatLonBox &boundingBox); - void setViewport( int tileZoomLevel, unsigned int minX, unsigned int minY, unsigned int maxX, unsigned int maxY ); + void queryTiles( int tileZoomLevel, unsigned int minX, unsigned int minY, unsigned int maxX, unsigned int maxY ); static unsigned int lon2tileX( qreal lon, unsigned int maxTileX ); static unsigned int lat2tileY( qreal lat, unsigned int maxTileY ); private: struct CacheDocument { /** The CacheDocument takes ownership of doc */ CacheDocument(GeoDataDocument *doc, VectorTileModel* vectorTileModel, const GeoDataLatLonBox &boundingBox); /** Remove the document from the tree and delete the document */ ~CacheDocument(); GeoDataDocument *const m_document; VectorTileModel *m_vectorTileModel; GeoDataLatLonBox m_boundingBox; private: Q_DISABLE_COPY( CacheDocument ) }; TileLoader *const m_loader; const GeoSceneVectorTileDataset *const m_layer; GeoDataTreeModel *const m_treeModel; QThreadPool *const m_threadPool; int m_tileLoadLevel; int m_tileZoomLevel; QList m_pendingDocuments; QList m_garbageQueue; QMap > m_documents; }; } #endif // MARBLE_VECTORTILEMODEL_H diff --git a/src/lib/marble/geodata/data/GeoDataLatLonBox.cpp b/src/lib/marble/geodata/data/GeoDataLatLonBox.cpp index f5a847113..ec57c5c19 100644 --- a/src/lib/marble/geodata/data/GeoDataLatLonBox.cpp +++ b/src/lib/marble/geodata/data/GeoDataLatLonBox.cpp @@ -1,815 +1,821 @@ // // This file is part of the Marble Virtual Globe. // // This program is free software licensed under the GNU LGPL. You can // find a copy of this license in LICENSE.txt in the top directory of // the source code. // // Copyright 2007 Andrew Manson // Copyright 2008-2009 Torsten Rahn // #include "GeoDataLatLonBox.h" #include "MarbleDebug.h" #include "GeoDataCoordinates.h" #include "GeoDataLineString.h" #include "GeoDataTypes.h" #include namespace Marble { const GeoDataLatLonBox GeoDataLatLonBox::empty = GeoDataLatLonBox(); class GeoDataLatLonBoxPrivate { public: GeoDataLatLonBoxPrivate() : m_north( 0.0 ), m_south( 0.0 ), m_east( 0.0 ), m_west( 0.0 ), m_rotation( 0.0 ) { } const char* nodeType() const { return GeoDataTypes::GeoDataLatLonBoxType; } qreal m_north; qreal m_south; qreal m_east; qreal m_west; qreal m_rotation; // NOT implemented yet! }; bool operator==( GeoDataLatLonBox const& lhs, GeoDataLatLonBox const& rhs ) { return lhs.d->m_west == rhs.d->m_west && lhs.d->m_east == rhs.d->m_east && lhs.d->m_north == rhs.d->m_north && lhs.d->m_south == rhs.d->m_south && lhs.d->m_rotation == rhs.d->m_rotation; } bool operator!=( GeoDataLatLonBox const& lhs, GeoDataLatLonBox const& rhs ) { return !( lhs == rhs ); } GeoDataLatLonBox::GeoDataLatLonBox() : GeoDataObject(), d( new GeoDataLatLonBoxPrivate ) { } GeoDataLatLonBox::GeoDataLatLonBox( qreal north, qreal south, qreal east, qreal west, GeoDataCoordinates::Unit unit ) : GeoDataObject(), d( new GeoDataLatLonBoxPrivate ) { setBoundaries( north, south, east, west, unit ); } GeoDataLatLonBox::GeoDataLatLonBox( const GeoDataLatLonBox & other ) : GeoDataObject( other ), d( new GeoDataLatLonBoxPrivate( *other.d ) ) { } GeoDataLatLonBox::~GeoDataLatLonBox() { delete d; } const char* GeoDataLatLonBox::nodeType() const { return d->nodeType(); } qreal GeoDataLatLonBox::north( GeoDataCoordinates::Unit unit ) const { if ( unit == GeoDataCoordinates::Degree ) { return d->m_north * RAD2DEG; } return d->m_north; } void GeoDataLatLonBox::setNorth( const qreal north, GeoDataCoordinates::Unit unit ) { switch( unit ){ default: case GeoDataCoordinates::Radian: d->m_north = GeoDataCoordinates::normalizeLat( north ); break; case GeoDataCoordinates::Degree: d->m_north = GeoDataCoordinates::normalizeLat( north * DEG2RAD ); break; } } qreal GeoDataLatLonBox::south( GeoDataCoordinates::Unit unit ) const { if ( unit == GeoDataCoordinates::Degree ) { return d->m_south * RAD2DEG; } return d->m_south; } void GeoDataLatLonBox::setSouth( const qreal south, GeoDataCoordinates::Unit unit ) { switch( unit ){ default: case GeoDataCoordinates::Radian: d->m_south = GeoDataCoordinates::normalizeLat( south ); break; case GeoDataCoordinates::Degree: d->m_south = GeoDataCoordinates::normalizeLat( south * DEG2RAD ); break; } } qreal GeoDataLatLonBox::east( GeoDataCoordinates::Unit unit ) const { if ( unit == GeoDataCoordinates::Degree ) { return d->m_east * RAD2DEG; } return d->m_east; } void GeoDataLatLonBox::setEast( const qreal east, GeoDataCoordinates::Unit unit ) { switch( unit ){ default: case GeoDataCoordinates::Radian: d->m_east = GeoDataCoordinates::normalizeLon( east ); break; case GeoDataCoordinates::Degree: d->m_east = GeoDataCoordinates::normalizeLon( east * DEG2RAD ); break; } } qreal GeoDataLatLonBox::west( GeoDataCoordinates::Unit unit ) const { if ( unit == GeoDataCoordinates::Degree ) { return d->m_west * RAD2DEG; } return d->m_west; } void GeoDataLatLonBox::setWest( const qreal west, GeoDataCoordinates::Unit unit ) { switch( unit ){ default: case GeoDataCoordinates::Radian: d->m_west = GeoDataCoordinates::normalizeLon( west ); break; case GeoDataCoordinates::Degree: d->m_west = GeoDataCoordinates::normalizeLon( west * DEG2RAD ); break; } } void GeoDataLatLonBox::setRotation( const qreal rotation, GeoDataCoordinates::Unit unit ) { switch( unit ){ default: case GeoDataCoordinates::Radian: d->m_rotation = rotation; break; case GeoDataCoordinates::Degree: d->m_rotation = rotation * DEG2RAD; break; } } qreal GeoDataLatLonBox::rotation( GeoDataCoordinates::Unit unit ) const { if ( unit == GeoDataCoordinates::Degree ) { return d->m_rotation * RAD2DEG; } return d->m_rotation; } void GeoDataLatLonBox::boundaries( qreal &north, qreal &south, qreal &east, qreal &west, GeoDataCoordinates::Unit unit ) const { switch( unit ){ default: case GeoDataCoordinates::Radian: north = d->m_north; south = d->m_south; east = d->m_east; west = d->m_west; break; case GeoDataCoordinates::Degree: north = d->m_north * RAD2DEG; south = d->m_south * RAD2DEG; east = d->m_east * RAD2DEG; west = d->m_west * RAD2DEG; break; } } void GeoDataLatLonBox::setBoundaries( qreal north, qreal south, qreal east, qreal west, GeoDataCoordinates::Unit unit ) { switch( unit ){ default: case GeoDataCoordinates::Radian: d->m_north = GeoDataCoordinates::normalizeLat( north ); d->m_south = GeoDataCoordinates::normalizeLat( south ); d->m_east = GeoDataCoordinates::normalizeLon( east ); d->m_west = GeoDataCoordinates::normalizeLon( west ); break; case GeoDataCoordinates::Degree: d->m_north = GeoDataCoordinates::normalizeLat( north * DEG2RAD ); d->m_south = GeoDataCoordinates::normalizeLat( south * DEG2RAD ); d->m_east = GeoDataCoordinates::normalizeLon( east * DEG2RAD ); d->m_west = GeoDataCoordinates::normalizeLon( west * DEG2RAD ); break; } } void GeoDataLatLonBox::scale(qreal verticalFactor, qreal horizontalFactor) const { GeoDataCoordinates const middle = center(); qreal const deltaY = 0.5 * height() * verticalFactor; qreal const deltaX = 0.5 * width() * horizontalFactor; d->m_north = GeoDataCoordinates::normalizeLat(middle.latitude() + deltaY); d->m_south = GeoDataCoordinates::normalizeLat(middle.latitude() - deltaY); - d->m_east = GeoDataCoordinates::normalizeLon(middle.longitude() + deltaX); - d->m_west = GeoDataCoordinates::normalizeLon(middle.longitude() - deltaX); + if (deltaX > 180 * DEG2RAD) { + d->m_east = M_PI; + d->m_west = -M_PI; + } + else { + d->m_east = GeoDataCoordinates::normalizeLon(middle.longitude() + deltaX); + d->m_west = GeoDataCoordinates::normalizeLon(middle.longitude() - deltaX); + } } GeoDataLatLonBox GeoDataLatLonBox::scaled(qreal verticalFactor, qreal horizontalFactor) const { GeoDataLatLonBox result = *this; result.scale(verticalFactor, horizontalFactor); return result; } qreal GeoDataLatLonBox::width( GeoDataCoordinates::Unit unit ) const { return GeoDataLatLonBox::width( d->m_east, d->m_west, unit ); } qreal GeoDataLatLonBox::width( qreal east, qreal west, GeoDataCoordinates::Unit unit ) { qreal width = fabs( (qreal)( GeoDataLatLonBox::crossesDateLine(east, west) ? 2 * M_PI - west + east : east - west ) ); // This also covers the case where this bounding box covers the whole // longitude range ( -180 <= lon <= + 180 ). if ( width > 2 * M_PI ) { width = 2 * M_PI; } if ( unit == GeoDataCoordinates::Degree ) { return width * RAD2DEG; } return width; } qreal GeoDataLatLonBox::height( GeoDataCoordinates::Unit unit ) const { return GeoDataLatLonBox::height(d->m_north, d->m_south, unit); } qreal GeoDataLatLonBox::height(qreal north, qreal south, GeoDataCoordinates::Unit unit) { qreal height = fabs( (qreal)( south - north ) ); if ( unit == GeoDataCoordinates::Degree ) { return height * RAD2DEG; } return height; } bool GeoDataLatLonBox::crossesDateLine() const { return GeoDataLatLonBox::crossesDateLine(d->m_east, d->m_west); } bool GeoDataLatLonBox::crossesDateLine(qreal east, qreal west) { if ( east < west || ( east == M_PI && west == -M_PI ) ) { return true; } return false; } GeoDataCoordinates GeoDataLatLonBox::center() const { if( isEmpty() ) return GeoDataCoordinates(); if( crossesDateLine() ) return GeoDataCoordinates( GeoDataCoordinates::normalizeLon( east() + 2 * M_PI - ( east() + 2 * M_PI - west() ) / 2 ) , north() - ( north() - south() ) / 2 ); else return GeoDataCoordinates( east() - ( east() - west() ) / 2, north() - ( north() - south() ) / 2 ); } bool GeoDataLatLonBox::containsPole( Pole pole ) const { switch ( pole ) { case NorthPole: return ( 2 * north() == +M_PI ); break; case SouthPole: return ( 2 * south() == -M_PI ); break; default: case AnyPole: return ( 2 * north() == +M_PI || 2 * south() == -M_PI ); break; } mDebug() << Q_FUNC_INFO << "Invalid pole"; return false; } bool GeoDataLatLonBox::contains(qreal lon, qreal lat) const { // We need to take care of the normal case ... if ( ( ( lon < d->m_west || lon > d->m_east ) && ( d->m_west < d->m_east ) ) || // ... and the case where the bounding box crosses the date line: ( ( lon < d->m_west && lon > d->m_east ) && ( d->m_west > d->m_east ) ) ) return false; if ( lat < d->m_south || lat > d->m_north ) return false; return true; } bool GeoDataLatLonBox::contains( const GeoDataCoordinates &point ) const { qreal lon, lat; point.geoCoordinates( lon, lat ); return contains(lon, lat); } bool GeoDataLatLonBox::contains( const GeoDataLatLonBox &other ) const { // check the contain criterion for the latitude first as this is trivial: if ( d->m_north >= other.north() && d->m_south <= other.south() ) { if ( !crossesDateLine() ) { if ( !other.crossesDateLine() ) { // "Normal" case: both bounding boxes don't cross the date line if ( d->m_west <= other.west() && d->m_east >= other.east() ) { return true; } } else { // The other bounding box crosses the date line, "this" one does not: // So the date line splits the other bounding box in two parts. // Hence "this" bounding box could be fully contained by one of them. // So for both cases we are able to ignore the "overhanging" portion // and thereby basically reduce the problem to the "normal" case: if ( ( other.west() <= d->m_west && d->m_east <= +M_PI ) || ( other.east() >= d->m_east && d->m_west >= -M_PI ) ) { return true; } } } else { if ( other.crossesDateLine() ) { // Other "Simple" case: both bounding boxes cross the date line if ( d->m_west <= other.west() && d->m_east >= other.east() ) { return true; } } else { // "This" bounding box crosses the date line, the other one does not. // So the date line splits "this" bounding box in two parts. // Hence the other bounding box could be fully contained by one of them. // So for both cases we are able to ignore the "overhanging" portion // and thereby basically reduce the problem to the "normal" case: if ( ( d->m_west <= other.west() && other.east() <= +M_PI ) || ( d->m_east >= other.east() && other.west() >= -M_PI ) ) { return true; } // if this bounding box covers the whole longitude range ( -180 <= lon <= + 180 ) // then of course the "inner" bounding box is "inside" if ( d->m_west == -M_PI && d->m_east == +M_PI ) { return true; } } } } return false; } bool GeoDataLatLonBox::intersects( const GeoDataLatLonBox &other ) const { if ( isEmpty() || other.isEmpty() ) { return false; } // check the intersection criterion for the latitude first: // Case 1: northern boundary of other box intersects: if ( ( d->m_north >= other.north() && d->m_south <= other.north() ) // Case 2: northern boundary of this box intersects: || ( other.north() >= d->m_north && other.south() <= d->m_north ) // Case 3: southern boundary of other box intersects: || ( d->m_north >= other.south() && d->m_south <= other.south() ) // Case 4: southern boundary of this box intersects: || ( other.north() >= d->m_south && other.south() <= d->m_south ) ) { if ( !crossesDateLine() ) { if ( !other.crossesDateLine() ) { // "Normal" case: both bounding boxes don't cross the date line // Case 1: eastern boundary of other box intersects: if ( ( d->m_east >= other.east() && d->m_west <= other.east() ) // Case 2: eastern boundary of this box intersects: || ( other.east() >= d->m_east && other.west() <= d->m_east ) // Case 3: western boundary of other box intersects: || ( d->m_east >= other.west() && d->m_west <= other.west() ) // Case 4: western boundary of this box intersects: || ( other.east() >= d->m_west && other.west() <= d->m_west ) ) { return true; } } else { // The other bounding box crosses the date line, "this" one does not: // So the date line splits the other bounding box in two parts. if ( d->m_west <= other.east() || d->m_east >= other.west() ) { return true; } } } else { if ( other.crossesDateLine() ) { // The trivial case: both bounding boxes cross the date line and intersect return true; } else { // "This" bounding box crosses the date line, the other one does not. // So the date line splits "this" bounding box in two parts. // // This also covers the case where this bounding box covers the whole // longitude range ( -180 <= lon <= + 180 ). if ( other.west() <= d->m_east || other.east() >= d->m_west ) { return true; } } } } return false; } GeoDataLatLonBox GeoDataLatLonBox::united( const GeoDataLatLonBox& other ) const { if ( isEmpty() ) { return other; } if ( other.isEmpty() ) { return *this; } GeoDataLatLonBox result; // use the position of the centers of the boxes to determine the "smallest" // box (i.e. should the total box go through IDL or not). this // determination does not depend on one box or the other crossing IDL too GeoDataCoordinates c1 = center(); GeoDataCoordinates c2 = other.center(); // do latitude first, quite simple result.setNorth(qMax( d->m_north, other.north() ) ); result.setSouth( qMin( d->m_south, other.south() ) ); qreal w1 = d->m_west; qreal w2 = other.west(); qreal e1 = d->m_east; qreal e2 = other.east(); bool const idl1 = d->m_east < d->m_west; bool const idl2 = other.d->m_east < other.d->m_west; if ( idl1 ) { w1 += 2* M_PI; e1 += 2* M_PI; } if ( idl2 ) { w2 += 2* M_PI; e2 += 2* M_PI; } // in the usual case, we take the maximum of east bounds, and // the minimum of west bounds. The exceptions are: // - centers of boxes are more than 180 apart // (so the smallest box should go around the IDL) // // - 1 but not 2 boxes are crossing IDL if ( fabs( c2.longitude()-c1.longitude() ) > M_PI || ( idl1 ^ idl2 ) ) { // exceptions, we go the unusual way: // min of east, max of west result.setEast( qMin( e1, e2 ) ); result.setWest( qMax( w1, w2 ) ); } else { // normal case, max of east, min of west result.setEast( qMax( e1, e2 ) ); result.setWest( qMin( w1, w2 ) ); } return result; } GeoDataLatLonBox GeoDataLatLonBox::toCircumscribedRectangle() const { QList coordinates; coordinates.append( GeoDataCoordinates( west(), north() ) ); coordinates.append( GeoDataCoordinates( west(), south() ) ); coordinates.append( GeoDataCoordinates( east() + ( crossesDateLine() ? 2 * M_PI : 0 ), north() ) ); coordinates.append( GeoDataCoordinates( east() + ( crossesDateLine() ? 2 * M_PI : 0 ), south() ) ); const qreal cosRotation = cos( rotation() ); const qreal sinRotation = sin( rotation() ); qreal centerLat = center().latitude(); qreal centerLon = center().longitude(); if ( GeoDataLatLonBox( 0, 0, center().longitude(), west() ).crossesDateLine() ) { if ( !centerLon ) centerLon += M_PI; else centerLon += 2 * M_PI; } GeoDataLatLonBox box; bool northSet = false; bool southSet = false; bool eastSet = false; bool westSet = false; foreach ( const GeoDataCoordinates& coord, coordinates ) { const qreal lon = coord.longitude(); const qreal lat = coord.latitude(); const qreal rotatedLon = ( lon - centerLon ) * cosRotation - ( lat - centerLat ) * sinRotation + centerLon; const qreal rotatedLat = ( lon - centerLon ) * sinRotation + ( lat - centerLat ) * cosRotation + centerLat; if ( !northSet || rotatedLat > box.north() ) { northSet = true; box.setNorth( rotatedLat ); } if ( !southSet || rotatedLat < box.south() ) { southSet = true; box.setSouth( rotatedLat ); } if ( !westSet || rotatedLon < box.west() ) { westSet = true; box.setWest( rotatedLon ); } if ( !eastSet || rotatedLon > box.east() ) { eastSet = true; box.setEast( rotatedLon ); } } box.setBoundaries( GeoDataCoordinates::normalizeLat( box.north() ), GeoDataCoordinates::normalizeLat( box.south() ), GeoDataCoordinates::normalizeLon( box.east() ), GeoDataCoordinates::normalizeLon( box.west() ) ); return box; } QString GeoDataLatLonBox::toString( GeoDataCoordinates::Unit unit ) const { switch( unit ){ default: case GeoDataCoordinates::Radian: return QString( "North: %1; West: %2; South: %3; East: %4" ) .arg( d->m_north ).arg( d->m_west ).arg( d->m_south ).arg( d->m_east ); break; case GeoDataCoordinates::Degree: return QString( "North: %1; West: %2; South: %3; East: %4" ) .arg( d->m_north * RAD2DEG ).arg( d->m_west * RAD2DEG ).arg( d->m_south * RAD2DEG ).arg( d->m_east * RAD2DEG ); break; } return QString( "GeoDataLatLonBox::text(): Error in unit: %1\n" ) .arg( unit ); } GeoDataLatLonBox& GeoDataLatLonBox::operator=( const GeoDataLatLonBox &other ) { GeoDataObject::operator=( other ); *d = *other.d; return *this; } GeoDataLatLonBox GeoDataLatLonBox::operator|( const GeoDataLatLonBox& other ) const { return united( other ); } GeoDataLatLonBox& GeoDataLatLonBox::operator|=( const GeoDataLatLonBox& other ) { *this = united( other ); return *this; } void GeoDataLatLonBox::pack( QDataStream& stream ) const { GeoDataObject::pack( stream ); stream << d->m_north << d->m_south << d->m_east << d->m_west << d->m_rotation; } void GeoDataLatLonBox::unpack( QDataStream& stream ) { GeoDataObject::unpack( stream ); stream >> d->m_north >> d->m_south >> d->m_east >> d->m_west >> d->m_rotation; } GeoDataLatLonBox GeoDataLatLonBox::fromLineString( const GeoDataLineString& lineString ) { // If the line string is empty return an empty boundingbox if ( lineString.isEmpty() ) { return GeoDataLatLonBox(); } qreal lon, lat; lineString.first().geoCoordinates( lon, lat ); GeoDataCoordinates::normalizeLonLat( lon, lat ); qreal north = lat; qreal south = lat; qreal west = lon; qreal east = lon; // If there's only a single node stored then the boundingbox only contains that point if ( lineString.size() == 1 ) return GeoDataLatLonBox( north, south, east, west ); // Specifies whether the polygon crosses the IDL bool idlCrossed = false; // "idlCrossState" specifies the state concerning IDL crossage. // This is needed in order to create optimal bounding boxes in case of covering the IDL // Every time the IDL gets crossed from east to west the idlCrossState value gets // increased by one. // Every time the IDL gets crossed from west to east the idlCrossState value gets // decreased by one. int idlCrossState = 0; int idlMaxCrossState = 0; int idlMinCrossState = 0; // Holds values for east and west while idlCrossState != 0 qreal otherWest = lon; qreal otherEast = lon; qreal previousLon = lon; int currentSign = ( lon < 0 ) ? -1 : +1; int previousSign = currentSign; QVector::ConstIterator it( lineString.constBegin() ); QVector::ConstIterator itEnd( lineString.constEnd() ); bool processingLastNode = false; while( it != itEnd ) { // Get coordinates and normalize them to the desired range. (it)->geoCoordinates( lon, lat ); GeoDataCoordinates::normalizeLonLat( lon, lat ); // Determining the maximum and minimum latitude if ( lat > north ) north = lat; if ( lat < south ) south = lat; currentSign = ( lon < 0 ) ? -1 : +1; // Once the polyline crosses the dateline the covered bounding box // would cover the whole [-M_PI; M_PI] range. // When looking separately at the longitude range that gets covered // east and west from the IDL we get two bounding boxes (we prefix // the resulting longitude range on the "other side" with "other"). // By picking the "inner" range values we get a more appropriate // optimized single bounding box. // IDL check if ( previousSign != currentSign && fabs( previousLon ) + fabs( lon ) > M_PI ) { // Initialize values for otherWest and otherEast if ( idlCrossed == false ) { otherWest = lon; otherEast = lon; idlCrossed = true; } // Determine the new IDL Cross State if ( previousLon < 0 ) { idlCrossState++; if ( idlCrossState > idlMaxCrossState ) { idlMaxCrossState = idlCrossState; } } else { idlCrossState--; if ( idlCrossState < idlMinCrossState ) { idlMinCrossState = idlCrossState; } } } if ( idlCrossState == 0 ) { if ( lon > east ) east = lon; if ( lon < west ) west = lon; } else { if ( lon > otherEast ) otherEast = lon; if ( lon < otherWest ) otherWest = lon; } previousLon = lon; previousSign = currentSign; if ( processingLastNode ) { break; } ++it; if( lineString.isClosed() && it == itEnd ) { it = lineString.constBegin(); processingLastNode = true; } } if ( idlCrossed ) { if ( idlMinCrossState < 0 ) { east = otherEast; } if ( idlMaxCrossState > 0 ) { west = otherWest; } if ( ( idlMinCrossState < 0 && idlMaxCrossState > 0 ) || idlMinCrossState < -1 || idlMaxCrossState > 1 || west <= east ) { east = +M_PI; west = -M_PI; // if polygon fully in south hemisphere, contain south pole if( north < 0 ) { south = -M_PI/2; } else { north = M_PI/2; } } } return GeoDataLatLonBox( north, south, east, west ); } bool GeoDataLatLonBox::isNull() const { if ( d->m_north == d->m_south && d->m_east == d->m_west ) return true; return false; } bool GeoDataLatLonBox::isEmpty() const { return *this == empty; } void GeoDataLatLonBox::clear() { *this = empty; } }