/* -*-c++-*- */ /* osgEarth - Geospatial SDK for OpenSceneGraph * Copyright 2019 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 */ #include #include #include #include using namespace osgEarth; using namespace osgEarth::Util; using namespace osgEarth::Features; using namespace osgEarth::Symbology; #define LC "[FlatteningLayer] " #define OE_TEST OE_DEBUG namespace { // linear interpolation between a and b double inline mix(double a, double b, double t) { return a + (b-a)*t; } // smoothstep (cos approx) interpolation between a and b double inline smoothstep(double a, double b, double t) { // smoothstep (approximates cosine): t = t*t*(3.0-2.0*t); return a + (b-a)*t; } double inline smootherstep(double a, double b, double t) { t = t*t*t*(t*(t*6.0 - 15.0)+10.0); return a + (b-a)*t; } // clamp "a" to [lo..hi]. double inline clamp(double a, double lo, double hi) { return osg::maximum(osg::minimum(a, hi), lo); } typedef osg::Vec3d POINT; typedef osg::Vec3d VECTOR; // iterator that permits the use of looping indexes. template struct CircleIterator { CircleIterator(const std::vector& v) : _v(v) { } int size() const { return _v.size(); } const T& operator[](int i) const { return _v[i % _v.size()]; } const std::vector& _v; }; // is P inside the CCW triangle ABC? bool triangleContains(const POINT& A, const POINT& B, const POINT& C, const POINT& P) { VECTOR AB = B-A, BC = C-B, CA = A-C; if (((P - A) ^ AB).z() > 0.0) return false; if (((P - B) ^ BC).z() > 0.0) return false; if (((P - C) ^ CA).z() > 0.0) return false; return true; } // Find a point internal to the polygon. // This will not always work with polygons that contain holes, // so we need to come up with a different algorithm if this becomes a problem. // Maybe try a random point generator and profile it. osg::Vec3d inline getInternalPoint(const Symbology::Polygon* p) { // Simple test: if the centroid is in the polygon, use it. osg::Vec3d centroid = p->getBounds().center(); if (p->contains2D(centroid.x(), centroid.y())) return centroid; // Concave/holey polygon, so try the hard way. // Ref: http://apodeline.free.fr/FAQ/CGAFAQ/CGAFAQ-3.html CircleIterator vi(p->asVector()); for (int i = 0; i < vi.size(); ++i) { const POINT& V = vi[i]; const POINT& A = vi[i-1]; const POINT& B = vi[i+1]; if (((V - A) ^ (B - V)).z() > 0.0) // Convex vertex? (assume CCW winding) { double minDistQV2 = DBL_MAX; // shortest distance from test point to candidate point int indexBestQ; // index of best candidate point // loop over all other verts (besides A, V, B): for (int j = i + 2; j < i + 2 + vi.size() - 3; ++j) { const POINT& Q = vi[j]; if (triangleContains(A, V, B, Q)) { double distQV2 = (Q - V).length2(); if (distQV2 < minDistQV2) { minDistQV2 = distQV2; indexBestQ = j; } } } POINT result; // If no inside point was found, return the midpoint of AB. if (minDistQV2 == DBL_MAX) { result = (A+B)*0.5; } // Otherwise, use the midpoint of QV. else { const POINT& Q = vi[indexBestQ]; result = (Q+V)*0.5; } // make sure the resulting point doesn't fall within any of the // polygon's holes. if (p->contains2D(result.x(), result.y())) { return result; } } } // Will only happen is holes prevent us from finding an internal point. OE_WARN << LC << "getInternalPoint failed miserably\n"; return p->getBounds().center(); } double getDistanceSquaredToClosestEdge(const osg::Vec3d& P, const Symbology::Polygon* poly) { double Dmin = DBL_MAX; ConstSegmentIterator segIter(poly, true); while (segIter.hasMore()) { const Segment segment = segIter.next(); const POINT& A = segment.first; const POINT& B = segment.second; const VECTOR AP = P-A, AB = B-A; double t = clamp((AP*AB)/AB.length2(), 0.0, 1.0); VECTOR PROJ = A + AB*t; double D = (P - PROJ).length2(); if (D < Dmin) Dmin = D; } return Dmin; } struct Widths { Widths(const Widths& rhs) { bufferWidth = rhs.bufferWidth; lineWidth = rhs.lineWidth; } Widths(double bufferWidth, double lineWidth) { this->bufferWidth = bufferWidth; this->lineWidth = lineWidth; } double bufferWidth; double lineWidth; }; typedef std::vector WidthsList; // Creates a heightfield that flattens an area intersecting the input polygon geometry. // The height of the area is found by sampling a point internal to the polygon. // bufferWidth = width of transition from flat area to natural terrain. bool integratePolygons(const TileKey& key, osg::HeightField* hf, const MultiGeometry* geom, const SpatialReference* geomSRS, WidthsList& widths, ElevationEnvelope* envelope, bool fillAllPixels, ProgressCallback* progress) { bool wroteChanges = false; const GeoExtent& ex = key.getExtent(); double col_interval = ex.width() / (double)(hf->getNumColumns()-1); double row_interval = ex.height() / (double)(hf->getNumRows()-1); POINT Pex, P, internalP; bool needsTransform = ex.getSRS() != geomSRS; for (unsigned col = 0; col < hf->getNumColumns(); ++col) { Pex.x() = ex.xMin() + (double)col * col_interval; for (unsigned row = 0; row < hf->getNumRows(); ++row) { // check for cancelation periodically //if (progress && progress->isCanceled()) // return false; Pex.y() = ex.yMin() + (double)row * row_interval; if (needsTransform) ex.getSRS()->transform(Pex, geomSRS, P); else P = Pex; bool done = false; double minD2 = DBL_MAX;//bufferWidth * bufferWidth; // minimum distance(squared) to closest polygon edge double bufferWidth = 0.0; const Symbology::Polygon* bestPoly = 0L; for (unsigned int geomIndex = 0; geomIndex < geom->getNumComponents(); geomIndex++) { Geometry* component = geom->getComponents()[geomIndex].get(); Widths width = widths[geomIndex]; ConstGeometryIterator giter(component, false); while (giter.hasMore() && !done) { const Symbology::Polygon* polygon = dynamic_cast(giter.next()); if (polygon) { // Does the point P fall within the polygon? if (polygon->contains2D(P.x(), P.y())) { // yes, flatten it to the polygon's centroid elevation; // and we're dont with this point. done = true; bestPoly = polygon; minD2 = -1.0; bufferWidth = width.bufferWidth; } // If not in the polygon, how far to the closest edge? else { double D2 = getDistanceSquaredToClosestEdge(P, polygon); if (D2 < minD2) { minD2 = D2; bestPoly = polygon; bufferWidth = width.bufferWidth; } } } } } if (bestPoly && minD2 != 0.0) { float h; POINT internalP = getInternalPoint(bestPoly); float elevInternal = envelope->getElevation(internalP.x(), internalP.y()); if (minD2 < 0.0) { h = elevInternal; } else { float elevNatural = envelope->getElevation(P.x(), P.y()); double blend = clamp(sqrt(minD2)/bufferWidth, 0.0, 1.0); // [0..1] 0=internal, 1=natural h = smootherstep(elevInternal, elevNatural, blend); } hf->setHeight(col, row, h); wroteChanges = true; } else if (fillAllPixels) { float h = envelope->getElevation(P.x(), P.y()); hf->setHeight(col, row, h); // do not set wroteChanges } } } return wroteChanges; } struct Sample { double D2; // distance to segment squared osg::Vec3d A; // endpoint of segment osg::Vec3d B; // other endpoint of segment; double T; // segment parameter of closest point // used later: float elevPROJ; // elevation at point on segment float elev; // flattened elevation double D; // distance double innerRadius; double outerRadius; bool operator < (struct Sample& rhs) const { return D2 < rhs.D2; } }; typedef std::vector Samples; bool EQ2(const osg::Vec3d& a, const osg::Vec3d& b) { return osg::equivalent(a.x(), b.x()) && osg::equivalent(a.y(), b.y()); } bool isSampleValid(const Sample* b1, Samples& samples) { for (unsigned i = 0; i < samples.size(); ++i) { Sample* b2 = &samples[i]; if (b1 == b2) continue; if (b1->T == 0.0 && (EQ2(b1->A, b2->A) || EQ2(b1->A, b2->B))) return false; if (b1->T == 1.0 && (EQ2(b1->B, b2->A) || EQ2(b1->B, b2->B))) return false; } return true; } // Inverse Direct Weighting (IDW) interpolation float interpolateSamplesIDW(Samples& samples) { // higher powerParam corresponds to increased proximity favoring const double powerParam = 2.5; if (samples.size() == 0) return 0.0f; if (samples.size() == 1) return samples[0].elev; double numer = 0.0; double denom = 0.0; for (unsigned i = 0; i < samples.size(); ++i) { if (osg::equivalent(samples[i].D, 0.0)) { numer = samples[i].elev, denom = 1.0; break; } else { double w = pow(1.0/samples[i].D, powerParam); numer += w * samples[i].elev; denom += w; } } return numer / denom; } // Linear interpolation (simple mean) float interpolateSamplesLinear(Samples& samples) { double numer = 0.0; for (unsigned i = 0; i 0 ? (numer / (double)(samples.size())) : FLT_MAX; } /** * Create a heightfield that flattens the terrain around linear geometry. * lineWidth = width of completely flat area * bufferWidth = width of transition from flat area to natural terrain * * Note: this algorithm only samples elevation data from the source (elevation pool). * As it progresses, however, it is creating new modified elevation data -- but later * points will continue to derive their source data from the original data. This means * there will be some discontinuities in the final data, especially along the edges of * the flattening buffer. * * There is not perfect solution for this, but one improvement would be to copy the * source elevation into the heightfield as a starting point, and then sample that * modifiable heightfield as we go along. */ bool integrateLines(const TileKey& key, osg::HeightField* hf, const MultiGeometry* geom, const SpatialReference* geomSRS, WidthsList& widths, ElevationEnvelope* envelope, bool fillAllPixels, ProgressCallback* progress) { bool wroteChanges = false; const GeoExtent& ex = key.getExtent(); double col_interval = ex.width() / (double)(hf->getNumColumns()-1); double row_interval = ex.height() / (double)(hf->getNumRows()-1); osg::Vec3d Pex, P, PROJ; bool needsTransform = ex.getSRS() != geomSRS; // Loop over the new heightfield. for (unsigned col = 0; col < hf->getNumColumns(); ++col) { Pex.x() = ex.xMin() + (double)col * col_interval; for (unsigned row = 0; row < hf->getNumRows(); ++row) { // check for cancelation periodically //if (progress && progress->isCanceled()) // return false; Pex.y() = ex.yMin() + (double)row * row_interval; // Move the point into the working SRS if necessary if (needsTransform) ex.getSRS()->transform(Pex, geomSRS, P); else P = Pex; // For each point, we need to find the closest line segments to that point // because the elevation values on these line segments will be the flattening // value. There may be more than one line segment that falls within the search // radius; we will collect up to MaxSamples of these for each heightfield point. static const unsigned Maxsamples = 4; Samples samples; for (unsigned int geomIndex = 0; geomIndex < geom->getNumComponents(); geomIndex++) { Widths w = widths[geomIndex]; double innerRadius = w.lineWidth * 0.5; double outerRadius = innerRadius + w.bufferWidth; double outerRadius2 = outerRadius * outerRadius; Geometry* component = geom->getComponents()[geomIndex].get(); // Search for line segments. ConstGeometryIterator giter(component); while (giter.hasMore()) { const Geometry* part = giter.next(); for (int i = 0; i < part->size()-1; ++i) { // AB is a candidate line segment: const osg::Vec3d& A = (*part)[i]; const osg::Vec3d& B = (*part)[i+1]; osg::Vec3d AB = B - A; // current segment AB double t; // parameter [0..1] on segment AB double D2; // shortest distance from point P to segment AB, squared double L2 = AB.length2(); // length (squared) of segment AB osg::Vec3d AP = P - A; // vector from endpoint A to point P if (L2 == 0.0) { // trivial case: zero-length segment t = 0.0; D2 = AP.length2(); } else { // Calculate parameter "t" [0..1] which will yield the closest point on AB to P. // Clamping it means the closest point won't be beyond the endpoints of the segment. t = clamp((AP * AB)/L2, 0.0, 1.0); // project our point P onto segment AB: PROJ.set( A + AB*t ); // measure the distance (squared) from P to the projected point on AB: D2 = (P - PROJ).length2(); } // If the distance from our point to the line segment falls within // the maximum flattening distance, store it. if (D2 <= outerRadius2) { // see if P is a new sample. Sample* b; if (samples.size() < Maxsamples) { // If we haven't collected the maximum number of samples yet, // just add this to the list: samples.push_back(Sample()); b = &samples.back(); } else { // If we are maxed out on samples, find the farthest one we have so far // and replace it if the new point is closer: unsigned max_i = 0; for (unsigned i=1; i samples[max_i].D2) max_i = i; b = &samples[max_i]; if (b->D2 < D2) b = 0L; } if (b) { b->D2 = D2; b->A = A; b->B = B; b->T = t; b->innerRadius = innerRadius; b->outerRadius = outerRadius; } } } } } // Remove unnecessary sample points that lie on the endpoint of a segment // that abuts another segment in our list. for (unsigned i = 0; i < samples.size();) { if (!isSampleValid(&samples[i], samples)) { samples[i] = samples[samples.size() - 1]; samples.resize(samples.size() - 1); } else ++i; } // Now that we are done searching for line segments close to our point, // we will collect the elevations at our sample points and use them to // create a new elevation value for our point. if (samples.size() > 0) { // The original elevation at our point: float elevP = envelope->getElevation(P.x(), P.y()); for (unsigned i = 0; i < samples.size(); ++i) { Sample& sample = samples[i]; sample.D = sqrt(sample.D2); // Blend factor. 0 = distance is less than or equal to the inner radius; // 1 = distance is greater than or equal to the outer radius. double blend = clamp( (sample.D - sample.innerRadius) / (sample.outerRadius - sample.innerRadius), 0.0, 1.0); if (sample.T == 0.0) { sample.elevPROJ = envelope->getElevation(sample.A.x(), sample.A.y()); if (sample.elevPROJ == NO_DATA_VALUE) sample.elevPROJ = elevP; } else if (sample.T == 1.0) { sample.elevPROJ = envelope->getElevation(sample.B.x(), sample.B.y()); if (sample.elevPROJ == NO_DATA_VALUE) sample.elevPROJ = elevP; } else { float elevA = envelope->getElevation(sample.A.x(), sample.A.y()); if (elevA == NO_DATA_VALUE) elevA = elevP; float elevB = envelope->getElevation(sample.B.x(), sample.B.y()); if (elevB == NO_DATA_VALUE) elevB = elevP; // linear interpolation of height from point A to point B on the segment: sample.elevPROJ = mix(elevA, elevB, sample.T); } // smoothstep interpolation of along the buffer (perpendicular to the segment) // will gently integrate the new value into the existing terrain. sample.elev = smootherstep(sample.elevPROJ, elevP, blend); } // Finally, combine our new elevation values and set the new value in the output. float finalElev = interpolateSamplesIDW(samples); if (finalElev < FLT_MAX) hf->setHeight(col, row, finalElev); else hf->setHeight(col, row, elevP); wroteChanges = true; } else if (fillAllPixels) { // No close segments were found, so just copy over the source data. float h = envelope->getElevation(P.x(), P.y()); hf->setHeight(col, row, h); // Note: do not set wroteChanges to true. } } } return wroteChanges; } bool integrate(const TileKey& key, osg::HeightField* hf, const MultiGeometry* geom, const SpatialReference* geomSRS, WidthsList& widths, ElevationEnvelope* envelope, bool fillAllPixels, ProgressCallback* progress) { if (geom->isLinear()) return integrateLines(key, hf, geom, geomSRS, widths, envelope, fillAllPixels, progress); else return integratePolygons(key, hf, geom, geomSRS, widths, envelope, fillAllPixels, progress); } } //........................................................................ FlatteningLayer::FlatteningLayer(const FlatteningLayerOptions& options) : ElevationLayer(&_optionsConcrete), _options(&_optionsConcrete), _optionsConcrete(options) { // Experiment with this and see what will work. _pool = new ElevationPool(); _pool->setTileSize(257u); init(); } void FlatteningLayer::init() { setTileSourceExpected(false); ElevationLayer::init(); } const Status& FlatteningLayer::open() { // ensure the caller named a feature source: if (!options().featureSource().isSet() && !options().featureSourceLayer().isSet()) { return setStatus(Status::Error(Status::ConfigurationError, "Missing required feature source")); } // If the feature source is inline, open it now. if (options().featureSource().isSet()) { // Create the feature source instance: FeatureSource* fs = FeatureSourceFactory::create(options().featureSource().get()); if (!fs) { return setStatus(Status::Error(Status::ServiceUnavailable, "Unable to create feature source as defined")); } // Open it: setStatus(fs->open(getReadOptions())); if (getStatus().isError()) return getStatus(); setFeatureSource(fs); if (getStatus().isError()) return getStatus(); } const Profile* profile = getProfile(); if ( !profile ) { profile = Registry::instance()->getGlobalGeodeticProfile(); setProfile( profile ); } return ElevationLayer::open(); } void FlatteningLayer::setFeatureSource(FeatureSource* fs) { if (fs) { _featureSource = fs; if (_featureSource) { if (!_featureSource->getFeatureProfile()) { setStatus(Status::Error(Status::ConfigurationError, "No feature profile (is the source open?)")); _featureSource = 0L; return; } } } } FlatteningLayer::~FlatteningLayer() { _featureLayerListener.clear(); } void FlatteningLayer::setFeatureSourceLayer(FeatureSourceLayer* layer) { if (layer) { if (layer->getStatus().isOK()) setFeatureSource(layer->getFeatureSource()); } else { setFeatureSource(0L); } } void FlatteningLayer::addedToMap(const Map* map) { // Initialize the elevation pool with our map: OE_INFO << LC << "Attaching elevation pool to map\n"; _pool->setMap( map ); // Listen for our feature source layer to arrive, if there is one. if (options().featureSourceLayer().isSet()) { _featureLayerListener.listen( map, options().featureSourceLayer().get(), this, &FlatteningLayer::setFeatureSourceLayer); } // Collect all elevation layers preceding this one and use them for flattening. ElevationLayerVector layers; map->getLayers(layers); for (ElevationLayerVector::iterator i = layers.begin(); i != layers.end(); ++i) { if (i->get() == this) { layers.erase(i); break; } else { OE_INFO << LC << "Using: " << i->get()->getName() << "\n"; } } if (!layers.empty()) { _pool->setElevationLayers(layers); } } void FlatteningLayer::removedFromMap(const Map* map) { _featureLayerListener.clear(); } void FlatteningLayer::createImplementation(const TileKey& key, osg::ref_ptr& hf, osg::ref_ptr& normalMap, ProgressCallback* progress) { if (getStatus().isError()) { return; } if (!_featureSource.valid()) { setStatus(Status(Status::ServiceUnavailable, "No feature source")); return; } const FeatureProfile* featureProfile = _featureSource->getFeatureProfile(); if (!featureProfile) { setStatus(Status(Status::ConfigurationError, "Feature profile is missing")); return; } const SpatialReference* featureSRS = featureProfile->getSRS(); if (!featureSRS) { setStatus(Status(Status::ConfigurationError, "Feature profile has no SRS")); return; } if (_pool->getElevationLayers().empty()) { OE_WARN << LC << "Internal error - Pool layer set is empty\n"; return; } OE_START_TIMER(create); // If the feature source has a tiling profile, we are going to have to map the incoming // TileKey to a set of intersecting TileKeys in the feature source's tiling profile. GeoExtent queryExtent = key.getExtent().transform(featureSRS); // Lat/Long extent: GeoExtent geoExtent = queryExtent.transform(featureSRS->getGeographicSRS()); // Buffer the query extent to include the potentially flattened area. /* double linewidth = SpatialReference::transformUnits( options().lineWidth().get(), featureSRS, geoExtent.getCentroid().y()); double bufferwidth = SpatialReference::transformUnits( options().bufferWidth().get(), featureSRS, geoExtent.getCentroid().y()); */ // TODO: JBFIX. Add a "max" setting somewhere. double linewidth = 10.0; double bufferwidth = 10.0; double queryBuffer = 0.5*linewidth + bufferwidth; queryExtent.expand(queryBuffer, queryBuffer); // We must do all the feature processing in a projected system since we're using vector math. #if 0 osg::ref_ptr workingSRS = queryExtent.getSRS(); //if (workingSRS->isGeographic()) { osg::Vec3d refPos = queryExtent.getCentroid(); workingSRS = workingSRS->createTangentPlaneSRS(refPos); } #else const SpatialReference* workingSRS = queryExtent.getSRS()->isGeographic() ? SpatialReference::get("spherical-mercator") : queryExtent.getSRS(); #endif bool needsTransform = !featureSRS->isHorizEquivalentTo(workingSRS); osg::ref_ptr< StyleSheet > styleSheet = new StyleSheet(); styleSheet->setScript(options().getScript()); osg::ref_ptr< Session > session = new Session( _map.get(), styleSheet.get()); // We will collection all the feature geometries in this multigeometry: MultiGeometry geoms; WidthsList widths; if (featureProfile->getProfile()) { // Tiled source, must resolve complete set of intersecting tiles: std::vector intersectingKeys; featureProfile->getProfile()->getIntersectingTiles(queryExtent, key.getLOD(), intersectingKeys); std::set featureKeys; for (int i = 0; i < intersectingKeys.size(); ++i) { if (intersectingKeys[i].getLOD() > featureProfile->getMaxLevel()) featureKeys.insert(intersectingKeys[i].createAncestorKey(featureProfile->getMaxLevel())); else featureKeys.insert(intersectingKeys[i]); } // Query and collect all the features we need for this tile. for (std::set::const_iterator i = featureKeys.begin(); i != featureKeys.end(); ++i) { Query query; query.tileKey() = *i; osg::ref_ptr cursor = _featureSource->createFeatureCursor(query, progress); while (cursor.valid() && cursor->hasMore()) { Feature* feature = cursor->nextFeature(); double lineWidth = 0.0; double bufferWidth = 0.0; if (options().lineWidth().isSet()) { NumericExpression lineWidthExpr(options().lineWidth().get()); lineWidth = feature->eval(lineWidthExpr, session.get()); } if (options().bufferWidth().isSet()) { NumericExpression bufferWidthExpr(options().bufferWidth().get()); bufferWidth = feature->eval(bufferWidthExpr, session.get()); } // Transform the feature geometry to our working (projected) SRS. if (needsTransform) feature->transform(workingSRS); lineWidth = SpatialReference::transformUnits( Distance(lineWidth), featureSRS, geoExtent.getCentroid().y()); bufferWidth = SpatialReference::transformUnits( Distance(bufferWidth), featureSRS, geoExtent.getCentroid().y()); //TODO: optimization: test the geometry bounds against the expanded tilekey bounds // in order to discard geometries we don't care about geoms.getComponents().push_back(feature->getGeometry()); widths.push_back(Widths(bufferWidth, lineWidth)); } } } else { // Non-tiled feaure source, just query arbitrary extent: // Set up the query; bounds must be in the feature SRS: Query query; query.bounds() = queryExtent.bounds(); // Run the query and fill the list. osg::ref_ptr cursor = _featureSource->createFeatureCursor(query, progress); while (cursor.valid() && cursor->hasMore()) { Feature* feature = cursor->nextFeature(); double lineWidth = 0.0; double bufferWidth = 0.0; if (options().lineWidth().isSet()) { NumericExpression lineWidthExpr(options().lineWidth().get()); lineWidth = feature->eval(lineWidthExpr, session.get()); } if (options().bufferWidth().isSet()) { NumericExpression bufferWidthExpr(options().bufferWidth().get()); bufferWidth = feature->eval(bufferWidthExpr, session.get()); } // Transform the feature geometry to our working (projected) SRS. if (needsTransform) feature->transform(workingSRS); lineWidth = SpatialReference::transformUnits( Distance(lineWidth), featureSRS, geoExtent.getCentroid().y()); bufferWidth = SpatialReference::transformUnits( Distance(bufferWidth), featureSRS, geoExtent.getCentroid().y()); // Transform the feature geometry to our working (projected) SRS. if (needsTransform) feature->transform(workingSRS); //TODO: optimization: test the geometry bounds against the expanded tilekey bounds // in order to discard geometries we don't care about geoms.getComponents().push_back(feature->getGeometry()); widths.push_back(Widths(bufferWidth, lineWidth)); } } if (!geoms.getComponents().empty()) { if (!hf.valid()) { // Make an empty heightfield to populate: hf = HeightFieldUtils::createReferenceHeightField( queryExtent, 257, 257, // base tile size for elevation data 0u, // no border true); // initialize to HAE (0.0) heights // Initialize to NO DATA. hf->getFloatArray()->assign(hf->getNumColumns()*hf->getNumRows(), NO_DATA_VALUE); } // Create an elevation query envelope at the LOD we are creating osg::ref_ptr envelope = _pool->createEnvelope(workingSRS, key.getLOD()); bool fill = (options().fill() == true); integrate(key, hf.get(), &geoms, workingSRS, widths, envelope.get(), fill, progress); } }