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