From 286f9e6e997e8a77e6437cf1513d312e6f367aad Mon Sep 17 00:00:00 2001 From: Glenn Waldron Date: Mon, 27 Jan 2025 12:53:02 -0500 Subject: [PATCH 1/3] RoadSurfaceLayer: fix the rendering order so that styles will either (a) render in their order or appearance; or (b) render in order of the render-order symbol if present --- src/osgEarthProcedural/RoadSurfaceLayer | 13 +- src/osgEarthProcedural/RoadSurfaceLayer.cpp | 127 +------------------- 2 files changed, 6 insertions(+), 134 deletions(-) diff --git a/src/osgEarthProcedural/RoadSurfaceLayer b/src/osgEarthProcedural/RoadSurfaceLayer index bfb550a16a..9d0c25c539 100644 --- a/src/osgEarthProcedural/RoadSurfaceLayer +++ b/src/osgEarthProcedural/RoadSurfaceLayer @@ -86,11 +86,7 @@ namespace osgEarth { namespace Procedural protected: // Layer // post-ctor initialization - virtual void init() override; - - protected: - - virtual ~RoadSurfaceLayer() { } + void init() override; private: osg::ref_ptr _session; @@ -98,13 +94,6 @@ namespace osgEarth { namespace Procedural using FeatureListCache = LRUCache; mutable std::unique_ptr _lru; FeatureFilterChain _filterChain; - - void getFeatures( - FeatureSource* featureSource, - const TileKey& key, - FeatureList& output, - ProgressCallback* progress) const; - osg::ref_ptr _rasterizer; }; diff --git a/src/osgEarthProcedural/RoadSurfaceLayer.cpp b/src/osgEarthProcedural/RoadSurfaceLayer.cpp index bd7698c430..96c2903caa 100644 --- a/src/osgEarthProcedural/RoadSurfaceLayer.cpp +++ b/src/osgEarthProcedural/RoadSurfaceLayer.cpp @@ -195,129 +195,12 @@ RoadSurfaceLayer::getStyleSheet() const return options().styleSheet().getLayer(); } -namespace -{ - typedef std::vector< std::pair< Style, FeatureList > > StyleToFeatures; - - void addFeatureToMap(Feature* feature, const Style& style, StyleToFeatures& map) - { - bool added = false; - - if (!style.getName().empty()) - { - // Try to find the style by name - for (int i = 0; i < map.size(); i++) - { - if (map[i].first.getName() == style.getName()) - { - map[i].second.push_back(feature); - added = true; - break; - } - } - } - - if (!added) - { - FeatureList list; - list.push_back(feature); - map.push_back(std::pair< Style, FeatureList>(style, list)); - } - } - - void sortFeaturesIntoStyleGroups(StyleSheet* styles, FeatureList& features, FilterContext &context, StyleToFeatures& map) - { - if (styles == nullptr) - return; - - if (styles->getSelectors().size() > 0) - { - for (StyleSelectors::const_iterator i = styles->getSelectors().begin(); - i != styles->getSelectors().end(); - ++i) - { - const StyleSelector& sel = i->second; - - if (sel.styleExpression().isSet()) - { - // establish the working bounds and a context: - StringExpression styleExprCopy(sel.styleExpression().get()); - - for (FeatureList::iterator itr = features.begin(); itr != features.end(); ++itr) - { - Feature* feature = itr->get(); - - // resolve the style: - Style combinedStyle; - - if (feature->style().isSet()) - { - // embedde style: - combinedStyle = feature->style().get(); - } - else - { - // evaluated style: - const std::string& styleString = feature->eval(styleExprCopy, &context); - if (!styleString.empty() && styleString != "null") - { - // if the style string begins with an open bracket, it's an inline style definition. - if (styleString.length() > 0 && styleString[0] == '{') - { - Config conf("style", styleString); - conf.setReferrer(sel.styleExpression().get().uriContext().referrer()); - conf.set("type", "text/css"); - combinedStyle = Style(conf); - } - - // otherwise, look up the style in the stylesheet. Do NOT fall back on a default - // style in this case: for style expressions, the user must be explicity about - // default styling; this is because there is no other way to exclude unwanted - // features. - else - { - const Style* selectedStyle = styles->getStyle(styleString, false); - if (selectedStyle) - combinedStyle = *selectedStyle; - } - } - } - - if (!combinedStyle.empty()) - { - addFeatureToMap(feature, combinedStyle, map); - } - - } - } - } - } - else - { - const Style* style = styles->getDefaultStyle(); - for (FeatureList::iterator itr = features.begin(); itr != features.end(); ++itr) - { - Feature* feature = itr->get(); - // resolve the style: - if (feature->style().isSet()) - { - addFeatureToMap(feature, feature->style().get(), map); - } - else - { - addFeatureToMap(feature, *style, map); - } - } - } - } -} - GeoImage RoadSurfaceLayer::createImageImplementation(const TileKey& key, ProgressCallback* progress) const { if (getStatus().isError()) { - return GeoImage::INVALID; + return GeoImage(getStatus()); } // take local refs to isolate this method from the member objects @@ -329,20 +212,20 @@ RoadSurfaceLayer::createImageImplementation(const TileKey& key, ProgressCallback if (!featureSource.valid()) { setStatus(Status(Status::ServiceUnavailable, "No feature source")); - return GeoImage::INVALID; + return GeoImage(getStatus()); } if (featureSource->getStatus().isError()) { setStatus(featureSource->getStatus()); - return GeoImage::INVALID; + return GeoImage(getStatus()); } osg::ref_ptr featureProfile = featureSource->getFeatureProfile(); if (!featureProfile.valid()) { setStatus(Status(Status::ConfigurationError, "Feature profile is missing")); - return GeoImage::INVALID; + return GeoImage(getStatus()); } if (!rasterizer.valid() || !session.valid()) @@ -354,7 +237,7 @@ RoadSurfaceLayer::createImageImplementation(const TileKey& key, ProgressCallback if (!featureSRS) { setStatus(Status(Status::ConfigurationError, "Feature profile has no SRS")); - return GeoImage::INVALID; + return GeoImage(getStatus()); } GeoExtent featureExtent = key.getExtent().transform(featureSRS); From f1e9d1d7eaa573ddb7e702d59f7812aa6ff1bf4e Mon Sep 17 00:00:00 2001 From: Glenn Waldron Date: Tue, 28 Jan 2025 15:19:51 -0500 Subject: [PATCH 2/3] FeatureImageLayer: add bufferWidth option; FeatureRasterizer: add a pixel buffer for better edge matching --- src/osgEarth/FeatureImageLayer | 1 + src/osgEarth/FeatureImageLayer.cpp | 6 +- src/osgEarth/FeatureRasterizer | 7 ++- src/osgEarth/FeatureRasterizer.cpp | 96 +++++++++++++++++++----------- 4 files changed, 72 insertions(+), 38 deletions(-) diff --git a/src/osgEarth/FeatureImageLayer b/src/osgEarth/FeatureImageLayer index 2dbed93b78..cd4eda4738 100644 --- a/src/osgEarth/FeatureImageLayer +++ b/src/osgEarth/FeatureImageLayer @@ -40,6 +40,7 @@ namespace osgEarth OE_OPTION_LAYER(FeatureSource, featureSource); OE_OPTION_VECTOR(ConfigOptions, filters); OE_OPTION_LAYER(StyleSheet, styleSheet); + OE_OPTION(Distance, bufferWidth, {}); OE_OPTION(double, gamma); OE_OPTION(bool, sdf); OE_OPTION(bool, sdf_invert); diff --git a/src/osgEarth/FeatureImageLayer.cpp b/src/osgEarth/FeatureImageLayer.cpp index 77866efeaf..2847bd8072 100644 --- a/src/osgEarth/FeatureImageLayer.cpp +++ b/src/osgEarth/FeatureImageLayer.cpp @@ -41,6 +41,7 @@ FeatureImageLayer::Options::getConfig() const Config conf = ImageLayer::Options::getConfig(); featureSource().set(conf, "features"); styleSheet().set(conf, "styles"); + conf.set("buffer_width", bufferWidth()); conf.set("gamma", gamma()); conf.set("sdf", sdf()); conf.set("sdf_invert", sdf_invert()); @@ -65,6 +66,7 @@ FeatureImageLayer::Options::fromConfig(const Config& conf) featureSource().get(conf, "features"); styleSheet().get(conf, "styles"); + conf.get("buffer_width", bufferWidth()); conf.get("gamma", gamma()); conf.get("sdf", sdf()); conf.get("sdf_invert", sdf_invert()); @@ -315,9 +317,11 @@ FeatureImageLayer::createImageImplementation(const TileKey& key, ProgressCallbac FeatureStyleSorter sorter; + // a buffer will pull in data from nearby tiles to mitigate edge artifacts + sorter.sort( key, - Distance(0, Units::METERS), + options().bufferWidth().value(), local._session.get(), local._filterChain, renderer, diff --git a/src/osgEarth/FeatureRasterizer b/src/osgEarth/FeatureRasterizer index 7eff15a401..9a6c03d341 100644 --- a/src/osgEarth/FeatureRasterizer +++ b/src/osgEarth/FeatureRasterizer @@ -75,16 +75,19 @@ namespace osgEarth private: GeoExtent _extent; + GeoExtent _originalExtent; + unsigned _width = 0, _height = 0; osg::ref_ptr< osg::Image > _image; osg::ref_ptr< MapboxGLGlyphManager > _glyphManager; float _pixelScale = 1.0f; + unsigned _bufferPixels = 2; enum RenderFormat { RF_BGRA, RF_ABGR }; - RenderFormat _implPixelFormat; - bool _inverted; + RenderFormat _implPixelFormat = RF_BGRA; + bool _inverted = false; SymbolBoundingBoxes _symbolBoundingBoxes; diff --git a/src/osgEarth/FeatureRasterizer.cpp b/src/osgEarth/FeatureRasterizer.cpp index eb48cda0d5..7b5352c898 100644 --- a/src/osgEarth/FeatureRasterizer.cpp +++ b/src/osgEarth/FeatureRasterizer.cpp @@ -242,7 +242,7 @@ namespace osgEarth { void rasterizeLines( const Geometry* geometry, const Color& color, - float lineWidth_px, + double lineWidth_px, RenderFrame& frame, BLContext& ctx) { @@ -284,11 +284,6 @@ namespace osgEarth { } }); - //BLImage texture; - //texture.readFromFile("../data/icon.png"); - //BLPattern pattern(texture); - //ctx.setStrokeStyle(pattern); - ctx.setStrokeStyle(BLRgba(color.r(), color.g(), color.b(), color.a())); ctx.setStrokeWidth(lineWidth_px); ctx.strokePath(path); @@ -654,15 +649,41 @@ using namespace osgEarth::FeatureImageLayerImpl; FeatureRasterizer::FeatureRasterizer( - unsigned int width, unsigned int height, + unsigned width, unsigned height, const GeoExtent& extent, const Color& backgroundColor) : - _extent(extent) + _width(width), + _height(height), + _originalExtent(extent) { + unsigned imageWidth = width; + unsigned imageHeight = height; + + if (_bufferPixels > 0) + { + imageWidth = width + 2 * _bufferPixels; + imageHeight = height + 2 * _bufferPixels; + + double pixelSizeS = extent.width() / (double)width; + double pixelSizeT = extent.height() / (double)height; + + _extent = GeoExtent( + extent.getSRS(), + extent.xMin() - pixelSizeS * (double)_bufferPixels, + extent.yMin() - pixelSizeT * (double)_bufferPixels, + extent.xMax() + pixelSizeS * (double)_bufferPixels, + extent.yMax() + pixelSizeT * (double)_bufferPixels); + } + else + { + _extent = _originalExtent; + } + // Allocate the image and initialize it to the background color _image = new osg::Image(); - _image->allocateImage(width, height, 1, GL_RGBA, GL_UNSIGNED_BYTE); + //_image->allocateImage(width, height, 1, GL_RGBA, GL_UNSIGNED_BYTE); + _image->allocateImage(imageWidth, imageHeight, 1, GL_RGBA, GL_UNSIGNED_BYTE); ImageUtils::PixelWriter write(_image.get()); #ifdef USE_BLEND2D @@ -679,10 +700,7 @@ FeatureRasterizer::FeatureRasterizer( } -FeatureRasterizer::FeatureRasterizer( - osg::Image* image, - const GeoExtent& extent) : - +FeatureRasterizer::FeatureRasterizer(osg::Image* image, const GeoExtent& extent) : _image(image), _extent(extent) { @@ -730,12 +748,18 @@ FeatureRasterizer::render_blend2d( const TextSymbol* masterText = style.getSymbol(); const SkinSymbol* masterSkin = style.getSymbol(); - // Converts coordinates to image space (s,t): + // Converts coordinates to image space (double s, t). + // Blend2D uses a coordinate system where (0,0) is the top-left corner of the image (on the actual corner, + // not the center up the upper-left pixel) and (s,t) is the bottom-right corner of the bottom-right + // pixel. The y-axis is positive down. + RenderFrame frame; frame.xmin = _extent.xMin(); frame.ymin = _extent.yMin(); frame.xmax = _extent.xMax(); frame.ymax = _extent.yMax(); + //frame.xf = (double)(_image->s()-1) / _extent.width(); + //frame.yf = (double)(_image->t()-1) / _extent.height(); frame.xf = (double)_image->s() / _extent.width(); frame.yf = (double)_image->t() / _extent.height(); @@ -760,8 +784,8 @@ FeatureRasterizer::render_blend2d( if (masterLine) { - float lineWidth_px = 1.0f; - float outlineWidth_px = 0.0f; + double lineWidth_px = 1.0f; + double outlineWidth_px = 0.0f; // Calculate the line width in pixels: if (masterLine->stroke()->width().isSet()) @@ -791,14 +815,6 @@ FeatureRasterizer::render_blend2d( _extent.getSRS()->getUnits(), _extent.yMax()); - //double lineWidth_map_south = lineWidth.asDistance( - // _extent.getSRS()->getUnits(), - // _extent.yMin()); - - //double lineWidth_map_north = lineWidth.asDistance( - // _extent.getSRS()->getUnits(), - // _extent.yMax()); - double lineWidth_map = std::min(lineWidth_map_south, lineWidth_map_north); double pixelSize_map = _extent.height() / (double)_image->t(); @@ -806,7 +822,7 @@ FeatureRasterizer::render_blend2d( lineWidth_px = (lineWidth_map / pixelSize_map); // enfore a minimum width of one pixel. - float minPixels = masterLine->stroke()->minPixels().getOrUse(1.0f); + double minPixels = masterLine->stroke()->minPixels().getOrUse(1.0); lineWidth_px = osg::clampAbove(lineWidth_px, minPixels); } @@ -832,20 +848,12 @@ FeatureRasterizer::render_blend2d( _extent.getSRS()->getUnits(), _extent.yMax()); - //double lineWidth_map_south = width.asDistance( - // _extent.getSRS()->getUnits(), - // _extent.yMin()); - - //double lineWidth_map_north = width.asDistance( - // _extent.getSRS()->getUnits(), - // _extent.yMax()); - double lineWidth_map = std::min(lineWidth_map_south, lineWidth_map_north); double pixelSize_map = _extent.height() / (double)_image->t(); outlineWidth_px = (lineWidth_map / pixelSize_map); // enfore a minimum width of one pixel. - float minPixels = masterLine->stroke()->minPixels().getOrUse(1.0f); + double minPixels = masterLine->stroke()->minPixels().getOrUse(1.0); outlineWidth_px = std::max(outlineWidth_px, minPixels); } @@ -1355,10 +1363,28 @@ FeatureRasterizer::finalize() } } + // unbuffer + if (_bufferPixels > 0) + { + auto finalImage = new osg::Image(); + finalImage->allocateImage(_width, _height, 1, _image->getPixelFormat(), _image->getDataType()); + ImageUtils::PixelReader read(_image.get()); + ImageUtils::PixelWriter write(finalImage); + + osg::Vec4 pixel; + ImageUtils::ImageIterator iter(finalImage); + iter.forEachPixel([&](auto& i) { + read(pixel, i.s() + _bufferPixels, i.t() + _bufferPixels, i.r()); + write(pixel, i.s(), i.t(), i.r()); + }); + + _image = finalImage; + } + if (_inverted) { _image->flipVertical(); } - return GeoImage(_image.release(), _extent); + return GeoImage(_image.release(), _originalExtent); } From 34b741551bc3b924f7fa4bc55f9216e2630b9797 Mon Sep 17 00:00:00 2001 From: Glenn Waldron Date: Tue, 28 Jan 2025 16:34:12 -0500 Subject: [PATCH 3/3] Add feature mutli-styling using the select symbol; feature_multi_styling.earth demo file --- src/osgEarth/FeatureStyleSorter.cpp | 78 ++++++++++++++++------------- src/osgEarth/StyleSheet.cpp | 4 +- tests/feature_multi_styling.earth | 74 +++++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 37 deletions(-) create mode 100644 tests/feature_multi_styling.earth diff --git a/src/osgEarth/FeatureStyleSorter.cpp b/src/osgEarth/FeatureStyleSorter.cpp index 76761fb7b3..a147b5ebd0 100644 --- a/src/osgEarth/FeatureStyleSorter.cpp +++ b/src/osgEarth/FeatureStyleSorter.cpp @@ -93,51 +93,57 @@ FeatureStyleSorter::sort_usingSelectors( { Feature* feature = itr->get(); - const std::string& styleString = feature->eval(styleExprCopy, &context); - if (!styleString.empty() && styleString != "null") + const std::string& delimitedStyleStrings = feature->eval(styleExprCopy, &context); + if (!delimitedStyleStrings.empty() && delimitedStyleStrings != "null") { - // resolve the style: - const Style* resolved_style = nullptr; - int resolved_index = 0; + std::vector styleStrings; + StringTokenizer(delimitedStyleStrings, styleStrings, ",", "", false, true); - // if the style string begins with an open bracket, it's an inline style definition. - if (styleString.length() > 0 && styleString[0] == '{') + for (auto& styleString : styleStrings) { - Config conf("style", styleString); - conf.setReferrer(sel.styleExpression().get().uriContext().referrer()); - conf.set("type", "text/css"); - auto& literal_style_and_index = literal_styles[conf.toJSON()]; - if (literal_style_and_index.first.empty()) + // resolve the style: + const Style* resolved_style = nullptr; + int resolved_index = 0; + + // if the style string begins with an open bracket, it's an inline style definition. + if (styleString.length() > 0 && styleString[0] == '{') { - literal_style_and_index.first = Style(conf); - // literal styles always come AFTER sheet styles - literal_style_and_index.second = literal_styles.size() + session->styles()->getStyles().size(); + Config conf("style", styleString); + conf.setReferrer(sel.styleExpression().get().uriContext().referrer()); + conf.set("type", "text/css"); + auto& literal_style_and_index = literal_styles[conf.toJSON()]; + if (literal_style_and_index.first.empty()) + { + literal_style_and_index.first = Style(conf); + // literal styles always come AFTER sheet styles + literal_style_and_index.second = literal_styles.size() + session->styles()->getStyles().size(); + } + resolved_style = &literal_style_and_index.first; + resolved_index = literal_style_and_index.second; } - resolved_style = &literal_style_and_index.first; - resolved_index = literal_style_and_index.second; - } - - // otherwise, look up the style in the stylesheet. Do NOT fall back on a default - // style in this case: for style expressions, the user must be explicit about - // default styling; this is because there is no other way to exclude unwanted - // features. - else - { - auto style_and_index = session->styles()->getStyleAndIndex(styleString); - //const Style* selected_style = session->styles()->getStyle(styleString, false); - if (style_and_index.first) + // otherwise, look up the style in the stylesheet. Do NOT fall back on a default + // style in this case: for style expressions, the user must be explicit about + // default styling; this is because there is no other way to exclude unwanted + // features. + else { - resolved_style = style_and_index.first; - resolved_index = style_and_index.second; + auto style_and_index = session->styles()->getStyleAndIndex(styleString); + + //const Style* selected_style = session->styles()->getStyle(styleString, false); + if (style_and_index.first) + { + resolved_style = style_and_index.first; + resolved_index = style_and_index.second; + } } - } - if (resolved_style) - { - auto& bucket = style_buckets[resolved_index]; - bucket.first = resolved_style; - bucket.second.emplace_back(feature); + if (resolved_style) + { + auto& bucket = style_buckets[resolved_index]; + bucket.first = resolved_style; + bucket.second.emplace_back(feature); + } } } } diff --git a/src/osgEarth/StyleSheet.cpp b/src/osgEarth/StyleSheet.cpp index 72ba1b277b..a42d9dd600 100644 --- a/src/osgEarth/StyleSheet.cpp +++ b/src/osgEarth/StyleSheet.cpp @@ -215,14 +215,16 @@ StyleSheet::Options::fromConfig(const Config& conf) auto_script << "// __oe_auto__\n"; auto_script << "function __oe_select_style() {\n"; + auto_script << " var combo = '';\n"; } - auto_script << " if (" << selector_symbol->predicate().get() << ") return \"" << style.getName() << "\";\n"; + auto_script << " if (" << selector_symbol->predicate().get() << ") combo = combo + '" << style.getName() << ",';\n"; } } if (auto_selector) { + auto_script << " if (combo.length > 0) return combo.substring(0, combo.length-1);\n"; auto_script << " return 'default';\n}\n"; auto new_code = auto_script.str(); diff --git a/tests/feature_multi_styling.earth b/tests/feature_multi_styling.earth new file mode 100644 index 0000000000..0f7ee4df75 --- /dev/null +++ b/tests/feature_multi_styling.earth @@ -0,0 +1,74 @@ + + + + + + https://readymap.org/readymap/mbtiles/daylight-v1.2/{z}/{x}/{-y}.pbf + 14 + 14 + spherical-mercator + pbf + @id + + highway + + + + + https://readymap.org/readymap/tiles/1.0.0/7 + + + + 512 + osm-roads + 25m + + + + + + + + + -3.2425e-07 + -89 + 2928.14m + -77.19548706709453 + 38.83147622799055 + + +