diff --git a/libs/core/CMakeLists.txt b/libs/core/CMakeLists.txt index 9c936f36..8e63ff6a 100644 --- a/libs/core/CMakeLists.txt +++ b/libs/core/CMakeLists.txt @@ -2,7 +2,7 @@ project(erdblick-core) # For WASM modules, add_executable is used instead of add_library. set(ERDBLICK_SOURCE_FILES - include/erdblick/renderer.h + include/erdblick/visualization.h include/erdblick/style.h include/erdblick/rule.h include/erdblick/buffer.h @@ -16,7 +16,7 @@ set(ERDBLICK_SOURCE_FILES include/erdblick/cesium-interface/cesium.h include/erdblick/cesium-interface/point-conversion.h - src/renderer.cpp + src/visualization.cpp src/style.cpp src/rule.cpp src/color.cpp diff --git a/libs/core/include/erdblick/cesium-interface/cesium.h b/libs/core/include/erdblick/cesium-interface/cesium.h index cbb28771..390430d4 100644 --- a/libs/core/include/erdblick/cesium-interface/cesium.h +++ b/libs/core/include/erdblick/cesium-interface/cesium.h @@ -9,18 +9,25 @@ namespace erdblick struct CesiumLib { CesiumClass ArcType; + CesiumClass BoundingSphere; CesiumClass Color; CesiumClass ColorGeometryInstanceAttribute; + CesiumClass ComponentDatatype; + CesiumClass Geometry; + CesiumClass GeometryAttribute; CesiumClass GeometryInstance; CesiumClass Material; + CesiumClass PerInstanceColorAppearance; + CesiumClass PolygonGeometry; + CesiumClass PolygonHierarchy; CesiumClass PolylineColorAppearance; CesiumClass PolylineGeometry; CesiumClass PolylineMaterialAppearance; CesiumClass Primitive; CesiumClass PrimitiveCollection; + CesiumClass PrimitiveType; [[nodiscard]] JsValue MaterialFromType(std::string const& type, JsValue const& options); - [[nodiscard]] JsValue ColorAttributeFromColor(JsValue const& color); private: friend CesiumLib& Cesium(); diff --git a/libs/core/include/erdblick/cesium-interface/object.h b/libs/core/include/erdblick/cesium-interface/object.h index 2ede65c3..9bb73d47 100644 --- a/libs/core/include/erdblick/cesium-interface/object.h +++ b/libs/core/include/erdblick/cesium-interface/object.h @@ -36,14 +36,19 @@ struct JsValue * Construct an Object as a new JS or JSON dictionary with provided initializers. * @param initializers An initializer list of key-value pairs. */ - static JsValue - newDict(std::initializer_list> initializers = {}); + static JsValue Dict(std::initializer_list> initializers = {}); /** * Construct an Object as a new JS or JSON list with provided initializers. * @param initializers An initializer list of CesiumObject items. */ - static JsValue newList(std::initializer_list initializers = {}); + static JsValue List(std::initializer_list initializers = {}); + + /** + * Construct an Object as a new JS Float64 TypedArray. + * @param coordinates Float64 buffer to fill the typed array. + */ + static JsValue Float64Array(std::vector const& coordinates); /** * Constructs a JavaScript or JSON null value. @@ -122,6 +127,7 @@ struct CesiumClass : public JsValue * For EMSCRIPTEN, it utilizes value_.new_(Args...). * For the mock version, it will return an empty nlohmann JSON object. */ + JsValue New(std::initializer_list> kwArgs = {}) const; template JsValue New(Args... args) const; diff --git a/libs/core/include/erdblick/cesium-interface/primitive.h b/libs/core/include/erdblick/cesium-interface/primitive.h index a47f1f24..fb4dc75a 100644 --- a/libs/core/include/erdblick/cesium-interface/primitive.h +++ b/libs/core/include/erdblick/cesium-interface/primitive.h @@ -2,7 +2,7 @@ #include "object.h" #include "mapget/model/tileid.h" -#include "rule.h" +#include "../rule.h" namespace erdblick { @@ -24,22 +24,72 @@ struct CesiumPrimitive static CesiumPrimitive withPolylineColorAppearance(); /** - * Add a 3D polyline to the primitive. The provided coordinates - * must already be transformed to Cesium cartesian coordinates. + * Create a primitive which uses the PerInstanceColorAppearance. + * See https://cesium.com/learn/cesiumjs/ref-doc/PerInstanceColorAppearance.html + * + * The parameter flatAndSynchronous must be set to true for primitives + * which contain basic triangle meshes. In the future, we can also have + * smoothly shaded triangle meshes by calling Cesium.GeometryPipeline.computeNormal + * and Cesium.GeometryPipeline.compressVertices on the mesh geometry. */ - void addLine(JsValue const& pointList, FeatureStyleRule const& style, uint32_t id); + static CesiumPrimitive withPerInstanceColorAppearance(bool flatAndSynchronous = false); + + /** + * Add a 3D polyline to the primitive. The provided vertices + * must be a JS list of Point objects in Cesium cartesian coordinates. + * + * Note: In order to visualize the line correctly, the primitive + * must have been constructed using withPolylineColorAppearance. + */ + void addPolyLine(JsValue const& vertices, FeatureStyleRule const& style, uint32_t id); + + /** + * Add a 3D polygon to the primitive. The provided vertices + * must be a JS list of Point objects in Cesium cartesian coordinates. + * + * Note: In order to visualize the polygon correctly, the primitive + * must have been constructed using withPerInstanceColorAppearance. + */ + void addPolygon(JsValue const& vertices, FeatureStyleRule const& style, uint32_t id); + + /** + * Add a 3D triangle mesh to the primitive. The provided vertices + * must be a JS Float64Array like [x0,y0,z0,x1,y1,z2...]. This is unlike other functions + * here which need a JS list of Point objects, due to Cesium internals. + * + * Note: In order to visualize the triangles correctly, the primitive + * must have been constructed using withPerInstanceColorAppearance(true). + */ + void addTriangles(JsValue const& float64Array, FeatureStyleRule const& style, uint32_t id); /** * Constructs a JS Primitive from the provided Geometry instances. */ - NativeJsValue toJsObject(); + [[nodiscard]] NativeJsValue toJsObject() const; + + /** + * Check if any geometry has been added to the primitive. + */ + [[nodiscard]] bool empty() const; private: + /** + * Add a Cesium GeometryInstance which wraps a Cesium Geometry, + * and add it to this primitive's geometryInstances_ collection. + */ + void addGeometryInstance(const FeatureStyleRule& style, uint32_t id, const JsValue& geom); + + /** Number of entries in geometryInstances_. */ + size_t numGeometryInstances_ = 0; + /** geometryInstances option for the Primitive JS Object ctor. */ - JsValue geometryInstances_ = JsValue::newList(); + JsValue geometryInstances_ = JsValue::List(); /** appearance option for the Primitive JS Object ctor. */ JsValue appearance_; + + /** Flag which enables the direct triangle display required for addTriangles. */ + bool flatAndSynchronous_ = false; }; } diff --git a/libs/core/include/erdblick/renderer.h b/libs/core/include/erdblick/renderer.h deleted file mode 100644 index 238a0ecc..00000000 --- a/libs/core/include/erdblick/renderer.h +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once - -#include "mapget/model/featurelayer.h" -#include "buffer.h" -#include "style.h" -#include "cesium-interface/object.h" - -namespace erdblick -{ - -class FeatureLayerRenderer -{ -public: - FeatureLayerRenderer(); - - /** - * Convert a TileFeatureLayer to a collection of Cesium scene - * primitives, using a particular style sheet. - */ - NativeJsValue - render(const FeatureLayerStyle& style, const std::shared_ptr& layer); -}; - -} diff --git a/libs/core/include/erdblick/rule.h b/libs/core/include/erdblick/rule.h index 4cef174c..e00bbe0a 100644 --- a/libs/core/include/erdblick/rule.h +++ b/libs/core/include/erdblick/rule.h @@ -14,15 +14,19 @@ namespace erdblick class FeatureStyleRule { public: - FeatureStyleRule(YAML::Node const& yaml); - bool match(mapget::Feature& feature) const; + explicit FeatureStyleRule(YAML::Node const& yaml); - const std::vector& geometryTypes() const; - glm::fvec4 const& color() const; - float width() const; + [[nodiscard]] bool match(mapget::Feature& feature) const; + [[nodiscard]] bool supports(mapget::Geometry::GeomType const& g) const; + [[nodiscard]] glm::fvec4 const& color() const; + [[nodiscard]] float width() const; private: - std::vector geometryTypes_; + static inline uint32_t geomTypeBit(mapget::Geometry::GeomType const& g) { + return 1 << static_cast>(g); + } + + uint32_t geometryTypes_ = 0; // bitfield from GeomType enum std::optional type_; std::string filter_; glm::fvec4 color_{.0, .0, .0, 1.}; diff --git a/libs/core/include/erdblick/testdataprovider.h b/libs/core/include/erdblick/testdataprovider.h index 7ede6d58..f1aa9c77 100644 --- a/libs/core/include/erdblick/testdataprovider.h +++ b/libs/core/include/erdblick/testdataprovider.h @@ -1,6 +1,8 @@ #pragma once #include "mapget/model/featurelayer.h" +#include +#include "style.h" namespace erdblick { @@ -30,6 +32,40 @@ class TestDataProvider } ] ] + }, + { + "name": "Sign", + "uniqueIdCompositions": [ + [ + { + "partId": "areaId", + "description": "String which identifies the map area.", + "datatype": "STR" + }, + { + "partId": "signId", + "description": "Globally Unique 32b integer.", + "datatype": "U32" + } + ] + ] + }, + { + "name": "Diamond", + "uniqueIdCompositions": [ + [ + { + "partId": "areaId", + "description": "String which identifies the map area.", + "datatype": "STR" + }, + { + "partId": "diamondId", + "description": "Globally Unique 32b integer.", + "datatype": "U32" + } + ] + ] } ] })"_json); @@ -40,6 +76,12 @@ class TestDataProvider std::shared_ptr getTestLayer(double camX, double camY, uint16_t level) { + static const std::vector signTypes{"Stop", "Yield", "Parking", "No Entry", "Speed Limit"}; + static const std::vector wayTypes{"Bike", "Pedestrian", "Any", "Vehicle"}; + + // Seed the random number generator for consistency + srand(time(nullptr)); + auto tileId = mapget::TileId::fromWgs84(camX, camY, level); // Create a basic TileFeatureLayer @@ -52,39 +94,162 @@ class TestDataProvider result->setPrefix({{"areaId", "TheBestArea"}}); // Create a function to generate a random coordinate between two given points - auto randomCoordinateBetween = [&](const auto& point1, const auto& point2) { + auto randomPointBetween = [&](const auto& point1, const auto& point2, double baseHeight) { auto x = point1.x + (point2.x - point1.x) * (rand() / static_cast(RAND_MAX)); auto y = point1.y + (point2.y - point1.y) * (rand() / static_cast(RAND_MAX)); - auto z = 100. / static_cast(level); + double heightOffset = (rand() / static_cast(RAND_MAX)) * 1000.0 - 500.0; // Between -500 and 500 + auto z = baseHeight + heightOffset; return mapget::Point{x, y, z}; }; - // Seed the random number generator for consistency - srand(time(nullptr)); + // Helper function to generate a random number of points with a given base height + auto generateRandomPoints = [&](int minPoints, int maxPoints, const auto& ne, const auto& sw) { + double baseHeight = 1000.0; + std::vector points; + int numPoints = minPoints + rand() % (maxPoints - minPoints + 1); // Random number of points between min and max + points.reserve(numPoints); + while (numPoints --> 0) { + points.push_back(randomPointBetween(ne, sw, baseHeight)); + } + return points; + }; - // Create 10 random lines inside the bounding box defined by ne and sw + // Create 10 random Way features inside the bounding box defined by NE and SW for (int i = 0; i < 10; i++) { + std::cout << "Generated Way " << i << std::endl; // Create a feature with line geometry auto feature = result->newFeature("Way", {{"wayId", 42 + i}}); + auto linePoints = generateRandomPoints(2, 8, tileId.ne(), tileId.sw()); + feature->addLine(linePoints); - // Generate random start and end points for the line - auto start = randomCoordinateBetween(tileId.ne(), tileId.sw()); - auto end = randomCoordinateBetween(tileId.ne(), tileId.sw()); - feature->addLine({start, end}); - - // Add a fixed attribute - feature->attributes()->addField("main_ingredient", "Pepper"); + // Add a random wayType attribute + int randomIndex = rand() % wayTypes.size(); + feature->attributes()->addField("wayType", wayTypes[randomIndex]); // Add an attribute layer - auto attrLayer = feature->attributeLayers()->newLayer("cheese"); - auto attr = attrLayer->newAttribute("mozzarella"); + auto attrLayer = feature->attributeLayers()->newLayer("lane"); + auto attr = attrLayer->newAttribute("numLanes"); attr->setDirection(mapget::Attribute::Direction::Positive); - attr->addField("smell", "neutral"); + attr->addField("count", (int64_t)rand()); } + // Create 10 random Sign features inside the bounding box defined by NE and SW + for (int i = 0; i < 10; i++) { + std::cout << "Generated Sign " << i << std::endl; + + // Create a feature with polygon geometry + auto feature = result->newFeature("Sign", {{"signId", 100 + i}}); + auto polyPoints = generateRandomPoints(2, 6, tileId.ne(), tileId.sw()); + feature->addPoly(polyPoints); + + // Add a random signType attribute + int randomIndex = rand() % signTypes.size(); + feature->attributes()->addField("signType", signTypes[randomIndex]); + } + + // Add a diamond mesh in the center of the tile. + auto diamondMeshFeature = result->newFeature("Diamond", {{"diamondId", 999}}); + auto center = tileId.center(); + auto size = tileId.size(); + size.x *= .25; + size.y *= .25; + size.z = 1000.; + double baseHeight = 1600.0; // Base height from previous code + // Define the vertices of the diamond + std::vector diamondVertices = { + {center.x, center.y - size.y, baseHeight}, // Top front vertex + {center.x - size.x, center.y, baseHeight}, // Left vertex + {center.x, center.y + size.y, baseHeight}, // Bottom front vertex + {center.x + size.x, center.y, baseHeight}, // Right vertex + {center.x, center.y, baseHeight + size.z}, // Top apex (center top vertex) + {center.x, center.y, baseHeight - size.z} // Bottom apex (center bottom vertex) + }; + // Form triangles for the 3D diamond + std::vector diamondTriangles = { + diamondVertices[4], diamondVertices[0], diamondVertices[1], // Top front-left triangle + diamondVertices[4], diamondVertices[1], diamondVertices[2], // Top left-right triangle + diamondVertices[4], diamondVertices[2], diamondVertices[3], // Top right-bottom triangle + diamondVertices[4], diamondVertices[3], diamondVertices[0], // Top bottom-front triangle + diamondVertices[5], diamondVertices[1], diamondVertices[0], // Bottom left-front triangle + diamondVertices[5], diamondVertices[2], diamondVertices[1], // Bottom right-left triangle + diamondVertices[5], diamondVertices[3], diamondVertices[2], // Bottom bottom-right triangle + diamondVertices[5], diamondVertices[0], diamondVertices[3] // Bottom front-bottom triangle + }; + diamondMeshFeature->addMesh(diamondTriangles); + return result; } + static FeatureLayerStyle style() + { + return FeatureLayerStyle(SharedUint8Array(R"yaml( + rules: + - geometry: + - line + type: "Way" + filter: "properties.wayType == 'Bike'" + color: "#3498db" # Blue color for Bike Way + width: 2.0 + + - geometry: + - line + type: "Way" + filter: "properties.wayType == 'Pedestrian'" + color: "#2ecc71" # Green color for Pedestrian Way + width: 1.5 + + - geometry: + - line + type: "Way" + filter: "properties.wayType == 'Any'" + color: "#f39c12" # Orange color for Any Way + width: 2.5 + + - geometry: + - line + type: "Way" + filter: "properties.wayType == 'Vehicle'" + color: "#e74c3c" # Red color for Vehicle Way + width: 3.0 + + - geometry: + - polygon + type: "Sign" + filter: "properties.signType == 'Stop'" + color: "#e74c3c" # Red color for Stop Sign + + - geometry: + - polygon + type: "Sign" + filter: "properties.signType == 'Yield'" + color: "#f39c12" # Orange color for Yield Sign + + - geometry: + - polygon + type: "Sign" + filter: "properties.signType == 'Parking'" + color: "#3498db" # Blue color for Parking Sign + + - geometry: + - polygon + type: "Sign" + filter: "properties.signType == 'No Entry'" + color: "#8e44ad" # Purple color for No Entry Sign + + - geometry: + - polygon + type: "Sign" + filter: "properties.signType == 'Speed Limit'" + color: "#2c3e50" # Dark color for Speed Limit Sign + + - geometry: + - mesh + type: "Diamond" + color: gold + opacity: 0.5 + )yaml")); + } + private: std::shared_ptr layerInfo_; std::shared_ptr fieldNames_; diff --git a/libs/core/include/erdblick/visualization.h b/libs/core/include/erdblick/visualization.h new file mode 100644 index 00000000..a5c18d89 --- /dev/null +++ b/libs/core/include/erdblick/visualization.h @@ -0,0 +1,59 @@ +#pragma once + +#include +#include +#include "style.h" +#include "cesium-interface/point-conversion.h" +#include "cesium-interface/primitive.h" + +namespace erdblick +{ + +/** + * Cesium Primitive Conversion for a TileFeatureLayer using a style. + */ +class FeatureLayerVisualization +{ +public: + /** + * Convert a TileFeatureLayer into Cesium primitives based on the provided style. + * @param style The style to apply to the features in the layer. + * @param layer A shared pointer to the TileFeatureLayer that needs to be visualized. + */ + FeatureLayerVisualization(const FeatureLayerStyle& style, const std::shared_ptr& layer); + + /** + * Returns all non-empty Cesium primitives which resulted from + * the given TileFeatureLayer conversion, in one PrimitiveCollection. + */ + [[nodiscard]] NativeJsValue primitiveCollection() const; + +private: + /** + * Add all geometry of some feature which is compatible with the given rule. + */ + void addFeature(mapget::model_ptr& feature, uint32_t id, FeatureStyleRule const& rule); + + /** + * Add some geometry. The Cesium conversion will be dispatched, + * based on the geometry type and the style rule instructions. + */ + void addGeometry(mapget::model_ptr const& geom, uint32_t id, FeatureStyleRule const& rule); + + /** + * Get some WGS84 points as a list of Cesium Cartesian points. + */ + static std::optional encodeVerticesAsList(mapget::model_ptr const& geom); + + /** + * Get some WGS84 points as a float64 buffer of Cesium Cartesian points. + */ + static std::optional encodeVerticesAsFloat64Array(mapget::model_ptr const& geom); + + bool featuresAdded_ = false; + CesiumPrimitive coloredLines_; + CesiumPrimitive coloredNontrivialMeshes_; + CesiumPrimitive coloredTrivialMeshes_; +}; + +} // namespace erdblick diff --git a/libs/core/src/bindings.cpp b/libs/core/src/bindings.cpp index ca5cd383..3f1e7272 100644 --- a/libs/core/src/bindings.cpp +++ b/libs/core/src/bindings.cpp @@ -2,7 +2,7 @@ #include "aabb.h" #include "buffer.h" -#include "renderer.h" +#include "visualization.h" #include "stream.h" #include "style.h" #include "testdataprovider.h" @@ -97,17 +97,17 @@ std::string getTileFeatureLayerKey(std::string const& mapId, std::string const& return tileKey.toString(); } -/** Create a test polyline. */ -NativeJsValue makeTestLine() { - CesiumPrimitive result; - result.addLine(JsValue::newList({ - JsValue(wgsToCartesian({42., 11., 0.})), - JsValue(wgsToCartesian({42., 12., 0.})) - }), YAML::Load("{geometry: ['line'], color: red}"), 0); - return result.toJsObject(); +/** Create a test tile over New York. */ +std::shared_ptr generateTestTile() { + return TestDataProvider().getTestLayer(-74.0060, 40.7128, 10); } -EMSCRIPTEN_BINDINGS(FeatureLayerRendererBind) +/** Create a test style. */ +FeatureLayerStyle generateTestStyle() { + return TestDataProvider::style(); +} + +EMSCRIPTEN_BINDINGS(erdblick) { // Activate this to see a lot more output from the WASM lib. // mapget::log().set_level(spdlog::level::debug); @@ -187,15 +187,10 @@ EMSCRIPTEN_BINDINGS(FeatureLayerRendererBind) return self.at(i); })); - ////////// FeatureLayerRenderer - em::class_("FeatureLayerRenderer") - .constructor() - .function("render", &FeatureLayerRenderer::render); - - ////////// TestDataProvider - em::class_("TestDataProvider") - .constructor() - .function("getTestLayer", &TestDataProvider::getTestLayer); + ////////// FeatureLayerVisualization + em::class_("FeatureLayerVisualization") + .constructor>() + .function("primitiveCollection", &FeatureLayerVisualization::primitiveCollection); ////////// TileLayerParser em::class_("TileLayerParser") @@ -228,6 +223,7 @@ EMSCRIPTEN_BINDINGS(FeatureLayerRendererBind) ////////// Get full id of a TileFeatureLayer em::function("getTileFeatureLayerKey", &getTileFeatureLayerKey); - ////////// Get a test line - em::function("makeTestLine", &makeTestLine); + ////////// Get a test tile/style + em::function("generateTestTile", &generateTestTile); + em::function("generateTestStyle", &generateTestStyle); } diff --git a/libs/core/src/cesium-interface/cesium.cpp b/libs/core/src/cesium-interface/cesium.cpp index 1238dd98..090fa5a0 100644 --- a/libs/core/src/cesium-interface/cesium.cpp +++ b/libs/core/src/cesium-interface/cesium.cpp @@ -5,15 +5,23 @@ namespace erdblick CesiumLib::CesiumLib() : ArcType("ArcType"), + BoundingSphere("BoundingSphere"), Color("Color"), ColorGeometryInstanceAttribute("ColorGeometryInstanceAttribute"), + ComponentDatatype("ComponentDatatype"), + Geometry("Geometry"), + GeometryAttribute("GeometryAttribute"), GeometryInstance("GeometryInstance"), Material("Material"), + PerInstanceColorAppearance("PerInstanceColorAppearance"), + PolygonGeometry("PolygonGeometry"), + PolygonHierarchy("PolygonHierarchy"), PolylineColorAppearance("PolylineColorAppearance"), PolylineGeometry("PolylineGeometry"), PolylineMaterialAppearance("PolylineMaterialAppearance"), Primitive("Primitive"), - PrimitiveCollection("PrimitiveCollection") + PrimitiveCollection("PrimitiveCollection"), + PrimitiveType("PrimitiveType") { } @@ -28,9 +36,4 @@ JsValue CesiumLib::MaterialFromType(std::string const& type, const JsValue& opti return JsValue(Material.call("fromType", type, *options)); } -JsValue CesiumLib::ColorAttributeFromColor(const JsValue& color) -{ - return JsValue(ColorGeometryInstanceAttribute.call("fromColor", *color)); -} - } diff --git a/libs/core/src/cesium-interface/object.cpp b/libs/core/src/cesium-interface/object.cpp index 4e4b9091..1e779ded 100644 --- a/libs/core/src/cesium-interface/object.cpp +++ b/libs/core/src/cesium-interface/object.cpp @@ -20,7 +20,7 @@ JsValue JsValue::fromGlobal(std::string const& globalName) #endif } -JsValue JsValue::newDict(std::initializer_list> initializers) +JsValue JsValue::Dict(std::initializer_list> initializers) { #ifdef EMSCRIPTEN auto obj = emscripten::val::object(); @@ -37,7 +37,7 @@ JsValue JsValue::newDict(std::initializer_list> #endif } -JsValue JsValue::newList(std::initializer_list initializers) +JsValue JsValue::List(std::initializer_list initializers) { #ifdef EMSCRIPTEN emscripten::val array = emscripten::val::array(); @@ -55,6 +55,20 @@ JsValue JsValue::newList(std::initializer_list initializers) #endif } +JsValue JsValue::Float64Array(const std::vector& coordinates) +{ +#ifdef EMSCRIPTEN + static thread_local auto JsFloat64ArrayType = emscripten::val::global("Float64Array"); + // Create a typed memory view directly pointing to the vector's data + auto memoryView = emscripten::typed_memory_view(coordinates.size(), coordinates.data()); + // Create a Float64Array from the memory view + auto float64Array = JsFloat64ArrayType.new_(memoryView); + return JsValue(float64Array); +#else + return JsValue(coordinates); +#endif +} + JsValue JsValue::operator[](std::string const& propertyName) { #ifdef EMSCRIPTEN @@ -84,4 +98,9 @@ CesiumClass::CesiumClass(const std::string& className) value_ = cesiumLibrary.value_[className]; } +JsValue CesiumClass::New(std::initializer_list> kwArgs) const +{ + return New(*JsValue::Dict(kwArgs)); +} + } // namespace erdblick diff --git a/libs/core/src/cesium-interface/primitive.cpp b/libs/core/src/cesium-interface/primitive.cpp index 4f879980..25dbd056 100644 --- a/libs/core/src/cesium-interface/primitive.cpp +++ b/libs/core/src/cesium-interface/primitive.cpp @@ -12,33 +12,91 @@ CesiumPrimitive CesiumPrimitive::withPolylineColorAppearance() return result; } -void CesiumPrimitive::addLine(JsValue const& pointList, FeatureStyleRule const& style, uint32_t id) +CesiumPrimitive CesiumPrimitive::withPerInstanceColorAppearance(bool flatAndSynchronous) { - auto polyline = Cesium().PolylineGeometry.New(*JsValue::newDict({ - {"positions", pointList}, + CesiumPrimitive result; + result.flatAndSynchronous_ = flatAndSynchronous; + result.appearance_ = Cesium().PerInstanceColorAppearance.New({ + {"flat", JsValue(flatAndSynchronous)} + }); + return result; +} + +void CesiumPrimitive::addPolyLine( + JsValue const& vertices, + FeatureStyleRule const& style, + uint32_t id) +{ + auto polyline = Cesium().PolylineGeometry.New({ + {"positions", vertices}, {"width", JsValue(style.width())}, {"arcType", Cesium().ArcType["NONE"]} - })); + }); + addGeometryInstance(style, id, polyline); +} + +void CesiumPrimitive::addPolygon( + const JsValue& vertices, + const FeatureStyleRule& style, + uint32_t id) +{ + auto polygon = Cesium().PolygonGeometry.New({ + {"polygonHierarchy", Cesium().PolygonHierarchy.New(*vertices)}, + {"arcType", Cesium().ArcType["GEODESIC"]}, + {"perPositionHeight", JsValue(true)} + }); + addGeometryInstance(style, id, polygon); +} + +void CesiumPrimitive::addTriangles( + const JsValue& float64Array, + const FeatureStyleRule& style, + uint32_t id) +{ + auto geometry = Cesium().Geometry.New( + {{"attributes", + JsValue::Dict( + {{"position", + Cesium().GeometryAttribute.New( + {{"componentDatatype", Cesium().ComponentDatatype["DOUBLE"]}, + {"componentsPerAttribute", JsValue(3)}, + {"values", float64Array}})}})}, + {"boundingSphere", + JsValue(Cesium().BoundingSphere.call("fromVertices", *float64Array))}}); + addGeometryInstance(style, id, geometry); +} + +void CesiumPrimitive::addGeometryInstance( + const FeatureStyleRule& style, + uint32_t id, + const JsValue& geom) +{ auto const& color = style.color(); - auto geometryInstance = Cesium().GeometryInstance.New(*JsValue::newDict({ - {"geometry", polyline}, - {"attributes", JsValue::newDict({ - {"color", Cesium().ColorGeometryInstanceAttribute.New( - color.r, color.g, color.b, color.a)}})}, + auto geometryInstance = Cesium().GeometryInstance.New({ + {"geometry", geom}, + {"attributes", + JsValue::Dict( + {{"color", + Cesium().ColorGeometryInstanceAttribute.New(color.r, color.g, color.b, color.a)}})}, {"id", JsValue(id)} - })); + }); + ++numGeometryInstances_; geometryInstances_.push(geometryInstance); } -NativeJsValue CesiumPrimitive::toJsObject() +NativeJsValue CesiumPrimitive::toJsObject() const { - auto result = Cesium().Primitive.New(*JsValue::newDict( - { - {"geometryInstances", geometryInstances_}, - {"appearance", appearance_}, - {"releaseGeometryInstances", JsValue(true)} - })); + auto result = Cesium().Primitive.New(*JsValue::Dict( + {{"geometryInstances", geometryInstances_}, + {"appearance", appearance_}, + {"releaseGeometryInstances", JsValue(true)}, + {"asynchronous", JsValue(!flatAndSynchronous_)}})); return *result; } +bool CesiumPrimitive::empty() const +{ + return numGeometryInstances_ == 0; +} + } diff --git a/libs/core/src/renderer.cpp b/libs/core/src/renderer.cpp deleted file mode 100644 index 0fe7060b..00000000 --- a/libs/core/src/renderer.cpp +++ /dev/null @@ -1,93 +0,0 @@ -#include -#include -#include -#include - -#include "glm/glm.hpp" -#include "glm/gtc/type_ptr.hpp" -#include "glm/gtc/matrix_transform.hpp" -#include "glm/gtx/quaternion.hpp" - -#include "renderer.h" -#include "cesium-interface/point-conversion.h" -#include "cesium-interface/primitive.h" - -using namespace mapget; - -namespace erdblick -{ - -namespace -{ - -/** GLTF conversion for one geometry type of one rule. */ -struct CesiumTileGeometry -{ - CesiumTileGeometry() : coloredLines_(CesiumPrimitive::withPolylineColorAppearance()) {} - - void addFeature(model_ptr& feature, uint32_t id, FeatureStyleRule const& rule) - { - feature->geom()->forEachGeometry( - [this, id, &rule](auto&& geom) - { - addGeometry(geom, id, rule); - return true; - }); - } - - void addGeometry(model_ptr const& geom, uint32_t id, FeatureStyleRule const& rule) - { - // TODO: Implement logic for points/meshes/polygons - if (geom->geomType() != Geometry::GeomType::Line) - return; - - auto jsPoints = JsValue::newList(); - - uint32_t count = 0; - geom->forEachPoint( - [&count, &jsPoints](auto&& vertex) - { - jsPoints.push(JsValue(wgsToCartesian(vertex))); - ++count; - return true; - }); - - if (!count) - return; - - coloredLines_.addLine(jsPoints, rule, id); - } - - CesiumPrimitive coloredLines_; -}; - -} // namespace - -FeatureLayerRenderer::FeatureLayerRenderer() = default; - -NativeJsValue FeatureLayerRenderer::render( - const erdblick::FeatureLayerStyle& style, - const std::shared_ptr& layer) -{ - CesiumTileGeometry tileGeometry; - - uint32_t featureId = 0; - bool featuresAdded = false; - for (auto&& feature : *layer) { - // TODO: Optimize performance by implementing style.rules(feature-type) - for (auto&& rule : style.rules()) { - if (rule.match(*feature)) { - tileGeometry.addFeature(feature, featureId, rule); - featuresAdded = true; - } - } - ++featureId; - } - - if (featuresAdded) - return tileGeometry.coloredLines_.toJsObject(); - else - return {}; // Equates to JS null -} - -} // namespace erdblick diff --git a/libs/core/src/rule.cpp b/libs/core/src/rule.cpp index 7d90270e..7db4fca8 100644 --- a/libs/core/src/rule.cpp +++ b/libs/core/src/rule.cpp @@ -17,16 +17,16 @@ FeatureStyleRule::FeatureStyleRule(YAML::Node const& yaml) for (auto const& geometryStr : yaml["geometry"]) { auto g = geometryStr.as(); if (g == "point") { - geometryTypes_.push_back(simfil::Geometry::GeomType::Points); + geometryTypes_ |= geomTypeBit(mapget::Geometry::GeomType::Points); } else if (g == "mesh") { - geometryTypes_.push_back(simfil::Geometry::GeomType::Mesh); + geometryTypes_ |= geomTypeBit(mapget::Geometry::GeomType::Mesh); } else if (g == "line") { - geometryTypes_.push_back(simfil::Geometry::GeomType::Line); + geometryTypes_ |= geomTypeBit(mapget::Geometry::GeomType::Line); } else if (g == "polygon") { - geometryTypes_.push_back(simfil::Geometry::GeomType::Polygon); + geometryTypes_ |= geomTypeBit(mapget::Geometry::GeomType::Polygon); } else { std::cout << "Unsupported geometry type: " << g << std::endl; @@ -75,9 +75,9 @@ bool FeatureStyleRule::match(mapget::Feature& feature) const return true; } -const std::vector& FeatureStyleRule::geometryTypes() const +bool FeatureStyleRule::supports(const mapget::GeomType& g) const { - return geometryTypes_; + return geometryTypes_ & geomTypeBit(g); } glm::fvec4 const& FeatureStyleRule::color() const diff --git a/libs/core/src/visualization.cpp b/libs/core/src/visualization.cpp new file mode 100644 index 00000000..e52703fe --- /dev/null +++ b/libs/core/src/visualization.cpp @@ -0,0 +1,100 @@ +#include "visualization.h" +#include "cesium-interface/point-conversion.h" +#include "cesium-interface/primitive.h" + +using namespace mapget; + +namespace erdblick { + +FeatureLayerVisualization::FeatureLayerVisualization(const FeatureLayerStyle& style, const std::shared_ptr& layer) + : coloredLines_(CesiumPrimitive::withPolylineColorAppearance()), + coloredNontrivialMeshes_(CesiumPrimitive::withPerInstanceColorAppearance(false)), + coloredTrivialMeshes_(CesiumPrimitive::withPerInstanceColorAppearance(true)) +{ + uint32_t featureId = 0; + for (auto&& feature : *layer) { + for (auto&& rule : style.rules()) { + if (rule.match(*feature)) { + addFeature(feature, featureId, rule); + featuresAdded_ = true; + } + } + ++featureId; + } +} + +NativeJsValue FeatureLayerVisualization::primitiveCollection() const { + if (!featuresAdded_) + return {}; + auto collection = Cesium().PrimitiveCollection.New(); + if (!coloredLines_.empty()) + collection.call("add", coloredLines_.toJsObject()); + if (!coloredNontrivialMeshes_.empty()) + collection.call("add", coloredNontrivialMeshes_.toJsObject()); + if (!coloredTrivialMeshes_.empty()) + collection.call("add", coloredTrivialMeshes_.toJsObject()); + return *collection; +} + +void FeatureLayerVisualization::addFeature(model_ptr& feature, uint32_t id, FeatureStyleRule const& rule) { + feature->geom()->forEachGeometry( + [this, id, &rule](auto&& geom) { + if (rule.supports(geom->geomType())) + addGeometry(geom, id, rule); + return true; + }); +} + +void FeatureLayerVisualization::addGeometry(model_ptr const& geom, uint32_t id, FeatureStyleRule const& rule) { + switch (geom->geomType()) { + case mapget::Geometry::GeomType::Polygon: + if (auto verts = encodeVerticesAsList(geom)) { + coloredNontrivialMeshes_.addPolygon(*verts, rule, id); + } + break; + case mapget::Geometry::GeomType::Line: + if (auto verts = encodeVerticesAsList(geom)) { + coloredLines_.addPolyLine(*verts, rule, id); + } + break; + case mapget::Geometry::GeomType::Mesh: + if (auto verts = encodeVerticesAsFloat64Array(geom)) { + coloredTrivialMeshes_.addTriangles(*verts, rule, id); + } + break; + case mapget::Geometry::GeomType::Points: + // TODO: Implement point support. + break; + } +} + +std::optional FeatureLayerVisualization::encodeVerticesAsList(model_ptr const& geom) { + auto jsPoints = JsValue::List(); + uint32_t count = 0; + geom->forEachPoint( + [&count, &jsPoints](auto&& vertex) { + jsPoints.push(JsValue(wgsToCartesian(vertex))); + ++count; + return true; + }); + if (!count) + return {}; + return jsPoints; +} + +std::optional FeatureLayerVisualization::encodeVerticesAsFloat64Array(model_ptr const& geom) { + std::vector cartesianCoords; + geom->forEachPoint( + [&cartesianCoords](auto&& vertex) { + auto cartesian = wgsToCartesian(vertex); + cartesianCoords.push_back(cartesian.x); + cartesianCoords.push_back(cartesian.y); + cartesianCoords.push_back(cartesian.z); + return true; + }); + if (cartesianCoords.empty()) + return {}; + return JsValue::Float64Array(cartesianCoords); +} + +} // namespace erdblick diff --git a/static/erdblick/debugapi.js b/static/erdblick/debugapi.js index d0e2129f..01c6c6ab 100644 --- a/static/erdblick/debugapi.js +++ b/static/erdblick/debugapi.js @@ -14,7 +14,9 @@ export class ErdblickDebugApi { * @param mapView Reference to a ErdblickView instance */ constructor(mapView) { - this.mapView = mapView; + this.view = mapView; + this.model = mapView.model; + this.coreLib = mapView.model.coreLib; } /** @@ -24,7 +26,7 @@ export class ErdblickDebugApi { */ setCamera(cameraInfoStr) { const cameraInfo = JSON.parse(cameraInfoStr); - this.mapView.viewer.camera.setView({ + this.view.viewer.camera.setView({ destination: Cesium.Cartesian3.fromArray(cameraInfo.position), orientation: { heading: cameraInfo.orientation.heading, @@ -41,15 +43,24 @@ export class ErdblickDebugApi { */ getCamera() { const position = [ - this.mapView.viewer.camera.position.x, - this.mapView.viewer.camera.position.y, - this.mapView.viewer.camera.position.z + this.view.viewer.camera.position.x, + this.view.viewer.camera.position.y, + this.view.viewer.camera.position.z ]; const orientation = { - heading: this.mapView.viewer.camera.heading, - pitch: this.mapView.viewer.camera.pitch, - roll: this.mapView.viewer.camera.roll + heading: this.view.viewer.camera.heading, + pitch: this.view.viewer.camera.pitch, + roll: this.view.viewer.camera.roll }; return JSON.stringify({ position, orientation }); } + + /** + * Generate a test TileFeatureLayer, and show it. + */ + showTestTile() { + let tile = this.coreLib.generateTestTile(); + let style = this.coreLib.generateTestStyle(); + this.model.addTileLayer(tile, style, true); + } } diff --git a/static/erdblick/features.js b/static/erdblick/features.js index 39601dbf..980078d1 100644 --- a/static/erdblick/features.js +++ b/static/erdblick/features.js @@ -6,23 +6,26 @@ import {uint8ArrayFromWasm, uint8ArrayToWasm} from "./wasm.js"; * Bundle of a WASM TileFeatureLayer and a rendered representation * in the form of a Cesium PrimitiveCollection. * - * The WASM TileFatureLayer object is stored as a blob when not needed, + * The WASM TileFeatureLayer object is stored as a blob when not needed, * to keep the memory usage within reasonable limits. To use the wrapped * WASM TileFeatureLayer, use the peek()-function. */ export class FeatureTile { // public: + forceShow; /** * Construct a FeatureTile object. * @param coreLib Reference to the WASM erdblick library. * @param parser Singleton TileLayerStream WASM object. * @param tileFeatureLayer Deserialized WASM TileFeatureLayer. + * @param preventCulling Set to true to prevent the tile from being removed when it isn't visible. */ - constructor(coreLib, parser, tileFeatureLayer) + constructor(coreLib, parser, tileFeatureLayer, preventCulling) { this.coreLib = coreLib; this.parser = parser; + this.preventCulling = preventCulling; this.id = tileFeatureLayer.id(); this.tileId = tileFeatureLayer.tileId(); this.children = undefined; @@ -33,14 +36,14 @@ export class FeatureTile } /** - * Convert this TileFeatureLayer to a Cesium TileSet which - * contains a single tile. Returns a promise which resolves to true, - * if there is a freshly baked Cesium3DTileset, or false, + * Convert this TileFeatureLayer to a Cesium Primitive which + * contains all visuals for this tile, given the style. + * Returns a promise which resolves to true, if there is a freshly baked + * Cesium Primitive under this.primitiveCollection, or false, * if no output was generated because the tile is empty. - * @param {*} cesiumConverter The Cesium primitive renderer that should be used. * @param {null} style The style that is used to make the conversion. */ - async render(cesiumConverter, style) + async render(style) { // Do not try to render if the underlying data is disposed. if (this.disposed) @@ -52,7 +55,8 @@ export class FeatureTile this.disposeRenderResult(); this.peek(tileFeatureLayer => { - this.primitiveCollection = cesiumConverter.render(style, tileFeatureLayer); + let visualization = new this.coreLib.FeatureLayerVisualization(style, tileFeatureLayer); + this.primitiveCollection = visualization.primitiveCollection(); }); // The primitive collection will be null if there were no features to render. diff --git a/static/erdblick/model.js b/static/erdblick/model.js index 245cd7b4..d3eab31a 100644 --- a/static/erdblick/model.js +++ b/static/erdblick/model.js @@ -25,7 +25,6 @@ export class ErdblickModel this.coreLib = coreLibrary; this.style = null; this.maps = null; - this.glbConverter = new coreLibrary.FeatureLayerRenderer(); this.loadedTileLayers = new Map(); this.currentFetch = null; this.currentViewport = { @@ -46,10 +45,8 @@ export class ErdblickModel this.tileParser.onTileParsedFromStream(tileFeatureLayer => { const isInViewport = this.currentVisibleTileIds.has(tileFeatureLayer.tileId()); const alreadyLoaded = this.loadedTileLayers.has(tileFeatureLayer.id()); - if (isInViewport && !alreadyLoaded) { - let tile = new FeatureTile(this.coreLib, this.tileParser, tileFeatureLayer); - this.addTileLayer(tile); - } + if (isInViewport && !alreadyLoaded) + this.addTileLayer(tileFeatureLayer); else tileFeatureLayer.delete(); }); @@ -148,7 +145,7 @@ export class ErdblickModel // Evict present non-required tile layers. let newTileLayers = new Map(); for (let tileLayer of this.loadedTileLayers.values()) { - if (!this.currentVisibleTileIds.has(tileLayer.tileId)) { + if (!tileLayer.preventCulling && !this.currentVisibleTileIds.has(tileLayer.tileId)) { this.tileLayerRemovedTopic.next(tileLayer); tileLayer.dispose(); } @@ -196,7 +193,8 @@ export class ErdblickModel this.currentFetch.go(); } - addTileLayer(tileLayer) { + addTileLayer(wasmTileLayer, style, preventCulling) { + let tileLayer = new FeatureTile(this.coreLib, this.tileParser, wasmTileLayer, preventCulling); if (this.loadedTileLayers.has(tileLayer.id)) { throw new Error(`Refusing to add tile layer ${tileLayer.id}, which is already present.`); } @@ -204,22 +202,23 @@ export class ErdblickModel // Schedule the visualization of the newly added tile layer, // but don't do it synchronously to avoid stalling the main thread. setTimeout(() => { - this.renderTileLayer(tileLayer); + this.renderTileLayer(tileLayer, false, style); }) } - renderTileLayer(tileLayer, removeFirst) { + renderTileLayer(tileLayer, removeFirst, style) { + style = style || this.style; if (removeFirst) { this.tileLayerRemovedTopic.next(tileLayer); } - tileLayer.render(this.glbConverter, this.style).then(wasRendered => { + tileLayer.render(style).then(wasRendered => { if (!wasRendered) return; // It is possible, that the tile went out of view while // we took our time to visualize it. In this case, don't // add it to the viewport. - const isInViewport = this.currentVisibleTileIds.has(tileLayer.tileId); + const isInViewport = tileLayer.preventCulling || this.currentVisibleTileIds.has(tileLayer.tileId); if (isInViewport) this.tileLayerAddedTopic.next(tileLayer); else diff --git a/static/erdblick/view.js b/static/erdblick/view.js index 496046d7..9f300ff6 100644 --- a/static/erdblick/view.js +++ b/static/erdblick/view.js @@ -50,7 +50,7 @@ export class ErdblickView // Add a handler for selection. this.mouseHandler.setInputAction(movement => { let feature = this.viewer.scene.pick(movement.position); - if (feature && feature.id && this.tileLayerForPrimitive.has(feature.primitive)) + if (feature && feature.id !== undefined && this.tileLayerForPrimitive.has(feature.primitive)) this.setPickedCesiumFeature(feature); else this.setPickedCesiumFeature(null); @@ -59,7 +59,7 @@ export class ErdblickView // Add a handler for hover (i.e., MOUSE_MOVE) functionality. this.mouseHandler.setInputAction(movement => { let feature = this.viewer.scene.pick(movement.endPosition); // Notice that for MOUSE_MOVE, it's endPosition - if (feature && feature.id && this.tileLayerForPrimitive.has(feature.primitive)) + if (feature && feature.id !== undefined && this.tileLayerForPrimitive.has(feature.primitive)) this.setHoveredCesiumFeature(feature); else this.setHoveredCesiumFeature(null); @@ -75,11 +75,14 @@ export class ErdblickView model.tileLayerAddedTopic.subscribe(tileLayer => { this.viewer.scene.primitives.add(tileLayer.primitiveCollection); - this.tileLayerForPrimitive.set(tileLayer.primitiveCollection, tileLayer); + for (let i = 0; i < tileLayer.primitiveCollection.length; ++i) + this.tileLayerForPrimitive.set(tileLayer.primitiveCollection.get(i), tileLayer); this.viewer.scene.requestRender(); - }) + }); model.tileLayerRemovedTopic.subscribe(tileLayer => { + if (!tileLayer.primitiveCollection) + return; if (this.pickedFeature && this.pickedFeature.primitive === tileLayer.primitiveCollection) { this.setPickedCesiumFeature(null); } @@ -87,8 +90,10 @@ export class ErdblickView this.setHoveredCesiumFeature(null); } this.viewer.scene.primitives.remove(tileLayer.primitiveCollection); - this.tileLayerForPrimitive.delete(tileLayer.primitiveCollection); - }) + for (let i = 0; i < tileLayer.primitiveCollection.length; ++i) + this.tileLayerForPrimitive.delete(tileLayer.primitiveCollection.get(i)); + this.viewer.scene.requestRender(); + }); model.zoomToWgs84PositionTopic.subscribe(pos => { this.viewer.camera.setView({ @@ -101,8 +106,6 @@ export class ErdblickView }); }); - let polylines = new Cesium.PolylineCollection(); - this.viewer.scene.primitives.add(polylines); this.viewer.scene.globe.baseColor = new Cesium.Color(0.1, 0.1, 0.1, 1); } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index b21ad210..d2f7e752 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -10,7 +10,7 @@ if (NOT TARGET Catch2) endif() add_executable(test.erdblick - renderer.cpp) + test-visualization.cpp) target_link_libraries(test.erdblick PUBLIC diff --git a/test/renderer.cpp b/test/renderer.cpp deleted file mode 100644 index a78b8395..00000000 --- a/test/renderer.cpp +++ /dev/null @@ -1,31 +0,0 @@ -#include - -#include "erdblick/renderer.h" -#include "erdblick/testdataprovider.h" - -#include - -using namespace erdblick; - -TEST_CASE("FeatureLayerRenderer", "[erdblick.renderer]") -{ - FeatureLayerStyle style(SharedUint8Array(R"( - name: DemoStyle - version: 1.0 - rules: - - geometry: ["line"] - color: #ffffff - - geometry: ["mesh"] - opacity: 0.9 - - geometry: ["point"] - )")); - - auto testLayer = TestDataProvider().getTestLayer(42., 11., 13); - - FeatureLayerRenderer renderer; - auto result = renderer.render(style, testLayer); - - std::cout << result << std::endl; - - REQUIRE(!result.empty()); -} \ No newline at end of file diff --git a/test/test-visualization.cpp b/test/test-visualization.cpp new file mode 100644 index 00000000..b7844dc0 --- /dev/null +++ b/test/test-visualization.cpp @@ -0,0 +1,17 @@ +#include + +#include "erdblick/testdataprovider.h" +#include "erdblick/visualization.h" + +#include + +using namespace erdblick; + +TEST_CASE("FeatureLayerVisualization", "[erdblick.renderer]") +{ + auto testLayer = TestDataProvider().getTestLayer(42., 11., 13); + FeatureLayerVisualization visualization(TestDataProvider::style(), testLayer); + auto result = visualization.primitiveCollection(); + std::cout << result << std::endl; + REQUIRE(!result.empty()); +} \ No newline at end of file