From 258fcfc0e282376cb392b3783c802dda37299dc8 Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Tue, 29 Aug 2023 14:12:32 +0200 Subject: [PATCH 01/16] Implemented tile ID calculation for Cesium viewport. --- libs/core/CMakeLists.txt | 1 - libs/core/include/erdblick/aabb.h | 30 ++++--- libs/core/src/aabb.cpp | 57 ++++-------- libs/core/src/bindings.cpp | 144 ++++++++++++++++-------------- static/mapcomponent/model.js | 39 ++++---- static/mapcomponent/view.js | 127 +++++++++++++++++++++++--- 6 files changed, 250 insertions(+), 148 deletions(-) diff --git a/libs/core/CMakeLists.txt b/libs/core/CMakeLists.txt index 821d47c1..1f68d6b1 100644 --- a/libs/core/CMakeLists.txt +++ b/libs/core/CMakeLists.txt @@ -52,4 +52,3 @@ target_link_libraries(erdblick-core glm mapget-model yaml-cpp) - diff --git a/libs/core/include/erdblick/aabb.h b/libs/core/include/erdblick/aabb.h index 6bf7be74..8145a1ff 100644 --- a/libs/core/include/erdblick/aabb.h +++ b/libs/core/include/erdblick/aabb.h @@ -20,8 +20,6 @@ using Wgs84Point = mapget::Point; class Wgs84AABB { public: - static TilePriorityFn radialDistancePrioFn(glm::vec2 camPos, float orientation); - using vec2_t = glm::dvec2; Wgs84AABB() = default; @@ -58,8 +56,9 @@ class Wgs84AABB /** Obtain the size of this bounding box. */ vec2_t const& size() const { return size_; } - /** Determine whether the horizontal extent of this bounding rect - * crosses the anti-meridian (lon == +/- 180°). + /** + * Determine whether the horizontal extent of this bounding rect + * crosses the anti-meridian (lon == +/- 180°). */ bool containsAntiMeridian() const; @@ -93,17 +92,24 @@ class Wgs84AABB /** Determine whether this bounding rect has an intersection with another bounding rect. */ bool intersects(Wgs84AABB const& other) const; - /** Obtain TileIds for a given tile level. + /** + * Obtain TileIds for a given tile level. Will fill in tileIds for the + * given level into resultTileIdsWithPriority up to resultTileIdsWithPriority.capacity(), + * so the function is guaranteed not to allocate any heap memory. + * Also annotates each returned TileId with a float as returned by the + * TilePenaltyFn lambda. */ - void tileIds(uint16_t level, std::vector &result) const; + void tileIdsWithPriority( + uint16_t level, + std::vector> &resultTileIdsWithPriority, + TilePriorityFn const& prioFn) const; - /** Same as tileIdsWithPriority, but strips the priority values - * and converts the linked list to a vector. + /** + * Returns a tile priority function based on the given camera position + * in WGS84, and orientation (bearing) in Radians. This priority function + * may be plugged into tileIdsWithPriority. */ - std::vector tileIds( - uint16_t level, - std::function const& tilePenaltyFun, - size_t limit) const; + static TilePriorityFn radialDistancePrioFn(glm::vec2 camPos, float orientation); private: vec2_t sw_{.0, .0}; diff --git a/libs/core/src/aabb.cpp b/libs/core/src/aabb.cpp index 9f771bf3..224c02b5 100644 --- a/libs/core/src/aabb.cpp +++ b/libs/core/src/aabb.cpp @@ -1,5 +1,7 @@ #include "aabb.h" +#include "glm/ext.hpp" + namespace erdblick { @@ -16,33 +18,6 @@ inline Wgs84Point point(glm::dvec3 const& p) return {p.x, p.y, p.z}; } -float fastAtan2(float y, float x) -{ - if (x == 0.0 && y == 0.0) { - return 0.0; // handle the case when both x and y are zero - } - - float abs_x = std::abs(x); - float abs_y = std::abs(y); - - float a = std::min(abs_x, abs_y) / std::max(abs_x, abs_y); - float s = a * a; - - float r = ((-0.0464964749f * s + 0.15931422f) * s - 0.327622764f) * s * a + a; - - if (abs_y > abs_x) { - r = 1.57079637f - r; - } - if (x < 0.0) { - r = 3.14159274f - r; - } - if (y < 0.0) { - r = -r; - } - - return r; -} - } // namespace Wgs84AABB::Wgs84AABB(const Wgs84Point& sw, glm::dvec2 size) : sw_(sw.x, sw.y), size_(size) @@ -165,15 +140,18 @@ bool Wgs84AABB::intersects(const Wgs84AABB& other) const contains(other.nw()) || other.intersects(*this); } -void Wgs84AABB::tileIds(uint16_t level, std::vector& tileIdsResult) const +void Wgs84AABB::tileIdsWithPriority( + uint16_t level, + std::vector> &tileIdsResult, + TilePriorityFn const& prioFn) const { if (containsAntiMeridian()) { auto normalizedViewports = splitOverAntiMeridian(); assert( !normalizedViewports.first.containsAntiMeridian() && !normalizedViewports.second.containsAntiMeridian()); - normalizedViewports.first.tileIds(level, tileIdsResult); - normalizedViewports.second.tileIds(level, tileIdsResult); + normalizedViewports.first.tileIdsWithPriority(level, tileIdsResult, prioFn); + normalizedViewports.second.tileIdsWithPriority(level, tileIdsResult, prioFn); } auto const tileWidth = 180. / static_cast(1 << level); @@ -191,7 +169,7 @@ void Wgs84AABB::tileIds(uint16_t level, std::vector& tileIdsResult) cons double y = minPoint.y; while (y <= maxPoint.y && remainingCapacity > 0) { auto tid = TileId::fromWgs84(x, y, level); - tileIdsResult.emplace_back(tid); + tileIdsResult.emplace_back(tid, prioFn(tid)); remainingCapacity -= 1; y += glm::min(tileWidth, glm::max(maxPoint.y - y, epsilon)); } @@ -204,18 +182,17 @@ TilePriorityFn Wgs84AABB::radialDistancePrioFn(glm::vec2 camPos, float orientati return [camPos, orientation](TileId const& tid) { auto center = tid.center(); - float xDiff = center.x - camPos.x; - float yDiff = center.y - camPos.y; + float xDiff = static_cast(center.x) - camPos.x; + float yDiff = static_cast(center.y) - camPos.y; auto angle = glm::atan(yDiff, xDiff); // Angle to east (x axis) direction. glm::atan is atan2. - angle -= orientation + - M_PI_2; // Difference w/ compass direction normalized from North to East - angle = glm::abs(glm::mod(angle, (float)M_2_PI)); // Map angle to circle - if (angle > M_PI) - angle = M_2_PI - angle; + angle -= orientation + glm::two_pi(); // Difference w/ compass direction normalized from North to East + angle = glm::abs(glm::mod(angle, glm::two_pi())); // Map angle to circle + if (angle > glm::pi()) + angle = glm::two_pi() - angle; - auto distance = yDiff + xDiff; // eventually use manhattan distance to avoid comp overhead? - return yDiff + xDiff + angle * distance; + auto distance = glm::sqrt(yDiff*yDiff + xDiff*xDiff); // Use manhattan distance to avoid comp overhead? + return distance + angle * distance; }; } diff --git a/libs/core/src/bindings.cpp b/libs/core/src/bindings.cpp index 2dc93cd2..5efa7eaa 100644 --- a/libs/core/src/bindings.cpp +++ b/libs/core/src/bindings.cpp @@ -12,9 +12,78 @@ using namespace erdblick; namespace em = emscripten; -Wgs84AABB createWgs84AABB(float x, float y, uint32_t softLimit, uint16_t level) +/** + * Gets the prioritized list of tile IDs for a given viewport, zoom level, and tile limit. + * + * This function takes a viewport, a zoom level, and a tile limit, and returns an array of tile IDs + * that are visible in the viewport, prioritized by radial distance from the camera position. + * + * The function first extracts the viewport properties and creates an Axis-Aligned Bounding Box (AABB) + * from the viewport boundaries. If the number of tile IDs in the AABB at the given zoom level exceeds + * the specified limit, a new AABB is created from the camera position and tile limit. + * + * The function then populates a vector of prioritized tile IDs by calculating the radial distance + * from the camera position to the center of each tile in the AABB. The tile IDs are then sorted by + * their radial distance, and the sorted array is converted to an emscripten value to be returned. + * Duplicate tile IDs are removed from the array before it is returned. + * + * @param viewport An emscripten value representing the viewport. The viewport is an object + * containing the following properties: + * - south: The southern boundary of the viewport. + * - west: The western boundary of the viewport. + * - width: The width of the viewport. + * - height: The height of the viewport. + * - camPosLon: The longitude of the camera position. + * - camPosLat: The latitude of the camera position. + * - orientation: The orientation of the viewport. + * @param level The zoom level for which to get the tile IDs. + * @param limit The maximum number of tile IDs to return. + * + * @return An emscripten value representing an array of prioritized tile IDs. + */ +em::val getTileIds(em::val viewport, int level, int limit) { - return Wgs84AABB::fromCenterAndTileLimit(Wgs84Point{x, y, 0}, softLimit, level); + double vpSouth = viewport["south"].as(); + double vpWest = viewport["west"].as(); + double vpWidth = viewport["width"].as(); + double vpHeight = viewport["height"].as(); + double camPosLon = viewport["camPosLon"].as(); + double camPosLat = viewport["camPosLat"].as(); + double orientation = viewport["orientation"].as(); + + Wgs84AABB aabb(Wgs84Point{vpWest, vpSouth, .0}, {vpWidth, vpHeight}); + if (aabb.numTileIds(level) > limit) + // Create a size-limited AABB from the tile limit. + aabb = Wgs84AABB::fromCenterAndTileLimit(Wgs84Point{camPosLon, camPosLat, .0}, limit, level); + + std::vector> prioritizedTileIds; + prioritizedTileIds.reserve(limit); + aabb.tileIdsWithPriority( + level, + prioritizedTileIds, + Wgs84AABB::radialDistancePrioFn({camPosLon, camPosLat}, orientation)); + + std::sort( + prioritizedTileIds.begin(), + prioritizedTileIds.end(), + [](auto const& l, auto const& r) { return l.second < r.second; }); + + em::val resultArray = em::val::array(); + int64_t prevTileId = -1; + for (const auto& tileId : prioritizedTileIds) { + if (tileId.first.value_ == prevTileId) + continue; + resultArray.call("push", tileId.first.value_); + prevTileId = tileId.first.value_; + } + + return resultArray; +} + +/** Get the position for a mapget tile id in WGS84. */ +mapget::Point getTilePosition(uint64_t tileIdValue) { + mapget::TileId tid(tileIdValue); + return tid.center(); } EMSCRIPTEN_BINDINGS(FeatureLayerRendererBind) @@ -27,7 +96,10 @@ EMSCRIPTEN_BINDINGS(FeatureLayerRendererBind) .function("getPointer", &SharedUint8Array::getPointer); ////////// Point - em::class_("Point"); + em::value_object("Point") + .field("x", &mapget::Point::x) + .field("y", &mapget::Point::y) + .field("z", &mapget::Point::z); ////////// FeatureLayerStyle em::class_("FeatureLayerStyle").constructor(); @@ -97,67 +169,7 @@ EMSCRIPTEN_BINDINGS(FeatureLayerRendererBind) { self.onTileParsed([cb](auto&& tile) { cb(tile); }); })) .function("parse", &TileLayerParser::parse); - ////////// Wgs84AABB - em::register_vector("VectorUint64"); - em::register_vector("VectorDouble"); - em::class_("Wgs84AABB") - .function( - "tileIds", - std::function( - [](Wgs84AABB& self, - double camX, - double camY, - double camOrientation, - uint32_t level, - uint32_t limit) -> em::val - { - // TODO: This tileIds-implementation is a work-in-progress - std::vector resultTiles; - resultTiles.reserve(limit); - self.tileIds(level, resultTiles); - - std::vector tileIdArray; - std::vector xArray; - std::vector yArray; - tileIdArray.reserve(resultTiles.size()); - xArray.reserve(resultTiles.size()); - yArray.reserve(resultTiles.size()); - for (auto& tile : resultTiles) { - tileIdArray.emplace_back((int64_t)tile.value_); - auto pos = tile.center(); - xArray.emplace_back(pos.x); - yArray.emplace_back(pos.y); - } - - em::val result = em::val::object(); - result.set("id", tileIdArray); - result.set("x", xArray); - result.set("y", yArray); - - return result; - // return em::val(resultWithPrio.size()); - })) - .function( - "ne", - std::function( - [](Wgs84AABB& self) -> em::val - { - Wgs84Point pt = self.ne(); - em::val list = em::val::array(); - list.set(0, pt.x); - list.set(1, pt.y); - return list; - })) - .function( - "sw", - std::function( - [](Wgs84AABB& self) -> em::val - { - Wgs84Point pt = self.sw(); - em::val list = em::val::array(); - list.set(0, pt.x); - list.set(1, pt.y); - return list; - })); - function("createWgs84AABB", &createWgs84AABB, em::allow_raw_pointers()); + ////////// Viewport TileID calculation + em::function("getTileIds", &getTileIds); + em::function("getTilePosition", &getTilePosition); } diff --git a/static/mapcomponent/model.js b/static/mapcomponent/model.js index b1194fd7..36079da7 100644 --- a/static/mapcomponent/model.js +++ b/static/mapcomponent/model.js @@ -10,6 +10,17 @@ const styleUrl = "/styles/demo-style.yaml"; const infoUrl = "/sources"; const tileUrl = "/tiles"; +export class MapViewerViewport { + constructor(south, west, width, height, camPosLon, camPosLat, orientation) { + this.south = south; + this.west = west; + this.width = width; + this.height = height; + this.camPosLon = camPosLon; + this.camPosLat = camPosLat; + this.orientation = orientation; + } +} export class MapViewerModel { @@ -28,18 +39,11 @@ export class MapViewerModel numLoadingBatches: 0, loadingBatchNames: new Set(), fetch: null, - stream: null + stream: null, + viewport: new MapViewerViewport, + visibleTileIds: [] }; - this._viewportUpdateThrottle = throttle( - minViewportChangedCallDelta, - (viewport, jumped, camPos, alt, tilt, orientation) => - { - this.update.viewport = viewport.clone(); - // this.update() - } - ); - /////////////////////////////////////////////////////////////////////////// // MODEL EVENTS // /////////////////////////////////////////////////////////////////////////// @@ -92,7 +96,11 @@ export class MapViewerModel // MAP UPDATE CONTROLS // /////////////////////////////////////////////////////////////////////////// - runUpdate() { + runUpdate() + { + // Get the tile IDs for the current viewport. + this.update.visibleTileIds = this.coreLib.getTileIds(this.update.viewport, 13, 128); + // TODO // if (this.update.fetch) // this.update.fetch.abort() @@ -146,11 +154,8 @@ export class MapViewerModel // public: - viewportChanged(viewport, jumped, camPos, alt, tilt, orientation) { - this._viewportUpdateThrottle(viewport, jumped, camPos, alt, tilt, orientation); - } - - go() { - // TODO: Implement Initial Data Request + setViewport(viewport) { + this.update.viewport = viewport; + this.runUpdate(); } } diff --git a/static/mapcomponent/view.js b/static/mapcomponent/view.js index 3ab09b5b..742d5253 100644 --- a/static/mapcomponent/view.js +++ b/static/mapcomponent/view.js @@ -1,4 +1,4 @@ -import {MapViewerModel} from "./model.js"; +import {MapViewerModel, MapViewerViewport} from "./model.js"; import {FeatureWrapper} from "./featurelayer.js"; export class MapViewerView @@ -10,18 +10,10 @@ export class MapViewerView */ constructor(model, containerDomElementId) { - // The base64 encoding of a 1x1 black PNG. - let blackPixelBase64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGNgYGAAAAAEAAH2FzhVAAAAAElFTkSuQmCC'; - + this.model = model; this.viewer = new Cesium.Viewer(containerDomElementId, { - // Create a SingleTileImageryProvider that uses the black pixel. - imageryProvider: new Cesium.SingleTileImageryProvider({ - url: blackPixelBase64, - rectangle: Cesium.Rectangle.MAX_VALUE, - tileWidth: 1, - tileHeight: 1 - }), + imageryProvider: false, baseLayerPicker: false, animation: false, geocoder: false, @@ -48,7 +40,7 @@ export class MapViewerView this.hoveredFeature = null; this.mouseHandler = new Cesium.ScreenSpaceEventHandler(this.viewer.scene.canvas); - /// Holds the currently selected feature. + // Holds the currently selected feature. this.selectionTopic = new rxjs.BehaviorSubject(null); // {FeatureWrapper} // Add a handler for selection. @@ -87,6 +79,43 @@ export class MapViewerView } }, Cesium.ScreenSpaceEventType.MOUSE_MOVE); + // Add a handler for camera movement + this.viewer.camera.changed.addEventListener(() => { + let canvas = this.viewer.scene.canvas; + let center = new Cesium.Cartesian2(canvas.clientWidth / 2, canvas.clientHeight / 2); + let centerCartesian = this.viewer.camera.pickEllipsoid(center); + let centerLon, centerLat; + + // If the center of the screen is not on the earth's surface (i.e., the horizon is below the viewport center), + // fallback to using the camera's position. + if (Cesium.defined(centerCartesian)) { + let centerCartographic = Cesium.Cartographic.fromCartesian(centerCartesian); + centerLon = Cesium.Math.toDegrees(centerCartographic.longitude); + centerLat = Cesium.Math.toDegrees(centerCartographic.latitude); + } else { + let cameraCartographic = Cesium.Cartographic.fromCartesian(this.viewer.camera.positionWC); + centerLon = Cesium.Math.toDegrees(cameraCartographic.longitude); + centerLat = Cesium.Math.toDegrees(cameraCartographic.latitude); + } + + let rectangle = this.viewer.camera.computeViewRectangle(); + + // Extract the WGS84 coordinates for the rectangle's corners + let west = Cesium.Math.toDegrees(rectangle.west); + let south = Cesium.Math.toDegrees(rectangle.south); + let east = Cesium.Math.toDegrees(rectangle.east); + let north = Cesium.Math.toDegrees(rectangle.north); + let sizeLon = east - west; + let sizeLat = north - south; + + // Create the viewport object + let viewport = new MapViewerViewport(south, west, sizeLon, sizeLat, centerLon, centerLat, this.viewer.camera.heading); + + // Pass the viewport object to the model + model.setViewport(viewport); + this.visualizeTileIds(); + }); + this.batchForTileSet = new Map(); model.batchAddedTopic.subscribe(batch => { @@ -98,6 +127,40 @@ export class MapViewerView this.viewer.scene.primitives.remove(batch.tileSet); this.batchForTileSet.delete(batch.tileSet); }) + + let polylines = new Cesium.PolylineCollection(); + + // Line over the equator divided into four 90-degree segments + this.viewer.entities.add({ + name: 'Equator', + polyline: { + positions: Cesium.Cartesian3.fromDegreesArray([ + -180, 0, // Start + -90, 0, // 1st quarter + 0, 0, // Halfway + 90, 0, // 3rd quarter + 180, 0 // End + ]), + width: 2, + material: Cesium.Color.RED.withAlpha(0.5) + } + }); + + // Line over the antimeridian + this.viewer.entities.add({ + name: 'Antimeridian', + polyline: { + positions: Cesium.Cartesian3.fromDegreesArray([ + -180, -90, + -180, 90 + ]), + width: 2, + material: Cesium.Color.BLUE.withAlpha(0.5) + } + }); + + this.viewer.scene.primitives.add(polylines); + this.viewer.scene.globe.baseColor = new Cesium.Color(0.1, 0.1, 0.1, 1); } resolveFeature(tileSet, index) { @@ -108,4 +171,44 @@ export class MapViewerView } return new FeatureWrapper(index, batch); } + + visualizeTileIds() { + // Remove previous points + if (this.points) { + for (let i = 0; i < this.points.length; i++) { + this.viewer.entities.remove(this.points[i]); + } + } + + // Get the tile IDs for the current viewport. + let tileIds = this.model.update.visibleTileIds; + + // Calculate total number of tile IDs + let totalTileIds = tileIds.length; + + // Initialize points array + this.points = []; + + // Iterate through each tile ID + for(let i = 0; i < totalTileIds; i++) { + // Get WGS84 coordinates for the tile ID + let position = this.model.coreLib.getTilePosition(tileIds[i]); + + // Calculate the color based on the position in the list + let colorValue = i / totalTileIds; + let color = Cesium.Color.fromHsl(0.6 - colorValue * 0.5, 1.0, 0.5); + + // Create a point and add it to the Cesium scene + let point = this.viewer.entities.add({ + position : Cesium.Cartesian3.fromDegrees(position.x, position.y), + point : { + pixelSize : 5, + color : color + } + }); + + // Add the point to the points array + this.points.push(point); + } + } } From bd3ed1359ef4aad631d2d263d5449cb43c755843 Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Fri, 1 Sep 2023 18:17:35 +0200 Subject: [PATCH 02/16] Essential implementations for viewport-driven model.update() --- CMakeLists.txt | 2 +- libs/core/include/erdblick/stream.h | 3 + libs/core/src/bindings.cpp | 32 +++- libs/core/src/stream.cpp | 33 ++-- static/index.html | 3 +- static/index.js | 5 - .../{featurelayer.js => featuretile.js} | 9 +- static/mapcomponent/fetch.js | 27 ++-- static/mapcomponent/model.js | 146 ++++++++++-------- static/mapcomponent/view.js | 6 +- 10 files changed, 160 insertions(+), 106 deletions(-) rename static/mapcomponent/{featurelayer.js => featuretile.js} (93%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7c074ea7..fc81df1c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,7 +38,7 @@ FetchContent_MakeAvailable(cesiumnative) FetchContent_Declare(mapget GIT_REPOSITORY "https://github.com/Klebert-Engineering/mapget" - GIT_TAG "main" + GIT_TAG "access-field-dict-offsets" GIT_SHALLOW ON) FetchContent_MakeAvailable(mapget) diff --git a/libs/core/include/erdblick/stream.h b/libs/core/include/erdblick/stream.h index db5d53da..49679481 100644 --- a/libs/core/include/erdblick/stream.h +++ b/libs/core/include/erdblick/stream.h @@ -12,10 +12,13 @@ class TileLayerParser explicit TileLayerParser(SharedUint8Array const& dataSourceInfo); void onTileParsed(std::function); void parse(SharedUint8Array const& dataSourceInfo); + void reset(); + mapget::TileLayerStream::FieldOffsetMap fieldDictOffsets(); private: std::map info_; std::unique_ptr reader_; + std::shared_ptr cachedFieldDicts_; std::function tileParsedFun_; }; diff --git a/libs/core/src/bindings.cpp b/libs/core/src/bindings.cpp index 5efa7eaa..2983a5ad 100644 --- a/libs/core/src/bindings.cpp +++ b/libs/core/src/bindings.cpp @@ -80,12 +80,22 @@ em::val getTileIds(em::val viewport, int level, int limit) return resultArray; } -/** Get the position for a mapget tile id in WGS84. */ +/** Get the center position for a mapget tile id in WGS84. */ mapget::Point getTilePosition(uint64_t tileIdValue) { mapget::TileId tid(tileIdValue); return tid.center(); } +/** Get the full key of a map tile feature layer. */ +std::string getTileFeatureLayerKey(std::string const& mapId, std::string const& layerId, uint64_t tileId) { + auto tileKey = mapget::MapTileKey(); + tileKey.layer_ = mapget::LayerType::Features; + tileKey.mapId_ = mapId; + tileKey.layerId_ = layerId; + tileKey.tileId_ = tileId; + return tileKey.toString(); +} + EMSCRIPTEN_BINDINGS(FeatureLayerRendererBind) { ////////// SharedUint8Array @@ -125,6 +135,10 @@ EMSCRIPTEN_BINDINGS(FeatureLayerRendererBind) "id", std::function( [](mapget::TileFeatureLayer const& self) { return self.id().toString(); })) + .function( + "tileId", + std::function( + [](mapget::TileFeatureLayer const& self) { return self.tileId().value_; })) .function( "center", std::function( @@ -167,9 +181,23 @@ EMSCRIPTEN_BINDINGS(FeatureLayerRendererBind) std::function( [](TileLayerParser& self, em::val cb) { self.onTileParsed([cb](auto&& tile) { cb(tile); }); })) - .function("parse", &TileLayerParser::parse); + .function("parse", &TileLayerParser::parse) + .function("reset", &TileLayerParser::reset) + .function( + "fieldDictOffsets", + std::function( + [](TileLayerParser& self) + { + auto result = em::val::object(); + for (auto const& [nodeId, fieldId] : self.fieldDictOffsets()) + result.set(nodeId, fieldId); + return result; + })); ////////// Viewport TileID calculation em::function("getTileIds", &getTileIds); em::function("getTilePosition", &getTilePosition); + + ////////// Get full id of a TileFeatureLayer + em::function("getTileFeatureLayerKey", &getTileFeatureLayerKey); } diff --git a/libs/core/src/stream.cpp b/libs/core/src/stream.cpp index 81ded513..5ebd84a6 100644 --- a/libs/core/src/stream.cpp +++ b/libs/core/src/stream.cpp @@ -8,23 +8,18 @@ namespace erdblick TileLayerParser::TileLayerParser(SharedUint8Array const& dataSourceInfo) { + // Create field dict cache + cachedFieldDicts_ = std::make_shared(); + // Parse data source info auto srcInfoParsed = nlohmann::json::parse(dataSourceInfo.toString()); - for (auto const& node : srcInfoParsed) { auto dsInfo = DataSourceInfo::fromJson(node); info_.emplace(dsInfo.mapId_, std::move(dsInfo)); } - // Create parser - reader_ = std::make_unique( - [this](auto&& mapId, auto&& layerId){ - return info_[std::string(mapId)].getLayer(std::string(layerId)); - }, - [this](auto&& layer){ - if (tileParsedFun_) - tileParsedFun_(layer); - }); + // Create fresh parser + reset(); } void TileLayerParser::onTileParsed(std::function fun) @@ -42,4 +37,22 @@ void TileLayerParser::parse(SharedUint8Array const& dataSourceInfo) } } +mapget::TileLayerStream::FieldOffsetMap TileLayerParser::fieldDictOffsets() +{ + return reader_->fieldDictCache()->fieldDictOffsets(); +} + +void TileLayerParser::reset() +{ + reader_ = std::make_unique( + [this](auto&& mapId, auto&& layerId){ + return info_[std::string(mapId)].getLayer(std::string(layerId)); + }, + [this](auto&& layer){ + if (tileParsedFun_) + tileParsedFun_(layer); + }, + cachedFieldDicts_); +} + } diff --git a/static/index.html b/static/index.html index 4dbc548d..402c6077 100644 --- a/static/index.html +++ b/static/index.html @@ -35,9 +35,8 @@
erdblick v0.2.0 // - //
-
+
diff --git a/static/index.js b/static/index.js index f2a72bcd..5d799ada 100644 --- a/static/index.js +++ b/static/index.js @@ -12,11 +12,6 @@ libErdblickCore().then(coreLib => let mapModel = new MapViewerModel(coreLib); let mapView = new MapViewerView(mapModel, 'cesiumContainer'); - window.loadAllTiles = () => { - $("#log").empty() - mapModel.runUpdate(); - } - window.reloadStyle = () => { mapModel.reloadStyle(); } diff --git a/static/mapcomponent/featurelayer.js b/static/mapcomponent/featuretile.js similarity index 93% rename from static/mapcomponent/featurelayer.js rename to static/mapcomponent/featuretile.js index eb3e0c1c..a295db3d 100644 --- a/static/mapcomponent/featurelayer.js +++ b/static/mapcomponent/featuretile.js @@ -22,12 +22,13 @@ function blobUriFromWasm(coreLib, fun, contentType) { * The tileset JSON and the GLTF blob are stored as browser Blob objects * (see https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL_static). */ -export class FeatureLayerTileSet +export class FeatureTile { // public: - constructor(batchName, tileFeatureLayer) + constructor(tileFeatureLayer) { - this.id = batchName; + this.id = tileFeatureLayer.id(); + this.tileId = tileFeatureLayer.tileId(); this.children = undefined; this.tileFeatureLayer = tileFeatureLayer; this.glbUrl = null; @@ -81,7 +82,7 @@ export class FeatureLayerTileSet } /** - * Wrapper which combines a FeatureLayerTileSet and the index of + * Wrapper which combines a FeatureTile and the index of * a feature within the tileset. Using the peek-function, it is * possible to access the WASM feature view in a memory-safe way. */ diff --git a/static/mapcomponent/fetch.js b/static/mapcomponent/fetch.js index 30e701e1..0c3dc39d 100644 --- a/static/mapcomponent/fetch.js +++ b/static/mapcomponent/fetch.js @@ -14,10 +14,11 @@ export class Fetch this.url = url; this.method = 'GET'; this.body = null; - this.signal = null; + this.abortSignal = new AbortSignal(); this.processChunks = false; this.jsonCallback = null; this.wasmCallback = null; + this.aborted = false; } /** @@ -40,16 +41,6 @@ export class Fetch return this; } - /** - * Method to set the signal for the request (for aborting the request). - * @param {AbortSignal} signal - The AbortSignal object. - * @return {Fetch} The Fetch instance for chaining. - */ - withSignal(signal) { - this.signal = signal; - return this; - } - /** * Method to enable chunk processing for the response. * @return {Fetch} The Fetch instance for chaining. @@ -90,7 +81,7 @@ export class Fetch // Currently, the connection stays open for five seconds. 'Connection': 'close' }, - signal: this.signal, + signal: this.abortSignal, keepalive: false, mode: "same-origin" }; @@ -188,7 +179,7 @@ export class Fetch */ runWasmCallback(uint8Array) { - if (!this.wasmCallback) + if (!this.wasmCallback || this.aborted) return; let sharedArr = new this.coreLib.SharedUint8Array(uint8Array.length); @@ -204,4 +195,14 @@ export class Fetch sharedArr.delete(); } + + /** + * Signal that the request should be aborted. + */ + abort() { + if (this.aborted) + return + this.abortSignal.abort(); + this.aborted = true; + } } diff --git a/static/mapcomponent/model.js b/static/mapcomponent/model.js index 36079da7..90a8962d 100644 --- a/static/mapcomponent/model.js +++ b/static/mapcomponent/model.js @@ -2,7 +2,7 @@ import {throttle} from "./utils.js"; import {Fetch} from "./fetch.js"; -import {FeatureLayerTileSet} from "./featurelayer.js"; +import {FeatureTile} from "./featuretile.js"; const minViewportChangedCallDelta = 200; // ms @@ -31,30 +31,21 @@ export class MapViewerModel this.style = null; this.sources = null; this.glbConverter = new coreLibrary.FeatureLayerRenderer(); - - this.registeredBatches = new Map(); - - this.update = { - running: false, - numLoadingBatches: 0, - loadingBatchNames: new Set(), - fetch: null, - stream: null, - viewport: new MapViewerViewport, - visibleTileIds: [] - }; + this.loadedTileLayers = new Map(); + this.currentFetch = null; + this.currentTileStream = null; + this.currentViewport = new MapViewerViewport; + this.currentVisibleTileIds = new Set(); /////////////////////////////////////////////////////////////////////////// // MODEL EVENTS // /////////////////////////////////////////////////////////////////////////// - /// Triggered upon GLB load finished, with the visual and picking geometry batch roots. - /// Received by frontend and MapViewerRenderingController. - this.batchAddedTopic = new rxjs.Subject(); // {MapViewerBatch} + /// Triggered when a tile layer is freshly rendered and should be added to the frontend. + this.tileLayerAddedTopic = new rxjs.Subject(); // {FeatureTile} - /// Triggered upon onBatchRemoved with the visual and picking geometry batch roots. - /// Received by frontend and MapViewerRenderingController. - this.batchRemovedTopic = new rxjs.Subject(); // {MapViewerBatch} + /// Triggered when a tile layer is being removed. + this.tileLayerRemovedTopic = new rxjs.Subject(); // {FeatureTile} /////////////////////////////////////////////////////////////////////////// // BOOTSTRAP // @@ -69,8 +60,8 @@ export class MapViewerModel this.style.delete() new Fetch(this.coreLib, styleUrl).withWasmCallback(styleYamlBuffer => { this.style = new this.coreLib.FeatureLayerStyle(styleYamlBuffer); - for (let [batchId, batch] of this.registeredBatches.entries()) { - this.renderBatch(batch, true) + for (let [batchId, batch] of this.loadedTileLayers.entries()) { + this.renderTileLayer(batch, true) } console.log("Loaded style.") }).go(); @@ -79,16 +70,20 @@ export class MapViewerModel reloadSources() { new Fetch(this.coreLib, infoUrl) .withWasmCallback(infoBuffer => { - if (this.update.stream) - this.update.stream.delete() - this.update.stream = new this.coreLib.TileLayerParser(infoBuffer); - this.update.stream.onTileParsed(tile => { - this.addBatch(tile) - $("#log").append(`Loaded ${tile.id()} 
`) + if (this.currentTileStream) + this.currentTileStream.delete() + this.currentTileStream = new this.coreLib.TileLayerParser(infoBuffer); + this.currentTileStream.onTileParsed(tileFeatureLayer => { + this.addTileLayer(new FeatureTile(tileFeatureLayer)) }); console.log("Loaded data source info.") }) - .withJsonCallback(result => {this.sources = result;}) + .withJsonCallback(result => { + this.sources = result; + for (let source of this.sources) { + $("#maps").append(`Map ${source.mapId} 
`) + } + }) .go(); } @@ -96,66 +91,85 @@ export class MapViewerModel // MAP UPDATE CONTROLS // /////////////////////////////////////////////////////////////////////////// - runUpdate() + update() { // Get the tile IDs for the current viewport. - this.update.visibleTileIds = this.coreLib.getTileIds(this.update.viewport, 13, 128); - - // TODO - // if (this.update.fetch) - // this.update.fetch.abort() - // if (this.update.stream) - // this.update.stream.clear() - - // TODO: Remove present batches + const requestTileIdList = this.coreLib.getTileIds(this.currentViewport, 13, 512); + this.currentVisibleTileIds = new Set(requestTileIdList); + + // Abort previous fetch operation. + if (this.currentFetch) + this.currentFetch.abort() + + // Make sure that there are no unparsed bytes lingering from the previous response stream. + this.currentTileStream.reset() + + // Evict present non-required tile layers. + let newTileLayers = new Map(); + for (let tileLayer of this.loadedTileLayers.values()) { + if (!this.currentVisibleTileIds.has(tileLayer.tileId)) { + this.tileLayerRemovedTopic.next(tileLayer.id); + tileLayer.dispose() + } + else + newTileLayers.set(tileLayer.id, tileLayer); + } + this.loadedTileLayers = newTileLayers; + // Request non-present required tile layers. let requests = [] for (let dataSource of this.sources) { - for (let [layerName, layer] of Object.entries(dataSource.layers)) { - requests.push({ - mapId: dataSource.mapId, - layerId: layerName, - tileIds: layer.coverage - }) + for (let [layerName, layer] of Object.entries(dataSource.layers)) + { + // Find tile IDs which are not yet loaded for this map layer combination. + let requestTilesForMapLayer = [] + for (let tileId of requestTilesForMapLayer) { + const tileMapLayerKey = this.coreLib.getTileFeatureLayerKey(dataSource.mapId, layerName, tileId); + if (!this.loadedTileLayers.has(tileMapLayerKey)) + requestTilesForMapLayer.push(tileId) + } + + // Only add a request if there are tiles to be loaded. + if (requestTilesForMapLayer) + requests.push({ + mapId: dataSource.mapId, + layerId: layerName, + tileIds: requestTilesForMapLayer + }) } } - new Fetch(this.coreLib, tileUrl) + this.currentFetch = new Fetch(this.coreLib, tileUrl) .withChunkProcessing() .withMethod("POST") - // TODO: Add fields dict offset info to request - .withBody({requests: requests}) - .withWasmCallback(tileBuffer => { - this.update.stream.parse(tileBuffer); + .withBody({ + requests: requests, + maxKnownFieldIds: this.currentTileStream.fieldDictOffsets() }) - .go(); + .withWasmCallback(tileBuffer => { + this.currentTileStream.parse(tileBuffer); + }); + this.currentFetch.go(); } - addBatch(tile) { - let batchName = tile.id(); - let batch = new FeatureLayerTileSet(batchName, tile) - this.registeredBatches.set(batchName, batch) - this.renderBatch(batch); + addTileLayer(tileLayer) { + this.loadedTileLayers.set(tileLayer.id, tileLayer) + this.renderTileLayer(tileLayer); } - renderBatch(batch, removeFirst) { + renderTileLayer(tileLayer, removeFirst) { if (removeFirst) { - this.batchRemovedTopic.next(batch) + this.tileLayerRemovedTopic.next(tileLayer) } - batch.render(this.coreLib, this.glbConverter, this.style, batch => { - this.batchAddedTopic.next(batch) + tileLayer.render(this.coreLib, this.glbConverter, this.style, _ => { + this.tileLayerAddedTopic.next(tileLayer) }) } - removeBatch(batchName) { - this.batchRemovedTopic.next(this.registeredBatches.get(batchName)); - this.registeredBatches.delete(batchName); - } - // public: setViewport(viewport) { - this.update.viewport = viewport; - this.runUpdate(); + this.currentViewport = viewport; + this.update(); } } diff --git a/static/mapcomponent/view.js b/static/mapcomponent/view.js index 742d5253..e39313cb 100644 --- a/static/mapcomponent/view.js +++ b/static/mapcomponent/view.js @@ -118,12 +118,12 @@ export class MapViewerView this.batchForTileSet = new Map(); - model.batchAddedTopic.subscribe(batch => { + model.tileLayerAddedTopic.subscribe(batch => { this.viewer.scene.primitives.add(batch.tileSet); this.batchForTileSet.set(batch.tileSet, batch); }) - model.batchRemovedTopic.subscribe(batch => { + model.tileLayerRemovedTopic.subscribe(batch => { this.viewer.scene.primitives.remove(batch.tileSet); this.batchForTileSet.delete(batch.tileSet); }) @@ -181,7 +181,7 @@ export class MapViewerView } // Get the tile IDs for the current viewport. - let tileIds = this.model.update.visibleTileIds; + let tileIds = this.model.currentVisibleTileIds; // Calculate total number of tile IDs let totalTileIds = tileIds.length; From 329721fe500621e9bc34b390b1f566e791d339f6 Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Mon, 4 Sep 2023 18:51:15 +0200 Subject: [PATCH 03/16] Viewport-driven updates with some debugging TODOs. --- libs/core/include/erdblick/renderer.h | 6 +- libs/core/src/renderer.cpp | 3 + static/index.css | 4 +- static/index.js | 4 -- static/mapcomponent/featuretile.js | 33 ++++++++++- static/mapcomponent/fetch.js | 12 +++- static/mapcomponent/model.js | 26 ++++++--- static/mapcomponent/view.js | 81 ++++++++++++++++++--------- 8 files changed, 120 insertions(+), 49 deletions(-) diff --git a/libs/core/include/erdblick/renderer.h b/libs/core/include/erdblick/renderer.h index 7bbb9adc..9f9f6273 100644 --- a/libs/core/include/erdblick/renderer.h +++ b/libs/core/include/erdblick/renderer.h @@ -13,8 +13,10 @@ class FeatureLayerRenderer FeatureLayerRenderer(); /** - * Convert a TileFeatureLayer to a GLB buffer. - * Returns the cartesian origin of the tile. + * Convert a TileFeatureLayer to a GLB buffer. Returns the + * cartesian origin of the tile. If there are no features to render, + * either because the layer is empty or because no style rule matched, + * then the size of the result buffer will be zero. */ mapget::Point render( const FeatureLayerStyle& style, diff --git a/libs/core/src/renderer.cpp b/libs/core/src/renderer.cpp index 14490bea..6ad47825 100644 --- a/libs/core/src/renderer.cpp +++ b/libs/core/src/renderer.cpp @@ -276,6 +276,9 @@ mapget::Point FeatureLayerRenderer::render( // NOLINT (render can be made stati } globalBufferSize += featureIdBuffer.size() * sizeof(uint32_t); + if (geomForRule.empty()) + return {tileOrigin.x, tileOrigin.y, tileOrigin.z}; + // Convert to GLTF std::vector buffer; buffer.resize(globalBufferSize); diff --git a/static/index.css b/static/index.css index 91d790de..b7b5dcde 100644 --- a/static/index.css +++ b/static/index.css @@ -90,11 +90,11 @@ body { left: 10px; } -#info > #log { +#info > #maps { display: none; } -#info.expanded > #log { +#info.expanded > #maps { display: block; } diff --git a/static/index.js b/static/index.js index 5d799ada..7842fb2d 100644 --- a/static/index.js +++ b/static/index.js @@ -16,10 +16,6 @@ libErdblickCore().then(coreLib => mapModel.reloadStyle(); } - window.zoomToBatch = (batchId) => { - mapView.viewer.zoomTo(mapModel.registeredBatches.get(batchId).tileSet); - } - mapView.selectionTopic.subscribe(selectedFeatureWrapper => { if (!selectedFeatureWrapper) { $("#selectionPanel").hide() diff --git a/static/mapcomponent/featuretile.js b/static/mapcomponent/featuretile.js index a295db3d..05c3c300 100644 --- a/static/mapcomponent/featuretile.js +++ b/static/mapcomponent/featuretile.js @@ -2,11 +2,15 @@ /** * Run a WASM function which places data in a SharedUint8Array, - * and then store this data under an object URL. + * and then store this data under an object URL. Will be aborted + * and return null, if the user function returns false. */ function blobUriFromWasm(coreLib, fun, contentType) { let sharedGlbArray = new coreLib.SharedUint8Array(); - fun(sharedGlbArray); + if (fun(sharedGlbArray) === false) { + sharedGlbArray.delete(); + return null; + } let objSize = sharedGlbArray.getSize(); let bufferPtr = Number(sharedGlbArray.getPointer()); let data = coreLib.HEAPU8.buffer.slice(bufferPtr, bufferPtr + objSize); @@ -41,22 +45,44 @@ export class FeatureTile */ render(coreLib, glbConverter, style, onResult) { + // Start timer + let startOverall = performance.now(); + this.disposeRenderResult(); + let startGLBConversion = performance.now(); let origin = null; this.glbUrl = blobUriFromWasm(coreLib, sharedBuffer => { origin = glbConverter.render(style, this.tileFeatureLayer, sharedBuffer); + if (sharedBuffer.getSize() === 0) + return false; }, "model/gltf-binary"); + let endGLBConversion = performance.now(); + console.log(`GLB conversion time: ${endGLBConversion - startGLBConversion}ms`); + + // The GLB URL will be null if there were no features to render. + if (this.glbUrl === null) + return; + let startTilesetConversion = performance.now(); this.tileSetUrl = blobUriFromWasm(coreLib, sharedBuffer => { glbConverter.makeTileset(this.glbUrl, origin, sharedBuffer); }, "application/json"); + let endTilesetConversion = performance.now(); + console.log(`Tileset conversion time: ${endTilesetConversion - startTilesetConversion}ms`); + let startTilesetFromUrl = performance.now(); Cesium.Cesium3DTileset.fromUrl(this.tileSetUrl, { featureIdLabel: "mapgetFeatureIndex" }).then(tileSet => { this.tileSet = tileSet; onResult(this); + + let endTilesetFromUrl = performance.now(); + console.log(`Cesium tileset from URL time: ${endTilesetFromUrl - startTilesetFromUrl}ms`); + + let endOverall = performance.now(); + console.log(`Overall execution time: ${endOverall - startOverall}ms`); }); } @@ -65,7 +91,8 @@ export class FeatureTile if (!this.tileSet) return; - this.tileSet.destroy(); + if (!this.tileSet.isDestroyed) + this.tileSet.destroy(); this.tileSet = null; URL.revokeObjectURL(this.tileSetUrl); this.tileSetUrl = null; diff --git a/static/mapcomponent/fetch.js b/static/mapcomponent/fetch.js index 0c3dc39d..74933ca5 100644 --- a/static/mapcomponent/fetch.js +++ b/static/mapcomponent/fetch.js @@ -14,7 +14,7 @@ export class Fetch this.url = url; this.method = 'GET'; this.body = null; - this.abortSignal = new AbortSignal(); + this.abortController = new AbortController(); this.processChunks = false; this.jsonCallback = null; this.wasmCallback = null; @@ -81,7 +81,7 @@ export class Fetch // Currently, the connection stays open for five seconds. 'Connection': 'close' }, - signal: this.abortSignal, + signal: this.abortController.signal, keepalive: false, mode: "same-origin" }; @@ -202,7 +202,13 @@ export class Fetch abort() { if (this.aborted) return - this.abortSignal.abort(); + try { + // For some reason, abort always throws an exception by design. + this.abortController.abort("User abort."); + } + catch (e) { + // Nothing to do. + } this.aborted = true; } } diff --git a/static/mapcomponent/model.js b/static/mapcomponent/model.js index 90a8962d..e7df5633 100644 --- a/static/mapcomponent/model.js +++ b/static/mapcomponent/model.js @@ -47,6 +47,9 @@ export class MapViewerModel /// Triggered when a tile layer is being removed. this.tileLayerRemovedTopic = new rxjs.Subject(); // {FeatureTile} + /// Triggered when the user requests to zoom to a map layer + this.zoomToWgs84Position = new rxjs.Subject(); // {.x,.y} + /////////////////////////////////////////////////////////////////////////// // BOOTSTRAP // /////////////////////////////////////////////////////////////////////////// @@ -80,8 +83,16 @@ export class MapViewerModel }) .withJsonCallback(result => { this.sources = result; - for (let source of this.sources) { - $("#maps").append(`Map ${source.mapId} 
`) + $("#maps").empty() + for (let dataSource of this.sources) { + for (let [layerName, layer] of Object.entries(dataSource.layers)) { + let mapsEntry = $(`Map ${dataSource.mapId} 
`); + $(mapsEntry[2]).on("click", _=>{ + // Grab first tile id from coverage and zoom to it. TODO: Zoom to extent of map instead. + this.zoomToWgs84Position.next(this.coreLib.getTilePosition(BigInt(layer.coverage[0]))); + }) + $("#maps").append(mapsEntry) + } } }) .go(); @@ -94,8 +105,8 @@ export class MapViewerModel update() { // Get the tile IDs for the current viewport. - const requestTileIdList = this.coreLib.getTileIds(this.currentViewport, 13, 512); - this.currentVisibleTileIds = new Set(requestTileIdList); + const allViewportTileIds = this.coreLib.getTileIds(this.currentViewport, 13, 512); + this.currentVisibleTileIds = new Set(allViewportTileIds); // Abort previous fetch operation. if (this.currentFetch) @@ -108,7 +119,8 @@ export class MapViewerModel let newTileLayers = new Map(); for (let tileLayer of this.loadedTileLayers.values()) { if (!this.currentVisibleTileIds.has(tileLayer.tileId)) { - this.tileLayerRemovedTopic.next(tileLayer.id); + console.log("Removing tile") + this.tileLayerRemovedTopic.next(tileLayer); tileLayer.dispose() } else @@ -123,10 +135,10 @@ export class MapViewerModel { // Find tile IDs which are not yet loaded for this map layer combination. let requestTilesForMapLayer = [] - for (let tileId of requestTilesForMapLayer) { + for (let tileId of allViewportTileIds) { const tileMapLayerKey = this.coreLib.getTileFeatureLayerKey(dataSource.mapId, layerName, tileId); if (!this.loadedTileLayers.has(tileMapLayerKey)) - requestTilesForMapLayer.push(tileId) + requestTilesForMapLayer.push(Number(tileId)) } // Only add a request if there are tiles to be loaded. diff --git a/static/mapcomponent/view.js b/static/mapcomponent/view.js index e39313cb..f13ccaf0 100644 --- a/static/mapcomponent/view.js +++ b/static/mapcomponent/view.js @@ -29,12 +29,12 @@ export class MapViewerView } ); - // let openStreetMap = new Cesium.UrlTemplateImageryProvider({ - // url: 'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png', - // maximumLevel: 19, - // }); - // let openStreetMapLayer = this.viewer.imageryLayers.addImageryProvider(openStreetMap); - // openStreetMapLayer.alpha = 0.5; + let openStreetMap = new Cesium.UrlTemplateImageryProvider({ + url: 'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png', + maximumLevel: 19, + }); + let openStreetMapLayer = this.viewer.imageryLayers.addImageryProvider(openStreetMap); + openStreetMapLayer.alpha = 0.3; this.pickedFeature = null; this.hoveredFeature = null; @@ -116,18 +116,37 @@ export class MapViewerView this.visualizeTileIds(); }); - this.batchForTileSet = new Map(); + this.tileLayerForTileSet = new Map(); - model.tileLayerAddedTopic.subscribe(batch => { - this.viewer.scene.primitives.add(batch.tileSet); - this.batchForTileSet.set(batch.tileSet, batch); + model.tileLayerAddedTopic.subscribe(tileLayer => { + this.viewer.scene.primitives.add(tileLayer.tileSet); + this.tileLayerForTileSet.set(tileLayer.tileSet, tileLayer); }) - model.tileLayerRemovedTopic.subscribe(batch => { - this.viewer.scene.primitives.remove(batch.tileSet); - this.batchForTileSet.delete(batch.tileSet); + model.tileLayerRemovedTopic.subscribe(tileLayer => { + if (this.pickedFeature && this.pickedFeature.tileset === tileLayer.tileSet) { + this.pickedFeature = null; + this.selectionTopic.next(null); + } + if (this.hoveredFeature && this.hoveredFeature.tileset === tileLayer.tileSet) { + this.hoveredFeature = null; + } + console.log("TileLayer removed from view.") + this.viewer.scene.primitives.remove(tileLayer.tileSet); + this.tileLayerForTileSet.delete(tileLayer.tileSet); }) + model.zoomToWgs84Position.subscribe(pos => { + this.viewer.camera.setView({ + destination: Cesium.Cartesian3.fromDegrees(pos.x, pos.y, 15000), // Converts lon/lat to Cartesian3 + orientation: { + heading: Cesium.Math.toRadians(0), // East, in radians + pitch: Cesium.Math.toRadians(-90), // Directly looking down + roll: 0 // No rotation + } + }); + }); + let polylines = new Cesium.PolylineCollection(); // Line over the equator divided into four 90-degree segments @@ -151,8 +170,8 @@ export class MapViewerView name: 'Antimeridian', polyline: { positions: Cesium.Cartesian3.fromDegreesArray([ - -180, -90, - -180, 90 + -180, -80, + -180, 80 ]), width: 2, material: Cesium.Color.BLUE.withAlpha(0.5) @@ -164,12 +183,12 @@ export class MapViewerView } resolveFeature(tileSet, index) { - let batch = this.batchForTileSet.get(tileSet); - if (!batch) { - console.error("Failed find batch for tileSet!"); + let tileLayer = this.tileLayerForTileSet.get(tileSet); + if (!tileLayer) { + console.error("Failed find tileLayer for tileSet!"); return null; } - return new FeatureWrapper(index, batch); + return new FeatureWrapper(index, tileLayer); } visualizeTileIds() { @@ -184,15 +203,18 @@ export class MapViewerView let tileIds = this.model.currentVisibleTileIds; // Calculate total number of tile IDs - let totalTileIds = tileIds.length; + let totalTileIds = tileIds.size; // Initialize points array this.points = []; - // Iterate through each tile ID - for(let i = 0; i < totalTileIds; i++) { + // Counter for iteration over Set + let i = 0; + + // Iterate through each tile ID using Set's forEach method + tileIds.forEach(tileId => { // Get WGS84 coordinates for the tile ID - let position = this.model.coreLib.getTilePosition(tileIds[i]); + let position = this.model.coreLib.getTilePosition(tileId); // Calculate the color based on the position in the list let colorValue = i / totalTileIds; @@ -200,15 +222,18 @@ export class MapViewerView // Create a point and add it to the Cesium scene let point = this.viewer.entities.add({ - position : Cesium.Cartesian3.fromDegrees(position.x, position.y), - point : { - pixelSize : 5, - color : color + position: Cesium.Cartesian3.fromDegrees(position.x, position.y), + point: { + pixelSize: 5, + color: color } }); // Add the point to the points array this.points.push(point); - } + + // Increment counter + i++; + }); } } From 7b8c704ccfbb0c34c38feeda13bedc6752c6eb6b Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Tue, 12 Sep 2023 15:13:36 +0200 Subject: [PATCH 04/16] Fix and polish streaming update logic. --- libs/core/CMakeLists.txt | 1 + libs/core/src/bindings.cpp | 2 + libs/core/src/stream.cpp | 4 +- static/index.css | 5 +++ static/mapcomponent/featuretile.js | 21 +++++----- static/mapcomponent/fetch.js | 61 +++++++++++++++++++++++------- static/mapcomponent/model.js | 59 ++++++++++++++++++++--------- static/mapcomponent/view.js | 4 +- 8 files changed, 112 insertions(+), 45 deletions(-) diff --git a/libs/core/CMakeLists.txt b/libs/core/CMakeLists.txt index 1f68d6b1..43c7eb20 100644 --- a/libs/core/CMakeLists.txt +++ b/libs/core/CMakeLists.txt @@ -24,6 +24,7 @@ if(${CMAKE_SYSTEM_NAME} STREQUAL "Emscripten") add_executable(erdblick-core ${ERDBLICK_SOURCE_FILES}) set_target_properties(erdblick-core PROPERTIES LINK_FLAGS "\ --bind \ + --profiling \ -s ENVIRONMENT=web \ -s MODULARIZE=1 \ -s EXPORT_ES6=1 \ diff --git a/libs/core/src/bindings.cpp b/libs/core/src/bindings.cpp index 2983a5ad..fb108b47 100644 --- a/libs/core/src/bindings.cpp +++ b/libs/core/src/bindings.cpp @@ -98,6 +98,8 @@ std::string getTileFeatureLayerKey(std::string const& mapId, std::string const& EMSCRIPTEN_BINDINGS(FeatureLayerRendererBind) { + mapget::log().set_level(spdlog::level::debug); + ////////// SharedUint8Array em::class_("SharedUint8Array") .constructor() diff --git a/libs/core/src/stream.cpp b/libs/core/src/stream.cpp index 5ebd84a6..4054299c 100644 --- a/libs/core/src/stream.cpp +++ b/libs/core/src/stream.cpp @@ -27,10 +27,10 @@ void TileLayerParser::onTileParsed(std::functionread(dataSourceInfo.toString()); + reader_->read(bytes.toString()); } catch(std::exception const& e) { std::cout << "ERROR: " << e.what() << std::endl; diff --git a/static/index.css b/static/index.css index b7b5dcde..76481e1f 100644 --- a/static/index.css +++ b/static/index.css @@ -94,6 +94,11 @@ body { display: none; } +#info > #maps > div { + margin-top: 0.5em; + margin-bottom: 0; +} + #info.expanded > #maps { display: block; } diff --git a/static/mapcomponent/featuretile.js b/static/mapcomponent/featuretile.js index 05c3c300..0a353fed 100644 --- a/static/mapcomponent/featuretile.js +++ b/static/mapcomponent/featuretile.js @@ -58,7 +58,7 @@ export class FeatureTile return false; }, "model/gltf-binary"); let endGLBConversion = performance.now(); - console.log(`GLB conversion time: ${endGLBConversion - startGLBConversion}ms`); + console.debug(`[${this.id}] GLB conversion time: ${endGLBConversion - startGLBConversion}ms`); // The GLB URL will be null if there were no features to render. if (this.glbUrl === null) @@ -69,7 +69,7 @@ export class FeatureTile glbConverter.makeTileset(this.glbUrl, origin, sharedBuffer); }, "application/json"); let endTilesetConversion = performance.now(); - console.log(`Tileset conversion time: ${endTilesetConversion - startTilesetConversion}ms`); + console.debug(`[${this.id}] Tileset conversion time: ${endTilesetConversion - startTilesetConversion}ms`); let startTilesetFromUrl = performance.now(); Cesium.Cesium3DTileset.fromUrl(this.tileSetUrl, { @@ -79,10 +79,10 @@ export class FeatureTile onResult(this); let endTilesetFromUrl = performance.now(); - console.log(`Cesium tileset from URL time: ${endTilesetFromUrl - startTilesetFromUrl}ms`); + console.debug(`[${this.id}] Cesium tileset from URL time: ${endTilesetFromUrl - startTilesetFromUrl}ms`); let endOverall = performance.now(); - console.log(`Overall execution time: ${endOverall - startOverall}ms`); + console.debug(`[${this.id}] Overall execution time: ${endOverall - startOverall}ms`); }); } @@ -90,9 +90,9 @@ export class FeatureTile { if (!this.tileSet) return; - if (!this.tileSet.isDestroyed) this.tileSet.destroy(); + this.tileSet = null; URL.revokeObjectURL(this.tileSetUrl); this.tileSetUrl = null; @@ -105,6 +105,7 @@ export class FeatureTile this.disposeRenderResult(); this.tileFeatureLayer.delete(); this.tileFeatureLayer = null; + console.debug(`[${this.id}] Disposed.`); } } @@ -115,9 +116,9 @@ export class FeatureTile */ export class FeatureWrapper { - constructor(index, featureLayerTileSet) { + constructor(index, featureTile) { this.index = index; - this.featureLayerTileSet = featureLayerTileSet; + this.featureTile = featureTile; } /** @@ -125,10 +126,10 @@ export class FeatureWrapper * The feature object will be deleted after the callback is called. */ peek(callback) { - if (!this.featureLayerTileSet.tileFeatureLayer) { - throw new RuntimeError("Unable to access feature of deleted layer."); + if (!this.featureTile.tileFeatureLayer) { + throw new Error(`Unable to access feature of deleted layer ${this.featureTile.id}!`); } - let feature = this.featureLayerTileSet.tileFeatureLayer.at(this.index); + let feature = this.featureTile.tileFeatureLayer.at(this.index); if (callback) { callback(feature); } diff --git a/static/mapcomponent/fetch.js b/static/mapcomponent/fetch.js index 74933ca5..77137b67 100644 --- a/static/mapcomponent/fetch.js +++ b/static/mapcomponent/fetch.js @@ -18,6 +18,7 @@ export class Fetch this.processChunks = false; this.jsonCallback = null; this.wasmCallback = null; + this.wasmBufferDeletedByUser = false; this.aborted = false; } @@ -63,10 +64,15 @@ export class Fetch /** * Method to set the callback for handling the WASM response. * @param {Function} callback - The callback function. + * @param {boolean} deletionByUser - Whether the passed WASM shared + * buffer should not be immediately deleted after the callback is + * called - this is used to in the streaming viewport response + * handling to ensure that the fetch operation is not stalled. * @return {Fetch} The Fetch instance for chaining. */ - withWasmCallback(callback) { + withWasmCallback(callback, deletionByUser) { this.wasmCallback = callback; + this.wasmBufferDeletedByUser = deletionByUser; return this; } @@ -100,7 +106,7 @@ export class Fetch console.assert(!this.processChunks) this.handleJsonResponse(response); } else if (this.processChunks) { - this.handleChunkedResponse(response); + this.handleChunkedResponse(response).then(_ => {}).catch(); } else { this.handleBlobResponse(response); } @@ -122,22 +128,47 @@ export class Fetch /** * Method to handle and process a chunked response. + * The chunks must be encoded as Version-Type-Length-Value (VTLV) frames, + * where Version is 6B, Type is 1B, Length is 4B (Little Endian). + * This is the chunk encoding used by the mapget TileLayerStream. * @param {Response} response - The fetch response. */ - handleChunkedResponse(response) { + async handleChunkedResponse(response) { const reader = response.body.getReader(); - let pump = () => { - return reader.read().then(({ done, value }) => { - if (value) { - let uint8Array = new Uint8Array(value.buffer); - this.runWasmCallback(uint8Array); + let accumulatedData = new Uint8Array(0); + + const processAccumulatedData = () => { + while (accumulatedData.length >= 11) { // Ensure we have at least VTL header + const type = accumulatedData[6]; + const length = new DataView(accumulatedData.buffer, 7, 4).getUint32(0, true); // Little-endian + + // Check if we have the full VTLV frame + if (accumulatedData.length >= 6 + 1 + 4 + length) { + const vtlvFrame = accumulatedData.slice(0, 11 + length); + this.runWasmCallback(vtlvFrame); + + // Remove the processed data from the beginning of accumulatedData + accumulatedData = accumulatedData.slice(11 + length); + } else { + break; } - if (done) - return; - return pump(); - }); + } + } + + while (true) { + const { done, value } = await reader.read(); + if (value && value.length) { + // Append new data to accumulatedData + const temp = new Uint8Array(accumulatedData.length + value.length); + temp.set(accumulatedData); + temp.set(value, accumulatedData.length); + accumulatedData = temp; + + // Try to process any complete VTLV frames + processAccumulatedData(); + } + if (done) break; } - pump(); } /** @@ -193,7 +224,9 @@ export class Fetch this.wasmCallback(sharedArr); } - sharedArr.delete(); + if (!this.wasmBufferDeletedByUser) { + sharedArr.delete(); + } } /** diff --git a/static/mapcomponent/model.js b/static/mapcomponent/model.js index e7df5633..7346b8ba 100644 --- a/static/mapcomponent/model.js +++ b/static/mapcomponent/model.js @@ -4,8 +4,6 @@ import {throttle} from "./utils.js"; import {Fetch} from "./fetch.js"; import {FeatureTile} from "./featuretile.js"; -const minViewportChangedCallDelta = 200; // ms - const styleUrl = "/styles/demo-style.yaml"; const infoUrl = "/sources"; const tileUrl = "/tiles"; @@ -29,10 +27,11 @@ export class MapViewerModel this.coreLib = coreLibrary; this.style = null; - this.sources = null; + this.maps = null; this.glbConverter = new coreLibrary.FeatureLayerRenderer(); this.loadedTileLayers = new Map(); this.currentFetch = null; + this.currentFetchId = 0; this.currentTileStream = null; this.currentViewport = new MapViewerViewport; this.currentVisibleTileIds = new Set(); @@ -77,17 +76,25 @@ export class MapViewerModel this.currentTileStream.delete() this.currentTileStream = new this.coreLib.TileLayerParser(infoBuffer); this.currentTileStream.onTileParsed(tileFeatureLayer => { - this.addTileLayer(new FeatureTile(tileFeatureLayer)) + // Schedule the addition of the parsed tile to the viewport. + const isInViewport = this.currentVisibleTileIds.has(tileFeatureLayer.tileId()); + const alreadyLoaded = this.loadedTileLayers.has(tileFeatureLayer.id()); + if (isInViewport && !alreadyLoaded) { + let tile = new FeatureTile(tileFeatureLayer); + this.addTileLayer(tile); + } + else + tileFeatureLayer.delete(); }); console.log("Loaded data source info.") }) .withJsonCallback(result => { - this.sources = result; + this.maps = Object.fromEntries(result.map(mapInfo => [mapInfo.mapId, mapInfo])); $("#maps").empty() - for (let dataSource of this.sources) { - for (let [layerName, layer] of Object.entries(dataSource.layers)) { - let mapsEntry = $(`Map ${dataSource.mapId} 
`); - $(mapsEntry[2]).on("click", _=>{ + for (let [mapName, map] of Object.entries(this.maps)) { + for (let [layerName, layer] of Object.entries(map.layers)) { + let mapsEntry = $(`
${mapName} / ${layerName} 
`); + $(mapsEntry[0][2]).on("click", _=>{ // Grab first tile id from coverage and zoom to it. TODO: Zoom to extent of map instead. this.zoomToWgs84Position.next(this.coreLib.getTilePosition(BigInt(layer.coverage[0]))); }) @@ -119,7 +126,6 @@ export class MapViewerModel let newTileLayers = new Map(); for (let tileLayer of this.loadedTileLayers.values()) { if (!this.currentVisibleTileIds.has(tileLayer.tileId)) { - console.log("Removing tile") this.tileLayerRemovedTopic.next(tileLayer); tileLayer.dispose() } @@ -130,13 +136,13 @@ export class MapViewerModel // Request non-present required tile layers. let requests = [] - for (let dataSource of this.sources) { - for (let [layerName, layer] of Object.entries(dataSource.layers)) + for (let [mapName, map] of Object.entries(this.maps)) { + for (let [layerName, layer] of Object.entries(map.layers)) { // Find tile IDs which are not yet loaded for this map layer combination. let requestTilesForMapLayer = [] for (let tileId of allViewportTileIds) { - const tileMapLayerKey = this.coreLib.getTileFeatureLayerKey(dataSource.mapId, layerName, tileId); + const tileMapLayerKey = this.coreLib.getTileFeatureLayerKey(mapName, layerName, tileId); if (!this.loadedTileLayers.has(tileMapLayerKey)) requestTilesForMapLayer.push(Number(tileId)) } @@ -144,13 +150,14 @@ export class MapViewerModel // Only add a request if there are tiles to be loaded. if (requestTilesForMapLayer) requests.push({ - mapId: dataSource.mapId, + mapId: mapName, layerId: layerName, tileIds: requestTilesForMapLayer }) } } + let fetchId = ++(this.currentFetchId); this.currentFetch = new Fetch(this.coreLib, tileUrl) .withChunkProcessing() .withMethod("POST") @@ -159,12 +166,23 @@ export class MapViewerModel maxKnownFieldIds: this.currentTileStream.fieldDictOffsets() }) .withWasmCallback(tileBuffer => { - this.currentTileStream.parse(tileBuffer); - }); + // Schedule the parsing of the newly arrived tile layer, + // but don't do it synchronously to avoid stalling the ongoing + // fetch operation. + setTimeout(_ => { + // Only process the buffer chunk, if the fetch operation + // for the chunk is the most recent one. + if (fetchId === this.currentFetchId) { + this.currentTileStream.parse(tileBuffer); + } + tileBuffer.delete(); + }, 0) + }, true); this.currentFetch.go(); } addTileLayer(tileLayer) { + console.assert(!this.loadedTileLayers.has(tileLayer.id)) this.loadedTileLayers.set(tileLayer.id, tileLayer) this.renderTileLayer(tileLayer); } @@ -174,7 +192,14 @@ export class MapViewerModel this.tileLayerRemovedTopic.next(tileLayer) } tileLayer.render(this.coreLib, this.glbConverter, this.style, _ => { - this.tileLayerAddedTopic.next(tileLayer) + // It is possible, that the tile went out of view while + // Cesium took its time to load it. In this case, don't + // add it to the viewport. + const isInViewport = this.currentVisibleTileIds.has(tileLayer.tileId); + if (isInViewport) + this.tileLayerAddedTopic.next(tileLayer) + else + tileLayer.disposeRenderResult() }) } diff --git a/static/mapcomponent/view.js b/static/mapcomponent/view.js index f13ccaf0..74ac38f1 100644 --- a/static/mapcomponent/view.js +++ b/static/mapcomponent/view.js @@ -1,5 +1,5 @@ import {MapViewerModel, MapViewerViewport} from "./model.js"; -import {FeatureWrapper} from "./featurelayer.js"; +import {FeatureWrapper} from "./featuretile.js"; export class MapViewerView { @@ -80,6 +80,7 @@ export class MapViewerView }, Cesium.ScreenSpaceEventType.MOUSE_MOVE); // Add a handler for camera movement + this.viewer.camera.percentageChanged = 0.1; this.viewer.camera.changed.addEventListener(() => { let canvas = this.viewer.scene.canvas; let center = new Cesium.Cartesian2(canvas.clientWidth / 2, canvas.clientHeight / 2); @@ -131,7 +132,6 @@ export class MapViewerView if (this.hoveredFeature && this.hoveredFeature.tileset === tileLayer.tileSet) { this.hoveredFeature = null; } - console.log("TileLayer removed from view.") this.viewer.scene.primitives.remove(tileLayer.tileSet); this.tileLayerForTileSet.delete(tileLayer.tileSet); }) From eddba08048afc4aeb673813de14893efb425ea78 Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Wed, 13 Sep 2023 10:11:22 +0200 Subject: [PATCH 05/16] Keep TileFeatureLayers in serialized JS blobs rather than WASM objects. --- libs/core/include/erdblick/stream.h | 41 +++++++- libs/core/src/bindings.cpp | 13 ++- libs/core/src/stream.cpp | 38 +++++-- static/index.html | 2 +- static/mapcomponent/featuretile.js | 149 +++++++++++++++++++--------- static/mapcomponent/model.js | 55 +++++----- 6 files changed, 212 insertions(+), 86 deletions(-) diff --git a/libs/core/include/erdblick/stream.h b/libs/core/include/erdblick/stream.h index 49679481..a41a9974 100644 --- a/libs/core/include/erdblick/stream.h +++ b/libs/core/include/erdblick/stream.h @@ -9,12 +9,47 @@ namespace erdblick class TileLayerParser { public: - explicit TileLayerParser(SharedUint8Array const& dataSourceInfo); - void onTileParsed(std::function); - void parse(SharedUint8Array const& dataSourceInfo); + explicit TileLayerParser(); + + /** + * Update the data source info metadata which the parser uses + * to supply parsed TileFeatureLayers with map metadata info. + */ + void setDataSourceInfo(SharedUint8Array const& dataSourceInfoJson); + + /** + * Serialize a TileFeatureLayer to a buffer. + */ + void writeTileFeatureLayer(mapget::TileFeatureLayer::Ptr const& tile, SharedUint8Array& buffer); + + /** + * Parse a TileFeatureLayer from a buffer as returned by writeTileFeatureLayer. + */ + mapget::TileFeatureLayer::Ptr readTileFeatureLayer(SharedUint8Array const& buffer); + + /** + * Reset the parser by removing any buffered unparsed stream chunks. + */ void reset(); + + /** + * Access the field id dictionary offsets as currently known by this parser. + * This is used to tell the server whether additional field-id mapping updates + * need to be sent. + */ mapget::TileLayerStream::FieldOffsetMap fieldDictOffsets(); + /** + * Stream-based parsing functionality: Set callback which is called + * as soon as a tile has been parsed. + */ + void onTileParsedFromStream(std::function); + + /** + * Add a chunk of streamed data into this TileLayerParser. + */ + void parseFromStream(SharedUint8Array const& buffer); + private: std::map info_; std::unique_ptr reader_; diff --git a/libs/core/src/bindings.cpp b/libs/core/src/bindings.cpp index fb108b47..fdfc98fc 100644 --- a/libs/core/src/bindings.cpp +++ b/libs/core/src/bindings.cpp @@ -177,13 +177,14 @@ EMSCRIPTEN_BINDINGS(FeatureLayerRendererBind) ////////// TileLayerParser em::class_("TileLayerParser") - .constructor() + .constructor<>() + .function("setDataSourceInfo", &TileLayerParser::setDataSourceInfo) .function( - "onTileParsed", + "onTileParsedFromStream", std::function( [](TileLayerParser& self, em::val cb) - { self.onTileParsed([cb](auto&& tile) { cb(tile); }); })) - .function("parse", &TileLayerParser::parse) + { self.onTileParsedFromStream([cb](auto&& tile) { cb(tile); }); })) + .function("parseFromStream", &TileLayerParser::parseFromStream) .function("reset", &TileLayerParser::reset) .function( "fieldDictOffsets", @@ -194,7 +195,9 @@ EMSCRIPTEN_BINDINGS(FeatureLayerRendererBind) for (auto const& [nodeId, fieldId] : self.fieldDictOffsets()) result.set(nodeId, fieldId); return result; - })); + })) + .function("readTileFeatureLayer", &TileLayerParser::readTileFeatureLayer) + .function("writeTileFeatureLayer", &TileLayerParser::writeTileFeatureLayer); ////////// Viewport TileID calculation em::function("getTileIds", &getTileIds); diff --git a/libs/core/src/stream.cpp b/libs/core/src/stream.cpp index 4054299c..3fc05713 100644 --- a/libs/core/src/stream.cpp +++ b/libs/core/src/stream.cpp @@ -6,28 +6,31 @@ using namespace mapget; namespace erdblick { -TileLayerParser::TileLayerParser(SharedUint8Array const& dataSourceInfo) +TileLayerParser::TileLayerParser() { // Create field dict cache cachedFieldDicts_ = std::make_shared(); + // Create fresh mapget stream parser. + reset(); +} + +void TileLayerParser::setDataSourceInfo(const erdblick::SharedUint8Array& dataSourceInfoJson) +{ // Parse data source info - auto srcInfoParsed = nlohmann::json::parse(dataSourceInfo.toString()); + auto srcInfoParsed = nlohmann::json::parse(dataSourceInfoJson.toString()); for (auto const& node : srcInfoParsed) { auto dsInfo = DataSourceInfo::fromJson(node); info_.emplace(dsInfo.mapId_, std::move(dsInfo)); } - - // Create fresh parser - reset(); } -void TileLayerParser::onTileParsed(std::function fun) +void TileLayerParser::onTileParsedFromStream(std::function fun) { tileParsedFun_ = std::move(fun); } -void TileLayerParser::parse(SharedUint8Array const& bytes) +void TileLayerParser::parseFromStream(SharedUint8Array const& bytes) { try { reader_->read(bytes.toString()); @@ -55,4 +58,25 @@ void TileLayerParser::reset() cachedFieldDicts_); } +void TileLayerParser::writeTileFeatureLayer( // NOLINT (Could be made static, but not due to Embind) + mapget::TileFeatureLayer::Ptr const& tile, + SharedUint8Array& buffer) +{ + std::stringstream serializedTile; + tile->write(serializedTile); + buffer.writeToArray(serializedTile.str()); +} + +mapget::TileFeatureLayer::Ptr TileLayerParser::readTileFeatureLayer(const SharedUint8Array& buffer) +{ + std::stringstream inputStream; + inputStream << buffer.toString(); + auto result = std::make_shared( + inputStream, + [this](auto&& mapId, auto&& layerId) + { return info_[std::string(mapId)].getLayer(std::string(layerId)); }, + [this](auto&& nodeId) { return cachedFieldDicts_->operator()(nodeId); }); + return result; +} + } diff --git a/static/index.html b/static/index.html index 402c6077..99b24ae8 100644 --- a/static/index.html +++ b/static/index.html @@ -15,7 +15,7 @@ - + diff --git a/static/mapcomponent/featuretile.js b/static/mapcomponent/featuretile.js index 0a353fed..74d4ee63 100644 --- a/static/mapcomponent/featuretile.js +++ b/static/mapcomponent/featuretile.js @@ -1,91 +1,132 @@ "use strict"; -/** - * Run a WASM function which places data in a SharedUint8Array, - * and then store this data under an object URL. Will be aborted - * and return null, if the user function returns false. - */ -function blobUriFromWasm(coreLib, fun, contentType) { - let sharedGlbArray = new coreLib.SharedUint8Array(); - if (fun(sharedGlbArray) === false) { - sharedGlbArray.delete(); - return null; - } - let objSize = sharedGlbArray.getSize(); - let bufferPtr = Number(sharedGlbArray.getPointer()); - let data = coreLib.HEAPU8.buffer.slice(bufferPtr, bufferPtr + objSize); - const blob = new Blob([data], { type: contentType }); - const glbUrl = URL.createObjectURL(blob); - sharedGlbArray.delete(); - return glbUrl; -} +import {blobUriFromWasm, uint8ArrayFromWasm, uint8ArrayToWasm} from "./blob.js"; /** * Bundle of a WASM TileFeatureLayer and a rendered representation * in the form of a Cesium 3D TileSet which references a binary GLTF tile. * The tileset JSON and the GLTF blob are stored as browser Blob objects * (see https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL_static). + * + * The WASM TileFatureLayer 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: - constructor(tileFeatureLayer) + /** + * Construct a FeatureTile object. + * @param coreLib Reference to the WASM erdblick library. + * @param parser Singleton TileLayerStream WASM object. + * @param tileFeatureLayer Deserialized WASM TileFeatureLayer. + */ + constructor(coreLib, parser, tileFeatureLayer) { + this.coreLib = coreLib; + this.parser = parser; this.id = tileFeatureLayer.id(); this.tileId = tileFeatureLayer.tileId(); this.children = undefined; - this.tileFeatureLayer = tileFeatureLayer; + this.tileFeatureLayerInitDeserialized = tileFeatureLayer; + this.tileFeatureLayerSerialized = null; this.glbUrl = null; this.tileSetUrl = null; this.tileSet = null; + this.disposed = false; } /** - * Convert this batch's tile to GLTF and broadcast the result + * 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, + * if no output was generated because the tile is empty. + * @param {*} glbConverter The WASM GLTF converter that should be used. + * @param {null} style The style that is used to make the conversion. */ - render(coreLib, glbConverter, style, onResult) + async render(glbConverter, style) { // Start timer let startOverall = performance.now(); + // Remove any previous render-result, as a new one is generated + // TODO: Ensure that the View also takes note of the removed Cesium3DTile. + // This will become apparent once interactive re-styling is a prime use-case. this.disposeRenderResult(); let startGLBConversion = performance.now(); let origin = null; - this.glbUrl = blobUriFromWasm(coreLib, sharedBuffer => { - origin = glbConverter.render(style, this.tileFeatureLayer, sharedBuffer); - if (sharedBuffer.getSize() === 0) - return false; - }, "model/gltf-binary"); + this.peek(tileFeatureLayer => { + this.glbUrl = blobUriFromWasm(this.coreLib, sharedBuffer => { + origin = glbConverter.render(style, tileFeatureLayer, sharedBuffer); + if (sharedBuffer.getSize() === 0) + return false; + }, "model/gltf-binary"); + }); let endGLBConversion = performance.now(); console.debug(`[${this.id}] GLB conversion time: ${endGLBConversion - startGLBConversion}ms`); // The GLB URL will be null if there were no features to render. if (this.glbUrl === null) - return; + return false; let startTilesetConversion = performance.now(); - this.tileSetUrl = blobUriFromWasm(coreLib, sharedBuffer => { + this.tileSetUrl = blobUriFromWasm(this.coreLib, sharedBuffer => { glbConverter.makeTileset(this.glbUrl, origin, sharedBuffer); }, "application/json"); let endTilesetConversion = performance.now(); console.debug(`[${this.id}] Tileset conversion time: ${endTilesetConversion - startTilesetConversion}ms`); let startTilesetFromUrl = performance.now(); - Cesium.Cesium3DTileset.fromUrl(this.tileSetUrl, { + this.tileSet = await Cesium.Cesium3DTileset.fromUrl(this.tileSetUrl, { featureIdLabel: "mapgetFeatureIndex" - }).then(tileSet => { - this.tileSet = tileSet; - onResult(this); + }) - let endTilesetFromUrl = performance.now(); - console.debug(`[${this.id}] Cesium tileset from URL time: ${endTilesetFromUrl - startTilesetFromUrl}ms`); + let endTilesetFromUrl = performance.now(); + console.debug(`[${this.id}] Cesium tileset from URL time: ${endTilesetFromUrl - startTilesetFromUrl}ms`); - let endOverall = performance.now(); - console.debug(`[${this.id}] Overall execution time: ${endOverall - startOverall}ms`); - }); + let endOverall = performance.now(); + console.debug(`[${this.id}] Overall execution time: ${endOverall - startOverall}ms`); + + return true; } + /** + * Deserialize the wrapped TileFeatureLayer, run a callback, then + * delete the deserialized WASM representation. + * @returns The value returned by the callback. + */ + peek(callback) { + // For the first call to peek, the tileFeatureLayer member + // is still set, and the tileFeatureLayerSerialized is not yet set. + let deserializedLayer = this.tileFeatureLayerInitDeserialized; + if (this.tileFeatureLayerInitDeserialized) { + this.tileFeatureLayerInitDeserialized = null; + this.tileFeatureLayerSerialized = uint8ArrayFromWasm(this.coreLib, bufferToWrite => { + this.parser.writeTileFeatureLayer(deserializedLayer, bufferToWrite); + }); + } + + if (!deserializedLayer) { + // Deserialize the WASM tileFeatureLayer from the blob. + console.assert(this.tileFeatureLayerSerialized); + uint8ArrayToWasm(this.coreLib, bufferToRead => { + deserializedLayer = this.parser.readTileFeatureLayer(bufferToRead); + }, this.tileFeatureLayerSerialized); + } + + // Run the callback with the deserialized layer, and + // store the result as the return value. + let result = callback(deserializedLayer); + + // Clean up. + deserializedLayer.delete(); + return result; + } + + /** + * Remove all data associated with a previous call to this.render(). + */ disposeRenderResult() { if (!this.tileSet) @@ -100,11 +141,17 @@ export class FeatureTile this.glbUrl = null; } + /** + * Clean up all data associated with this FeatureTile instance. + */ dispose() { this.disposeRenderResult(); - this.tileFeatureLayer.delete(); - this.tileFeatureLayer = null; + if (this.tileFeatureLayerInitDeserialized) { + this.tileFeatureLayerInitDeserialized.delete(); + this.tileFeatureLayerInitDeserialized = null; + } + this.disposed = true; console.debug(`[${this.id}] Disposed.`); } } @@ -116,6 +163,12 @@ export class FeatureTile */ export class FeatureWrapper { + /** + * Construct a feature wrapper from a featureTile and a feature index + * within that tile. + * @param index The index of the feature within the tile. + * @param featureTile {FeatureTile} The feature tile container. + */ constructor(index, featureTile) { this.index = index; this.featureTile = featureTile; @@ -126,13 +179,15 @@ export class FeatureWrapper * The feature object will be deleted after the callback is called. */ peek(callback) { - if (!this.featureTile.tileFeatureLayer) { + if (this.featureTile.disposed) { throw new Error(`Unable to access feature of deleted layer ${this.featureTile.id}!`); } - let feature = this.featureTile.tileFeatureLayer.at(this.index); - if (callback) { - callback(feature); - } - feature.delete(); + this.featureTile.peek(tileFeatureLayer => { + let feature = tileFeatureLayer.at(this.index); + if (callback) { + callback(feature); + } + feature.delete(); + }); } } diff --git a/static/mapcomponent/model.js b/static/mapcomponent/model.js index 7346b8ba..1d9b36e3 100644 --- a/static/mapcomponent/model.js +++ b/static/mapcomponent/model.js @@ -25,17 +25,29 @@ export class MapViewerModel constructor(coreLibrary) { this.coreLib = coreLibrary; - this.style = null; this.maps = null; this.glbConverter = new coreLibrary.FeatureLayerRenderer(); this.loadedTileLayers = new Map(); this.currentFetch = null; this.currentFetchId = 0; - this.currentTileStream = null; this.currentViewport = new MapViewerViewport; this.currentVisibleTileIds = new Set(); + // Instantiate the TileLayerParser, and set its callback + // for when a new tile is received. + this.tileParser = new this.coreLib.TileLayerParser(); + 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); + } + else + tileFeatureLayer.delete(); + }); + /////////////////////////////////////////////////////////////////////////// // MODEL EVENTS // /////////////////////////////////////////////////////////////////////////// @@ -54,14 +66,21 @@ export class MapViewerModel /////////////////////////////////////////////////////////////////////////// this.reloadStyle() - this.reloadSources() + this.reloadDataSources() } - reloadStyle() { + reloadStyle() + { + // Delete the old style if present. if (this.style) this.style.delete() + + // Fetch the new one. new Fetch(this.coreLib, styleUrl).withWasmCallback(styleYamlBuffer => { + // Parse the style description into a WASM style object. this.style = new this.coreLib.FeatureLayerStyle(styleYamlBuffer); + + // Re-render all present batches with the new style. for (let [batchId, batch] of this.loadedTileLayers.entries()) { this.renderTileLayer(batch, true) } @@ -69,23 +88,10 @@ export class MapViewerModel }).go(); } - reloadSources() { + reloadDataSources() { new Fetch(this.coreLib, infoUrl) .withWasmCallback(infoBuffer => { - if (this.currentTileStream) - this.currentTileStream.delete() - this.currentTileStream = new this.coreLib.TileLayerParser(infoBuffer); - this.currentTileStream.onTileParsed(tileFeatureLayer => { - // Schedule the addition of the parsed tile to the viewport. - const isInViewport = this.currentVisibleTileIds.has(tileFeatureLayer.tileId()); - const alreadyLoaded = this.loadedTileLayers.has(tileFeatureLayer.id()); - if (isInViewport && !alreadyLoaded) { - let tile = new FeatureTile(tileFeatureLayer); - this.addTileLayer(tile); - } - else - tileFeatureLayer.delete(); - }); + this.tileParser.setDataSourceInfo(infoBuffer); console.log("Loaded data source info.") }) .withJsonCallback(result => { @@ -120,7 +126,7 @@ export class MapViewerModel this.currentFetch.abort() // Make sure that there are no unparsed bytes lingering from the previous response stream. - this.currentTileStream.reset() + this.tileParser.reset() // Evict present non-required tile layers. let newTileLayers = new Map(); @@ -163,7 +169,7 @@ export class MapViewerModel .withMethod("POST") .withBody({ requests: requests, - maxKnownFieldIds: this.currentTileStream.fieldDictOffsets() + maxKnownFieldIds: this.tileParser.fieldDictOffsets() }) .withWasmCallback(tileBuffer => { // Schedule the parsing of the newly arrived tile layer, @@ -173,7 +179,7 @@ export class MapViewerModel // Only process the buffer chunk, if the fetch operation // for the chunk is the most recent one. if (fetchId === this.currentFetchId) { - this.currentTileStream.parse(tileBuffer); + this.tileParser.parseFromStream(tileBuffer); } tileBuffer.delete(); }, 0) @@ -191,7 +197,10 @@ export class MapViewerModel if (removeFirst) { this.tileLayerRemovedTopic.next(tileLayer) } - tileLayer.render(this.coreLib, this.glbConverter, this.style, _ => { + tileLayer.render(this.glbConverter, this.style).then(wasRendered => { + if (!wasRendered) + return; + // It is possible, that the tile went out of view while // Cesium took its time to load it. In this case, don't // add it to the viewport. From c8fe955d947adf70ca87e2aaedec32cc581df316 Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Wed, 13 Sep 2023 10:24:44 +0200 Subject: [PATCH 06/16] Fix and clean up map 'Focus' button. --- static/index.js | 17 +++++++++++++++++ static/mapcomponent/model.js | 17 +++++------------ static/mapcomponent/view.js | 2 +- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/static/index.js b/static/index.js index 7842fb2d..2562978a 100644 --- a/static/index.js +++ b/static/index.js @@ -28,6 +28,23 @@ libErdblickCore().then(coreLib => $("#selectionPanel").show() }) }) + + mapModel.mapInfoTopic.subscribe(mapInfo => { + let mapSettingsBox = $("#maps"); + mapSettingsBox.empty() + for (let [mapName, map] of Object.entries(this.maps)) { + for (let [layerName, layer] of Object.entries(map.layers)) { + let mapsEntry = $(`
${mapName} / ${layerName} 
`); + $(mapsEntry.find("button")).on("click", _=>{ + // Grab first tile id from coverage and zoom to it. + // TODO: Zoom to extent of map instead. + if (layer.coverage[0] !== undefined) + this.zoomToWgs84PositionTopic.next(this.coreLib.getTilePosition(BigInt(layer.coverage[0]))); + }) + mapSettingsBox.append(mapsEntry) + } + } + }) }) $(document).ready(function() { diff --git a/static/mapcomponent/model.js b/static/mapcomponent/model.js index 1d9b36e3..8eaab48f 100644 --- a/static/mapcomponent/model.js +++ b/static/mapcomponent/model.js @@ -59,7 +59,10 @@ export class MapViewerModel this.tileLayerRemovedTopic = new rxjs.Subject(); // {FeatureTile} /// Triggered when the user requests to zoom to a map layer - this.zoomToWgs84Position = new rxjs.Subject(); // {.x,.y} + this.zoomToWgs84PositionTopic = new rxjs.Subject(); // {.x,.y} + + /// Triggered when the map info is updated + this.mapInfoTopic = new rxjs.Subject(); // {: } /////////////////////////////////////////////////////////////////////////// // BOOTSTRAP // @@ -96,17 +99,7 @@ export class MapViewerModel }) .withJsonCallback(result => { this.maps = Object.fromEntries(result.map(mapInfo => [mapInfo.mapId, mapInfo])); - $("#maps").empty() - for (let [mapName, map] of Object.entries(this.maps)) { - for (let [layerName, layer] of Object.entries(map.layers)) { - let mapsEntry = $(`
${mapName} / ${layerName} 
`); - $(mapsEntry[0][2]).on("click", _=>{ - // Grab first tile id from coverage and zoom to it. TODO: Zoom to extent of map instead. - this.zoomToWgs84Position.next(this.coreLib.getTilePosition(BigInt(layer.coverage[0]))); - }) - $("#maps").append(mapsEntry) - } - } + this.mapInfoTopic.next(this.maps) }) .go(); } diff --git a/static/mapcomponent/view.js b/static/mapcomponent/view.js index 74ac38f1..5c78f431 100644 --- a/static/mapcomponent/view.js +++ b/static/mapcomponent/view.js @@ -136,7 +136,7 @@ export class MapViewerView this.tileLayerForTileSet.delete(tileLayer.tileSet); }) - model.zoomToWgs84Position.subscribe(pos => { + model.zoomToWgs84PositionTopic.subscribe(pos => { this.viewer.camera.setView({ destination: Cesium.Cartesian3.fromDegrees(pos.x, pos.y, 15000), // Converts lon/lat to Cartesian3 orientation: { From b8c234e988c4f26a901ea761aec686d7186ae797 Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Wed, 13 Sep 2023 12:28:37 +0200 Subject: [PATCH 07/16] Cleanup and fix viewport calculation in antimeridian case. --- libs/core/src/bindings.cpp | 2 +- .../featuretile.js => erdblick/features.js} | 14 ++- static/{mapcomponent => erdblick}/fetch.js | 2 + static/{mapcomponent => erdblick}/model.js | 20 ++++- static/{mapcomponent => erdblick}/timer.js | 2 + static/{mapcomponent => erdblick}/view.js | 87 +++++++++++-------- static/index.css | 2 +- static/index.html | 11 ++- static/index.js | 12 +-- static/mapcomponent/utils.js | 40 --------- 10 files changed, 94 insertions(+), 98 deletions(-) rename static/{mapcomponent/featuretile.js => erdblick/features.js} (95%) rename static/{mapcomponent => erdblick}/fetch.js (96%) rename static/{mapcomponent => erdblick}/model.js (93%) rename static/{mapcomponent => erdblick}/timer.js (97%) rename static/{mapcomponent => erdblick}/view.js (78%) delete mode 100644 static/mapcomponent/utils.js diff --git a/libs/core/src/bindings.cpp b/libs/core/src/bindings.cpp index fdfc98fc..a55d2208 100644 --- a/libs/core/src/bindings.cpp +++ b/libs/core/src/bindings.cpp @@ -86,7 +86,7 @@ mapget::Point getTilePosition(uint64_t tileIdValue) { return tid.center(); } -/** Get the full key of a map tile feature layer. */ +/** Get the full string key of a map tile feature layer. */ std::string getTileFeatureLayerKey(std::string const& mapId, std::string const& layerId, uint64_t tileId) { auto tileKey = mapget::MapTileKey(); tileKey.layer_ = mapget::LayerType::Features; diff --git a/static/mapcomponent/featuretile.js b/static/erdblick/features.js similarity index 95% rename from static/mapcomponent/featuretile.js rename to static/erdblick/features.js index 74d4ee63..9c3a905e 100644 --- a/static/mapcomponent/featuretile.js +++ b/static/erdblick/features.js @@ -1,6 +1,6 @@ "use strict"; -import {blobUriFromWasm, uint8ArrayFromWasm, uint8ArrayToWasm} from "./blob.js"; +import {blobUriFromWasm, uint8ArrayFromWasm, uint8ArrayToWasm} from "./wasm.js"; /** * Bundle of a WASM TileFeatureLayer and a rendered representation @@ -117,7 +117,10 @@ export class FeatureTile // Run the callback with the deserialized layer, and // store the result as the return value. - let result = callback(deserializedLayer); + let result = null; + if (callback) { + result = callback(deserializedLayer); + } // Clean up. deserializedLayer.delete(); @@ -177,17 +180,20 @@ export class FeatureWrapper /** * Run a callback with the WASM Feature object referenced by this wrapper. * The feature object will be deleted after the callback is called. + * @returns The value returned by the callback. */ peek(callback) { if (this.featureTile.disposed) { throw new Error(`Unable to access feature of deleted layer ${this.featureTile.id}!`); } - this.featureTile.peek(tileFeatureLayer => { + return this.featureTile.peek(tileFeatureLayer => { let feature = tileFeatureLayer.at(this.index); + let result = null; if (callback) { - callback(feature); + result = callback(feature); } feature.delete(); + return result; }); } } diff --git a/static/mapcomponent/fetch.js b/static/erdblick/fetch.js similarity index 96% rename from static/mapcomponent/fetch.js rename to static/erdblick/fetch.js index 77137b67..c2ab50bb 100644 --- a/static/mapcomponent/fetch.js +++ b/static/erdblick/fetch.js @@ -1,3 +1,5 @@ +"use strict"; + /** * A class to fetch data from a URL and process the response * for usage in JavaScript and WebAssembly. diff --git a/static/mapcomponent/model.js b/static/erdblick/model.js similarity index 93% rename from static/mapcomponent/model.js rename to static/erdblick/model.js index 8eaab48f..c68f9a27 100644 --- a/static/mapcomponent/model.js +++ b/static/erdblick/model.js @@ -1,13 +1,16 @@ "use strict"; -import {throttle} from "./utils.js"; import {Fetch} from "./fetch.js"; -import {FeatureTile} from "./featuretile.js"; +import {FeatureTile} from "./features.js"; const styleUrl = "/styles/demo-style.yaml"; const infoUrl = "/sources"; const tileUrl = "/tiles"; +/** + * Viewport object which can be interpreted by the erdblick-core WASM + * `getTileIds` function. + */ export class MapViewerViewport { constructor(south, west, width, height, camPosLon, camPosLat, orientation) { this.south = south; @@ -20,7 +23,18 @@ export class MapViewerViewport { } } -export class MapViewerModel +/** + * Erdblick view-model class. This class is responsible for keeping track + * of the following objects: + * (1) available maps + * (2) currently loaded tiles + * (3) available style sheets. + * + * As the viewport changes, it requests new tiles from the mapget server + * and triggers their conversion to Cesium tiles according to the active + * style sheets. + */ +export class ErdblickModel { constructor(coreLibrary) { diff --git a/static/mapcomponent/timer.js b/static/erdblick/timer.js similarity index 97% rename from static/mapcomponent/timer.js rename to static/erdblick/timer.js index d534a099..8812b562 100644 --- a/static/mapcomponent/timer.js +++ b/static/erdblick/timer.js @@ -1,3 +1,5 @@ +"use strict"; + export class SingleShotTimer { constructor(interval, callback, waitForRestart) { diff --git a/static/mapcomponent/view.js b/static/erdblick/view.js similarity index 78% rename from static/mapcomponent/view.js rename to static/erdblick/view.js index 5c78f431..94f436e9 100644 --- a/static/mapcomponent/view.js +++ b/static/erdblick/view.js @@ -1,11 +1,13 @@ -import {MapViewerModel, MapViewerViewport} from "./model.js"; -import {FeatureWrapper} from "./featuretile.js"; +"use strict"; -export class MapViewerView +import {ErdblickModel, MapViewerViewport} from "./model.js"; +import {FeatureWrapper} from "./features.js"; + +export class ErdblickView { /** * Construct a Cesium View with a Model. - * @param {MapViewerModel} model + * @param {ErdblickModel} model * @param containerDomElementId Div which hosts the Cesium view. */ constructor(model, containerDomElementId) @@ -82,39 +84,7 @@ export class MapViewerView // Add a handler for camera movement this.viewer.camera.percentageChanged = 0.1; this.viewer.camera.changed.addEventListener(() => { - let canvas = this.viewer.scene.canvas; - let center = new Cesium.Cartesian2(canvas.clientWidth / 2, canvas.clientHeight / 2); - let centerCartesian = this.viewer.camera.pickEllipsoid(center); - let centerLon, centerLat; - - // If the center of the screen is not on the earth's surface (i.e., the horizon is below the viewport center), - // fallback to using the camera's position. - if (Cesium.defined(centerCartesian)) { - let centerCartographic = Cesium.Cartographic.fromCartesian(centerCartesian); - centerLon = Cesium.Math.toDegrees(centerCartographic.longitude); - centerLat = Cesium.Math.toDegrees(centerCartographic.latitude); - } else { - let cameraCartographic = Cesium.Cartographic.fromCartesian(this.viewer.camera.positionWC); - centerLon = Cesium.Math.toDegrees(cameraCartographic.longitude); - centerLat = Cesium.Math.toDegrees(cameraCartographic.latitude); - } - - let rectangle = this.viewer.camera.computeViewRectangle(); - - // Extract the WGS84 coordinates for the rectangle's corners - let west = Cesium.Math.toDegrees(rectangle.west); - let south = Cesium.Math.toDegrees(rectangle.south); - let east = Cesium.Math.toDegrees(rectangle.east); - let north = Cesium.Math.toDegrees(rectangle.north); - let sizeLon = east - west; - let sizeLat = north - south; - - // Create the viewport object - let viewport = new MapViewerViewport(south, west, sizeLon, sizeLat, centerLon, centerLat, this.viewer.camera.heading); - - // Pass the viewport object to the model - model.setViewport(viewport); - this.visualizeTileIds(); + this.updateViewport(); }); this.tileLayerForTileSet = new Map(); @@ -191,6 +161,49 @@ export class MapViewerView return new FeatureWrapper(index, tileLayer); } + updateViewport() { + let canvas = this.viewer.scene.canvas; + let center = new Cesium.Cartesian2(canvas.clientWidth / 2, canvas.clientHeight / 2); + let centerCartesian = this.viewer.camera.pickEllipsoid(center); + let centerLon, centerLat; + + if (Cesium.defined(centerCartesian)) { + let centerCartographic = Cesium.Cartographic.fromCartesian(centerCartesian); + centerLon = Cesium.Math.toDegrees(centerCartographic.longitude); + centerLat = Cesium.Math.toDegrees(centerCartographic.latitude); + } else { + let cameraCartographic = Cesium.Cartographic.fromCartesian(this.viewer.camera.positionWC); + centerLon = Cesium.Math.toDegrees(cameraCartographic.longitude); + centerLat = Cesium.Math.toDegrees(cameraCartographic.latitude); + } + + let rectangle = this.viewer.camera.computeViewRectangle(); + + let west = Cesium.Math.toDegrees(rectangle.west); + let south = Cesium.Math.toDegrees(rectangle.south); + let east = Cesium.Math.toDegrees(rectangle.east); + let north = Cesium.Math.toDegrees(rectangle.north); + let sizeLon = east - west; + let sizeLat = north - south; + + // Handle the antimeridian. + // TODO: Must also handle north pole. + if (west > -180 && sizeLon > 180.) { + sizeLon = 360. - sizeLon; + } + + // Grow the viewport rectangle by 25% + let expandLon = sizeLon * 0.25; + let expandLat = sizeLat * 0.25; + if (west > east) { + sizeLon += 360.; + } + + let viewport = new MapViewerViewport(south, west, sizeLon, sizeLat, centerLon, centerLat, this.viewer.camera.heading); + this.model.setViewport(viewport); + this.visualizeTileIds(); + } + visualizeTileIds() { // Remove previous points if (this.points) { diff --git a/static/index.css b/static/index.css index 76481e1f..0e663f07 100644 --- a/static/index.css +++ b/static/index.css @@ -19,7 +19,7 @@ body { -o-user-select:none; } -#cesiumContainer .cesium-widget-credits { +#mapViewContainer .cesium-widget-credits { display:none !important; } diff --git a/static/index.html b/static/index.html index 99b24ae8..a71b0b59 100644 --- a/static/index.html +++ b/static/index.html @@ -15,11 +15,10 @@ - - - - - + + + + @@ -30,7 +29,7 @@ -
+
diff --git a/static/index.js b/static/index.js index 2562978a..afcd6ac3 100644 --- a/static/index.js +++ b/static/index.js @@ -1,5 +1,5 @@ -import { MapViewerView } from "./mapcomponent/view.js"; -import { MapViewerModel } from "./mapcomponent/model.js"; +import { ErdblickView } from "./erdblick/view.js"; +import { ErdblickModel } from "./erdblick/model.js"; import libErdblickCore from "./libs/core/erdblick-core.js"; // --------------------------- Initialize Map Componesnt -------------------------- @@ -9,8 +9,8 @@ libErdblickCore().then(coreLib => { console.log(" ...done.") - let mapModel = new MapViewerModel(coreLib); - let mapView = new MapViewerView(mapModel, 'cesiumContainer'); + let mapModel = new ErdblickModel(coreLib); + let mapView = new ErdblickView(mapModel, 'mapViewContainer'); window.reloadStyle = () => { mapModel.reloadStyle(); @@ -32,14 +32,14 @@ libErdblickCore().then(coreLib => mapModel.mapInfoTopic.subscribe(mapInfo => { let mapSettingsBox = $("#maps"); mapSettingsBox.empty() - for (let [mapName, map] of Object.entries(this.maps)) { + for (let [mapName, map] of Object.entries(mapInfo)) { for (let [layerName, layer] of Object.entries(map.layers)) { let mapsEntry = $(`
${mapName} / ${layerName} 
`); $(mapsEntry.find("button")).on("click", _=>{ // Grab first tile id from coverage and zoom to it. // TODO: Zoom to extent of map instead. if (layer.coverage[0] !== undefined) - this.zoomToWgs84PositionTopic.next(this.coreLib.getTilePosition(BigInt(layer.coverage[0]))); + mapModel.zoomToWgs84PositionTopic.next(coreLib.getTilePosition(BigInt(layer.coverage[0]))); }) mapSettingsBox.append(mapsEntry) } diff --git a/static/mapcomponent/utils.js b/static/mapcomponent/utils.js deleted file mode 100644 index 58f97201..00000000 --- a/static/mapcomponent/utils.js +++ /dev/null @@ -1,40 +0,0 @@ -"use strict"; - -import {SingleShotTimer} from "./timer.js"; - -/** - * Throttle calls to a function to always have at least a specific interval inbetween. - */ -export function throttle(minIv, fn) -{ - let lastExecTimestamp = new Date(0); - let lastArgs = []; - let finalCallTimer = new SingleShotTimer(minIv, ()=>fn(...lastArgs), true); - - return (...args) => - { - lastArgs = args; - let currentTime = new Date(); - if (currentTime - lastExecTimestamp < minIv) { - finalCallTimer.restart(); - return; - } - finalCallTimer.stop(); - lastExecTimestamp = currentTime; - fn(...args); - }; -} - -/** - * Option the first value of a cookie with a specific name. - */ -export function cookieValue(cookieName) { - for (let cookie of document.cookie.split(';')) { - let keyValuePair = cookie.trim().split("="); - if (keyValuePair.length < 2) - continue; - if (keyValuePair[0].trim() === cookieName) - return keyValuePair[1].trim(); - } - return null; -} From 185884ad852ecb763f77523ae98474e35112c1c3 Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Thu, 14 Sep 2023 13:24:35 +0200 Subject: [PATCH 08/16] Change mapget branch back to main --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fc81df1c..7c074ea7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,7 +38,7 @@ FetchContent_MakeAvailable(cesiumnative) FetchContent_Declare(mapget GIT_REPOSITORY "https://github.com/Klebert-Engineering/mapget" - GIT_TAG "access-field-dict-offsets" + GIT_TAG "main" GIT_SHALLOW ON) FetchContent_MakeAvailable(mapget) From 26b077565f8ca541f952670104d1e02499d82a40 Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Fri, 15 Sep 2023 08:23:05 +0200 Subject: [PATCH 09/16] Add wasm.js --- static/erdblick/wasm.js | 53 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 static/erdblick/wasm.js diff --git a/static/erdblick/wasm.js b/static/erdblick/wasm.js new file mode 100644 index 00000000..94dcfa50 --- /dev/null +++ b/static/erdblick/wasm.js @@ -0,0 +1,53 @@ +"use strict"; + +/** + * Run a WASM function which places data in a SharedUint8Array, + * and then store this data under an object URL. Will be aborted + * and return null, if the user function returns false. + */ +export function blobUriFromWasm(coreLib, fun, contentType) { + let sharedGlbArray = new coreLib.SharedUint8Array(); + if (fun(sharedGlbArray) === false) { + sharedGlbArray.delete(); + return null; + } + let objSize = sharedGlbArray.getSize(); + let bufferPtr = Number(sharedGlbArray.getPointer()); + let data = coreLib.HEAPU8.buffer.slice(bufferPtr, bufferPtr + objSize); + const blob = new Blob([data], { type: contentType }); + const glbUrl = URL.createObjectURL(blob); + sharedGlbArray.delete(); + return glbUrl; +} + +/** + * Run a WASM function which places data in a SharedUint8Array, + * and then retrieve this data as a Uint8Array. Will return null + * if the user function returns false. + */ +export function uint8ArrayFromWasm(coreLib, fun) { + let sharedGlbArray = new coreLib.SharedUint8Array(); + if (fun(sharedGlbArray) === false) { + sharedGlbArray.delete(); + return null; + } + let objSize = sharedGlbArray.getSize(); + let bufferPtr = Number(sharedGlbArray.getPointer()); + let data = new Uint8Array(coreLib.HEAPU8.buffer.slice(bufferPtr, bufferPtr + objSize)); + sharedGlbArray.delete(); + return data; +} + +/** + * Copy the contents of a given Uint8Array to a WASM function + * through a SharedUint8Array. If the operation fails or the WASM function + * returns false, null is returned. + */ +export function uint8ArrayToWasm(coreLib, fun, inputData) { + let sharedGlbArray = new coreLib.SharedUint8Array(inputData.length); + let bufferPtr = Number(sharedGlbArray.getPointer()); + coreLib.HEAPU8.set(inputData, bufferPtr); + let result = fun(sharedGlbArray); + sharedGlbArray.delete(); + return (result === false) ? null : result; +} From d2ad2a65976ba706a35c2b8471524a02d465b939 Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Wed, 27 Sep 2023 12:50:34 +0200 Subject: [PATCH 10/16] Move --profiling flag to comment. --- libs/core/CMakeLists.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libs/core/CMakeLists.txt b/libs/core/CMakeLists.txt index 43c7eb20..c708f32d 100644 --- a/libs/core/CMakeLists.txt +++ b/libs/core/CMakeLists.txt @@ -24,7 +24,6 @@ if(${CMAKE_SYSTEM_NAME} STREQUAL "Emscripten") add_executable(erdblick-core ${ERDBLICK_SOURCE_FILES}) set_target_properties(erdblick-core PROPERTIES LINK_FLAGS "\ --bind \ - --profiling \ -s ENVIRONMENT=web \ -s MODULARIZE=1 \ -s EXPORT_ES6=1 \ @@ -34,6 +33,9 @@ if(${CMAKE_SYSTEM_NAME} STREQUAL "Emscripten") -s JS_MATH=0 \ -s ALLOW_MEMORY_GROWTH=1 \ ") + + # Add this option to see WASM function names in Browser performance traces. + # --profiling \ else() add_library(erdblick-core ${ERDBLICK_SOURCE_FILES}) endif() From fbf2b99995148a557dbc9e056d703d891bc0e93c Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Wed, 27 Sep 2023 12:51:05 +0200 Subject: [PATCH 11/16] Some more cleanup for radialDistancePrioFn --- libs/core/include/erdblick/aabb.h | 2 +- libs/core/src/aabb.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/core/include/erdblick/aabb.h b/libs/core/include/erdblick/aabb.h index 8145a1ff..db58ca08 100644 --- a/libs/core/include/erdblick/aabb.h +++ b/libs/core/include/erdblick/aabb.h @@ -109,7 +109,7 @@ class Wgs84AABB * in WGS84, and orientation (bearing) in Radians. This priority function * may be plugged into tileIdsWithPriority. */ - static TilePriorityFn radialDistancePrioFn(glm::vec2 camPos, float orientation); + static TilePriorityFn radialDistancePrioFn(glm::vec2 const& camPos, float orientation); private: vec2_t sw_{.0, .0}; diff --git a/libs/core/src/aabb.cpp b/libs/core/src/aabb.cpp index 224c02b5..6a8d0653 100644 --- a/libs/core/src/aabb.cpp +++ b/libs/core/src/aabb.cpp @@ -177,7 +177,7 @@ void Wgs84AABB::tileIdsWithPriority( } } -TilePriorityFn Wgs84AABB::radialDistancePrioFn(glm::vec2 camPos, float orientation) +TilePriorityFn Wgs84AABB::radialDistancePrioFn(glm::vec2 const& camPos, float orientation) { return [camPos, orientation](TileId const& tid) { @@ -191,7 +191,7 @@ TilePriorityFn Wgs84AABB::radialDistancePrioFn(glm::vec2 camPos, float orientati if (angle > glm::pi()) angle = glm::two_pi() - angle; - auto distance = glm::sqrt(yDiff*yDiff + xDiff*xDiff); // Use manhattan distance to avoid comp overhead? + auto distance = glm::sqrt(yDiff*yDiff + xDiff*xDiff); return distance + angle * distance; }; } From abe5efdcef5e7c7e4a5e435576c3b53112521ef6 Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Wed, 27 Sep 2023 12:52:24 +0200 Subject: [PATCH 12/16] Add Viewport type as value_object binding. --- libs/core/src/bindings.cpp | 50 ++++++++++++++++++++++---------------- static/erdblick/model.js | 26 +++++++------------- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/libs/core/src/bindings.cpp b/libs/core/src/bindings.cpp index a55d2208..c39eb753 100644 --- a/libs/core/src/bindings.cpp +++ b/libs/core/src/bindings.cpp @@ -12,6 +12,20 @@ using namespace erdblick; namespace em = emscripten; +/** + * WGS84 Viewport Descriptor, which may be used with the + * `getTileIds` function below. + */ +struct Viewport { + double south = .0; // The southern boundary of the viewport (degrees). + double west = .0; // The western boundary of the viewport (degrees). + double width = .0; // The width of the viewport (degrees). + double height = .0; // The height of the viewport (degrees). + double camPosLon = .0; // The longitude of the camera position (degrees). + double camPosLat = .0; // The latitude of the camera position (degrees). + double orientation = .0; // The compass orientation of the camera (radians). +}; + /** * Gets the prioritized list of tile IDs for a given viewport, zoom level, and tile limit. * @@ -27,41 +41,25 @@ namespace em = emscripten; * their radial distance, and the sorted array is converted to an emscripten value to be returned. * Duplicate tile IDs are removed from the array before it is returned. * - * @param viewport An emscripten value representing the viewport. The viewport is an object - * containing the following properties: - * - south: The southern boundary of the viewport. - * - west: The western boundary of the viewport. - * - width: The width of the viewport. - * - height: The height of the viewport. - * - camPosLon: The longitude of the camera position. - * - camPosLat: The latitude of the camera position. - * - orientation: The orientation of the viewport. + * @param viewport The viewport descriptor for which tile ids are needed. * @param level The zoom level for which to get the tile IDs. * @param limit The maximum number of tile IDs to return. * * @return An emscripten value representing an array of prioritized tile IDs. */ -em::val getTileIds(em::val viewport, int level, int limit) +em::val getTileIds(Viewport const& vp, int level, int limit) { - double vpSouth = viewport["south"].as(); - double vpWest = viewport["west"].as(); - double vpWidth = viewport["width"].as(); - double vpHeight = viewport["height"].as(); - double camPosLon = viewport["camPosLon"].as(); - double camPosLat = viewport["camPosLat"].as(); - double orientation = viewport["orientation"].as(); - - Wgs84AABB aabb(Wgs84Point{vpWest, vpSouth, .0}, {vpWidth, vpHeight}); + Wgs84AABB aabb(Wgs84Point{vp.west, vp.south, .0}, {vp.width, vp.height}); if (aabb.numTileIds(level) > limit) // Create a size-limited AABB from the tile limit. - aabb = Wgs84AABB::fromCenterAndTileLimit(Wgs84Point{camPosLon, camPosLat, .0}, limit, level); + aabb = Wgs84AABB::fromCenterAndTileLimit(Wgs84Point{vp.camPosLon, vp.camPosLat, .0}, limit, level); std::vector> prioritizedTileIds; prioritizedTileIds.reserve(limit); aabb.tileIdsWithPriority( level, prioritizedTileIds, - Wgs84AABB::radialDistancePrioFn({camPosLon, camPosLat}, orientation)); + Wgs84AABB::radialDistancePrioFn({vp.camPosLon, vp.camPosLat}, vp.orientation)); std::sort( prioritizedTileIds.begin(), @@ -113,6 +111,16 @@ EMSCRIPTEN_BINDINGS(FeatureLayerRendererBind) .field("y", &mapget::Point::y) .field("z", &mapget::Point::z); + ////////// Viewport + em::value_object("Viewport") + .field("south", &Viewport::south) + .field("west", &Viewport::west) + .field("width", &Viewport::width) + .field("height", &Viewport::height) + .field("camPosLon", &Viewport::camPosLon) + .field("camPosLat", &Viewport::camPosLat) + .field("orientation", &Viewport::orientation); + ////////// FeatureLayerStyle em::class_("FeatureLayerStyle").constructor(); diff --git a/static/erdblick/model.js b/static/erdblick/model.js index c68f9a27..072bb261 100644 --- a/static/erdblick/model.js +++ b/static/erdblick/model.js @@ -7,22 +7,6 @@ const styleUrl = "/styles/demo-style.yaml"; const infoUrl = "/sources"; const tileUrl = "/tiles"; -/** - * Viewport object which can be interpreted by the erdblick-core WASM - * `getTileIds` function. - */ -export class MapViewerViewport { - constructor(south, west, width, height, camPosLon, camPosLat, orientation) { - this.south = south; - this.west = west; - this.width = width; - this.height = height; - this.camPosLon = camPosLon; - this.camPosLat = camPosLat; - this.orientation = orientation; - } -} - /** * Erdblick view-model class. This class is responsible for keeping track * of the following objects: @@ -45,7 +29,15 @@ export class ErdblickModel this.loadedTileLayers = new Map(); this.currentFetch = null; this.currentFetchId = 0; - this.currentViewport = new MapViewerViewport; + this.currentViewport = { + south: .0, + west: .0, + width: .0, + height: .0, + camPosLon: .0, + camPosLat: .0, + orientation: .0, + }; this.currentVisibleTileIds = new Set(); // Instantiate the TileLayerParser, and set its callback From beaded4e23addc867510706a4097e50ad4329bd8 Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Wed, 27 Sep 2023 12:52:50 +0200 Subject: [PATCH 13/16] Deactivate mapget debug log mode. --- libs/core/src/bindings.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/core/src/bindings.cpp b/libs/core/src/bindings.cpp index c39eb753..bd5d45d9 100644 --- a/libs/core/src/bindings.cpp +++ b/libs/core/src/bindings.cpp @@ -96,7 +96,8 @@ std::string getTileFeatureLayerKey(std::string const& mapId, std::string const& EMSCRIPTEN_BINDINGS(FeatureLayerRendererBind) { - mapget::log().set_level(spdlog::level::debug); + // Activate this to see a lot more output from the WASM lib. + // mapget::log().set_level(spdlog::level::debug); ////////// SharedUint8Array em::class_("SharedUint8Array") From ea4474df23a967eb543b28effc431d3e382898d3 Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Wed, 27 Sep 2023 12:53:45 +0200 Subject: [PATCH 14/16] Consistent usage of punctuation and semicolons in view and model. --- static/erdblick/model.js | 40 ++++++++++++++++++---------------- static/erdblick/view.js | 47 +++++++++++++++++++++------------------- 2 files changed, 46 insertions(+), 41 deletions(-) diff --git a/static/erdblick/model.js b/static/erdblick/model.js index 072bb261..7b986cf3 100644 --- a/static/erdblick/model.js +++ b/static/erdblick/model.js @@ -64,25 +64,25 @@ export class ErdblickModel /// Triggered when a tile layer is being removed. this.tileLayerRemovedTopic = new rxjs.Subject(); // {FeatureTile} - /// Triggered when the user requests to zoom to a map layer + /// Triggered when the user requests to zoom to a map layer. this.zoomToWgs84PositionTopic = new rxjs.Subject(); // {.x,.y} - /// Triggered when the map info is updated + /// Triggered when the map info is updated. this.mapInfoTopic = new rxjs.Subject(); // {: } /////////////////////////////////////////////////////////////////////////// // BOOTSTRAP // /////////////////////////////////////////////////////////////////////////// - this.reloadStyle() - this.reloadDataSources() + this.reloadStyle(); + this.reloadDataSources(); } reloadStyle() { // Delete the old style if present. if (this.style) - this.style.delete() + this.style.delete(); // Fetch the new one. new Fetch(this.coreLib, styleUrl).withWasmCallback(styleYamlBuffer => { @@ -91,9 +91,9 @@ export class ErdblickModel // Re-render all present batches with the new style. for (let [batchId, batch] of this.loadedTileLayers.entries()) { - this.renderTileLayer(batch, true) + this.renderTileLayer(batch, true); } - console.log("Loaded style.") + console.log("Loaded style."); }).go(); } @@ -101,11 +101,11 @@ export class ErdblickModel new Fetch(this.coreLib, infoUrl) .withWasmCallback(infoBuffer => { this.tileParser.setDataSourceInfo(infoBuffer); - console.log("Loaded data source info.") + console.log("Loaded data source info."); }) .withJsonCallback(result => { this.maps = Object.fromEntries(result.map(mapInfo => [mapInfo.mapId, mapInfo])); - this.mapInfoTopic.next(this.maps) + this.mapInfoTopic.next(this.maps); }) .go(); } @@ -122,17 +122,17 @@ export class ErdblickModel // Abort previous fetch operation. if (this.currentFetch) - this.currentFetch.abort() + this.currentFetch.abort(); // Make sure that there are no unparsed bytes lingering from the previous response stream. - this.tileParser.reset() + this.tileParser.reset(); // Evict present non-required tile layers. let newTileLayers = new Map(); for (let tileLayer of this.loadedTileLayers.values()) { if (!this.currentVisibleTileIds.has(tileLayer.tileId)) { this.tileLayerRemovedTopic.next(tileLayer); - tileLayer.dispose() + tileLayer.dispose(); } else newTileLayers.set(tileLayer.id, tileLayer); @@ -149,7 +149,7 @@ export class ErdblickModel for (let tileId of allViewportTileIds) { const tileMapLayerKey = this.coreLib.getTileFeatureLayerKey(mapName, layerName, tileId); if (!this.loadedTileLayers.has(tileMapLayerKey)) - requestTilesForMapLayer.push(Number(tileId)) + requestTilesForMapLayer.push(Number(tileId)); } // Only add a request if there are tiles to be loaded. @@ -158,7 +158,7 @@ export class ErdblickModel mapId: mapName, layerId: layerName, tileIds: requestTilesForMapLayer - }) + }); } } @@ -187,14 +187,16 @@ export class ErdblickModel } addTileLayer(tileLayer) { - console.assert(!this.loadedTileLayers.has(tileLayer.id)) - this.loadedTileLayers.set(tileLayer.id, tileLayer) + if (this.loadedTileLayers.has(tileLayer.id)) { + throw new Error(`Refusing to add tile layer ${tileLayer.id}, which is already present.`); + } + this.loadedTileLayers.set(tileLayer.id, tileLayer); this.renderTileLayer(tileLayer); } renderTileLayer(tileLayer, removeFirst) { if (removeFirst) { - this.tileLayerRemovedTopic.next(tileLayer) + this.tileLayerRemovedTopic.next(tileLayer); } tileLayer.render(this.glbConverter, this.style).then(wasRendered => { if (!wasRendered) @@ -205,9 +207,9 @@ export class ErdblickModel // add it to the viewport. const isInViewport = this.currentVisibleTileIds.has(tileLayer.tileId); if (isInViewport) - this.tileLayerAddedTopic.next(tileLayer) + this.tileLayerAddedTopic.next(tileLayer); else - tileLayer.disposeRenderResult() + tileLayer.disposeRenderResult(); }) } diff --git a/static/erdblick/view.js b/static/erdblick/view.js index 94f436e9..e3233646 100644 --- a/static/erdblick/view.js +++ b/static/erdblick/view.js @@ -1,6 +1,6 @@ "use strict"; -import {ErdblickModel, MapViewerViewport} from "./model.js"; +import {ErdblickModel} from "./model.js"; import {FeatureWrapper} from "./features.js"; export class ErdblickView @@ -81,7 +81,7 @@ export class ErdblickView } }, Cesium.ScreenSpaceEventType.MOUSE_MOVE); - // Add a handler for camera movement + // Add a handler for camera movement. this.viewer.camera.percentageChanged = 0.1; this.viewer.camera.changed.addEventListener(() => { this.updateViewport(); @@ -108,10 +108,10 @@ export class ErdblickView model.zoomToWgs84PositionTopic.subscribe(pos => { this.viewer.camera.setView({ - destination: Cesium.Cartesian3.fromDegrees(pos.x, pos.y, 15000), // Converts lon/lat to Cartesian3 + destination: Cesium.Cartesian3.fromDegrees(pos.x, pos.y, 15000), // Converts lon/lat to Cartesian3. orientation: { - heading: Cesium.Math.toRadians(0), // East, in radians - pitch: Cesium.Math.toRadians(-90), // Directly looking down + heading: Cesium.Math.toRadians(0), // East, in radians. + pitch: Cesium.Math.toRadians(-90), // Directly looking down. roll: 0 // No rotation } }); @@ -119,7 +119,7 @@ export class ErdblickView let polylines = new Cesium.PolylineCollection(); - // Line over the equator divided into four 90-degree segments + // Line over the equator divided into four 90-degree segments. this.viewer.entities.add({ name: 'Equator', polyline: { @@ -135,7 +135,7 @@ export class ErdblickView } }); - // Line over the antimeridian + // Line over the antimeridian. this.viewer.entities.add({ name: 'Antimeridian', polyline: { @@ -195,17 +195,20 @@ export class ErdblickView // Grow the viewport rectangle by 25% let expandLon = sizeLon * 0.25; let expandLat = sizeLat * 0.25; - if (west > east) { - sizeLon += 360.; - } - - let viewport = new MapViewerViewport(south, west, sizeLon, sizeLat, centerLon, centerLat, this.viewer.camera.heading); - this.model.setViewport(viewport); + this.model.setViewport({ + south: south - expandLat, + west: west - expandLat, + width: sizeLon + expandLon*2, + height: sizeLat + expandLat*2, + camPosLon: centerLon, + camPosLat: centerLat, + orientation: this.viewer.camera.heading, + }); this.visualizeTileIds(); } visualizeTileIds() { - // Remove previous points + // Remove previous points. if (this.points) { for (let i = 0; i < this.points.length; i++) { this.viewer.entities.remove(this.points[i]); @@ -215,25 +218,25 @@ export class ErdblickView // Get the tile IDs for the current viewport. let tileIds = this.model.currentVisibleTileIds; - // Calculate total number of tile IDs + // Calculate total number of tile IDs. let totalTileIds = tileIds.size; - // Initialize points array + // Initialize points array. this.points = []; - // Counter for iteration over Set + // Counter for iteration over Set. let i = 0; - // Iterate through each tile ID using Set's forEach method + // Iterate through each tile ID using Set's forEach method. tileIds.forEach(tileId => { // Get WGS84 coordinates for the tile ID let position = this.model.coreLib.getTilePosition(tileId); - // Calculate the color based on the position in the list + // Calculate the color based on the position in the list. let colorValue = i / totalTileIds; let color = Cesium.Color.fromHsl(0.6 - colorValue * 0.5, 1.0, 0.5); - // Create a point and add it to the Cesium scene + // Create a point and add it to the Cesium scene. let point = this.viewer.entities.add({ position: Cesium.Cartesian3.fromDegrees(position.x, position.y), point: { @@ -242,10 +245,10 @@ export class ErdblickView } }); - // Add the point to the points array + // Add the point to the points array. this.points.push(point); - // Increment counter + // Increment counter. i++; }); } From 181de6aa9c903659b8ffeb151cc30d820386fe34 Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Wed, 27 Sep 2023 12:54:04 +0200 Subject: [PATCH 15/16] Add TODO regarding reset to original feature color. --- static/erdblick/view.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/static/erdblick/view.js b/static/erdblick/view.js index e3233646..a09443cd 100644 --- a/static/erdblick/view.js +++ b/static/erdblick/view.js @@ -49,7 +49,9 @@ export class ErdblickView this.mouseHandler.setInputAction(movement => { // If there was a previously picked feature, reset its color. if (this.pickedFeature) { - this.pickedFeature.color = Cesium.Color.WHITE; // Assuming the original color is WHITE. Adjust as necessary. + // TODO: Feature color must be reset to its original color, + // based on the style sheet. + this.pickedFeature.color = Cesium.Color.WHITE; } let feature = this.viewer.scene.pick(movement.position); From 1ff8c90d6a1aee924e33f50acf1a358f2202e546 Mon Sep 17 00:00:00 2001 From: Joseph Birkner Date: Wed, 27 Sep 2023 19:04:14 +0200 Subject: [PATCH 16/16] Re-add profiling flag and fix longitude expansion. --- libs/core/CMakeLists.txt | 4 +--- static/erdblick/view.js | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/libs/core/CMakeLists.txt b/libs/core/CMakeLists.txt index c708f32d..43c7eb20 100644 --- a/libs/core/CMakeLists.txt +++ b/libs/core/CMakeLists.txt @@ -24,6 +24,7 @@ if(${CMAKE_SYSTEM_NAME} STREQUAL "Emscripten") add_executable(erdblick-core ${ERDBLICK_SOURCE_FILES}) set_target_properties(erdblick-core PROPERTIES LINK_FLAGS "\ --bind \ + --profiling \ -s ENVIRONMENT=web \ -s MODULARIZE=1 \ -s EXPORT_ES6=1 \ @@ -33,9 +34,6 @@ if(${CMAKE_SYSTEM_NAME} STREQUAL "Emscripten") -s JS_MATH=0 \ -s ALLOW_MEMORY_GROWTH=1 \ ") - - # Add this option to see WASM function names in Browser performance traces. - # --profiling \ else() add_library(erdblick-core ${ERDBLICK_SOURCE_FILES}) endif() diff --git a/static/erdblick/view.js b/static/erdblick/view.js index a09443cd..6168b0f8 100644 --- a/static/erdblick/view.js +++ b/static/erdblick/view.js @@ -199,7 +199,7 @@ export class ErdblickView let expandLat = sizeLat * 0.25; this.model.setViewport({ south: south - expandLat, - west: west - expandLat, + west: west - expandLon, width: sizeLon + expandLon*2, height: sizeLat + expandLat*2, camPosLon: centerLon,