1 // SPDX-FileCopyrightText: 2008 David Roberts <dvdr18@gmail.com>
2 // SPDX-FileCopyrightText: 2009 Jens-Michael Hoffmann <jensmh@gmx.de>
3 // SPDX-FileCopyrightText: 2011 Bernhard Beschow <bbeschow@cs.tu-berlin.de>
4 //
5 // SPDX-License-Identifier: LGPL-2.1-or-later
6
7
8 #include "MergedLayerDecorator.h"
9
10 #include "blendings/Blending.h"
11 #include "blendings/BlendingFactory.h"
12 #include "SunLocator.h"
13 #include "MarbleMath.h"
14 #include "MarbleDebug.h"
15 #include "GeoDataGroundOverlay.h"
16 #include "GeoSceneTextureTileDataset.h"
17 #include "ImageF.h"
18 #include "StackedTile.h"
19 #include "TileLoaderHelper.h"
20 #include "TextureTile.h"
21 #include "TileLoader.h"
22 #include "RenderState.h"
23
24 #include "GeoDataCoordinates.h"
25
26 #include <QPointer>
27 #include <QPainter>
28 #include <QPainterPath>
29
30 using namespace Marble;
31
32 class Q_DECL_HIDDEN MergedLayerDecorator::Private
33 {
34 public:
35 Private( TileLoader *tileLoader, const SunLocator *sunLocator );
36
37 static int maxDivisor( int maximum, int fullLength );
38
39 StackedTile *createTile( const QVector<QSharedPointer<TextureTile> > &tiles ) const;
40
41 void renderGroundOverlays( QImage *tileImage, const QVector<QSharedPointer<TextureTile> > &tiles ) const;
42 void paintSunShading( QImage *tileImage, const TileId &id ) const;
43 void paintTileId( QImage *tileImage, const TileId &id ) const;
44
45 void detectMaxTileLevel();
46 QVector<const GeoSceneTextureTileDataset *> findRelevantTextureLayers( const TileId &stackedTileId ) const;
47
48 TileLoader *const m_tileLoader;
49 const SunLocator *const m_sunLocator;
50 BlendingFactory m_blendingFactory;
51 QVector<const GeoSceneTextureTileDataset *> m_textureLayers;
52 QList<const GeoDataGroundOverlay *> m_groundOverlays;
53 int m_maxTileLevel;
54 QString m_themeId;
55 int m_levelZeroColumns;
56 int m_levelZeroRows;
57 bool m_showSunShading;
58 bool m_showCityLights;
59 bool m_showTileId;
60 };
61
Private(TileLoader * tileLoader,const SunLocator * sunLocator)62 MergedLayerDecorator::Private::Private( TileLoader *tileLoader, const SunLocator *sunLocator ) :
63 m_tileLoader( tileLoader ),
64 m_sunLocator( sunLocator ),
65 m_blendingFactory( sunLocator ),
66 m_textureLayers(),
67 m_maxTileLevel( 0 ),
68 m_themeId(),
69 m_levelZeroColumns( 0 ),
70 m_levelZeroRows( 0 ),
71 m_showSunShading( false ),
72 m_showCityLights( false ),
73 m_showTileId( false )
74 {
75 }
76
MergedLayerDecorator(TileLoader * const tileLoader,const SunLocator * sunLocator)77 MergedLayerDecorator::MergedLayerDecorator( TileLoader * const tileLoader,
78 const SunLocator* sunLocator )
79 : d( new Private( tileLoader, sunLocator ) )
80 {
81 }
82
~MergedLayerDecorator()83 MergedLayerDecorator::~MergedLayerDecorator()
84 {
85 delete d;
86 }
87
setTextureLayers(const QVector<const GeoSceneTextureTileDataset * > & textureLayers)88 void MergedLayerDecorator::setTextureLayers( const QVector<const GeoSceneTextureTileDataset *> &textureLayers )
89 {
90 if ( textureLayers.count() > 0 ) {
91 const GeoSceneTileDataset *const firstTexture = textureLayers.at( 0 );
92 d->m_levelZeroColumns = firstTexture->levelZeroColumns();
93 d->m_levelZeroRows = firstTexture->levelZeroRows();
94 d->m_blendingFactory.setLevelZeroLayout( d->m_levelZeroColumns, d->m_levelZeroRows );
95 d->m_themeId = QLatin1String("maps/") + firstTexture->sourceDir();
96 }
97
98 d->m_textureLayers = textureLayers;
99
100 d->detectMaxTileLevel();
101 }
102
updateGroundOverlays(const QList<const GeoDataGroundOverlay * > & groundOverlays)103 void MergedLayerDecorator::updateGroundOverlays(const QList<const GeoDataGroundOverlay *> &groundOverlays )
104 {
105 d->m_groundOverlays = groundOverlays;
106 }
107
108
textureLayersSize() const109 int MergedLayerDecorator::textureLayersSize() const
110 {
111 return d->m_textureLayers.size();
112 }
113
maximumTileLevel() const114 int MergedLayerDecorator::maximumTileLevel() const
115 {
116 return d->m_maxTileLevel;
117 }
118
tileColumnCount(int level) const119 int MergedLayerDecorator::tileColumnCount( int level ) const
120 {
121 Q_ASSERT( !d->m_textureLayers.isEmpty() );
122
123 const int levelZeroColumns = d->m_textureLayers.at( 0 )->levelZeroColumns();
124
125 return TileLoaderHelper::levelToColumn( levelZeroColumns, level );
126 }
127
tileRowCount(int level) const128 int MergedLayerDecorator::tileRowCount( int level ) const
129 {
130 Q_ASSERT( !d->m_textureLayers.isEmpty() );
131
132 const int levelZeroRows = d->m_textureLayers.at( 0 )->levelZeroRows();
133
134 return TileLoaderHelper::levelToRow( levelZeroRows, level );
135 }
136
tileProjection() const137 const GeoSceneAbstractTileProjection *MergedLayerDecorator::tileProjection() const
138 {
139 Q_ASSERT( !d->m_textureLayers.isEmpty() );
140
141 return d->m_textureLayers.at(0)->tileProjection();
142 }
143
tileSize() const144 QSize MergedLayerDecorator::tileSize() const
145 {
146 Q_ASSERT( !d->m_textureLayers.isEmpty() );
147
148 return d->m_textureLayers.at( 0 )->tileSize();
149 }
150
createTile(const QVector<QSharedPointer<TextureTile>> & tiles) const151 StackedTile *MergedLayerDecorator::Private::createTile( const QVector<QSharedPointer<TextureTile> > &tiles ) const
152 {
153 Q_ASSERT( !tiles.isEmpty() );
154
155 const TileId firstId = tiles.first()->id();
156 const TileId id( 0, firstId.zoomLevel(), firstId.x(), firstId.y() );
157
158 // Image for blending all the texture tiles on it
159 QImage resultImage;
160
161 // if there are more than one active texture layers, we have to convert the
162 // result tile into QImage::Format_ARGB32_Premultiplied to make blending possible
163 const bool withConversion = tiles.count() > 1 || m_showSunShading || m_showTileId || !m_groundOverlays.isEmpty();
164 for ( const QSharedPointer<TextureTile> &tile: tiles ) {
165
166 // Image blending. If there are several images in the same tile (like clouds
167 // or hillshading images over the map) blend them all into only one image
168
169 const Blending *const blending = tile->blending();
170 if ( blending ) {
171
172 mDebug() << Q_FUNC_INFO << "blending";
173
174 if ( resultImage.isNull() ) {
175 resultImage = QImage( tile->image()->size(), QImage::Format_ARGB32_Premultiplied );
176 }
177
178 blending->blend( &resultImage, tile.data() );
179 }
180 else {
181 mDebug() << Q_FUNC_INFO << "no blending defined => copying top over bottom image";
182 if ( withConversion ) {
183 resultImage = tile->image()->convertToFormat( QImage::Format_ARGB32_Premultiplied );
184 } else {
185 resultImage = tile->image()->copy();
186 }
187 }
188 }
189
190 renderGroundOverlays( &resultImage, tiles );
191
192 if ( m_showSunShading && !m_showCityLights ) {
193 paintSunShading( &resultImage, id );
194 }
195
196 if ( m_showTileId ) {
197 paintTileId( &resultImage, id );
198 }
199
200 return new StackedTile( id, resultImage, tiles );
201 }
202
renderGroundOverlays(QImage * tileImage,const QVector<QSharedPointer<TextureTile>> & tiles) const203 void MergedLayerDecorator::Private::renderGroundOverlays( QImage *tileImage, const QVector<QSharedPointer<TextureTile> > &tiles ) const
204 {
205
206 /* All tiles are covering the same area. Pick one. */
207 const TileId tileId = tiles.first()->id();
208
209 const GeoDataLatLonBox tileLatLonBox = findRelevantTextureLayers(tileId).first()->tileProjection()->geoCoordinates(tileId);
210
211 /* Map the ground overlay to the image. */
212 for ( int i = 0; i < m_groundOverlays.size(); ++i ) {
213
214 const GeoDataGroundOverlay* overlay = m_groundOverlays.at( i );
215 if ( !overlay->isGloballyVisible() ) {
216 continue;
217 }
218
219 const GeoDataLatLonBox overlayLatLonBox = overlay->latLonBox();
220
221 if ( !tileLatLonBox.intersects( overlayLatLonBox.toCircumscribedRectangle() ) ) {
222 continue;
223 }
224
225 const qreal pixelToLat = tileLatLonBox.height() / tileImage->height();
226 const qreal pixelToLon = tileLatLonBox.width() / tileImage->width();
227
228 const qreal latToPixel = overlay->icon().height() / overlayLatLonBox.height();
229 const qreal lonToPixel = overlay->icon().width() / overlayLatLonBox.width();
230
231 const qreal global_height = tileImage->height()
232 * TileLoaderHelper::levelToRow( m_levelZeroRows, tileId.zoomLevel() );
233 const qreal pixel2Rad = M_PI / global_height;
234 const qreal rad2Pixel = global_height / M_PI;
235
236 qreal latPixelPosition = rad2Pixel/2 * gdInv(tileLatLonBox.north());
237 const bool isMercatorTileProjection = (m_textureLayers.at( 0 )->tileProjectionType() == GeoSceneAbstractTileProjection::Mercator);
238
239 for ( int y = 0; y < tileImage->height(); ++y ) {
240 QRgb *scanLine = ( QRgb* ) ( tileImage->scanLine( y ) );
241
242 const qreal lat = isMercatorTileProjection
243 ? gd(2 * (latPixelPosition - y) * pixel2Rad )
244 : tileLatLonBox.north() - y * pixelToLat;
245
246 for ( int x = 0; x < tileImage->width(); ++x, ++scanLine ) {
247 qreal lon = GeoDataCoordinates::normalizeLon( tileLatLonBox.west() + x * pixelToLon );
248
249 GeoDataCoordinates coords(lon, lat);
250 GeoDataCoordinates rotatedCoords(coords);
251
252 if (overlay->latLonBox().rotation() != 0) {
253 // Possible TODO: Make this faster by creating the axisMatrix beforehand
254 // and just call Quaternion::rotateAroundAxis(const matrix &m) here.
255 rotatedCoords = coords.rotateAround(overlayLatLonBox.center(), -overlay->latLonBox().rotation());
256 }
257
258 // TODO: The rotated latLonBox is bigger. We need to take this into account.
259 // (Currently the GroundOverlay sometimes gets clipped because of that)
260 if ( overlay->latLonBox().contains( rotatedCoords ) ) {
261
262 qreal px = GeoDataLatLonBox::width( rotatedCoords.longitude(), overlayLatLonBox.west() ) * lonToPixel;
263 qreal py = (qreal)( overlay->icon().height() ) - ( GeoDataLatLonBox::height( rotatedCoords.latitude(), overlayLatLonBox.south() ) * latToPixel ) - 1;
264
265 if ( px >= 0 && px < overlay->icon().width() && py >= 0 && py < overlay->icon().height() ) {
266 int alpha = qAlpha( overlay->icon().pixel( px, py ) );
267 if ( alpha != 0 )
268 {
269 QRgb result = ImageF::pixelF( overlay->icon(), px, py );
270
271 if (alpha == 255)
272 {
273 *scanLine = result;
274 }
275 else
276 {
277 *scanLine = qRgb( ( alpha * qRed(result) + (255 - alpha) * qRed(*scanLine) ) / 255,
278 ( alpha * qGreen(result) + (255 - alpha) * qGreen(*scanLine) ) / 255,
279 ( alpha * qBlue(result) + (255 - alpha) * qBlue(*scanLine) ) / 255 );
280 }
281 }
282 }
283 }
284 }
285 }
286 }
287 }
288
loadTile(const TileId & stackedTileId)289 StackedTile *MergedLayerDecorator::loadTile( const TileId &stackedTileId )
290 {
291 const QVector<const GeoSceneTextureTileDataset *> textureLayers = d->findRelevantTextureLayers( stackedTileId );
292 QVector<QSharedPointer<TextureTile> > tiles;
293 tiles.reserve(textureLayers.size());
294
295 for ( const GeoSceneTextureTileDataset *layer: textureLayers ) {
296 const TileId tileId( layer->sourceDir(), stackedTileId.zoomLevel(),
297 stackedTileId.x(), stackedTileId.y() );
298
299 mDebug() << Q_FUNC_INFO << layer->sourceDir() << tileId << layer->tileSize() << layer->fileFormat();
300
301 // Blending (how to merge the images into an only image)
302 const Blending *blending = d->m_blendingFactory.findBlending( layer->blending() );
303 if ( blending == nullptr && !layer->blending().isEmpty() ) {
304 mDebug() << Q_FUNC_INFO << "could not find blending" << layer->blending();
305 }
306
307 const GeoSceneTextureTileDataset *const textureLayer = static_cast<const GeoSceneTextureTileDataset *>( layer );
308 const QImage tileImage = d->m_tileLoader->loadTileImage( textureLayer, tileId, DownloadBrowse );
309
310 QSharedPointer<TextureTile> tile( new TextureTile( tileId, tileImage, blending ) );
311 tiles.append( tile );
312 }
313
314 Q_ASSERT( !tiles.isEmpty() );
315
316 return d->createTile( tiles );
317 }
318
renderState(const TileId & stackedTileId) const319 RenderState MergedLayerDecorator::renderState( const TileId &stackedTileId ) const
320 {
321 QString const nameTemplate = "Tile %1/%2/%3";
322 RenderState state( nameTemplate.arg( stackedTileId.zoomLevel() )
323 .arg( stackedTileId.x() )
324 .arg( stackedTileId.y() ) );
325 const QVector<const GeoSceneTextureTileDataset *> textureLayers = d->findRelevantTextureLayers( stackedTileId );
326 for ( const GeoSceneTextureTileDataset *layer: textureLayers ) {
327 const TileId tileId( layer->sourceDir(), stackedTileId.zoomLevel(),
328 stackedTileId.x(), stackedTileId.y() );
329 RenderStatus tileStatus = Complete;
330 switch ( TileLoader::tileStatus( layer, tileId ) ) {
331 case TileLoader::Available:
332 tileStatus = Complete;
333 break;
334 case TileLoader::Expired:
335 tileStatus = WaitingForUpdate;
336 break;
337 case TileLoader::Missing:
338 tileStatus = WaitingForData;
339 break;
340 }
341
342 state.addChild( RenderState( layer->name(), tileStatus ) );
343 }
344
345 return state;
346 }
347
hasTextureLayer() const348 bool MergedLayerDecorator::hasTextureLayer() const
349 {
350 return !d->m_textureLayers.isEmpty();
351 }
352
updateTile(const StackedTile & stackedTile,const TileId & tileId,const QImage & tileImage)353 StackedTile *MergedLayerDecorator::updateTile( const StackedTile &stackedTile, const TileId &tileId, const QImage &tileImage )
354 {
355 Q_ASSERT( !tileImage.isNull() );
356
357 d->detectMaxTileLevel();
358
359 QVector<QSharedPointer<TextureTile> > tiles = stackedTile.tiles();
360
361 for ( int i = 0; i < tiles.count(); ++ i) {
362 if ( tiles[i]->id() == tileId ) {
363 const Blending *blending = tiles[i]->blending();
364
365 tiles[i] = QSharedPointer<TextureTile>( new TextureTile( tileId, tileImage, blending ) );
366 }
367 }
368
369 return d->createTile( tiles );
370 }
371
downloadStackedTile(const TileId & id,DownloadUsage usage)372 void MergedLayerDecorator::downloadStackedTile( const TileId &id, DownloadUsage usage )
373 {
374 const QVector<const GeoSceneTextureTileDataset *> textureLayers = d->findRelevantTextureLayers( id );
375
376 for ( const GeoSceneTextureTileDataset *textureLayer: textureLayers ) {
377 if ( TileLoader::tileStatus( textureLayer, id ) != TileLoader::Available || usage == DownloadBrowse ) {
378 d->m_tileLoader->downloadTile( textureLayer, id, usage );
379 }
380 }
381 }
382
setShowSunShading(bool show)383 void MergedLayerDecorator::setShowSunShading( bool show )
384 {
385 d->m_showSunShading = show;
386 }
387
showSunShading() const388 bool MergedLayerDecorator::showSunShading() const
389 {
390 return d->m_showSunShading;
391 }
392
setShowCityLights(bool show)393 void MergedLayerDecorator::setShowCityLights( bool show )
394 {
395 d->m_showCityLights = show;
396 }
397
showCityLights() const398 bool MergedLayerDecorator::showCityLights() const
399 {
400 return d->m_showCityLights;
401 }
402
setShowTileId(bool visible)403 void MergedLayerDecorator::setShowTileId( bool visible )
404 {
405 d->m_showTileId = visible;
406 }
407
paintSunShading(QImage * tileImage,const TileId & id) const408 void MergedLayerDecorator::Private::paintSunShading( QImage *tileImage, const TileId &id ) const
409 {
410 if ( tileImage->depth() != 32 )
411 return;
412
413 // TODO add support for 8-bit maps?
414 // add sun shading
415 const qreal global_width = tileImage->width()
416 * TileLoaderHelper::levelToColumn( m_levelZeroColumns, id.zoomLevel() );
417 const qreal global_height = tileImage->height()
418 * TileLoaderHelper::levelToRow( m_levelZeroRows, id.zoomLevel() );
419 const qreal lon_scale = 2*M_PI / global_width;
420 const qreal lat_scale = -M_PI / global_height;
421 const int tileHeight = tileImage->height();
422 const int tileWidth = tileImage->width();
423
424 // First we determine the supporting point interval for the interpolation.
425 const int n = maxDivisor( 30, tileWidth );
426 const int ipRight = n * (int)( tileWidth / n );
427
428 for ( int cur_y = 0; cur_y < tileHeight; ++cur_y ) {
429 const qreal lat = lat_scale * ( id.y() * tileHeight + cur_y ) - 0.5*M_PI;
430 const qreal a = sin( (lat+DEG2RAD * m_sunLocator->getLat() )/2.0 );
431 const qreal c = cos(lat)*cos( -DEG2RAD * m_sunLocator->getLat() );
432
433 QRgb* scanline = (QRgb*)tileImage->scanLine( cur_y );
434
435 qreal lastShade = -10.0;
436
437 int cur_x = 0;
438
439 while ( cur_x < tileWidth ) {
440
441 const bool interpolate = ( cur_x != 0 && cur_x < ipRight && cur_x + n < tileWidth );
442
443 qreal shade = 0;
444
445 if ( interpolate ) {
446 const int check = cur_x + n;
447 const qreal checklon = lon_scale * ( id.x() * tileWidth + check );
448 shade = m_sunLocator->shading( checklon, a, c );
449
450 // if the shading didn't change across the interpolation
451 // interval move on and don't change anything.
452 if ( shade == lastShade && shade == 1.0 ) {
453 scanline += n;
454 cur_x += n;
455 continue;
456 }
457 if ( shade == lastShade && shade == 0.0 ) {
458 for ( int t = 0; t < n; ++t ) {
459 SunLocator::shadePixel(*scanline, shade);
460 ++scanline;
461 }
462 cur_x += n;
463 continue;
464 }
465 for ( int t = 0; t < n ; ++t ) {
466 const qreal lon = lon_scale * ( id.x() * tileWidth + cur_x );
467 shade = m_sunLocator->shading( lon, a, c );
468 SunLocator::shadePixel(*scanline, shade);
469 ++scanline;
470 ++cur_x;
471 }
472 }
473
474 else {
475 // Make sure we don't exceed the image memory
476 if ( cur_x < tileWidth ) {
477 const qreal lon = lon_scale * ( id.x() * tileWidth + cur_x );
478 shade = m_sunLocator->shading( lon, a, c );
479 SunLocator::shadePixel(*scanline, shade);
480 ++scanline;
481 ++cur_x;
482 }
483 }
484 lastShade = shade;
485 }
486 }
487 }
488
paintTileId(QImage * tileImage,const TileId & id) const489 void MergedLayerDecorator::Private::paintTileId( QImage *tileImage, const TileId &id ) const
490 {
491 QString filename = QString( "%1_%2.jpg" )
492 .arg(id.x(), tileDigits, 10, QLatin1Char('0'))
493 .arg(id.y(), tileDigits, 10, QLatin1Char('0'));
494
495 QPainter painter( tileImage );
496
497 QColor foreground;
498 QColor background;
499
500 if ( ( (qreal)(id.x())/2 == id.x()/2 && (qreal)(id.y())/2 == id.y()/2 )
501 || ( (qreal)(id.x())/2 != id.x()/2 && (qreal)(id.y())/2 != id.y()/2 )
502 )
503 {
504 foreground.setNamedColor( "#FFFFFF" );
505 background.setNamedColor( "#000000" );
506 }
507 else {
508 foreground.setNamedColor( "#000000" );
509 background.setNamedColor( "#FFFFFF" );
510 }
511
512 int strokeWidth = 10;
513 QPen testPen( foreground );
514 testPen.setWidth( strokeWidth );
515 testPen.setJoinStyle( Qt::MiterJoin );
516
517 painter.setPen( testPen );
518 painter.drawRect( strokeWidth / 2, strokeWidth / 2,
519 tileImage->width() - strokeWidth,
520 tileImage->height() - strokeWidth );
521 QFont testFont(QStringLiteral("Sans Serif"), 12);
522 QFontMetrics testFm( testFont );
523 painter.setFont( testFont );
524
525 QPen outlinepen( foreground );
526 outlinepen.setWidthF( 6 );
527
528 painter.setPen( outlinepen );
529 painter.setBrush( background );
530
531 QPainterPath outlinepath;
532
533 QPointF baseline1( ( tileImage->width() - testFm.boundingRect(filename).width() ) / 2,
534 ( tileImage->height() * 0.25) );
535 outlinepath.addText( baseline1, testFont, QString( "level: %1" ).arg(id.zoomLevel()) );
536
537 QPointF baseline2( ( tileImage->width() - testFm.boundingRect(filename).width() ) / 2,
538 tileImage->height() * 0.50 );
539 outlinepath.addText( baseline2, testFont, filename );
540
541 QPointF baseline3( ( tileImage->width() - testFm.boundingRect(filename).width() ) / 2,
542 tileImage->height() * 0.75 );
543 outlinepath.addText( baseline3, testFont, m_themeId );
544
545 painter.drawPath( outlinepath );
546
547 painter.setPen( Qt::NoPen );
548 painter.drawPath( outlinepath );
549 }
550
detectMaxTileLevel()551 void MergedLayerDecorator::Private::detectMaxTileLevel()
552 {
553 if ( m_textureLayers.isEmpty() ) {
554 m_maxTileLevel = -1;
555 return;
556 }
557
558 m_maxTileLevel = TileLoader::maximumTileLevel( *m_textureLayers.at( 0 ) );
559 }
560
findRelevantTextureLayers(const TileId & stackedTileId) const561 QVector<const GeoSceneTextureTileDataset *> MergedLayerDecorator::Private::findRelevantTextureLayers( const TileId &stackedTileId ) const
562 {
563 QVector<const GeoSceneTextureTileDataset *> result;
564
565 for ( const GeoSceneTextureTileDataset *candidate: m_textureLayers ) {
566 Q_ASSERT( candidate );
567 // check, if layer provides tiles for the current level
568 if ( !candidate->hasMaximumTileLevel() ||
569 candidate->maximumTileLevel() >= stackedTileId.zoomLevel() ) {
570 //check if the tile intersects with texture bounds
571 if (candidate->latLonBox().isNull()) {
572 result.append(candidate);
573 }
574 else {
575 const GeoDataLatLonBox bbox = candidate->tileProjection()->geoCoordinates(stackedTileId);
576
577 if (candidate->latLonBox().intersects(bbox)) {
578 result.append( candidate );
579 }
580 }
581 }
582 }
583
584 return result;
585 }
586
587 // TODO: This should likely go into a math class in the future ...
588
maxDivisor(int maximum,int fullLength)589 int MergedLayerDecorator::Private::maxDivisor( int maximum, int fullLength )
590 {
591 // Find the optimal interpolation interval n for the
592 // current image canvas width
593 int best = 2;
594
595 int nEvalMin = fullLength;
596 for ( int it = 1; it <= maximum; ++it ) {
597 // The optimum is the interval which results in the least amount
598 // supporting points taking into account the rest which can't
599 // get used for interpolation.
600 int nEval = fullLength / it + fullLength % it;
601 if ( nEval < nEvalMin ) {
602 nEvalMin = nEval;
603 best = it;
604 }
605 }
606 return best;
607 }
608