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/TDTiles>
20 #include <osgEarth/Async>
21 #include <osgEarth/Utils>
22 #include <osgEarth/Registry>
23 #include <osgEarth/URI>
24 #include <osgDB/FileNameUtils>
25 
26 using namespace osgEarth;
27 
28 #define LC "[3DTiles] "
29 
30 //........................................................................
31 
32 #define PSEUDOLOADER_TILE_CONTENT_EXT "osgearth_3dtiles_content"
33 #define PSEUDOLOADER_TILE_CHILDREN_EXT "osgearth_3dtiles_children"
34 #define PSEUDOLOADER_EXTERNAL_TILESET_EXT "osgearth_3dtiles_external_tileset"
35 #define TAG_INVOKER "osgEarth::TDTiles::Invoker"
36 
37 namespace osgEarth { namespace TDTiles
38 {
39     //! Operation that loads all of a tilenode's children.
40     struct LoadChildren : public AsyncFunction
41     {
42         osg::observer_ptr<TDTiles::TileNode> _tileNode;
LoadChildrenosgEarth::TDTiles::LoadChildren43         LoadChildren(TDTiles::TileNode* tileNode) : _tileNode(tileNode) { }
operator ()osgEarth::TDTiles::LoadChildren44         virtual ReadResult operator()() const
45         {
46             OE_DEBUG << LC << "LoadChildren" << std::endl;
47             osg::ref_ptr<TDTiles::TileNode> tileNode;
48             if (_tileNode.lock(tileNode))
49             {
50                 return tileNode->loadChildren();
51             }
52             else return ReadResult(ReadResult::RESULT_NOT_FOUND);
53         }
54     };
55 
56     //! Operation that loads one child of a TileNode.
57     struct LoadChild : public AsyncFunction
58     {
59         osg::observer_ptr<TDTiles::TileNode> _tileNode;
60         unsigned _index;
LoadChildosgEarth::TDTiles::LoadChild61         LoadChild(TDTiles::TileNode* tileNode, unsigned index)
62             : _tileNode(tileNode), _index(index) { }
operator ()osgEarth::TDTiles::LoadChild63         virtual ReadResult operator()() const
64         {
65             OE_DEBUG << LC << "LoadChild" << std::endl;
66             osg::ref_ptr<TDTiles::TileNode> tileNode;
67             if (_tileNode.lock(tileNode))
68             {
69                 return tileNode->loadChild(_index);
70             }
71             else return ReadResult(ReadResult::RESULT_NOT_FOUND);
72         }
73     };
74 
75     //! Operation that loads the content of a TileNode (from a URI)
76     struct LoadTileContent : public AsyncFunction
77     {
78         osg::observer_ptr<TDTiles::TileNode> _tileNode;
LoadTileContentosgEarth::TDTiles::LoadTileContent79         LoadTileContent(TDTiles::TileNode* tileNode) : _tileNode(tileNode) { }
operator ()osgEarth::TDTiles::LoadTileContent80         virtual ReadResult operator()() const
81         {
82             OE_DEBUG << LC << "LoadTileContent" << std::endl;
83             osg::ref_ptr<TDTiles::TileNode> tileNode;
84             if (_tileNode.lock(tileNode))
85             {
86                 return tileNode->loadContent();
87             }
88             else return ReadResult(ReadResult::RESULT_NOT_FOUND);
89         }
90     };
91 
92     //! Operation that loads an external tile set
93     struct LoadExternalTileset : public AsyncFunction
94     {
95         osg::observer_ptr<TDTilesetGroup> _group;
LoadExternalTilesetosgEarth::TDTiles::LoadExternalTileset96         LoadExternalTileset(TDTilesetGroup* group) : _group(group) { }
operator ()osgEarth::TDTiles::LoadExternalTileset97         virtual ReadResult operator()() const
98         {
99             OE_DEBUG << LC << "LoadExternalTileset" << std::endl;
100             osg::ref_ptr<TDTilesetGroup> group;
101             if (_group.lock(group))
102             {
103                 const URI& uri = group->getTilesetURL();
104                 OE_INFO << LC << "Loading external tileset " << uri.full() << std::endl;
105 
106                 ReadResult r = uri.readString(group->getReadOptions());
107                 if (r.succeeded())
108                 {
109                     TDTiles::Tileset* tileset = TDTiles::Tileset::create(r.getString(), uri.context());
110                     if (tileset)
111                     {
112                         return group->loadRoot(tileset);
113                     }
114                     else
115                     {
116                         return ReadResult("Invalid tileset JSON");
117                     }
118                 }
119                 return ReadResult("Failed to load external tileset");
120             }
121             else return ReadResult();
122         }
123     };
124 }}
125 
126 //........................................................................
127 
128 void
fromJSON(const Json::Value & value)129 TDTiles::Asset::fromJSON(const Json::Value& value)
130 {
131     if (value.isMember("version"))
132         version() = value.get("version", "").asString();
133     if (value.isMember("tilesetVersion"))
134         tilesetVersion() = value.get("tilesetVersion", "").asString();
135 }
136 
137 Json::Value
getJSON() const138 TDTiles::Asset::getJSON() const
139 {
140     Json::Value value(Json::objectValue);
141     if (version().isSet())
142         value["version"] = version().get();
143     if (tilesetVersion().isSet())
144         value["tilesetVersion"] = tilesetVersion().get();
145     return value;
146 }
147 
148 //........................................................................
149 
150 void
fromJSON(const Json::Value & value)151 TDTiles::BoundingVolume::fromJSON(const Json::Value& value)
152 {
153     if (value.isMember("region"))
154     {
155         const Json::Value& a = value["region"];
156         if (a.isArray() && a.size() == 6)
157         {
158             Json::Value::const_iterator i = a.begin();
159             region()->xMin() = (*i++).asDouble();
160             region()->yMin() = (*i++).asDouble();
161             region()->xMax() = (*i++).asDouble();
162             region()->yMax() = (*i++).asDouble();
163             region()->zMin() = (*i++).asDouble();
164             region()->zMax() = (*i++).asDouble();
165         }
166         else OE_WARN << "Invalid region array" << std::endl;
167     }
168 
169     if (value.isMember("sphere"))
170     {
171         const Json::Value& a = value["sphere"];
172         if (a.isArray() && a.size() == 4)
173         {
174             Json::Value::const_iterator i = a.begin();
175             sphere()->center().x() = (*i++).asDouble();
176             sphere()->center().y() = (*i++).asDouble();
177             sphere()->center().z() = (*i++).asDouble();
178             sphere()->radius()     = (*i++).asDouble();
179         }
180     }
181     if (value.isMember("box"))
182     {
183         const Json::Value& a = value["box"];
184         if (a.isArray() && a.size() == 12)
185         {
186             Json::Value::const_iterator i = a.begin();
187             osg::Vec3 center((*i++).asDouble(), (*i++).asDouble(), (*i++).asDouble());
188             osg::Vec3 xvec((*i++).asDouble(), (*i++).asDouble(), (*i++).asDouble());
189             osg::Vec3 yvec((*i++).asDouble(), (*i++).asDouble(), (*i++).asDouble());
190             osg::Vec3 zvec((*i++).asDouble(), (*i++).asDouble(), (*i++).asDouble());
191             box()->expandBy(center+xvec);
192             box()->expandBy(center-xvec);
193             box()->expandBy(center+yvec);
194             box()->expandBy(center-yvec);
195             box()->expandBy(center+zvec);
196             box()->expandBy(center-zvec);
197         }
198         else OE_WARN << "Invalid box array" << std::endl;
199     }
200 }
201 
202 Json::Value
getJSON() const203 TDTiles::BoundingVolume::getJSON() const
204 {
205     Json::Value value(Json::objectValue);
206 
207     if (region().isSet())
208     {
209         Json::Value a(Json::arrayValue);
210         a.append(region()->xMin());
211         a.append(region()->yMin());
212         a.append(region()->xMax());
213         a.append(region()->yMax());
214         a.append(region()->zMin());
215         a.append(region()->zMax());
216         value["region"] = a;
217     }
218     else if (sphere().isSet())
219     {
220         Json::Value a(Json::arrayValue);
221         a.append(sphere()->center().x());
222         a.append(sphere()->center().y());
223         a.append(sphere()->center().z());
224         a.append(sphere()->radius());
225         value["sphere"] = a;
226     }
227     else if (box().isSet())
228     {
229         OE_WARN << LC << "box not implemented" << std::endl;
230     }
231     return value;
232 }
233 
234 osg::BoundingSphere
asBoundingSphere() const235 TDTiles::BoundingVolume::asBoundingSphere() const
236 {
237     const SpatialReference* epsg4979 = SpatialReference::get("epsg:4979");
238     if (!epsg4979)
239         return osg::BoundingSphere();
240 
241     if (region().isSet())
242     {
243         GeoExtent extent(epsg4979,
244             osg::RadiansToDegrees(region()->xMin()),
245             osg::RadiansToDegrees(region()->yMin()),
246             osg::RadiansToDegrees(region()->xMax()),
247             osg::RadiansToDegrees(region()->yMax()));
248 
249         osg::BoundingSphered bs = extent.createWorldBoundingSphere(region()->zMin(), region()->zMax());
250         return osg::BoundingSphere(bs.center(), bs.radius());
251     }
252 
253     else if (sphere().isSet())
254     {
255         return sphere().get();
256     }
257 
258     else if (box().isSet())
259     {
260         return osg::BoundingSphere(box()->center(), box()->radius());
261     }
262 
263     return osg::BoundingSphere();
264 }
265 
266 //........................................................................
267 
268 void
fromJSON(const Json::Value & value,LoadContext & lc)269 TDTiles::TileContent::fromJSON(const Json::Value& value, LoadContext& lc)
270 {
271     if (value.isMember("boundingVolume"))
272         boundingVolume() = BoundingVolume(value.get("boundingVolume", Json::nullValue));
273     if (value.isMember("uri"))
274         uri() = URI(value.get("uri", "").asString(), lc._uc);
275     if (value.isMember("url"))
276         uri() = URI(value.get("url", "").asString(), lc._uc);
277 }
278 
279 Json::Value
getJSON() const280 TDTiles::TileContent::getJSON() const
281 {
282     Json::Value value(Json::objectValue);
283     if (boundingVolume().isSet())
284         value["boundingVolume"] = boundingVolume()->getJSON();
285     if (uri().isSet())
286         value["uri"] = uri()->base();
287     return value;
288 }
289 
290 //........................................................................
291 
292 void
fromJSON(const Json::Value & value,LoadContext & uc)293 TDTiles::Tile::fromJSON(const Json::Value& value, LoadContext& uc)
294 {
295     if (value.isMember("boundingVolume"))
296         boundingVolume() = value["boundingVolume"];
297     if (value.isMember("viewerRequestVolume"))
298         viewerRequestVolume() = value["viewerRequestVolume"];
299     if (value.isMember("geometricError"))
300         geometricError() = value.get("geometricError", 0.0).asDouble();
301     if (value.isMember("content"))
302         content() = TileContent(value["content"], uc);
303 
304     if (value.isMember("refine"))
305     {
306         refine() = osgEarth::ciEquals(value["refine"].asString(), "add") ? REFINE_ADD : REFINE_REPLACE;
307         uc._defaultRefine = refine().get();
308     }
309     else
310     {
311         refine() = uc._defaultRefine;
312     }
313 
314     if (value.isMember("transform"))
315     {
316         const Json::Value& digits = value["transform"];
317         double c[16];
318         if (digits.isArray() && digits.size() == 16)
319         {
320             unsigned k=0;
321             for(Json::Value::const_iterator i = digits.begin(); i != digits.end(); ++i)
322                 c[k++] = (*i).asDouble();
323             transform() = osg::Matrix(c);
324         }
325     }
326 
327     if (value.isMember("children"))
328     {
329         const Json::Value& a = value["children"];
330         if (a.isArray())
331         {
332             for (Json::Value::const_iterator i = a.begin(); i != a.end(); ++i)
333             {
334                 osg::ref_ptr<Tile> tile = new Tile(*i, uc);
335                 children().push_back(tile.get());
336             }
337         }
338     }
339 }
340 
341 Json::Value
getJSON() const342 TDTiles::Tile::getJSON() const
343 {
344     Json::Value value(Json::objectValue);
345 
346     if (boundingVolume().isSet())
347         value["boundingVolume"] = boundingVolume()->getJSON();
348     if (viewerRequestVolume().isSet())
349         value["viewerRequestVolume"] = viewerRequestVolume()->getJSON();
350     if (geometricError().isSet())
351         value["geometricError"] = geometricError().get();
352     if (refine().isSet())
353         value["refine"] = (refine().get() == REFINE_ADD) ? "add" : "replace";
354     if (content().isSet())
355         value["content"] = content()->getJSON();
356 
357 
358     if (!children().empty())
359     {
360         Json::Value collection(Json::arrayValue);
361         for(unsigned i=0; i<children().size(); ++i)
362         {
363             Tile* child = children()[i].get();
364             if (child)
365             {
366                 collection.append(child->getJSON());
367             }
368         }
369         value["children"] = collection;
370     }
371 
372     return value;
373 }
374 
375 //........................................................................
376 
377 void
fromJSON(const Json::Value & value,LoadContext & uc)378 TDTiles::Tileset::fromJSON(const Json::Value& value, LoadContext& uc)
379 {
380     if (value.isMember("asset"))
381         asset() = Asset(value.get("asset", Json::nullValue));
382     if (value.isMember("boundingVolume"))
383         boundingVolume() = BoundingVolume(value.get("boundingVolume", Json::nullValue));
384     if (value.isMember("geometricError"))
385         geometricError() = value.get("geometricError", 0.0).asDouble();
386     if (value.isMember("root"))
387         root() = new Tile(value["root"], uc);
388 }
389 
390 Json::Value
getJSON() const391 TDTiles::Tileset::getJSON() const
392 {
393     Json::Value value(Json::objectValue);
394     if (asset().isSet())
395         value["asset"] = asset()->getJSON();
396     if (boundingVolume().isSet())
397         value["boundingVolume"] = boundingVolume()->getJSON();
398     if (geometricError().isSet())
399         value["geometricError"] = geometricError().get();
400     if (root().valid())
401         value["root"] = root()->getJSON();
402     return value;
403 }
404 
405 TDTiles::Tileset*
create(const std::string & json,const URIContext & uc)406 TDTiles::Tileset::create(const std::string& json, const URIContext& uc)
407 {
408     Json::Reader reader;
409     Json::Value root(Json::objectValue);
410     if (!reader.parse(json, root, false))
411         return NULL;
412 
413     LoadContext lc;
414     lc._uc = uc;
415     lc._defaultRefine = REFINE_REPLACE;
416 
417     return new TDTiles::Tileset(root, lc);
418 }
419 
420 //........................................................................
421 
TileNode(TDTiles::Tile * tile,TDTiles::ContentHandler * handler,const osgDB::Options * readOptions)422 TDTiles::TileNode::TileNode(TDTiles::Tile* tile,
423                             TDTiles::ContentHandler* handler,
424                             const osgDB::Options* readOptions) :
425     _tile(tile),
426     _handler(handler),
427     _readOptions(readOptions)
428 {
429     // the transform to localize this tile:
430     if (tile->transform().isSet())
431     {
432         setMatrix(tile->transform().get());
433     }
434 
435     // install a bounding volume:
436     osg::BoundingSphere bs;
437     if (tile->boundingVolume().isSet())
438     {
439         bs = tile->boundingVolume()->asBoundingSphere();
440 
441         // if both a transform and a region are set, assume the radius is
442         // fine but the center point should be localized to the transform. -gw
443         // (just guessing)
444         if (tile->transform().isSet() && tile->boundingVolume()->region().isSet())
445         {
446             bs.center().set(0,0,0);
447         }
448     }
449     // tag this object as the invoker of the paging request:
450     osg::ref_ptr<osgDB::Options> newOptions = Registry::instance()->cloneOrCreateOptions(readOptions);
451     OptionsData<TDTiles::TileNode>::set(newOptions.get(), TAG_INVOKER, this);
452 
453     // aka "maximum meters per pixel for which to use me"
454     float geometricError = tile->geometricError().getOrUse(FLT_MAX);
455 
456     // actual content for this tile (optional)
457     osg::ref_ptr<osg::Node> contentNode = loadContent();
458 
459     if (tile->refine() == REFINE_REPLACE)
460     {
461         //osg::ref_ptr<GeometricErrorPagedLOD> plod = new GeometricErrorPagedLOD(_handler.get());
462         osg::ref_ptr<AsyncLOD> lod = new AsyncLOD();
463         lod->setMode(AsyncLOD::MODE_GEOMETRIC_ERROR);
464         lod->setPolicy(AsyncLOD::POLICY_REPLACE);
465 
466         if (bs.valid())
467         {
468             lod->setCenter(bs.center());
469             lod->setRadius(bs.radius());
470         }
471 
472         if (contentNode.valid())
473         {
474             lod->setName(_tile->content()->uri()->base());
475             lod->addChild(contentNode, 0.0f, FLT_MAX);
476         }
477 
478         if (tile->children().size() > 0)
479         {
480             // Async children as a group. All must load before replacing child 0.
481             // TODO: consider a way to load each child asyncrhonously but still
482             // block the refinement until all are loaded.
483             lod->addChild(new LoadChildren(this), 0.0f, geometricError);
484         }
485 
486         addChild(lod);
487     }
488 
489     else // tile->refine() == REFINE_ADD
490     {
491         if (contentNode.valid())
492         {
493             addChild(contentNode.get());
494         }
495 
496         for (unsigned i = 0; i < _tile->children().size(); ++i)
497         {
498             // Each tile gets its own async load since they don't depend on each other
499             // nor do they depend on a parent loading first.
500             osg::ref_ptr<AsyncLOD> lod = new AsyncLOD();
501             lod->setMode(AsyncLOD::MODE_GEOMETRIC_ERROR);
502             lod->setPolicy(AsyncLOD::POLICY_ACCUMULATE);
503 
504             TDTiles::Tile* childTile = _tile->children()[i].get();
505 
506             if (childTile->boundingVolume().isSet())
507             {
508                 osg::BoundingSphere bs = childTile->boundingVolume()->asBoundingSphere();
509                 if (bs.valid())
510                 {
511                     lod->setCenter(bs.center());
512                     lod->setRadius(bs.radius());
513                 }
514             }
515 
516             // backup plan if the child's BV isn't set - use parent BV
517             else if (bs.valid())
518             {
519                 lod->setCenter(bs.center());
520                 lod->setRadius(bs.radius());
521             }
522 
523             // Load this child asynchronously:
524             lod->addChild(new LoadChild(this, i), 0.0, geometricError);
525 
526             addChild(lod);
527         }
528     }
529 }
530 
531 osg::ref_ptr<osg::Node>
loadChildren() const532 TDTiles::TileNode::loadChildren() const
533 {
534     osg::ref_ptr<osg::Group> children = new osg::Group();
535 
536     for (std::vector<osg::ref_ptr<TDTiles::Tile> >::iterator i = _tile->children().begin();
537         i != _tile->children().end();
538         ++i)
539     {
540         TDTiles::Tile* childTile = i->get();
541         if (childTile)
542         {
543             // create a new TileNode:
544             TileNode* child = new TileNode(childTile, _handler.get(), _readOptions.get());
545             children->addChild(child);
546         }
547     }
548 
549     return children;
550 }
551 
552 osg::ref_ptr<osg::Node>
loadContent() const553 TDTiles::TileNode::loadContent() const
554 {
555     osg::ref_ptr<osg::Node> result;
556 
557     if (osgDB::getLowerCaseFileExtension(_tile->content()->uri()->base()) == "json")
558     {
559         // external tileset reference
560         TDTilesetGroup* group = new TDTilesetGroup();
561         group->setTilesetURL(_tile->content()->uri().get());
562         result = group;
563     }
564     else if (_handler.valid())
565     {
566         // Custom handler? invoke it now
567         result = _handler->createNode(_tile.get(), _readOptions.get());
568     }
569 
570     return result;
571 }
572 
573 osg::ref_ptr<osg::Node>
loadChild(unsigned i) const574 TDTiles::TileNode::loadChild(unsigned i) const
575 {
576     TDTiles::Tile* child = _tile->children()[i].get();
577     osg::ref_ptr<TDTiles::TileNode> node = new TDTiles::TileNode(
578         child, _handler.get(), _readOptions.get());
579     return node;
580 }
581 
582 //........................................................................
583 
ContentHandler()584 TDTiles::ContentHandler::ContentHandler()
585 {
586     //nop
587 }
588 
589 osg::ref_ptr<osg::Node>
createNode(TDTiles::Tile * tile,const osgDB::Options * readOptions) const590 TDTiles::ContentHandler::createNode(TDTiles::Tile* tile, const osgDB::Options* readOptions) const
591 {
592     osg::ref_ptr<osg::Node> result;
593 
594     // default action: just try to load a node using OSG
595     if (tile->content().isSet() &&
596         tile->content()->uri().isSet() &&
597         !tile->content()->uri()->empty())
598     {
599         Registry::instance()->startActivity(tile->content()->uri()->base());
600 
601         osgEarth::ReadResult rr = tile->content()->uri()->readNode(readOptions);
602         if (rr.succeeded())
603         {
604             result = rr.releaseNode();
605         }
606         else
607         {
608             OE_WARN << LC << "Read error: " << rr.errorDetail() << std::endl;
609         }
610         Registry::instance()->endActivity(tile->content()->uri()->base());
611     }
612     return result;
613 }
614 
615 //........................................................................
616 
TDTilesetGroup()617 TDTilesetGroup::TDTilesetGroup()
618 {
619     _handler = new TDTiles::ContentHandler();
620 }
621 
TDTilesetGroup(TDTiles::ContentHandler * handler)622 TDTilesetGroup::TDTilesetGroup(TDTiles::ContentHandler* handler) :
623     _handler(handler)
624 {
625     if (!_handler.valid())
626     {
627         _handler = new TDTiles::ContentHandler();
628     }
629 }
630 
631 void
setReadOptions(const osgDB::Options * value)632 TDTilesetGroup::setReadOptions(const osgDB::Options* value)
633 {
634     _readOptions = value;
635 }
636 
637 const osgDB::Options*
getReadOptions() const638 TDTilesetGroup::getReadOptions() const
639 {
640     return _readOptions.get();
641 }
642 
643 TDTiles::ContentHandler*
getContentHandler() const644 TDTilesetGroup::getContentHandler() const
645 {
646     return _handler.get();
647 }
648 
649 osg::ref_ptr<osg::Node>
loadRoot(TDTiles::Tileset * tileset) const650 TDTilesetGroup::loadRoot(TDTiles::Tileset* tileset) const
651 {
652     osg::ref_ptr<osg::Node> result;
653 
654     // Set up the root tile.
655     if (tileset->root().valid())
656     {
657         float maxMetersPerPixel = tileset->geometricError().getOrUse(FLT_MAX);
658 
659         // create the root tile node and defer loading of its content:
660         osg::Node* tileNode = new TDTiles::TileNode(tileset->root().get(), _handler.get(), _readOptions.get());
661 
662         AsyncLOD* lod = new AsyncLOD();
663         lod->setMode(AsyncLOD::MODE_GEOMETRIC_ERROR);
664         lod->addChild(tileNode, 0.0, maxMetersPerPixel);
665 
666         result = lod;
667     }
668 
669     return result;
670 }
671 
672 void
setTileset(TDTiles::Tileset * tileset)673 TDTilesetGroup::setTileset(TDTiles::Tileset* tileset)
674 {
675     // clear out the node in preparation for a new tileset
676     removeChildren(0, getNumChildren());
677 
678     // create the root tile node and defer loading of its content:
679     osg::ref_ptr<osg::Node> tileNode = loadRoot(tileset);
680     if (tileNode.valid())
681     {
682         addChild(tileNode);
683     }
684 }
685 
686 void
setTilesetURL(const URI & location)687 TDTilesetGroup::setTilesetURL(const URI& location)
688 {
689     _tilesetURI = location;
690 
691     // reset:
692     removeChildren(0, getNumChildren());
693 
694     AsyncLOD* lod = new AsyncLOD();
695     lod->setMode(AsyncLOD::MODE_GEOMETRIC_ERROR);
696     lod->setName(location.base());
697     lod->addChild(new TDTiles::LoadExternalTileset(this), 0.0, FLT_MAX);
698 
699     addChild(lod);
700 }
701 
702 const URI&
getTilesetURL() const703 TDTilesetGroup::getTilesetURL() const
704 {
705     return _tilesetURI;
706 }
707