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