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 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
12 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
13 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
14 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
15 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
16 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
17 * IN THE SOFTWARE.
18 *
19 * You should have received a copy of the GNU Lesser General Public License
20 * along with this program.  If not, see <http://www.gnu.org/licenses/>
21 */
22 
23 #include <osgViewer/Viewer>
24 #include <osgDB/WriteFile>
25 #include <osgUtil/Simplifier>
26 #include <osgEarth/Notify>
27 #include <osgEarthUtil/EarthManipulator>
28 #include <osgEarthUtil/ExampleResources>
29 #include <osgEarthUtil/Controls>
30 #include <osgEarthUtil/ViewFitter>
31 #include <osgEarth/MapNode>
32 #include <osgEarth/ThreadingUtils>
33 #include <osgEarth/TDTiles>
34 #include <osgEarth/Random>
35 #include <osgEarth/TileKey>
36 #include <osgEarth/CullingUtils>
37 #include <osgEarthAnnotation/FeatureNode>
38 #include <osgEarthFeatures/FeatureSource>
39 #include <osgEarthFeatures/FeatureCursor>
40 #include <osgEarthFeatures/FeatureModelLayer>
41 #include <osgEarthFeatures/ResampleFilter>
42 #include <osgEarthDrivers/feature_ogr/OGRFeatureOptions>
43 #include <iostream>
44 
45 #define LC "[3dtiles test] "
46 
47 using namespace osgEarth;
48 using namespace osgEarth::Util;
49 using namespace osgEarth::Features;
50 using namespace osgEarth::Annotation;
51 using namespace osgEarth::Symbology;
52 using namespace osgEarth::Drivers;
53 namespace ui = osgEarth::Util::Controls;
54 
55 struct App
56 {
57     ui::HSliderControl* _sse;
58     TDTiles::ContentHandler* _handler;
59     EarthManipulator* _manip;
60     osg::ref_ptr<TDTilesetGroup> _tileset;
61     osgViewer::View* _view;
62     LODScaleGroup* _sseGroup;
63     float _maxSSE;
64     bool _randomColors;
65 
changeSSEApp66     void changeSSE()
67     {
68         _sseGroup->setLODScaleFactor(_sse->getValue());
69     }
70 
zoomToDataApp71     void zoomToData()
72     {
73         if (_tileset->getBound().valid())
74         {
75             const osg::BoundingSphere& bs = _tileset->getBound();
76 
77             const SpatialReference* wgs84 = SpatialReference::get("wgs84");
78 
79             std::vector<GeoPoint> points;
80             points.push_back(GeoPoint());
81             points.back().fromWorld(wgs84, bs.center());
82 
83             Viewpoint vp;
84             ViewFitter fit(wgs84, _view->getCamera());
85             fit.setBuffer(bs.radius()*2.0);
86             fit.createViewpoint(points, vp);
87 
88             _manip->setViewpoint(vp);
89         }
90     }
91 };
92 
93 OE_UI_HANDLER(changeSSE);
94 OE_UI_HANDLER(zoomToData);
95 
makeUI(App & app)96 ui::Control* makeUI(App& app)
97 {
98     ui::Grid* container = new ui::Grid();
99 
100     int r=0;
101     container->setControl(0, r, new ui::LabelControl("Screen-space error (px)"));
102     app._sse = container->setControl(1, r, new ui::HSliderControl(app._maxSSE, 1.0f, app._maxSSE, new changeSSE(app)));
103     app._sse->setHorizFill(true, 300.0f);
104     container->setControl(2, r, new ui::LabelControl(app._sse));
105 
106     ++r;
107     container->setControl(0, r, new ui::ButtonControl("Zoom to data", new zoomToData(app)));
108 
109     return container;
110 }
111 
112 int
usage(const std::string & message)113 usage(const std::string& message)
114 {
115     OE_WARN
116         << "\n\n" << message
117         << "\n\nUsage: osgearth_3dtiles"
118         << "\n"
119         << "\n   --build [earthfile]          ; build a B3DM file"
120         << "\n     --extent <minLat> <minLon>"
121         << "\n              <maxLat> <maxLon> ; Extents to build (degrees)"
122         << "\n     --out <filename>           ; output file with .b3dm extension"
123         << "\n     --error <meters>           ; geometric error (meters) of data (default=100)"
124         << "\n     --resolution <meters>      ; downsample data to this resolution"
125         << "\n     --limit <n>                ; Only generate <n> tiles (for testing)"
126         << "\n"
127         << "\n   --view                       ; view a 3dtiles dataset"
128         << "\n     --tileset <filename>       ; 3dtiles tileset JSON file to load"
129         << "\n     --maxsse <n>               ; maximum screen space error in pixels for UI"
130         << "\n     --features                 ; treat the 3dtiles content as feature data"
131         << "\n     --random-colors            ; randomly color feature tiles (instead of one color)"
132         << std::endl;
133         //<< MapNodeHelper().usage() << std::endl;
134 
135     return -1;
136 }
137 
138 /**
139  * Custom TDTiles ContentHandler that reads feature data from a URL
140  * and renders a simple osg::Node from it.
141  */
142 struct FeatureRenderer : public TDTiles::ContentHandler
143 {
144     App& _app;
145     mutable Random _random;
146 
FeatureRendererFeatureRenderer147     FeatureRenderer(App& app) : _app(app) { }
148 
createNodeFeatureRenderer149     osg::ref_ptr<osg::Node> createNode(TDTiles::Tile* tile, const osgDB::Options* readOptions) const
150     {
151         osg::ref_ptr<osg::Node> node;
152 
153         if (tile->content().isSet() && tile->content()->uri().isSet())
154         {
155             OE_INFO << "Rendering feature tile: " << tile->content()->uri()->base() << std::endl;
156 
157             OGRFeatureOptions ogr;
158             ogr.url() = tile->content()->uri().get();
159             osg::ref_ptr<FeatureSource> fs = FeatureSourceFactory::create(ogr);
160             if (fs.valid() && fs->open().isOK())
161             {
162                 osg::ref_ptr<FeatureCursor> cursor = fs->createFeatureCursor(0L);
163                 FeatureList features;
164                 cursor->fill(features);
165                 if (!features.empty())
166                 {
167                     Style style;
168                     osg::Vec4 color;
169                     if (_app._randomColors)
170                         color.set(_random.next(), _random.next(), _random.next(), 1.0f);
171                     else
172                         color.set(1.0, 0.6, 0.0, 1.0);
173 
174                     style.getOrCreate<PolygonSymbol>()->fill()->color() = color;
175                     style.getOrCreate<AltitudeSymbol>()->clamping() = AltitudeSymbol::CLAMP_TO_TERRAIN;
176                     style.getOrCreate<AltitudeSymbol>()->technique() = AltitudeSymbol::TECHNIQUE_DRAPE;
177                     node = new FeatureNode(features, style);
178                 }
179             }
180         }
181         else
182         {
183             //nop - skip tile with no content
184         }
185 
186         return node;
187     }
188 };
189 
190 int
main_view(osg::ArgumentParser & arguments)191 main_view(osg::ArgumentParser& arguments)
192 {
193     App app;
194 
195     std::string tilesetLocation;
196     if (!arguments.read("--tileset", tilesetLocation))
197         return usage("Missing required --tileset");
198 
199     bool readFeatures = arguments.read("--features");
200     app._randomColors = arguments.read("--random-colors");
201 
202     app._maxSSE = 7.0f;
203     arguments.read("--maxsse", app._maxSSE);
204 
205     // load the tile set:
206     URI tilesetURI(tilesetLocation);
207     ReadResult rr = tilesetURI.readString();
208     if (rr.failed())
209         return usage(Stringify()<<"Error loading tileset: " <<rr.errorDetail());
210 
211     TDTiles::Tileset* tileset = TDTiles::Tileset::create(rr.getString(), tilesetLocation);
212     if (!tileset)
213         return usage("Bad tileset");
214 
215     // create a viewer:
216     osgViewer::Viewer viewer(arguments);
217     app._view = &viewer;
218 
219     viewer.setCameraManipulator( app._manip = new EarthManipulator(arguments) );
220 
221     // load an earth file, and support all or our example command-line options
222     // and earth file <external> tags
223     osg::ref_ptr<osg::Node> node = MapNodeHelper().load(arguments, &viewer);
224     if (!node.valid())
225         return usage("Failed to load an earth file");
226 
227     MapNode* mapNode = MapNode::get(node.get());
228 
229     if (readFeatures)
230         app._tileset = new TDTilesetGroup(new FeatureRenderer(app));
231     else
232         app._tileset = new TDTilesetGroup();
233 
234 
235     app._sseGroup = new LODScaleGroup();
236     app._sseGroup->addChild(app._tileset.get());
237 
238     //app._handler = app._tileset->getContentHandler();
239 
240     ui::ControlCanvas::get(&viewer)->addControl(makeUI(app));
241 
242     app._tileset->setTileset(tileset);
243     app._tileset->setReadOptions(mapNode->getMap()->getReadOptions());
244 
245     mapNode->addChild(app._sseGroup);
246 
247     viewer.setSceneData( node.get() );
248     return viewer.run();
249 }
250 
251 TileKey
parseKey(const std::string & input,const Profile * profile)252 parseKey(const std::string& input, const Profile* profile)
253 {
254     std::vector<std::string> tileparts;
255     StringTokenizer(input, tileparts, "/", "", false, true);
256     if (tileparts.size() != 3)
257         return TileKey::INVALID;
258 
259     return TileKey(
260         atoi(tileparts[0].c_str()),
261         atoi(tileparts[1].c_str()),
262         atoi(tileparts[2].c_str()),
263         profile);
264 }
265 
266 TDTiles::Tile*
createTile(osg::Node * node,const GeoExtent & extent,double error,const std::string & filenamePrefix,TDTiles::RefinePolicy refine)267 createTile(osg::Node* node, const GeoExtent& extent, double error, const std::string& filenamePrefix, TDTiles::RefinePolicy refine)
268 {
269     std::string filename = Stringify() << filenamePrefix << "_" << int(error) << ".b3dm";
270 
271     if (!osgDB::writeNodeFile(*node, filename))
272     {
273         OE_WARN << "Failed to write to output file (" << filename << ")" << std::endl;
274         return NULL;
275     }
276 
277     osg::ref_ptr<TDTiles::Tile> tile = new TDTiles::Tile();
278     tile->content()->uri() = filename;
279 
280     // Set up a bounding region (radians)
281     // TODO: calculate the correct Z min/max instead of hard-coding
282     tile->boundingVolume()->region()->set(
283         osg::DegreesToRadians(extent.xMin()),
284         osg::DegreesToRadians(extent.yMin()),
285         -1000.0,
286         osg::DegreesToRadians(extent.xMax()),
287         osg::DegreesToRadians(extent.yMax()),
288         3000.0);
289 
290     tile->geometricError() = 0.0; //error;
291     tile->refine() = refine;
292 
293     return tile.release();
294 }
295 
296 int
main_build(osg::ArgumentParser & arguments)297 main_build(osg::ArgumentParser& arguments)
298 {
299     // Process:
300     // 1. Open an earth file and load a Map.
301     // 2. Find a feature model layer.
302     // 3. Read in one tile from the feature source.
303     // 4. Compile it into OSG geometry with a matrix transform
304     // 5. Save that geometry to a B3DM file.
305 
306     // Estalish extents for the build.
307     double minLat, minLon, maxLat, maxLon;
308     if (!arguments.read("--extent", minLat, minLon, maxLat, maxLon))
309         return usage("Missing required --extent");
310 
311     GeoExtent extent(SpatialReference::get("wgs84"), minLon, minLat, maxLon, maxLat);
312     if (!extent.isValid())
313         return usage("Invalid extent");
314 
315     // Output filename prefix
316     std::string prefix;
317     if (!arguments.read("--out", prefix))
318         return usage("Missing required --out <prefix>");
319     if (!prefix.empty()) prefix = prefix + "_";
320 
321     // Geometric error (only render the data if it's on-screen size
322     // is greater than "maxSSE" pixels per "error" meters.
323     // For example, if error=100m, and maxSSE=16px, the data will only
324     // render once 100m of data takes up at least 16px of screen space.
325     double tilesetError;
326     if (!arguments.read("--error", tilesetError))
327         tilesetError = 500.0;
328 
329     double resolution = 0.0;
330     arguments.read("--resolution", resolution);
331 
332     TDTiles::RefinePolicy refine = TDTiles::REFINE_REPLACE;
333     if (arguments.read("--add"))
334         refine = TDTiles::REFINE_ADD;
335 
336     int limit = INT_MAX;
337     arguments.read("--limit", limit);
338 
339     // Open an earth file and load a Map.
340     osg::ref_ptr<osg::Node> earthFile = osgDB::readNodeFiles(arguments);
341     MapNode* mapNode = MapNode::get(earthFile.get());
342     if (!mapNode)
343         return usage("Unable to load an earth file");
344 
345     // Open all the layers:
346     mapNode->openMapLayers();
347 
348     // Locate the first FeatureModelLayer in the map:
349     const Map* map = mapNode->getMap();
350     FeatureModelLayer* fml = map->getLayer<FeatureModelLayer>();
351     if ( !fml )
352         return usage("Unable to find a FeatureModel layer in the Map");
353     if ( !fml->getStatus().isOK() )
354         return usage(fml->getStatus().message());
355 
356     // Extract the feature source from the layer:
357     FeatureSource* fs = fml->getFeatureSource();
358     if ( !fs )
359         return usage("Unable to get feature source from layer");
360     if ( !fs->getStatus().isOK() )
361         return usage(fs->getStatus().message());
362     if ( fs->open(0L).isError() )
363         return usage(fs->getStatus().message());
364 
365     const FeatureProfile* fp = fs->getFeatureProfile();
366     if (!fp)
367         return usage("Unable to get a feature profile");
368     if (!fp->getProfile())
369         return usage("Unagle to find a tiling profile in the feature profile");
370 
371     std::vector<TileKey> keys;
372     fp->getProfile()->getIntersectingTiles(extent, fp->getMaxLevel(), keys);
373     if (keys.empty())
374         return usage("No data in requested extent");
375     OE_INFO << "Found " << keys.size() << " tiles in extent" << std::endl;
376 
377     StyleSheet* sheet = fml->options().styles().get();
378     if (!sheet)
379         return usage("Missing stylesheet");
380 
381     // Make sure the b3dm plugin is loaded
382     std::string libname = osgDB::Registry::instance()->createLibraryNameForExtension("gltf");
383     osgDB::Registry::instance()->loadLibrary(libname);
384 
385     osg::ref_ptr<Session> session = new Session(map, sheet, fs, 0L);
386     GeometryCompiler compiler(fml->options());
387 
388     // Read all the styles in the stylesheet and sort them by geometric error:
389     std::map<double, Style> styles;
390     const StyleMap& smap = sheet->styles();
391     for(StyleMap::const_iterator i = smap.begin(); i != smap.end(); ++i)
392     {
393         styles[atoi(i->first.c_str())] = i->second;
394         OE_INFO << LC << "Found style \"" << i->first << "\"" << std::endl;
395     }
396 
397     // Create the tileset file
398     osg::ref_ptr<TDTiles::Tileset> tileset = new TDTiles::Tileset();
399     tileset->asset()->version() = "1.0";
400     tileset->root() = new TDTiles::Tile();
401     tileset->root()->refine() = TDTiles::REFINE_ADD;
402     tileset->root()->geometricError() = tilesetError;
403 
404     // track the largest tile radius - we will use this as the geometric error
405     // for the group.
406     double maxTileRadius = 0.0;
407 
408     // track the total geospatial extent - this will become the bounding volume
409     // for the group
410     GeoExtent total;
411 
412     //TODO: for loop on the feature keys
413     int count = 0;
414     for(std::vector<TileKey>::const_iterator key = keys.begin(); key != keys.end(); ++key)
415     {
416         // Check the "radius"
417         double tileRadius = key->getExtent().computeBoundingGeoCircle().getRadius();
418         maxTileRadius = osg::maximum(tileRadius, maxTileRadius);
419 
420         // Query the features corresponding to the tile key:
421         Query query;
422         query.tileKey() = *key;
423 
424         std::string filenamePrefix = Stringify() << prefix << key->getLOD() << "_" << key->getTileX() << "_" << key->getTileY();
425 
426         osg::ref_ptr<TDTiles::Tile> parent = tileset->root();
427 
428         for(std::map<double, Style>::reverse_iterator i = styles.rbegin(); i != styles.rend(); ++i)
429         {
430             double error = i->first;
431             const Style& style = i->second;
432 
433             osg::ref_ptr<FeatureCursor> cursor = fs->createFeatureCursor(query, 0L);
434             if (!cursor.valid())
435                 continue;
436 
437             // Compile into OSG geometry
438             FeatureList features;
439             cursor->fill(features);
440             if (features.empty())
441                 continue;
442 
443             OE_INFO << LC << "Tile " << key->str() << ", features=" << features.size() << ", width=" << key->getExtent().width() << ", error=" << error << std::endl;
444 
445             FilterContext fc(session.get(), fp, key->getExtent());
446 
447             // first simplify the feature set
448             if (resolution > 0.0)
449             {
450                 ResampleFilter resample(resolution, DBL_MAX);
451                 fc = resample.push(features, fc);
452             }
453 
454             osg::ref_ptr<osg::Node> result = compiler.compile(features, style, fc);
455             if (!result)
456                 return usage("Failed to compile features into OSG geometry");
457 
458             if (result->getBound().valid())
459             {
460                 GeoExtent extent = key->getExtent().transform(SpatialReference::get("wgs84"));
461                 if (total.isValid())
462                     total.expandToInclude(extent);
463                 else
464                     total = extent;
465 
466                 osg::ref_ptr<TDTiles::Tile> tile = createTile(result.get(), extent, error, filenamePrefix, refine);
467                 parent->children().push_back(tile);
468 
469                 parent->geometricError() = error;
470 
471                 parent = tile;
472             }
473         }
474 
475         if (++count >= limit)
476             break;
477     }
478 
479     // TODO: calculate the correct Z min/max instead of hard-coding
480     tileset->root()->boundingVolume()->region()->set(
481         osg::DegreesToRadians(total.xMin()),
482         osg::DegreesToRadians(total.yMin()),
483         -1000.0,
484         osg::DegreesToRadians(total.xMax()),
485         osg::DegreesToRadians(total.yMax()),
486         3000.0);
487 
488     tileset->geometricError() = tileset->root()->geometricError().get() * 1.5;
489 
490     // write out the tileset file ("tileset.json")
491     std::ofstream out("tileset.json");
492     Json::Value tilesetJSON = tileset->getJSON();
493     Json::StyledStreamWriter writer;
494     writer.write(out, tilesetJSON);
495     out.close();
496 
497     return 0;
498 }
499 
500 int
main(int argc,char ** argv)501 main(int argc, char** argv)
502 {
503     osg::ArgumentParser arguments(&argc, argv);
504 
505     if (arguments.read("--help"))
506         return usage("Help!");
507 
508     if (arguments.read("--build"))
509         return main_build(arguments);
510     else if (arguments.read("--view"))
511         return main_view(arguments);
512     else
513         return usage("Missing required --build or --view");
514 }
515