1 /* -*-c++-*- */
2 /* osgEarth - Geospatial SDK for OpenSceneGraph
3  * Copyright 2019 Pelican Mapping
4  * http://osgearth.org
5  *
6  * osgEarth is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU Lesser General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU Lesser General Public License for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public License
17  * along with this program.  If not, see <http://www.gnu.org/licenses/>
18  */
19 #include <osgEarth/LandCoverLayer>
20 #include <osgEarth/Registry>
21 #include <osgEarth/Map>
22 #include <osgEarth/SimplexNoise>
23 #include <osgEarth/Progress>
24 
25 using namespace osgEarth;
26 
27 #define LC "[LandCoverLayer] "
28 
29 REGISTER_OSGEARTH_LAYER(land_cover, LandCoverLayer);
30 
31 namespace
32 {
getSplatCoords(const TileKey & key,float baseLOD,const osg::Vec2 & covUV)33     osg::Vec2 getSplatCoords(const TileKey& key, float baseLOD, const osg::Vec2& covUV)
34     {
35         osg::Vec2 out;
36 
37         float dL = (float)key.getLOD() - baseLOD;
38         float factor = pow(2.0f, dL);
39         float invFactor = 1.0/factor;
40         out.set( covUV.x()*invFactor, covUV.y()*invFactor );
41 
42         // For upsampling we need to calculate an offset as well
43         if ( factor >= 1.0 )
44         {
45             unsigned wide, high;
46             key.getProfile()->getNumTiles(key.getLOD(), wide, high);
47 
48             float tileX = (float)key.getTileX();
49             float tileY = (float)(wide-1-key.getTileY()); // swap Y. (not done in the shader version.)
50 
51             osg::Vec2 a( floor(tileX*invFactor), floor(tileY*invFactor) );
52             osg::Vec2 b( a.x()*factor, a.y()*factor );
53             osg::Vec2 c( (a.x()+1.0f)*factor, (a.y()+1.0f)*factor );
54             osg::Vec2 offset( (tileX-b.x())/(c.x()-b.x()), (tileY-b.y())/(c.y()-b.y()) );
55 
56             out += offset;
57         }
58 
59         return out;
60     }
61 
warpCoverageCoords(const osg::Vec2 & covIn,float noise,float warp)62     osg::Vec2 warpCoverageCoords(const osg::Vec2& covIn, float noise, float warp)
63     {
64         float n1 = 2.0 * noise - 1.0;
65         return osg::Vec2(
66             covIn.x() + sin(n1*osg::PI*2.0) * warp,
67             covIn.y() + sin(n1*osg::PI*2.0) * warp);
68     }
69 
getNoise(SimplexNoise & noiseGen,const osg::Vec2 & uv)70     float getNoise(SimplexNoise& noiseGen, const osg::Vec2& uv)
71     {
72         // TODO: check that u and v are 0..s and not 0..s-1
73         double n = noiseGen.getTiledValue(uv.x(), uv.y());
74         n = osg::clampBetween(n, 0.0, 1.0);
75         return n;
76     }
77 
78 
79     typedef std::vector<int> CodeMap;
80 
81     struct ILayer
82     {
83         GeoImage  image;
84         float     scale;
85         osg::Vec2 bias;
86         bool      valid;
87         float     warp;
88         ImageUtils::PixelReader* read;
89         unsigned* codeTable;
90         unsigned loads;
91 
ILayer__anon749ad3880111::ILayer92         ILayer() : valid(true), read(0L), scale(1.0f), warp(0.0f), loads(0) { }
93 
~ILayer__anon749ad3880111::ILayer94         ~ILayer() { if (read) delete read; }
95 
load__anon749ad3880111::ILayer96         void load(const TileKey& key, LandCoverCoverageLayer* sourceLayer, ProgressCallback* progress)
97         {
98             if (sourceLayer->getEnabled() &&
99                 sourceLayer->isKeyInLegalRange(key) &&
100                 sourceLayer->mayHaveData(key))
101             {
102                 for(TileKey k = key; k.valid() && !image.valid(); k = k.createParentKey())
103                 {
104                     image = sourceLayer->createImage(k, progress);
105 
106                     // check for cancelation:
107                     if (progress && progress->isCanceled())
108                     {
109                         break;
110                     }
111                 }
112             }
113 
114             valid = image.valid();
115 
116             if ( valid )
117             {
118                 scale = key.getExtent().width() / image.getExtent().width();
119                 bias.x() = (key.getExtent().xMin() - image.getExtent().xMin()) / image.getExtent().width();
120                 bias.y() = (key.getExtent().yMin() - image.getExtent().yMin()) / image.getExtent().height();
121 
122                 read = new ImageUtils::PixelReader(image.getImage());
123 
124                 // cannot interpolate coverage data:
125                 read->setBilinear( false );
126 
127                 warp = sourceLayer->options().warp().get();
128             }
129         }
130     };
131 
132     // Constructs a code map (int to int) for a coverage layer. We will use this
133     // code map to map coverage layer codes to dictionary codes.
buildCodeMap(const LandCoverCoverageLayer * coverage,CodeMap & codemap)134     void buildCodeMap(const LandCoverCoverageLayer* coverage, CodeMap& codemap)
135     {
136         if (!coverage) {
137             OE_WARN << LC << "ILLEGAL: coverage not passed to buildCodeMap\n";
138             return;
139         }
140         if (!coverage->getDictionary()) {
141             OE_WARN << LC << "ILLEGAL: coverage dictionary not set in buildCodeMap\n";
142             return;
143         }
144 
145         int highestValue = 0;
146 
147         for (LandCoverValueMappingVector::const_iterator k = coverage->getMappings().begin();
148             k != coverage->getMappings().end();
149             ++k)
150         {
151             const LandCoverValueMapping* mapping = k->get();
152             int value = mapping->getValue();
153             if (value > highestValue)
154                 highestValue = value;
155         }
156 
157         codemap.assign(highestValue+1, -1);
158 
159         for (LandCoverValueMappingVector::const_iterator k = coverage->getMappings().begin();
160             k != coverage->getMappings().end();
161             ++k)
162         {
163             const LandCoverValueMapping* mapping = k->get();
164             int value = mapping->getValue();
165             const LandCoverClass* lcClass = coverage->getDictionary()->getClassByName(mapping->getLandCoverClassName());
166             if (lcClass)
167             {
168                 codemap[value] = lcClass->getValue();
169             }
170         }
171     }
172 
173     typedef std::vector<osg::ref_ptr<LandCoverCoverageLayer> > LandCoverCoverageLayerVector;
174 
175     /*
176      * TileSource that provides GeoImage's to the LandCoverLayer.
177      */
178     class LandCoverTileSource : public TileSource
179     {
180     public:
181         LandCoverTileSource(const LandCoverLayerOptions& options);
182 
183     public: // TileSource
184         Status initialize(const osgDB::Options* readOptions);
185 
186         osg::Image* createImage(const TileKey& key, ProgressCallback* progress);
187 
getCachePolicyHint() const188         CachePolicy getCachePolicyHint() const {
189             return CachePolicy::NO_CACHE;
190         }
191 
192         const LandCoverLayerOptions* _options;
options() const193         const LandCoverLayerOptions& options() const { return *_options; }
194 
195         void setDictionary(LandCoverDictionary*);
196 
197         // image layers, one per data source
198         LandCoverCoverageLayerVector _coverages;
199 
200         // code maps (vector index is the source code; value is the destination code)
201         std::vector<CodeMap> _codemaps;
202 
203         // todo
204         std::vector<float> _warps;
205 
206         osg::ref_ptr<osgDB::Options> _readOptions;
207 
208         osg::ref_ptr<LandCoverDictionary> _lcDictionary;
209 
210         friend class LandCoverLayer;
211     };
212 
213 
LandCoverTileSource(const LandCoverLayerOptions & options)214     LandCoverTileSource::LandCoverTileSource(const LandCoverLayerOptions& options) :
215         TileSource(options),
216         _options(&options)
217     {
218         // Increase the L2 cache size since the parent LandCoverLayer is going to be
219         // using meta-tiling to create mosaics for warping
220         setDefaultL2CacheSize(64);
221     }
222 
223     Status
initialize(const osgDB::Options * readOptions)224     LandCoverTileSource::initialize(const osgDB::Options* readOptions)
225     {
226         const Profile* profile = getProfile();
227         if ( !profile )
228         {
229             profile = osgEarth::Registry::instance()->getGlobalGeodeticProfile();
230             setProfile( profile );
231         }
232 
233         for(unsigned i=0; i<options().coverages().size(); ++i)
234         {
235             LandCoverCoverageLayerOptions coverageOptions = options().coverages()[i];
236             if (coverageOptions.enabled() == false)
237                 continue;
238 
239             // We never want to cache data from a coverage, because the "parent" layer
240             // will be caching the entire result of a multi-coverage composite.
241             coverageOptions.cachePolicy() = CachePolicy::NO_CACHE;
242 
243             // Create the coverage layer:
244             LandCoverCoverageLayer* layer = new LandCoverCoverageLayer( coverageOptions );
245 
246             // Set up and open it.
247             layer->setTargetProfileHint( profile );
248             layer->setReadOptions(readOptions);
249             const Status& s = layer->open();
250             if (s.isOK())
251             {
252                 _coverages.push_back(layer);
253                 _codemaps.resize(_codemaps.size()+1);
254                 OE_INFO << LC << "Opened coverage \"" << layer->getName() << "\"\n";
255             }
256             else
257             {
258                 OE_WARN << "Layer \"" << layer->getName() << "\": " << s.toString() << std::endl;
259             }
260 
261             // Integrate data extents into this tile source.
262             const DataExtentList& de = layer->getDataExtents();
263             for(DataExtentList::const_iterator dei = de.begin(); dei != de.end(); ++dei)
264             {
265                 if (!profile || dei->getSRS()->isHorizEquivalentTo(profile->getSRS()))
266                 {
267                     getDataExtents().push_back(*dei);
268                 }
269                 else
270                 {
271                     // Transform the data extents to the layer profile
272                     GeoExtent ep = dei->transform(profile->getSRS());
273                     DataExtent de(ep);
274                     if (dei->minLevel().isSet())
275                         de.minLevel() = profile->getEquivalentLOD(layer->getProfile(), dei->minLevel().get());
276                     if (dei->maxLevel().isSet())
277                         de.maxLevel() = profile->getEquivalentLOD(layer->getProfile(), dei->maxLevel().get());
278                     getDataExtents().push_back(de);
279                 }
280             }
281         }
282 
283         return STATUS_OK;
284     }
285 
286     void
setDictionary(LandCoverDictionary * lcd)287     LandCoverTileSource::setDictionary(LandCoverDictionary* lcd)
288     {
289         _lcDictionary = lcd;
290 
291         for (unsigned i = 0; i<_coverages.size(); ++i)
292         {
293             _coverages[i]->setDictionary(lcd);
294             buildCodeMap(_coverages[i].get(), _codemaps[i]);
295         }
296     }
297 
298     //TODO: overriding createImage directly like this will bypass caching.
299     //      This is a temporary solution; need to refactor.
300     osg::Image*
createImage(const TileKey & key,ProgressCallback * progress)301     LandCoverTileSource::createImage(const TileKey& key, ProgressCallback* progress)
302     {
303         if ( _coverages.empty() )
304             return 0L;
305 
306         std::vector<ILayer> layers(_coverages.size());
307 
308         // Allocate the new coverage image; it will contain unnormalized values.
309         osg::ref_ptr<osg::Image> out = new osg::Image();
310         ImageUtils::markAsUnNormalized(out.get(), true);
311 
312         // Allocate a suitable format:
313         GLint internalFormat = GL_R16F;
314 
315         int tilesize = getPixelsPerTile();
316 
317         out->allocateImage(tilesize, tilesize, 1, GL_RGB, GL_FLOAT);
318         out->setInternalTextureFormat(internalFormat);
319 
320         osg::Vec2 cov;    // coverage coordinates
321 
322         ImageUtils::PixelWriter write( out.get() );
323 
324         float du = 1.0f / (float)(out->s()-1);
325         float dv = 1.0f / (float)(out->t()-1);
326 
327         osg::Vec4 nodata(NO_DATA_VALUE, NO_DATA_VALUE, NO_DATA_VALUE, NO_DATA_VALUE);
328 
329         unsigned pixelsWritten = 0u;
330 
331         for(float u=0.0f; u<=1.0f; u+=du)
332         {
333             for(float v=0.0f; v<=1.0f; v+=dv)
334             {
335                 bool wrotePixel = false;
336                 for(int L = layers.size()-1; L >= 0 && !wrotePixel; --L)
337                 {
338                     if (progress && progress->isCanceled())
339                     {
340                         OE_DEBUG << LC << key.str() << " canceled" << std::endl;
341                         return 0L;
342                     }
343 
344                     ILayer& layer = layers[L];
345                     if ( !layer.valid )
346                         continue;
347 
348                     if ( !layer.image.valid() )
349                         layer.load(key, _coverages[L].get(), progress);
350 
351                     if (!layer.valid)
352                         continue;
353 
354                     osg::Vec2 cov(layer.scale*u + layer.bias.x(), layer.scale*v + layer.bias.y());
355 
356                     if ( cov.x() >= 0.0f && cov.x() <= 1.0f && cov.y() >= 0.0f && cov.y() <= 1.0f )
357                     {
358                         osg::Vec4 texel = (*layer.read)(cov.x(), cov.y());
359 
360                         if ( texel.r() != NO_DATA_VALUE )
361                         {
362                             // store the warp factor in the green channel
363                             texel.g() = layer.warp;
364 
365                             // store the layer index in the blue channel
366                             texel.b() = (float)L;
367 
368                             if (texel.r() < 1.0f)
369                             {
370                                 // normalized code; convert
371                                 int code = (int)(texel.r()*255.0f);
372                                 if (code < _codemaps[L].size())
373                                 {
374                                     int value = _codemaps[L][code];
375                                     if (value >= 0)
376                                     {
377                                         texel.r() = (float)value;
378                                         write.f(texel, u, v);
379                                         wrotePixel = true;
380                                         pixelsWritten++;
381                                     }
382                                 }
383                             }
384                             else
385                             {
386                                 // unnormalized
387                                 int code = (int)texel.r();
388                                 if (code < _codemaps[L].size() && _codemaps[L][code] >= 0)
389                                 {
390                                     texel.r() = (float)_codemaps[L][code];
391                                     write.f(texel, u, v);
392                                     wrotePixel = true;
393                                     pixelsWritten++;
394                                 }
395                             }
396                         }
397                     }
398                 }
399 
400                 if ( !wrotePixel )
401                 {
402                     write.f(nodata, u, v);
403                 }
404             }
405         }
406 
407         return pixelsWritten > 0u? out.release() : 0L;
408     }
409 }
410 
411 //........................................................................
412 
413 #undef  LC
414 #define LC "[LandCoverLayerOptions] "
415 
LandCoverLayerOptions(const ConfigOptions & options)416 LandCoverLayerOptions::LandCoverLayerOptions(const ConfigOptions& options) :
417 ImageLayerOptions(options),
418 _noiseLOD(12u),
419 _warp(0.0f)
420 {
421     fromConfig(_conf);
422 }
423 
424 void
fromConfig(const Config & conf)425 LandCoverLayerOptions::fromConfig(const Config& conf)
426 {
427     conf.get("warp", _warp);
428     conf.get("noise_lod", _noiseLOD);
429 
430     ConfigSet layerConfs = conf.child("coverages").children("coverage");
431     for (ConfigSet::const_iterator i = layerConfs.begin(); i != layerConfs.end(); ++i)
432     {
433         _coverages.push_back(LandCoverCoverageLayerOptions(*i));
434     }
435 }
436 
437 Config
getConfig() const438 LandCoverLayerOptions::getConfig() const
439 {
440     Config conf = ImageLayerOptions::getConfig();
441 
442     conf.set("warp", _warp);
443     conf.set("noise_lod", _noiseLOD);
444 
445     if (_coverages.size() > 0)
446     {
447         Config images("coverages");
448         for (std::vector<LandCoverCoverageLayerOptions>::const_iterator i = _coverages.begin();
449             i != _coverages.end();
450             ++i)
451         {
452             images.add("coverage", i->getConfig());
453         }
454         conf.set(images);
455     }
456 
457     return conf;
458 }
459 
460 //...........................................................................
461 
462 #undef  LC
463 #define LC "[LandCoverLayer] "
464 
465 
LandCoverLayer()466 LandCoverLayer::LandCoverLayer() :
467 ImageLayer(&_optionsConcrete),
468 _options(&_optionsConcrete)
469 {
470     init();
471 }
472 
LandCoverLayer(const LandCoverLayerOptions & options)473 LandCoverLayer::LandCoverLayer(const LandCoverLayerOptions& options) :
474 ImageLayer(&_optionsConcrete),
475 _options(&_optionsConcrete),
476 _optionsConcrete(options)
477 {
478     init();
479 }
480 
481 void
init()482 LandCoverLayer::init()
483 {
484     options().coverage() = true;
485     options().visible() = false;
486     options().shared() = true;
487     ImageLayer::init();
488 }
489 
490 void
addedToMap(const Map * map)491 LandCoverLayer::addedToMap(const Map* map)
492 {
493     // Find a land cover dictionary if there is one.
494     // There had better be one, or we are not going to get very far!
495     // This is called after createTileSource, so the TileSource should exist at this point.
496     // Note. If the land cover dictionary isn't already in the Map...this will fail! (TODO)
497     // Consider a LayerListener. (TODO)
498     _lcDictionary = map->getLayer<LandCoverDictionary>();
499     if (_lcDictionary.valid() && getTileSource())
500     {
501         static_cast<LandCoverTileSource*>(getTileSource())->setDictionary(_lcDictionary.get());
502     }
503     else
504     {
505         OE_WARN << LC << "Did not find a LandCoverDictionary in the Map!\n";
506     }
507 }
508 
509 TileSource*
createTileSource()510 LandCoverLayer::createTileSource()
511 {
512     return new LandCoverTileSource(options());
513 }
514 
515 bool
readMetaImage(MetaImage & metaImage,const TileKey & key,double u,double v,osg::Vec4f & output,ProgressCallback * progress)516 LandCoverLayer::readMetaImage(MetaImage& metaImage, const TileKey& key, double u, double v, osg::Vec4f& output, ProgressCallback* progress)
517 {
518     // Find the key containing the uv coordinates.
519     int x = int(floor(u));
520     int y = int(floor(v));
521     TileKey actualKey = (x != 0 || y != 0)? key.createNeighborKey(x, -y) : key;
522 
523     if (actualKey.valid())
524     {
525         // Transform the uv to be relative to the tile it's actually in.
526         // Don't forget: stupid computer requires us to handle negatives by hand.
527         u = u >= 0.0 ? fmod(u, 1.0) : 1.0+fmod(u, 1.0);
528         v = v >= 0.0 ? fmod(v, 1.0) : 1.0+fmod(v, 1.0);
529 
530         MetaImageComponent* comp = 0L;
531 
532         MetaImage::iterator i = metaImage.find(actualKey);
533         if (i != metaImage.end())
534         {
535             comp = &i->second;
536         }
537         else
538         {
539             // compute the closest ancestor key with actual data for the neighbor key:
540             TileKey bestkey = getBestAvailableTileKey(actualKey);
541 
542             // should not need this fallback loop but let's do it anyway
543             GeoImage tile;
544             while (bestkey.valid() && !tile.valid())
545             {
546                 // load the image and store it to the metaimage.
547                 tile = ImageLayer::createImageImplementation(bestkey, progress);
548                 if (tile.valid())
549                 {
550                     comp = &metaImage[actualKey];
551                     comp->image = tile.getImage();
552                     actualKey.getExtent().createScaleBias(bestkey.getExtent(), comp->scaleBias);
553                     comp->pixel.setImage(comp->image.get());
554                     comp->pixel.setBilinear(false);
555                 }
556                 else
557                 {
558                     bestkey = bestkey.createParentKey();
559                 }
560 
561                 if (progress && progress->isCanceled())
562                     return false;
563             }
564         }
565 
566         if (comp)
567         {
568             // scale/bias to this tile's extent and sample the image.
569             u = u * comp->scaleBias(0, 0) + comp->scaleBias(3, 0);
570             v = v * comp->scaleBias(1, 1) + comp->scaleBias(3, 1);
571             output = comp->pixel(u, v);
572             return true;
573         }
574     }
575     return false;
576 }
577 
578 GeoImage
createImageImplementation(const TileKey & key,ProgressCallback * progress)579 LandCoverLayer::createImageImplementation(const TileKey& key, ProgressCallback* progress)
580 {
581     MetaImage metaImage;
582 
583     // Grab a test sample to see what the output parameters should be:
584     osg::Vec4f dummy;
585     if (!readMetaImage(metaImage, getBestAvailableTileKey(key), 0.5, 0.5, dummy, progress))
586         return GeoImage::INVALID;
587     osg::Image* mainImage = metaImage.begin()->second.image.get();
588 
589     // Allocate the output image:
590     osg::ref_ptr<osg::Image> output = new osg::Image();
591     output->allocateImage(
592         mainImage->s(),
593         mainImage->t(),
594         mainImage->r(),
595         mainImage->getPixelFormat(),
596         mainImage->getDataType(),
597         mainImage->getPacking());
598     output->setInternalTextureFormat(mainImage->getInternalTextureFormat());
599     ImageUtils::markAsUnNormalized(output.get(), true);
600     ImageUtils::PixelWriter write(output.get());
601 
602     // Configure the noise function:
603     SimplexNoise noiseGen;
604     noiseGen.setNormalize(true);
605     noiseGen.setRange(0.0, 1.0);
606     noiseGen.setFrequency(4.0);
607     noiseGen.setPersistence(0.8);
608     noiseGen.setLacunarity(2.2);
609     noiseGen.setOctaves(8);
610 
611     osg::Vec2d cov;
612     osg::Vec2 noiseCoords;
613     osg::Vec4 pixel;
614     osg::Vec4 warpedPixel;
615     osg::Vec4 nodata(NO_DATA_VALUE, NO_DATA_VALUE, NO_DATA_VALUE, NO_DATA_VALUE);
616 
617     // scales the key's LOD to the noise function's LOD:
618     float pdL = pow(2, (float)key.getLOD() - options().noiseLOD().get());
619 
620     // loop over the pixels and write each one.
621     for (int t = 0; t < output->t(); ++t)
622     {
623         double v = (double)t / (double)(output->t() - 1);
624         for (int s = 0; s < output->s(); ++s)
625         {
626             double u = (double)s / (double)(output->s() - 1);
627 
628             // coverage sampling coordinates:
629             cov.set(u, v);
630 
631             // first read the unwarped pixel to get the warping value.
632             // (warp is stored in pixel.g)
633             bool wrotePixel = false;
634             if (readMetaImage(metaImage, key, u, v, pixel, progress) &&
635                 pixel.g() != NO_DATA_VALUE)
636             {
637                 float warp = pixel.g() * pdL;
638                 if (warp != 0.0)
639                 {
640                     // use the noise function to warp the uv coordinates:
641                     noiseCoords = getSplatCoords(key, options().noiseLOD().get(), cov);
642                     double noise = getNoise(noiseGen, noiseCoords);
643                     cov = warpCoverageCoords(cov, noise, warp);
644 
645                     // now read the pixel at the warped location:
646                     if (readMetaImage(metaImage, key, cov.x(), cov.y(), warpedPixel, progress) &&
647                         warpedPixel.b() != NO_DATA_VALUE)
648                     {
649                         // only apply the warping if the location of the warped pixel
650                         // came from the same source layer. Otherwise you will get some
651                         // unsavory speckling. (Layer index is stored in pixel.b)
652                         if (pixel.b() == warpedPixel.b())
653                             write(warpedPixel, s, t);
654                         else
655                             write(pixel, s, t);
656 
657                         wrotePixel = true;
658                     }
659                 }
660                 else
661                 {
662                     write(pixel, s, t);
663                     wrotePixel = true;
664                 }
665             }
666 
667             // if we didn't get a coverage value, just write NODATA
668             if (!wrotePixel)
669             {
670                 write(nodata, s, t);
671             }
672 
673             if (progress && progress->isCanceled())
674             {
675                 OE_DEBUG << LC << key.str() << " canceled" << std::endl;
676                 return GeoImage::INVALID;
677             }
678         }
679     }
680 
681     return GeoImage(output.get(), key.getExtent());
682 }
683 
684 const LandCoverClass*
getClassByUV(const GeoImage & tile,double u,double v) const685 LandCoverLayer::getClassByUV(const GeoImage& tile, double u, double v) const
686 {
687     if (!tile.valid())
688         return 0L;
689 
690     if (!_lcDictionary.valid())
691         return 0L;
692 
693     ImageUtils::PixelReader read(tile.getImage());
694     read.setBilinear(false); // nearest neighbor only!
695     float value = read(u, v).r();
696 
697     return _lcDictionary->getClassByValue((int)value);
698 }
699