Cogs.Core
SeaCurrentsExtension.cpp
1#include "SeaCurrentsExtension.h"
2#include "Quadtree.h"
3
4#include "SeaCurrentsSystem.h"
5#include "Utilities/VectorPermutation.h"
6#include "ResourceManifest.h"
7
8#include "Context.h"
9#include "EntityStore.h"
10#include "ExtensionRegistry.h"
11#include "Components/Core/TransformComponent.h"
12#include "Resources/ResourceStore.h"
13#include "Serialization/EntityReader.h"
14#include "Utilities/Parsing.h"
15
16#include "Foundation/HashFunctions.h"
17#include "Foundation/Geometry/GeodeticUtils.h"
18#include "Foundation/Platform/IO.h"
19#include "Foundation/Logging/Logger.h"
20
21#ifndef EMSCRIPTEN
22#include "hdf5.h"
23#include "H5LTpublic.h"
24#endif
25
26#include <map>
27#include <set>
28
29namespace {
30 Cogs::Logging::Log gLogger = Cogs::Logging::getLogger("SeaCurrentsExtension");
31
32 struct RawCurrentData {
33 float mSpeed;
34 float mDirection;
35 };
36
37 struct RawPositionData {
38 float mLatitude;
39 float mLongitude;
40 };
41
42 constexpr const char* cSeaCurrentsEntityDefinition = R"(
43 { "name": "SeaCurrents",
44 "components" : [
45 "SeaCurrentsComponent",
46 "TransformComponent",
47 "SceneComponent"
48 ]
49 })";
50
51}
52
53namespace Cogs::Core {
54
55 void ComputePriority(SeaCurrentsComponent& seaCurrentsComp, const std::pair<glm::vec2, glm::vec2>& bounds, float threshold, int lowestPrio);
56
58 {
59 static constexpr const char* cExtensionKey = "SeaCurrents";
60
62 reinterpret_cast<LoadFromMemoryFn>(&SeaCurrentsExtension::loadFromMemory),
63 reinterpret_cast<LoadFromFileFn>(&SeaCurrentsExtension::loadFromFile),
64 nullptr,
65 nullptr,
66 };
67
68 SeaCurrentsExtension() { ExtensionRegistry::add(this, COGS_CORE_VERSION_STRING); }
69
70 virtual bool initializeStatic() override {
71 SeaCurrentsComponent::registerType();
72 return true;
73 }
74
75 virtual const char* getExtensionKey() const override { return cExtensionKey; }
76 virtual const void* getPublicAPI() const override { return &api; }
77
78 virtual bool initialize(Context* context) override {
79
80 const std::string resourceArchive = "Cogs.Core.Extensions.SeaCurrents.zip";
81
82
83#ifdef EMSCRIPTEN
84
85 bool resourceArchiveAdded = false;
86 auto manifest = getResourceManifest(context);
87 for (auto& item : manifest) {
88 if (item.find(resourceArchive) != std::string::npos) {
89 context->resourceStore->addResourceArchive(item);
90 resourceArchiveAdded = true;
91 break;
92 }
93 }
94 if (!resourceArchiveAdded) {
95 context->resourceStore->addResourceArchive(resourceArchive);
96 }
97
98#else
99
100 context->resourceStore->addSearchPath("../Extensions/SeaCurrents/Data/");
101 context->resourceStore->addResourceArchive(resourceArchive);
102
103#endif
104
105 readEntityDefinition(cSeaCurrentsEntityDefinition, context->store);
106
107 ExtensionRegistry::registerExtensionSystem<SeaCurrentsSystem>(context, SystemPriority::Geometry, 16);
108 return true;
109 }
110
111 static bool loadFromMemory(void* ctx, EntityId entityId, const char* extension, void* data, size_t dataSize, uint64_t timestamp, int utmZone = -1) {
112 Context* context = static_cast<Context*>(ctx);
113 EntityPtr entity = context->store->getEntity(entityId);
114 SeaCurrentsComponent* component = entity ? entity->getComponent<SeaCurrentsComponent>() : nullptr;
115 bool ret = false;
116
117 if (component) {
118#ifndef EMSCRIPTEN
119 // To stop build warning unused variable 'gLogger'
120 (void)gLogger;
121 hid_t fileId = H5LTopen_file_image(data, dataSize, H5LT_FILE_IMAGE_DONT_COPY | H5LT_FILE_IMAGE_DONT_RELEASE);
122 hid_t groupId;
123 uint32_t numInstances;
124 uint32_t dataCodingFormat;
125
126 groupId = H5Gopen(fileId, "/SurfaceCurrent", H5P_DEFAULT);
127 dataCodingFormat = readAttribute<uint32_t>(groupId, "dataCodingFormat");
128 numInstances = readAttribute<uint32_t>(groupId, "numInstances");
129 H5Gclose(groupId);
130
131 // TODO: Figure out what multiple instances represent... If there is such a thing.
132 if (numInstances == 1) {
133 groupId = H5Gopen(fileId, "/SurfaceCurrent/SurfaceCurrent.01", H5P_DEFAULT);
134
135 uint64_t firstTime = parseISO8601(readAttribute<std::string>(groupId, "dateTimeOfFirstRecord"));
136 uint64_t lastTime = parseISO8601(readAttribute<std::string>(groupId, "dateTimeOfLastRecord"));
137 uint32_t noOfRecords = readAttribute<uint32_t>(groupId, "numberOfTimes");
138 uint32_t closestRecord;
139 float eastBound = readAttribute<float>(groupId, "eastBoundLongitude");
140 float westBound = readAttribute<float>(groupId, "westBoundLongitude");
141 float southBound = readAttribute<float>(groupId, "southBoundLatitude");
142 float northBound = readAttribute<float>(groupId, "northBoundLatitude");
143 hid_t rawCurrentDataMemType = H5Tcreate(H5T_COMPOUND, sizeof(RawCurrentData));
144 double lowerEasting;
145 double lowerNorthing;
146 double upperEasting;
147 double upperNorthing;
148
149 LLtoUTM(lowerEasting, lowerNorthing, southBound, westBound, utmZone, true);
150 LLtoUTM(upperEasting, upperNorthing, northBound, eastBound, utmZone, true);
151
152 glm::dvec2 centre((lowerEasting + upperEasting) * 0.5f, (lowerNorthing + upperNorthing) * 0.5f);
153
154 H5Tinsert(rawCurrentDataMemType, "surfaceCurrentSpeed", HOFFSET(RawCurrentData, mSpeed), H5T_NATIVE_FLOAT);
155 H5Tinsert(rawCurrentDataMemType, "surfaceCurrentDirection", HOFFSET(RawCurrentData, mDirection), H5T_NATIVE_FLOAT);
156
157 if (timestamp <= firstTime) {
158 closestRecord = 0;
159 }
160 else if (timestamp >= lastTime) {
161 closestRecord = noOfRecords;
162 }
163 else {
164 closestRecord = static_cast<uint32_t>((timestamp - firstTime) * noOfRecords / (lastTime - firstTime));
165 }
166
167 char filename[500];
168 hid_t datasetId;
169
170 sprintf(filename, "/SurfaceCurrent/SurfaceCurrent.01/Group_%03d/values", std::min(std::max(1U, closestRecord), noOfRecords));
171 datasetId = H5Dopen(fileId, filename, H5P_DEFAULT);
172
173 switch (dataCodingFormat) {
174 case 1: { // Time series at fixed stations...
175 assert(false);
176 break;
177 }
178 case 2: { // Regularly gridded arrays...
179 uint64_t width = readAttribute<uint32_t>(groupId, "numPointsLongitudinal");
180 uint64_t height = readAttribute<uint32_t>(groupId, "numPointsLatitudinal");
181 std::vector<RawCurrentData> buffer(width * height);
182
183 if (H5Dread(datasetId, rawCurrentDataMemType, H5S_ALL, H5S_ALL, H5P_DEFAULT, buffer.data()) >= 0) {
184 float y = southBound;
185 float dx = readAttribute<float>(groupId, "gridSpacingLongitudinal");
186 float dy = readAttribute<float>(groupId, "gridSpacingLatitudinal");
187 auto i = buffer.begin();
188 double easting;
189 double northing;
190
191 component->positions.clear();
192 component->speeds.clear();
193 component->angles.clear();
194 component->priorities.clear();
195 component->setChanged();
196
197 for (uint64_t cy = height; cy--; y += dy) {
198 float x = westBound;
199
200 for (uint64_t cx = width; cx--; x += dx, ++i) {
201 if (i->mSpeed >= 0.0f) {
202 LLtoUTM(easting, northing, y, x, utmZone, true);
203
204 easting -= centre.x;
205 northing -= centre.y;
206
207 component->positions.push_back(glm::vec2(static_cast<float>(easting), static_cast<float>(northing)));
208 component->speeds.push_back(i->mSpeed);
209 component->angles.push_back(i->mDirection);
210 }
211 }
212 }
213
214 entity->getComponent<TransformComponent>()->coordinates = glm::vec3(centre, 0.0f);
215 entity->getComponent<TransformComponent>()->setChanged();
216 ret = true;
217 }
218 break;
219 }
220 case 3: { // Ungeorectified gridded arrays...
221 uint32_t noOfNodes = readAttribute<uint32_t>(groupId, "numberOfNodes");
222 hid_t rawPositionDataMemType = H5Tcreate(H5T_COMPOUND, sizeof(RawPositionData));
223 std::vector<RawPositionData> positions(noOfNodes);
224 hid_t positionsId = H5Dopen(fileId, "/SurfaceCurrent/SurfaceCurrent.01/Positioning/geometryValues", H5P_DEFAULT);
225
226 H5Tinsert(rawPositionDataMemType, "latitude", HOFFSET(RawPositionData, mLatitude), H5T_NATIVE_FLOAT);
227 H5Tinsert(rawPositionDataMemType, "longitude", HOFFSET(RawPositionData, mLongitude), H5T_NATIVE_FLOAT);
228
229 if (H5Dread(positionsId, rawPositionDataMemType, H5S_ALL, H5S_ALL, H5P_DEFAULT, positions.data()) >= 0) {
230 std::vector<RawCurrentData> currents(noOfNodes);
231
232 if (H5Dread(datasetId, rawCurrentDataMemType, H5S_ALL, H5S_ALL, H5P_DEFAULT, currents.data()) >= 0) {
233 double easting;
234 double northing;
235 auto pi = positions.begin();
236 auto ci = currents.begin();
237
238 component->positions.clear();
239 component->speeds.clear();
240 component->angles.clear();
241 component->priorities.clear();
242 component->setChanged();
243
244 for (; noOfNodes--; ++ci, ++pi) {
245 LLtoUTM(easting, northing, pi->mLatitude, pi->mLongitude, utmZone, true);
246
247 easting -= centre.x;
248 northing -= centre.y;
249
250 component->positions.push_back(glm::vec2(static_cast<float>(easting), static_cast<float>(northing)));
251 component->speeds.push_back(ci->mSpeed);
252 component->angles.push_back(ci->mDirection);
253 }
254 entity->getComponent<TransformComponent>()->coordinates = glm::vec3(centre, 0.0f);
255 entity->getComponent<TransformComponent>()->setChanged();
256 ret = true;
257 }
258 }
259 H5Dclose(positionsId);
260 H5Tclose(rawPositionDataMemType);
261 break;
262 }
263 case 4: { // moving platform...
264 assert(false);
265 break;
266 }
267 }
268
269 // Some special cases
270 // Terminating conditions. Single point input, malformed inputs, etc...
271 if (component->positions.size() != 0) {
272
273 //Guarantee size consistency of component data vectors
274 component->priorities.resize(component->positions.size());
275
276 if (component->positions.size() == 1) {
277 component->priorities[0] = 1;
278 component->priorityLevel = 1;
279 }
280 else {
281 glm::vec2 minPos(std::numeric_limits<float>::max(), std::numeric_limits<float>::max());
282 glm::vec2 maxPos(std::numeric_limits<float>::lowest(), std::numeric_limits<float>::lowest());
283
284 // compute bounds and ensure all variables have valid data
285 std::vector<glm::vec2>::iterator minId, maxId;
286 minId = std::min_element(component->positions.begin(), component->positions.end(), [](const glm::vec2& a, const glm::vec2& b) {
287 return (a == glm::min(a, b)) ? true : false;
288 });
289 maxId = std::max_element(component->positions.begin(), component->positions.end(), [](const glm::vec2& a, const glm::vec2& b) {
290 return (b == glm::max(a, b)) ? true : false;
291 });
292
293 if (minId != component->positions.end()) minPos = *minId;
294 if (maxId != component->positions.end()) maxPos = *maxId;
295
296 //non mutating data members
297 const std::pair<glm::vec2, glm::vec2> bounds = { minPos, maxPos };
298
299 // Only perform if the bounds are within a reasonable area.
300 constexpr float deltaBounds = 0.0001f;
301 if (deltaBounds <= glm::distance(bounds.first, bounds.second)) {
302
303 // Compute the priority of each data point
304 //ComputePriority(*component, bounds, deltaBounds, (int)component->positions.size());
305 // create the quadtree here priority will be value of depth
306 SeaCurrentQuadtree tree(minPos, maxPos);
307 for (const auto& pos : component->positions)
308 {
309 tree.insert(tree.getRoot(), 0, pos);
310 }
311 // assign priorites by navigating the tree
312 for (size_t i = 0; i < component->positions.size(); i++)
313 {
314 int priority = tree.search(tree.getRoot(), component->positions[i], 0);
315 component->priorities[i] = static_cast<int>(std::round(priority));
316 }
317
318 // sort data by priority
319 auto p = Permutation::get_permutation(component->priorities,
320 [](const int& a, const int& b) { return a < b; });
321
322 // sort the component array order based on priorities->
323 Permutation::apply_permutation(component->priorities, p);
324 Permutation::apply_permutation(component->positions, p);
325 Permutation::apply_permutation(component->speeds, p);
326 Permutation::apply_permutation(component->angles, p);
327 component->priorityLevel = component->priorities.back();
328 }
329 }
330 }
331
332 H5Dclose(datasetId);
333 H5Tclose(rawCurrentDataMemType);
334 H5Gclose(groupId);
335 }
336 H5Fclose(fileId);
337
338 (void)extension;
339#else
340 (void)data;
341 (void)dataSize;
342 (void)timestamp;
343 (void)utmZone;
344 LOG_ERROR(gLogger, "%s: Unsupported extension .h5 in EMSCRIPTEN build", IO::extension(extension).c_str());
345#endif
346 }
347 return ret;
348 }
349
350 static bool loadFromFile(void* context, EntityId entityId, const char* filename, uint64_t timestamp, int utmZone = -1) {
351 ResourceBuffer buffer = reinterpret_cast<Context*>(context)->resourceStore->getResourceContents(filename);
352
353 if (!buffer.empty()) {
354 return loadFromMemory(context, entityId, filename, buffer.data(), buffer.size(), timestamp, utmZone);
355 }
356 return false;
357 }
358#ifndef EMSCRIPTEN
359 template<typename T> static T readAttribute(hid_t ownerId, const StringView& name) {
360 hid_t attributeId = H5Aopen(ownerId, name.data(), H5P_DEFAULT);
361 T value = {};
362
363 if (attributeId != H5I_INVALID_HID) {
364 hid_t typeId = getTypeId<T>();
365
366 assert(typeId != H5I_INVALID_HID);
367
368 H5Aread(attributeId, typeId, &value);
369 H5Aclose(attributeId);
370 }
371 return value;
372 }
373
374 template<> std::string readAttribute<std::string>(hid_t ownerId, const StringView& name) {
375 hid_t attributeId = H5Aopen(ownerId, name.data(), H5P_DEFAULT);
376 std::string value;
377
378 if (attributeId != H5I_INVALID_HID) {
379 H5A_info_t info;
380 char* ptr;
381
382 H5Aget_info(attributeId, &info);
383 if (H5Aread(attributeId, H5Aget_type(attributeId), &ptr) >= 0) {
384 value = std::string(ptr, info.data_size);
385 H5free_memory(ptr);
386 }
387 H5Aclose(attributeId);
388 }
389 return value;
390 }
391
392 template<typename T> static hid_t getTypeId() { return H5I_INVALID_HID; }
393 template<> hid_t getTypeId<int32_t>() { return H5T_NATIVE_INT32; }
394 template<> hid_t getTypeId<uint32_t>() { return H5T_NATIVE_UINT32; }
395 template<> hid_t getTypeId<float>() { return H5T_NATIVE_FLOAT; }
396 template<> hid_t getTypeId<double>() { return H5T_NATIVE_DOUBLE; }
397#endif
398 }
399 seaCurrentsExtensionInstance;
400
401
402 void ComputePriority(SeaCurrentsComponent& seaCurrentsComp, const std::pair<glm::vec2, glm::vec2>& bounds, float threshold, int lowestPrio) {
403
404 // if all points have a priority we are finished.
405 if (std::find(seaCurrentsComp.priorities.begin(), seaCurrentsComp.priorities.end(), 0) == seaCurrentsComp.priorities.end()) {
406 return;
407 }
408
409 std::map<int, std::vector<int>> neighbours;
410 int priorityModifier = 0;
411
412 //for each point count the number of neighbours
413 for (int idx = 0, pointCount = (int)seaCurrentsComp.positions.size(); idx < pointCount; ++idx) {
414
415 if (seaCurrentsComp.priorities[idx] != 0) continue;
416
417 for (int idy = 0; idy < pointCount; ++idy) {
418 // if points are close enough create the relationship
419
420 if (idx == idy) continue;
421
422 if (glm::distance(seaCurrentsComp.positions[idx], seaCurrentsComp.positions[idy]) < threshold) {
423 neighbours[idx].push_back(idy);
424 if (seaCurrentsComp.priorities[idy] == 0) neighbours[idy].push_back(idx);
425 }
426 }
427 }
428
429 // if no neighbours are found we need to keep checking with a bigger threshold, otherwise sort the data and assign priorities.
430 if (!neighbours.empty())
431 {
432 std::vector<std::pair<int, int>> sizeKeyTable;
433 std::set<int> toIgnore;
434
435 // sort data by number of neighbours
436 sizeKeyTable.reserve(neighbours.size());
437 for (auto& k : neighbours) {
438 if (seaCurrentsComp.priorities[k.first] == 0) {
439 sizeKeyTable.emplace_back(static_cast<int>(k.second.size()), k.first);
440 }
441 }
442
443 std::stable_sort(sizeKeyTable.begin(), sizeKeyTable.end(), [](const std::pair<int, int>& a, const std::pair<int, int>& b) { return a.first > b.first; });
444
445 // assign priority to clustered neighbours and continue with the recursion
446 for (const auto& point : sizeKeyTable) {
447
448 // with the sorted dataset we have to assign a priority to the most crowded point, but we also have to skip it's immediate neighbours
449 if (std::find(toIgnore.begin(), toIgnore.end(), point.second) != toIgnore.end())
450 continue;
451
452 int32_t val = int32_t(lowestPrio - (sizeKeyTable.begin()->first - (point.first - 1))); // decrement prio by (max - (current-1))
453 seaCurrentsComp.priorities[point.second] = val;
454
455 for (auto& k : neighbours[point.second]) { // ignore immediate neighbours
456 toIgnore.insert(k);
457 }
458 }
459
460 priorityModifier = sizeKeyTable.begin()->first;
461 // process discarded points within this threshold
462 ComputePriority(seaCurrentsComp, bounds, threshold, lowestPrio - priorityModifier);
463 }
464 else // increase threshold and continue
465 ComputePriority(seaCurrentsComp, bounds, threshold * 2.0f, lowestPrio - priorityModifier);
466 }
467
468
469}
470
471#if defined( _WIN32 )
472#define SEA_CURRENTS_EXT_API __declspec(dllexport)
473#else
474#define SEA_CURRENTS_EXT_API
475#endif
476
477extern "C" SEA_CURRENTS_EXT_API Cogs::Core::Extension * getExtension() {
478 return &Cogs::Core::seaCurrentsExtensionInstance;
479}
480
481extern "C" SEA_CURRENTS_EXT_API SeaCurrentsModuleAPI * getAPI() {
482 return &Cogs::Core::seaCurrentsExtensionInstance.api;
483}
void setChanged()
Sets the component to the ComponentFlags::Changed state with carry.
Definition: Component.h:202
ComponentType * getComponent() const
Definition: Component.h:159
A Context instance contains all the services, systems and runtime components needed to use Cogs.
Definition: Context.h:83
class EntityStore * store
Entity store.
Definition: Context.h:231
std::unique_ptr< class ResourceStore > resourceStore
ResourceStore service instance.
Definition: Context.h:210
EntityPtr getEntity(const StringView &name, bool logIfNotFound=true) const
Retrieve a reference to the shared entity pointer to the Entity with the given name.
static void add(Extension *extension, StringView version)
Adds the given extension to the registry, ensuring the initialization methods are called at appropria...
Log implementation class.
Definition: LogManager.h:139
Contains the Engine, Renderer, resource managers and other systems needed to run Cogs....
std::shared_ptr< ComponentModel::Entity > EntityPtr
Smart pointer for Entity access.
Definition: EntityPtr.h:12
uint64_t COGSCORE_DLL_API parseISO8601(const StringView &iso8601)
Parse ISO 8601 timestamps.
Definition: Parsing.cpp:575
constexpr Log getLogger(const char(&name)[LEN]) noexcept
Definition: LogManager.h:180
void setChanged(Cogs::Core::Context *context, Cogs::ComponentModel::Component *component, Reflection::FieldId fieldId)
Must be Called after changing a Component field. Mark field changed. Request engine update.
Definition: FieldSetter.h:25
Defines an extension to Cogs.Core and provides methods to override in order to initialize extension c...
Component for displaying surface currents based on the IHO S111 standard.
std::vector< float > speeds
Speed in knots of the current at each position.
std::vector< int > priorities
Priority of each arrow.
std::vector< float > angles
Compass direction of the current at each position.
int priorityLevel
Hide arrows with priority greater than this.
std::vector< glm::vec2 > positions
Position of each arrow in this component in world coordinates.
virtual bool initialize(Context *context) override
Initialize extension for the given context.
virtual bool initializeStatic() override
Initialize extension statically.
virtual const char * getExtensionKey() const override
Get the extensions unique key, used to check for extension presence and retrieve extension specific d...
virtual const void * getPublicAPI() const override
Retrieve a pointer to a struct containing all publicly available function pointers.
@ Geometry
Run at the time geometry data is updated.
Definition: Engine.h:70