/* -*-c++-*- */ /* osgEarth - Geospatial SDK for OpenSceneGraph * Copyright 2020 Pelican Mapping * http://osgearth.org * * osgEarth is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see */ #ifndef OSGEARTH_COVERAGE_LAYER_H #define OSGEARTH_COVERAGE_LAYER_H #include #include #include #include #include #include #include #include namespace osgEarth { /** * CoverageLayer is a data-only layer that creates gridded coverages. * A "coverage" is a 2-dimensional data structure, like and image, * but in which each cell contains a user-defined data structure. * * Internally, a Coverage is an Image consisting of a discrete * integer value at each pixel. A mapping tables provides information * on how to interpret specific values. The user of a CoverageLayer * must know the format of the data structure in use, and must * use that structure as the template parameter when initializing a CoverageCreator * and calling createMappings() and createCoverage(). * * This is a data-only layer. The terrain engine will not render it. */ class OSGEARTH_EXPORT CoverageLayer : public TileLayer { public: struct SourceLayerOptions : public ConfigOptions { OE_OPTION_LAYER(ImageLayer, source); OE_OPTION(Config, mappings); Config getConfig() const; void fromConfig(const Config& conf); }; struct OSGEARTH_EXPORT Options : public TileLayer::Options { META_LayerOptions(osgEarth, Options, TileLayer::Options); OE_OPTION(Config, presets); OE_OPTION_VECTOR(SourceLayerOptions, layers); virtual Config getConfig() const; private: void fromConfig(const Config& conf); }; public: META_Layer(osgEarth, CoverageLayer, Options, TileLayer, Coverage); //! Table that holds the mappings from coverage values //! to user data structures. template struct Mappings : public std::unordered_map { T _nodata; inline const T& get(unsigned value) const; }; //! Table of preset sample values. struct Presets : public std::unordered_map { Presets() : _nodata() { } const Config _nodata; inline const Config& get(const std::string& name) const; }; //! Create the value mapping table. You can then pass //! this table to the createCoverage() method. template void createMappings(const SourceLayerOptions&, Mappings& output) const; //! Find one of the source layers in this coverage. //! @return The source layer, or nullptr if we cannot find it ImageLayer* getSourceLayerByName(const std::string& name) const; /** * Factory is a utility class that initializes the mappings between the int values and * the user-defined datastructure. Initialize one of these first and call createCoverage on it. */ template class Factory { public: using Ptr = std::unique_ptr>; static Ptr create(CoverageLayer* layer) { return Ptr(new Factory(layer)); } Factory(CoverageLayer* layer) : _layer(layer) { // Preload the mappings for each layer for (auto& layer : _layer->options().layers()) { ImageLayer* imageLayer = layer.source().getLayer(); if (imageLayer == nullptr || !imageLayer->isOpen()) continue; Mappings& mappings = _layerToMappings[imageLayer]; _layer->createMappings(layer, mappings); } } //! Create a GeoCoverage for a given tile key. //! @param key TileKey for which to create a coverage grid //! @param progress Progress indicator (can be nullptr) inline GeoCoverage createCoverage(const TileKey& key, ProgressCallback* progress) { OE_PROFILING_ZONE; OE_PROFILING_ZONE_TEXT(_layer->getName() + " " + key.str()); GeoCoverage result; if (!readFromCache(key, result, progress)) { populate(result, key, progress); writeToCache(key, result, progress); } return result; } inline bool readFromCache(const TileKey& key, GeoCoverage& result, ProgressCallback* p) { if (_layer->_memCache.valid()) { char memCacheKey[64]; sprintf(memCacheKey, "%d/%s/%s", _layer->getRevision(), key.str().c_str(), key.getProfile()->getHorizSignature().c_str()); CacheBin* bin = _layer->_memCache->getOrCreateDefaultBin(); ReadResult r = bin->readObject(memCacheKey, nullptr); if (r.succeeded()) { result = GeoCoverage(Coverage::create(), key.getExtent()); Config conf; conf.fromJSON(r.getString()); result.setConfig(conf); return true; } } CacheBin* cacheBin = _layer->getCacheBin(key.getProfile()); const CachePolicy& policy = _layer->getCacheSettings()->cachePolicy().get(); if (!policy.isCacheOnly() && !_layer->getProfile()) { _layer->disable("Could not establish a valid profile"); return false; } // First, attempt to read from the cache. Since the cached data is stored in the // map profile, we can try this first. if (cacheBin && policy.isCacheReadable()) { // the cache key combines the Key and the horizontal profile. std::string cacheKey = Cache::makeCacheKey(key.str() + "-" + key.getProfile()->getHorizSignature(), "coverage"); ReadResult r = cacheBin->readString(cacheKey, nullptr); if (r.succeeded() && !policy.isExpired(r.lastModifiedTime())) { result = GeoCoverage(Coverage::create(), key.getExtent()); Config conf; conf.fromJSON(r.getString()); result.setConfig(conf); return true; } } // The data was not in the cache. If we are cache-only, fail sliently if (policy.isCacheOnly()) { // If it's cache only and we have an expired but cached image, just return it. return !result.empty(); } return result.valid(); } inline void writeToCache(const TileKey& key, const GeoCoverage& result, ProgressCallback* p) { if (result.empty()) return; osg::ref_ptr so; if (_layer->_memCache.valid()) { char memCacheKey[64]; sprintf(memCacheKey, "%d/%s/%s", _layer->getRevision(), key.str().c_str(), key.getProfile()->getHorizSignature().c_str()); CacheBin* bin = _layer->_memCache->getOrCreateDefaultBin(); Config conf = result.getConfig(); so = new StringObject(conf.toJSON(false)); bin->write(memCacheKey, so.get(), nullptr); } // If we got a result, the cache is valid and we are caching in the map profile, // write to the map cache. CacheBin* cacheBin = _layer->getCacheBin(key.getProfile()); const CachePolicy& policy = _layer->getCacheSettings()->cachePolicy().get(); if (cacheBin && policy.isCacheWriteable()) { std::string cacheKey = Cache::makeCacheKey(key.str()+"-"+key.getProfile()->getHorizSignature(), "coverage"); if (!so.valid()) { Config conf = result.getConfig(); so = new StringObject(conf.toJSON(false)); } cacheBin->write(cacheKey, so.get(), nullptr); } } private: //! Create a GeoCoverage for a given tile key. //! @param key TileKey for which to create a coverage grid //! @param mappings Value mapping table returned by createMappings() //! @param progress Progress indicator (can be nullptr) inline GeoCoverage createCoverage( const TileKey& key, ImageLayer* source, ProgressCallback* progress) { typename Coverage::Ptr result; if (source && source->isOpen()) { auto mappingsItr = _layerToMappings.find(source); if (mappingsItr == _layerToMappings.end()) { return GeoCoverage(); } auto& mappings = mappingsItr->second; GeoImage image = source->createImage(key, progress); if (image.valid()) { osg::Vec4 pixel; GeoImagePixelReader read(image); read.setBilinear(false); // unnecessary //bool normalized = image.getImage()->getDataType() == GL_UNSIGNED_BYTE; if (progress && progress->isCanceled()) return GeoCoverage(); OE_PROFILING_ZONE; OE_PROFILING_ZONE_TEXT("decode"); result = Coverage::create(); result->allocate(read.s(), read.t()); const T* sample_ptr = nullptr; unsigned value_prev; int k = -1; GeoImageIterator iter(image); iter.forEachPixel([&]() { read(pixel, iter.s(), iter.t()); unsigned value; //if (normalized) if (pixel.r() < 1.0f) value = (unsigned)(pixel.r() * 255.0f); else value = (unsigned)pixel.r(); if (!sample_ptr || value != value_prev) { sample_ptr = &mappings.get(value); k = -1; } if (sample_ptr->valid()) k = result->write(*sample_ptr, iter.s(), iter.t(), k); value_prev = value; }); } } return GeoCoverage(result, key.getExtent()); } bool populate( GeoCoverage& output, const TileKey& key, ProgressCallback* progress) { auto& sources = _layer->options().layers(); // trivial rejection when no sources are present if (sources.empty()) { return false; } // trivial case of a SINGLE layer, when no compositing // is necessary if (sources.size() == 1) { auto imageLayer = sources.front().source().getLayer(); if (imageLayer->isOpen()) { output = createCoverage(key, imageLayer, progress); } return output.valid(); } // Compositing is necessary. Collect the best available // coverage from each source, and then composite them based // on their order of appearance. // Iterate backwards since the last source has the highest priority. // If we get a coverage with all valid values, we are finished. std::vector> inputs; bool fallback = false; for (auto i = sources.rbegin(); i != sources.rend(); ++i) { auto imageLayer = i->source().getLayer(); if (imageLayer && imageLayer->isOpen()) { GeoCoverage input; TileKey inputKey = key; if (fallback) { // This path runs when we already have a partial tile from // a higher priority layer, and need to fill in the empty samples // from other lower priority layers. // NB: layer->mayHaveData() is insufficient here. We want to be sure // to fail and fall back until we get data, and mayHaveData() will return // false if our LOD exceeds that of the source layer. Instead we just query // the "best available" key and start the search from there if that // key is valid. (It will be invalid, for example, if the extents don't intersect.) inputKey = imageLayer->getBestAvailableTileKey(inputKey); while (inputKey.valid() && !input.valid() && imageLayer->isKeyInLegalRange(inputKey)) { input = createCoverage(inputKey, imageLayer, progress); if (!input.valid()) inputKey.makeParent(); } } else if (imageLayer->mayHaveData(inputKey)) { // This path runs when we have no data at all yet. input = createCoverage(inputKey, imageLayer, progress); } // if we got a valid coverage, add it to the composition list. if (input.valid()) { if (!input.empty()) { fallback = true; inputs.emplace_back(std::move(input)); // if this coverage is fully populated, we're done if (inputs.back().nodataCount() == 0) break; } } } } // Got nothing? bye if (inputs.empty()) { return false; } // Special case: we found one valid coverage - just return that. if (inputs.size() == 1 && inputs.back().getExtent() == key.getExtent()) { output = std::move(inputs.back()); return true; } // Time to composite. // make a new coverage with the same dimensions as our // highest priority input: T value; osg::Matrix scalebias; unsigned width = inputs.front().s(); unsigned height = inputs.front().t(); typename Coverage::Ptr composite = Coverage::create(); composite->allocate(width, height); for(int i=0; ihasDataAt(s, t) == false) // nodata, OK to overwrite: { if (input.read(value, input_s, input_t)) // true if has data { composite->write(value, s, t); } } } } if (progress && progress->isCanceled()) return false; } // done. wrap it and return it. output = GeoCoverage(composite, key.getExtent()); return true; } private: CoverageLayer* _layer; std::unordered_map> _layerToMappings; }; protected: // Layer virtual Status openImplementation() override; public: // Layer virtual void addedToMap(const Map*) override; virtual void removedFromMap(const Map*) override; }; // inline functions for CoverageLayer template const T& CoverageLayer::Mappings::get(unsigned value) const { auto i = this->find(value); return i != this->end() ? i->second : _nodata; } const Config& CoverageLayer::Presets::get(const std::string& name) const { auto i = this->find(name); return i != this->end() ? i->second : _nodata; } template void CoverageLayer::createMappings( const SourceLayerOptions& layer, CoverageLayer::Mappings& output) const { Presets presets; for (auto& child : options().presets()->children("preset")) { presets[child.value("name")] = child; } for (auto& child : layer.mappings()->children("mapping")) { optional value; std::string preset_name; if (child.get("value", value)) { if (value == 0u) { OE_WARN << "[CoverageLayer] " << getName() << " : Illegal value 0 in mapping (zero is reserved for 'no data'); skipping." << std::endl; } else if (child.get("preset", preset_name)) { const Config& p = presets.get(preset_name); Config temp = child; temp.merge(p); output[value.get()] = T(temp); } else { output[value.get()] = T(child); } } } } } // namespace osgEarth #endif // OSGEARTH_COVERAGE_LAYER_H