diff --git a/data/bing.map.json b/data/bing.map.json index 512fffdd..59fc1b2b 100644 --- a/data/bing.map.json +++ b/data/bing.map.json @@ -2,21 +2,13 @@ "name" : "Bing Maps Example", "layers": [ { - "imagery_set": "Aerial", - "key": "", "name": "Bing Imagery", - "profile": { - "extent": { - "srs": "spherical-mercator", - "xmax": 20037508.34278925, - "xmin": -20037508.34278925, - "ymax": 20037508.34278925, - "ymin": -20037508.34278925 - }, - "tx": 2, - "ty": 2 - }, - "type": "BingImage" + "type": "BingImage", + "imagery_set": "Aerial", + "key": "" + }, + { + "type": "BingElevation" } ] } \ No newline at end of file diff --git a/src/rocky/BingElevationLayer.cpp b/src/rocky/BingElevationLayer.cpp index f8f026d8..4d0c138e 100644 --- a/src/rocky/BingElevationLayer.cpp +++ b/src/rocky/BingElevationLayer.cpp @@ -65,7 +65,8 @@ BingElevationLayer::openImplementation(const IOOptions& io) if (parent.failed()) return parent; - _profile = Profile::SPHERICAL_MERCATOR; + setProfileDefault(Profile(SRS::SPHERICAL_MERCATOR, Profile::SPHERICAL_MERCATOR.extent().bounds(), 2, 2)); + setDataExtents({ profile().extent() }); ROCKY_TODO("When disk cache is implemented, disable it here as it violates the ToS"); diff --git a/src/rocky/BingImageLayer.cpp b/src/rocky/BingImageLayer.cpp index af00cb0c..d0ce7595 100644 --- a/src/rocky/BingImageLayer.cpp +++ b/src/rocky/BingImageLayer.cpp @@ -68,7 +68,7 @@ BingImageLayer::openImplementation(const IOOptions& io) if (parent.failed()) return parent; - setProfile(Profile(SRS::SPHERICAL_MERCATOR, Profile::SPHERICAL_MERCATOR.extent().bounds(), 2, 2)); + setProfileDefault(Profile(SRS::SPHERICAL_MERCATOR, Profile::SPHERICAL_MERCATOR.extent().bounds(), 2, 2)); setDataExtents({ profile().extent() }); _tileURICache = std::make_unique(); diff --git a/src/rocky/ElevationLayer.cpp b/src/rocky/ElevationLayer.cpp index e3769269..2f755f92 100644 --- a/src/rocky/ElevationLayer.cpp +++ b/src/rocky/ElevationLayer.cpp @@ -42,6 +42,35 @@ namespace } } + +namespace +{ + class HeightfieldMosaic : public Inherit + { + public: + HeightfieldMosaic(unsigned s, unsigned t) : + super(s, t) + { + //nop + } + + HeightfieldMosaic(const HeightfieldMosaic& rhs) : + super(rhs), + dependencies(rhs.dependencies) + { + //nop + } + + virtual ~HeightfieldMosaic() + { + cleanupOperation(); + } + + std::vector> dependencies; + std::function cleanupOperation; + }; +} + //------------------------------------------------------------------------ ElevationLayer::ElevationLayer() : @@ -91,6 +120,8 @@ ElevationLayer::construct(const JSON& conf) // elevation layers do not render directly; rather, a composite of elevation data // feeds the terrain engine to permute the mesh. //setRenderType(RENDERTYPE_NONE); + + _dependencyCache = std::make_shared>(); } JSON @@ -185,19 +216,16 @@ ElevationLayer::normalizeNoDataValues(Heightfield* hf) const shared_ptr ElevationLayer::assembleHeightfield(const TileKey& key, const IOOptions& io) const { - shared_ptr output; - - // Collect the heightfields for each of the intersecting tiles. - std::vector geohf_list; + std::shared_ptr output; // Determine the intersecting keys std::vector intersectingKeys; + unsigned targetLOD; - if (key.levelOfDetail() > 0u) - { - key.getIntersectingKeys(profile(), intersectingKeys); - } + targetLOD = key.LOD(); + key.getIntersectingKeys(profile(), intersectingKeys); +#if 0 else { // LOD is zero - check whether the LOD mapping went out of range, and if so, @@ -206,15 +234,15 @@ ElevationLayer::assembleHeightfield(const TileKey& key, const IOOptions& io) con // surpasses the max data LOD of the tile source. unsigned numTilesThatMayHaveData = 0u; - int intersectionLOD = profile().getEquivalentLOD(key.profile(), key.levelOfDetail()); + targetLOD = profile().getEquivalentLOD(key.profile(), key.levelOfDetail()); - while (numTilesThatMayHaveData == 0u && intersectionLOD >= 0) + while (numTilesThatMayHaveData == 0u && targetLOD >= 0) { intersectingKeys.clear(); TileKey::getIntersectingKeys( key.extent(), - intersectionLOD, + targetLOD, profile(), intersectingKeys); @@ -226,98 +254,129 @@ ElevationLayer::assembleHeightfield(const TileKey& key, const IOOptions& io) con } } - --intersectionLOD; + --targetLOD; } } +#endif // collect heightfield for each intersecting key. Note, we're hitting the // underlying tile source here, so there's no vetical datum shifts happening yet. // we will do that later. + std::vector sources; + if (intersectingKeys.size() > 0) { - for(auto& layerKey : intersectingKeys) + bool hasAtLeastOneSourceAtTargetLOD = false; + + for (auto& intersectingKey : intersectingKeys) { - if ( isKeyInLegalRange(layerKey) ) + TileKey subKey = intersectingKey; + Result subTile; + while (subKey.valid() && !subTile.status.ok()) { - std::shared_lock L(layerStateMutex()); - auto result = createHeightfieldImplementation(layerKey, io); + subTile = createHeightfieldImplementation_internal(subKey, io); + if (subTile.status.failed()) + subKey.makeParent(); + + if (io.canceled()) + return {}; + } - if (result.status.ok() && result.value.valid()) + if (subTile.status.ok()) + { + if (subKey.levelOfDetail() == targetLOD) { - geohf_list.push_back(result.value); + hasAtLeastOneSourceAtTargetLOD = true; } + + // got a valid image, so add it to our sources collection: + sources.emplace_back(subTile.value); } } - // If we actually got a Heightfield, resample/reproject it to match the incoming TileKey's extents. - if (geohf_list.size() > 0) + // If we actually got at least one piece of usable data, + // move ahead and build a mosaic of all sources. + if (hasAtLeastOneSourceAtTargetLOD) { - unsigned width = 0; - unsigned height = 0; - auto keyExtent = key.extent(); + unsigned cols = 0; + unsigned rows = 0; - // determine the final dimensions - for(auto& geohf : geohf_list) + // output size is the max of all the source sizes. + for (auto& source : sources) { - width = std::max(width, geohf.heightfield()->width()); - height = std::max(height, geohf.heightfield()->height()); + cols = std::max(cols, source.heightfield()->width()); + rows = std::max(rows, source.heightfield()->height()); } // assume all tiles to mosaic are in the same SRS. - SRSOperation xform = keyExtent.srs().to(geohf_list[0].srs()); + SRSOperation xform = key.extent().srs().to(sources[0].srs()); // Now sort the heightfields by resolution to make sure we're sampling // the highest resolution one first. - std::sort(geohf_list.begin(), geohf_list.end(), GeoHeightfield::SortByResolutionFunctor()); + std::sort(sources.begin(), sources.end(), GeoHeightfield::SortByResolutionFunctor()); // new output HF: - output = Heightfield::create(width, height); + output = HeightfieldMosaic::create(cols, rows); + + // Cache pointers to the source images that mosaic to create this tile. + output->dependencies.reserve(sources.size()); + for (auto& source : sources) + output->dependencies.push_back(source.heightfield()); + + // Clean up orphaned entries any time a tile destructs. + output->cleanupOperation = [captured{ std::weak_ptr(_dependencyCache) }, key]() { + auto cache = captured.lock(); + if (cache) + cache->clean(); + }; // working set of points. it's much faster to xform an entire vector all at once. std::vector points; - points.assign(width * height, { 0, 0, NO_DATA_VALUE }); + points.reserve(cols * rows); // .assign(cols* rows, { 0, 0, NO_DATA_VALUE }); double minx, miny, maxx, maxy; key.extent().getBounds(minx, miny, maxx, maxy); - double dx = (maxx - minx) / (double)(width - 1); - double dy = (maxy - miny) / (double)(height - 1); + double dx = (maxx - minx) / (double)(cols); + double dy = (maxy - miny) / (double)(rows); // build a grid of sample points: - for (unsigned r = 0; r < height; ++r) + for (unsigned r = 0; r < rows; ++r) { - double y = miny + (dy * (double)r); - for (unsigned c = 0; c < width; ++c) + double y = miny + (0.5*dy) + (dy * (double)r); + for (unsigned c = 0; c < cols; ++c) { - double x = minx + (dx * (double)c); - points[r * width + c].x = x; - points[r * width + c].y = y; + double x = minx + (0.5*dx) + (dx * (double)c); + points[r * cols + c] = { x, y, NO_DATA_VALUE }; } } // transform the sample points to the SRS of our source data tiles: if (xform.valid()) + { xform.transformArray(&points[0], points.size()); + } // sample the heights: - for (unsigned k = 0; k < geohf_list.size(); ++k) + for (auto& point : points) { - for (auto& point : points) + for(unsigned i = 0; point.z == NO_DATA_VALUE && i < sources.size(); ++i) { - if (point.z == NO_DATA_VALUE) - point.z = geohf_list[k].heightAtLocation(point.x, point.y, Image::BILINEAR); + point.z = sources[i].heightAtLocation(point.x, point.y, Image::BILINEAR); } } // transform the elevations back to the SRS of our tilekey (vdatum transform): if (xform.valid()) + { xform.inverseArray(&points[0], points.size()); + } // assign the final heights to the heightfield. - for (unsigned r = 0; r < height; ++r) + for (unsigned r = 0; r < rows; ++r) { - for (unsigned c = 0; c < width; ++c) + for (unsigned c = 0; c < cols; ++c) { - output->heightAt(c, r) = (float)(points[r * width + c].z); + output->heightAt(c, r) = (float)(points[r * cols + c].z); } } } @@ -352,6 +411,20 @@ ElevationLayer::createHeightfield( return createHeightfieldInKeyProfile(key, io); } +Result +ElevationLayer::createHeightfieldImplementation_internal( + const TileKey& key, + const IOOptions& io) const +{ + std::shared_lock lock(layerStateMutex()); + auto result = createHeightfieldImplementation(key, io); + if (result.status.failed()) + { + Log()->debug("Failed to create heightfield for key {0} : {1}", key.str(), result.status.message); + } + return result; +} + Result ElevationLayer::createHeightfieldInKeyProfile( const TileKey& key, @@ -374,8 +447,7 @@ ElevationLayer::createHeightfieldInKeyProfile( if (key.profile() == my_profile) { - std::shared_lock L(layerStateMutex()); - auto r = createHeightfieldImplementation(key, io); + auto r = createHeightfieldImplementation_internal(key, io); if (r.status.failed()) return r; diff --git a/src/rocky/ElevationLayer.h b/src/rocky/ElevationLayer.h index da048262..05a881f3 100644 --- a/src/rocky/ElevationLayer.h +++ b/src/rocky/ElevationLayer.h @@ -133,6 +133,12 @@ namespace ROCKY_NAMESPACE util::Gate _sentry; mutable util::LRUCache> _L2cache; + + Result createHeightfieldImplementation_internal( + const TileKey& key, + const IOOptions& io) const; + + std::shared_ptr> _dependencyCache; }; diff --git a/src/rocky/ImageLayer.cpp b/src/rocky/ImageLayer.cpp index 9ddabb4d..a41a2b9d 100644 --- a/src/rocky/ImageLayer.cpp +++ b/src/rocky/ImageLayer.cpp @@ -230,26 +230,27 @@ ImageLayer::assembleImage(const TileKey& key, const IOOptions& io) const for (auto& intersectingKey : intersectingKeys) { TileKey subKey = intersectingKey; - Result subImage; - while (subKey.valid() && !subImage.status.ok()) + Result subTile; + while (subKey.valid() && !subTile.status.ok()) { - subImage = createImageImplementation_internal(subKey, io); - if (subImage.status.failed()) + subTile = createImageImplementation_internal(subKey, io); + if (subTile.status.failed()) subKey.makeParent(); if (io.canceled()) return {}; } - if (subImage.status.ok()) + if (subTile.status.ok()) { - // got a valid image, so add it to our sources collection: - sources.emplace_back(subKey, subImage.value); - if (subKey.levelOfDetail() == targetLOD) { hasAtLeastOneSourceAtTargetLOD = true; } + + // got a valid image, so add it to our sources collection: + sources.emplace_back(subKey, subTile.value); + } } @@ -281,7 +282,7 @@ ImageLayer::assembleImage(const TileKey& key, const IOOptions& io) const // new output: output = CompositeImage::create(Image::R8G8B8A8_UNORM, cols, rows); - // Cache pointers to the source images that mosaic to create this image. + // Cache pointers to the source images that mosaic to create this tile. output->dependencies.reserve(sources.size()); for (auto& source : sources) output->dependencies.push_back(source.second.image()); @@ -320,21 +321,19 @@ ImageLayer::assembleImage(const TileKey& key, const IOOptions& io) const } // Mosaic our sources into a single output image. + glm::fvec4 pixel; for (unsigned r = 0; r < rows; ++r) { - for (unsigned int c = 0; c < cols; ++c) + for (unsigned c = 0; c < cols; ++c) { unsigned i = r * cols + c; - // For each sample point, try each heightfield. The first one with a valid elevation wins. - glm::fvec4 pixel(0, 0, 0, 0); + // check each source (high to low LOD) until we get a valid pixel. + pixel = { 0,0,0,0 }; - // sources are ordered from low to high LOD, so iterater backwards. for (unsigned k = 0; k < sources.size(); ++k) { - auto& image = sources[k].second; - - if (image.read(pixel, points[i].x, points[i].y) && pixel.a > 0.0f) + if (sources[k].second.read(pixel, points[i].x, points[i].y) && pixel.a > 0.0f) { break; } diff --git a/src/rocky/ImageLayer.h b/src/rocky/ImageLayer.h index f98a7d38..fe1ecbbc 100644 --- a/src/rocky/ImageLayer.h +++ b/src/rocky/ImageLayer.h @@ -12,70 +12,6 @@ namespace ROCKY_NAMESPACE { - /** - * A "cache" of weak pointers to images, keyed by tile key. This will keep - * references to images that are still in use elsewhere in the system in - * order to prevent re-fetching or re-mosacing the same data over and over. - */ - template - class DependencyCache - { - public: - //! Fetch a value from teh cache or nullptr if it's not there - std::shared_ptr operator[](const Key& key) - { - const std::lock_guard lock{ _mutex }; - ++_gets; - auto itr = _map.find(key); - if (itr != _map.end()) - { - auto result = itr->second.lock(); - if (result) ++_hits; - return result; - } - return nullptr; - } - - //! Enter a value into the cache, returning an existing value if there is one - std::shared_ptr put(const Key& key, const std::shared_ptr& value) - { - const std::lock_guard lock{ _mutex }; - auto itr = _map.find(key); - if (itr != _map.end()) - { - std::shared_ptr preexisting = itr->second.lock(); - if (preexisting) - return preexisting; - } - _map[key] = value; - return value; - } - - //! Clean the cache by purging entries whose weak pointers have expired - void clean() - { - const std::lock_guard lock{ _mutex }; - for (auto itr = _map.begin(), end = _map.end(); itr != end;) - { - if (!itr->second.lock()) - itr = _map.erase(itr); - else - ++itr; - } - } - - float hitRatio() const - { - return _gets > 0.0f ? _hits / _gets : 0.0f; - } - - private: - std::unordered_map> _map; - float _gets = 0.0f; - float _hits = 0.0f; - std::mutex _mutex; - }; - /** * A map terrain layer containing bitmap image data. */ diff --git a/src/rocky/TileLayer.h b/src/rocky/TileLayer.h index 9c6bafed..cc3a1ba4 100644 --- a/src/rocky/TileLayer.h +++ b/src/rocky/TileLayer.h @@ -199,4 +199,69 @@ namespace ROCKY_NAMESPACE friend class Map; }; + /** + * A "cache" of weak pointers to values, keyed by tile key. This will keep + * references to data that are still in use elsewhere in the system in + * order to prevent re-fetching or re-mosacing the same data over and over. + */ + template + class DependencyCache + { + public: + //! Fetch a value from teh cache or nullptr if it's not there + std::shared_ptr operator[](const Key& key) + { + const std::lock_guard lock{ _mutex }; + ++_gets; + auto itr = _map.find(key); + if (itr != _map.end()) + { + auto result = itr->second.lock(); + if (result) ++_hits; + return result; + } + return nullptr; + } + + //! Enter a value into the cache, returning an existing value if there is one + std::shared_ptr put(const Key& key, const std::shared_ptr& value) + { + const std::lock_guard lock{ _mutex }; + auto itr = _map.find(key); + if (itr != _map.end()) + { + std::shared_ptr preexisting = itr->second.lock(); + if (preexisting) + return preexisting; + } + _map[key] = value; + return value; + } + + //! Clean the cache by purging entries whose weak pointers have expired + void clean() + { + const std::lock_guard lock{ _mutex }; + for (auto itr = _map.begin(), end = _map.end(); itr != end;) + { + if (!itr->second.lock()) + itr = _map.erase(itr); + else + ++itr; + } + } + + float hitRatio() const + { + return _gets > 0.0f ? _hits / _gets : 0.0f; + } + + private: + std::unordered_map> _map; + float _gets = 0.0f; + float _hits = 0.0f; + std::mutex _mutex; + }; + + } // namespace TileLayer