/src/proj/src/transformations/tinshift_gpkg.cpp
Line | Count | Source |
1 | | /****************************************************************************** |
2 | | * Project: PROJ |
3 | | * Purpose: Functionality related to TIN based transformations using a |
4 | | *GeoPackage file Author: Even Rouault, <even.rouault at spatialys.com> |
5 | | * |
6 | | ****************************************************************************** |
7 | | * Copyright (c) 2025, Even Rouault, <even.rouault at spatialys.com> |
8 | | * |
9 | | * SPDX-License-Identifier: MIT |
10 | | *****************************************************************************/ |
11 | | |
12 | | #define FROM_PROJ_CPP |
13 | | |
14 | | #include "tinshift_gpkg.hpp" |
15 | | #include "proj/internal/include_nlohmann_json.hpp" |
16 | | #include "proj/internal/internal.hpp" |
17 | | |
18 | | #ifdef EMBED_RESOURCE_FILES |
19 | | #include "embedded_resources.h" |
20 | | #endif |
21 | | |
22 | | #include <algorithm> |
23 | | #include <array> |
24 | | #include <cassert> |
25 | | #include <cmath> |
26 | | #include <cstring> |
27 | | #include <limits> |
28 | | #include <sstream> // std::ostringstream |
29 | | |
30 | | #include <sqlite3.h> |
31 | | |
32 | | using json = nlohmann::json; |
33 | | using namespace NS_PROJ; |
34 | | |
35 | | // --------------------------------------------------------------------------- |
36 | | |
37 | 0 | const char *TINShiftGeopackageException::what() const noexcept { |
38 | 0 | return msg_.c_str(); |
39 | 0 | } |
40 | | |
41 | | // --------------------------------------------------------------------------- |
42 | | |
43 | | std::unique_ptr<TINShiftGeopackageFile> |
44 | 0 | TINShiftGeopackageFile::open(PJ_CONTEXT *ctx, const std::string &filename) { |
45 | 0 | std::string sqliteURI(filename); |
46 | |
|
47 | 0 | auto poTinshift = |
48 | 0 | std::unique_ptr<TINShiftGeopackageFile>(new TINShiftGeopackageFile()); |
49 | 0 | if (internal::starts_with(filename, "http://") || |
50 | 0 | internal::starts_with(filename, "https://")) { |
51 | 0 | poTinshift->sqlite_vfs_ = SQLite3VFS::createNetwork(ctx); |
52 | 0 | } |
53 | |
|
54 | 0 | while (true) { |
55 | 0 | if (sqlite3_open_v2(sqliteURI.c_str(), &poTinshift->sqlite_handle_, |
56 | 0 | SQLITE_OPEN_READONLY | SQLITE_OPEN_URI, |
57 | 0 | poTinshift->sqlite_vfs_ |
58 | 0 | ? poTinshift->sqlite_vfs_->name() |
59 | 0 | : nullptr) != SQLITE_OK || |
60 | 0 | !poTinshift->sqlite_handle_) { |
61 | |
|
62 | 0 | #ifdef EMBED_RESOURCE_FILES |
63 | 0 | if (!poTinshift->sqlite_vfs_) { |
64 | 0 | unsigned int size = 0; |
65 | 0 | const unsigned char *in_memory_data = |
66 | 0 | pj_get_embedded_resource(filename.c_str(), &size); |
67 | 0 | if (!in_memory_data) { |
68 | 0 | throw TINShiftGeopackageException(std::string("Open of ") |
69 | 0 | .append(filename) |
70 | 0 | .append(" failed")); |
71 | 0 | } |
72 | | |
73 | 0 | poTinshift->sqlite_vfs_ = |
74 | 0 | SQLite3VFS::createMem(in_memory_data, size); |
75 | 0 | if (poTinshift->sqlite_vfs_ == nullptr) { |
76 | 0 | throw TINShiftGeopackageException(std::string("Open of ") |
77 | 0 | .append(filename) |
78 | 0 | .append(" failed")); |
79 | 0 | } |
80 | | |
81 | 0 | std::ostringstream buffer; |
82 | 0 | buffer << "file:/"; |
83 | 0 | buffer << filename; |
84 | 0 | buffer << "?immutable=1&ptr="; |
85 | 0 | buffer << reinterpret_cast<uintptr_t>(in_memory_data); |
86 | 0 | buffer << "&sz="; |
87 | 0 | buffer << size; |
88 | 0 | buffer << "&max="; |
89 | 0 | buffer << size; |
90 | 0 | buffer << "&vfs="; |
91 | 0 | buffer << poTinshift->sqlite_vfs_->name(); |
92 | 0 | sqliteURI = buffer.str(); |
93 | |
|
94 | 0 | continue; |
95 | 0 | } |
96 | 0 | #endif |
97 | | |
98 | 0 | throw TINShiftGeopackageException( |
99 | 0 | std::string("Open of ").append(sqliteURI).append(" failed")); |
100 | 0 | } else { |
101 | 0 | break; |
102 | 0 | } |
103 | 0 | } |
104 | | |
105 | 0 | poTinshift->getMetadata(); |
106 | 0 | poTinshift->prepareDatabaseQuery(); |
107 | |
|
108 | 0 | return poTinshift; |
109 | 0 | } |
110 | | |
111 | | // --------------------------------------------------------------------------- |
112 | | |
113 | 0 | TINShiftGeopackageFile::~TINShiftGeopackageFile() { |
114 | 0 | if (select_stmt_) |
115 | 0 | sqlite3_finalize(select_stmt_); |
116 | 0 | if (sqlite_handle_) |
117 | 0 | sqlite3_close(sqlite_handle_); |
118 | 0 | } |
119 | | |
120 | | // --------------------------------------------------------------------------- |
121 | | |
122 | | namespace { |
123 | | |
124 | 0 | static std::string getString(const json &j, const char *key, bool optional) { |
125 | 0 | if (!j.contains(key)) { |
126 | 0 | if (optional) { |
127 | 0 | return std::string(); |
128 | 0 | } |
129 | 0 | throw TINShiftGeopackageException(std::string("Missing \"") + key + |
130 | 0 | "\" key"); |
131 | 0 | } |
132 | 0 | const json v = j[key]; |
133 | 0 | if (!v.is_string()) { |
134 | 0 | throw TINShiftGeopackageException(std::string("The value of \"") + key + |
135 | 0 | "\" should be a string"); |
136 | 0 | } |
137 | 0 | return v.get<std::string>(); |
138 | 0 | } |
139 | | |
140 | 0 | static std::string getReqString(const json &j, const char *key) { |
141 | 0 | return getString(j, key, false); |
142 | 0 | } |
143 | | |
144 | 0 | static double getReqDouble(const json &j, const char *key) { |
145 | 0 | if (!j.contains(key)) { |
146 | 0 | throw TINShiftGeopackageException(std::string("Missing \"") + key + |
147 | 0 | "\" key"); |
148 | 0 | } |
149 | | |
150 | 0 | const json v = j[key]; |
151 | 0 | if (!v.is_number()) { |
152 | 0 | throw TINShiftGeopackageException(std::string("The value of \"") + key + |
153 | 0 | "\" should be a number"); |
154 | 0 | } |
155 | 0 | return v.get<double>(); |
156 | 0 | } |
157 | | |
158 | 0 | static int getReqInt(const json &j, const char *key) { |
159 | 0 | if (!j.contains(key)) { |
160 | 0 | throw TINShiftGeopackageException(std::string("Missing \"") + key + |
161 | 0 | "\" key"); |
162 | 0 | } |
163 | | |
164 | 0 | const json v = j[key]; |
165 | 0 | if (!v.is_number()) { |
166 | 0 | throw TINShiftGeopackageException(std::string("The value of \"") + key + |
167 | 0 | "\" should be an integer"); |
168 | 0 | } |
169 | 0 | return v.get<int>(); |
170 | 0 | } |
171 | | |
172 | | // --------------------------------------------------------------------------- |
173 | | |
174 | 0 | static json getArrayMember(const json &j, const char *key) { |
175 | 0 | if (!j.contains(key)) { |
176 | 0 | throw TINShiftGeopackageException(std::string("Missing \"") + key + |
177 | 0 | "\" key"); |
178 | 0 | } |
179 | 0 | const json obj = j[key]; |
180 | 0 | if (!obj.is_array()) { |
181 | 0 | throw TINShiftGeopackageException(std::string("The value of \"") + key + |
182 | 0 | "\" should be a array"); |
183 | 0 | } |
184 | 0 | return obj; |
185 | 0 | } |
186 | | |
187 | | } // namespace |
188 | | // --------------------------------------------------------------------------- |
189 | | |
190 | 0 | void TINShiftGeopackageFile::getMetadata() { |
191 | 0 | std::string metadata; |
192 | | // Get JSON metadata entry from gpkg_metadata table |
193 | 0 | { |
194 | 0 | char **papszResult = nullptr; |
195 | 0 | int nRows = 0; |
196 | 0 | char *pszErrMsg = nullptr; |
197 | 0 | if (sqlite3_get_table(sqlite_handle_, |
198 | 0 | "SELECT metadata FROM gpkg_metadata WHERE " |
199 | 0 | "md_standard_uri = 'https://proj.org'", |
200 | 0 | &papszResult, &nRows, nullptr, |
201 | 0 | &pszErrMsg) != SQLITE_OK) { |
202 | 0 | sqlite3_free_table(papszResult); |
203 | 0 | auto ex = TINShiftGeopackageException( |
204 | 0 | std::string("Cannot get metadata: ").append(pszErrMsg)); |
205 | 0 | sqlite3_free(pszErrMsg); |
206 | 0 | throw ex; |
207 | 0 | } |
208 | 0 | if (nRows != 1 || !papszResult[1]) { |
209 | 0 | sqlite3_free_table(papszResult); |
210 | 0 | throw TINShiftGeopackageException( |
211 | 0 | "Cannot get metadata in gpkg_metadata table"); |
212 | 0 | } |
213 | 0 | metadata = papszResult[1]; |
214 | 0 | sqlite3_free_table(papszResult); |
215 | 0 | } |
216 | | |
217 | 0 | json j; |
218 | 0 | try { |
219 | 0 | j = json::parse(metadata); |
220 | 0 | } catch (const std::exception &e) { |
221 | 0 | throw TINShiftGeopackageException( |
222 | 0 | std::string("Cannot parse JSON metadata: ").append(e.what())); |
223 | 0 | } |
224 | 0 | if (!j.is_object()) { |
225 | 0 | throw TINShiftGeopackageException("JSON metadata is not an object"); |
226 | 0 | } |
227 | | |
228 | 0 | if (getReqString(j, "file_type") != "triangulation_file") { |
229 | 0 | throw TINShiftGeopackageException("File is not of the expected type"); |
230 | 0 | } |
231 | | |
232 | 0 | const std::string version = getReqString(j, "format_version"); |
233 | 0 | if (!internal::starts_with(version, "1.")) { |
234 | 0 | throw TINShiftGeopackageException(std::string("format_version = ") |
235 | 0 | .append(version) |
236 | 0 | .append(" is not supported")); |
237 | 0 | } |
238 | | |
239 | 0 | const auto jTransformedComponents = |
240 | 0 | getArrayMember(j, "transformed_components"); |
241 | 0 | for (const json &jComp : jTransformedComponents) { |
242 | 0 | if (!jComp.is_string()) { |
243 | 0 | throw TINShiftGeopackageException( |
244 | 0 | "transformed_components[] item is not a string"); |
245 | 0 | } |
246 | 0 | const auto jCompStr = jComp.get<std::string>(); |
247 | 0 | if (jCompStr == "horizontal") { |
248 | 0 | horizontalTransformation_ = true; |
249 | 0 | } else if (jCompStr == "vertical") { |
250 | 0 | verticalTransformation_ = true; |
251 | 0 | } else { |
252 | 0 | throw TINShiftGeopackageException( |
253 | 0 | "transformed_components[] = " + jCompStr + " is not handled"); |
254 | 0 | } |
255 | 0 | } |
256 | | |
257 | 0 | if (horizontalTransformation_) { |
258 | 0 | min_shift_x_ = getReqDouble(j, "min_shift_x"); |
259 | 0 | min_shift_y_ = getReqDouble(j, "min_shift_y"); |
260 | 0 | max_shift_x_ = getReqDouble(j, "max_shift_x"); |
261 | 0 | max_shift_y_ = getReqDouble(j, "max_shift_y"); |
262 | 0 | } |
263 | |
|
264 | 0 | if (j.contains("fallback_strategy")) { |
265 | 0 | const auto fallback_strategy = getReqString(j, "fallback_strategy"); |
266 | 0 | if (fallback_strategy == "nearest_side") { |
267 | 0 | fallbackStrategy_ = FALLBACK_NEAREST_SIDE; |
268 | 0 | } else if (fallback_strategy == "nearest_centroid") { |
269 | 0 | fallbackStrategy_ = FALLBACK_NEAREST_CENTROID; |
270 | 0 | } else if (fallback_strategy == "none") { |
271 | 0 | fallbackStrategy_ = FALLBACK_NONE; |
272 | 0 | } else { |
273 | 0 | throw TINShiftGeopackageException("invalid fallback_strategy"); |
274 | 0 | } |
275 | 0 | } |
276 | | |
277 | 0 | if (fallbackStrategy_ != FALLBACK_NONE) { |
278 | 0 | numVertices_ = getReqInt(j, "num_vertices"); |
279 | 0 | if (numVertices_ <= 0) { |
280 | 0 | throw TINShiftGeopackageException("invalid value for num_vertices"); |
281 | 0 | } |
282 | 0 | } |
283 | | |
284 | | // Get bounding box of vertices table |
285 | 0 | { |
286 | 0 | char **papszResult = nullptr; |
287 | 0 | int nRows = 0; |
288 | 0 | char *pszErrMsg = nullptr; |
289 | 0 | if (sqlite3_get_table(sqlite_handle_, |
290 | 0 | "SELECT min_x, min_y, max_x, max_y FROM " |
291 | 0 | "gpkg_contents WHERE table_name = 'vertices'", |
292 | 0 | &papszResult, &nRows, nullptr, |
293 | 0 | &pszErrMsg) != SQLITE_OK) { |
294 | 0 | sqlite3_free_table(papszResult); |
295 | 0 | auto ex = TINShiftGeopackageException( |
296 | 0 | std::string("Cannot get bounding box of vertices table: ") |
297 | 0 | .append(pszErrMsg)); |
298 | 0 | sqlite3_free(pszErrMsg); |
299 | 0 | throw ex; |
300 | 0 | } |
301 | 0 | if (nRows != 1 || !papszResult[4] || !papszResult[5] || |
302 | 0 | !papszResult[6] || !papszResult[7]) { |
303 | 0 | sqlite3_free_table(papszResult); |
304 | 0 | throw TINShiftGeopackageException( |
305 | 0 | "Cannot get bounding box of vertices table"); |
306 | 0 | } |
307 | 0 | min_x_ = internal::c_locale_stod(papszResult[4]); |
308 | 0 | min_y_ = internal::c_locale_stod(papszResult[5]); |
309 | 0 | max_x_ = internal::c_locale_stod(papszResult[6]); |
310 | 0 | max_y_ = internal::c_locale_stod(papszResult[7]); |
311 | 0 | sqlite3_free_table(papszResult); |
312 | 0 | if (!(min_x_ < max_x_) || !(min_y_ < max_y_)) { |
313 | 0 | throw TINShiftGeopackageException( |
314 | 0 | "Invalid bounding box of vertices table"); |
315 | 0 | } |
316 | 0 | } |
317 | 0 | } |
318 | | |
319 | | // --------------------------------------------------------------------------- |
320 | | |
321 | 0 | void TINShiftGeopackageFile::prepareDatabaseQuery() { |
322 | | // Get field names of vertices table |
323 | 0 | { |
324 | 0 | char **papszResult = nullptr; |
325 | 0 | int nRows = 0; |
326 | 0 | int nCols = 0; |
327 | 0 | char *pszErrMsg = nullptr; |
328 | 0 | if (sqlite3_get_table(sqlite_handle_, "PRAGMA table_info(vertices)", |
329 | 0 | &papszResult, &nRows, &nCols, |
330 | 0 | &pszErrMsg) != SQLITE_OK) { |
331 | 0 | sqlite3_free_table(papszResult); |
332 | 0 | auto ex = TINShiftGeopackageException( |
333 | 0 | std::string("Cannot get definition of table vertices: ") |
334 | 0 | .append(pszErrMsg)); |
335 | 0 | sqlite3_free(pszErrMsg); |
336 | 0 | throw ex; |
337 | 0 | } |
338 | 0 | if (nRows == 0 || nCols != 6) { |
339 | 0 | sqlite3_free_table(papszResult); |
340 | 0 | throw TINShiftGeopackageException( |
341 | 0 | "Cannot get definition of table vertices"); |
342 | 0 | } |
343 | 0 | for (int i = 0; i < nRows; ++i) { |
344 | 0 | const char *pszColName = papszResult[(i + 1) * nCols + 1]; |
345 | 0 | if (pszColName && strcmp(pszColName, "fid") != 0 && |
346 | 0 | strcmp(pszColName, "geom") != 0) { |
347 | 0 | std::string osColName(pszColName); |
348 | 0 | if (osColName == "source_z") |
349 | 0 | sourceZ_ = true; |
350 | 0 | else if (osColName == "target_x") |
351 | 0 | targetX_ = true; |
352 | 0 | else if (osColName == "target_y") |
353 | 0 | targetY_ = true; |
354 | 0 | else if (osColName == "target_z") |
355 | 0 | targetZ_ = true; |
356 | 0 | else if (osColName == "offset_z") |
357 | 0 | offsetZ_ = true; |
358 | 0 | vertices_cols_.push_back(std::move(osColName)); |
359 | 0 | } |
360 | 0 | } |
361 | 0 | sqlite3_free_table(papszResult); |
362 | 0 | } |
363 | | |
364 | 0 | if (horizontalTransformation_ && !targetX_) |
365 | 0 | throw TINShiftGeopackageException( |
366 | 0 | "target_x field missing in table vertices"); |
367 | 0 | if (horizontalTransformation_ && !targetY_) |
368 | 0 | throw TINShiftGeopackageException( |
369 | 0 | "target_y field missing in table vertices"); |
370 | 0 | if (verticalTransformation_ && !((sourceZ_ && targetZ_) || offsetZ_)) |
371 | 0 | throw TINShiftGeopackageException("(source_z and target_z) or offset_z " |
372 | 0 | "fields missing in table vertices"); |
373 | | |
374 | 0 | std::vector<std::string> colsToRequest; |
375 | 0 | if (horizontalTransformation_) { |
376 | 0 | colsToRequest.push_back("target_x"); |
377 | 0 | colsToRequest.push_back("target_y"); |
378 | 0 | } |
379 | 0 | if (verticalTransformation_) { |
380 | 0 | if (sourceZ_) { |
381 | 0 | colsToRequest.push_back("source_z"); |
382 | 0 | colsToRequest.push_back("target_z"); |
383 | 0 | } else { |
384 | 0 | colsToRequest.push_back("offset_z"); |
385 | 0 | } |
386 | 0 | } |
387 | |
|
388 | 0 | std::string sql( |
389 | 0 | "SELECT v1.geom AS v1_geom, v2.geom AS v2_geom, v3.geom AS v3_geom"); |
390 | 0 | for (const std::string &col : colsToRequest) { |
391 | 0 | sql += ", v1."; |
392 | 0 | sql += col; |
393 | 0 | sql += " AS v1_"; |
394 | 0 | sql += col; |
395 | |
|
396 | 0 | sql += ", v2."; |
397 | 0 | sql += col; |
398 | 0 | sql += " AS v2_"; |
399 | 0 | sql += col; |
400 | |
|
401 | 0 | sql += ", v3."; |
402 | 0 | sql += col; |
403 | 0 | sql += " AS v3_"; |
404 | 0 | sql += col; |
405 | 0 | } |
406 | 0 | sql += " FROM triangles_def " |
407 | 0 | "LEFT JOIN vertices v1 ON idx_vertex1 = v1.fid " |
408 | 0 | "LEFT JOIN vertices v2 ON idx_vertex2 = v2.fid " |
409 | 0 | "LEFT JOIN vertices v3 ON idx_vertex3 = v3.fid " |
410 | 0 | "WHERE triangles_def.fid IN (" |
411 | 0 | "SELECT id FROM rtree_triangles_geom " |
412 | 0 | "WHERE maxx >= ? AND minx <= ? AND maxy >= ? AND miny <= ?)"; |
413 | |
|
414 | 0 | sqlite3_prepare_v2(sqlite_handle_, sql.c_str(), -1, &select_stmt_, nullptr); |
415 | 0 | if (!select_stmt_) { |
416 | 0 | throw TINShiftGeopackageException(sqlite3_errmsg(sqlite_handle_)); |
417 | 0 | } |
418 | 0 | } |
419 | | |
420 | | // --------------------------------------------------------------------------- |
421 | | |
422 | | /************************************************************************/ |
423 | | /* swap_words() */ |
424 | | /* */ |
425 | | /* Convert the byte order of the given word(s) in place. */ |
426 | | /************************************************************************/ |
427 | | |
428 | | static const int byte_order_test = 1; |
429 | | #define IS_LSB \ |
430 | 0 | (1 == (reinterpret_cast<const unsigned char *>(&byte_order_test))[0]) |
431 | | |
432 | | static void swap_words(void *dataIn, size_t word_size, size_t word_count) |
433 | | |
434 | 0 | { |
435 | 0 | unsigned char *data = static_cast<unsigned char *>(dataIn); |
436 | 0 | for (size_t word = 0; word < word_count; word++) { |
437 | 0 | for (size_t i = 0; i < word_size / 2; i++) { |
438 | 0 | unsigned char t; |
439 | |
|
440 | 0 | t = data[i]; |
441 | 0 | data[i] = data[word_size - i - 1]; |
442 | 0 | data[word_size - i - 1] = t; |
443 | 0 | } |
444 | |
|
445 | 0 | data += word_size; |
446 | 0 | } |
447 | 0 | } |
448 | | |
449 | | // --------------------------------------------------------------------------- |
450 | | |
451 | 0 | static inline double sqr(double x) { return x * x; } |
452 | | static inline double squared_distance(double x1, double y1, double x2, |
453 | 0 | double y2) { |
454 | 0 | return sqr(x1 - x2) + sqr(y1 - y2); |
455 | 0 | } |
456 | | static double sq_distance_point_segment(double x, double y, double x1, |
457 | | double y1, double x2, double y2, |
458 | 0 | double dist12) { |
459 | | // squared distance of point x/y to line segment x1/y1 -- x2/y2 |
460 | 0 | const double t = ((x - x1) * (x2 - x1) + (y - y1) * (y2 - y1)) / dist12; |
461 | 0 | if (t <= 0.0) { |
462 | | // closest to x1/y1 |
463 | 0 | return squared_distance(x, y, x1, y1); |
464 | 0 | } |
465 | 0 | if (t >= 1.0) { |
466 | | // closest to y2/y2 |
467 | 0 | return squared_distance(x, y, x2, y2); |
468 | 0 | } |
469 | | |
470 | | // closest to line segment x1/y1 -- x2/y2 |
471 | 0 | return squared_distance(x, y, x1 + t * (x2 - x1), y1 + t * (y2 - y1)); |
472 | 0 | } |
473 | | |
474 | | // --------------------------------------------------------------------------- |
475 | | |
476 | | bool TINShiftGeopackageFile::findTriangle( |
477 | | double x, double y, bool forwardDirection, double &lambda1, double &lambda2, |
478 | | double &lambda3, std::vector<double> &valsVertex1, |
479 | 0 | std::vector<double> &valsVertex2, std::vector<double> &valsVertex3) { |
480 | |
|
481 | 0 | const bool useForwardDirection = |
482 | 0 | forwardDirection || !horizontalTransformation_; |
483 | | |
484 | | // Compute barycentric coefficients |
485 | 0 | const auto computeLambdas = [x, y, &lambda1, &lambda2, |
486 | 0 | &lambda3](const std::array<double, 3> &vX, |
487 | 0 | const std::array<double, 3> &vY, |
488 | 0 | bool checkWithinTriangle) { |
489 | 0 | constexpr double EPSILON = 1e-10; |
490 | 0 | const double det_T = (vY[1] - vY[2]) * (vX[0] - vX[2]) + |
491 | 0 | (vX[2] - vX[1]) * (vY[0] - vY[2]); |
492 | 0 | if (std::fabs(det_T) < EPSILON) { |
493 | | // Degenerate triangle |
494 | 0 | return false; |
495 | 0 | } |
496 | 0 | lambda1 = |
497 | 0 | ((vY[1] - vY[2]) * (x - vX[2]) + (vX[2] - vX[1]) * (y - vY[2])) / |
498 | 0 | det_T; |
499 | 0 | lambda2 = |
500 | 0 | ((vY[2] - vY[0]) * (x - vX[2]) + (vX[0] - vX[2]) * (y - vY[2])) / |
501 | 0 | det_T; |
502 | 0 | lambda3 = 1 - lambda1 - lambda2; |
503 | 0 | return !checkWithinTriangle || |
504 | 0 | (lambda1 >= -EPSILON && lambda1 <= 1 + EPSILON && |
505 | 0 | lambda2 >= -EPSILON && lambda2 <= 1 + EPSILON && |
506 | 0 | lambda3 >= -EPSILON && lambda3 <= 1 + EPSILON); |
507 | 0 | }; |
508 | | |
509 | | // Get the current triangle's vertices coordinates |
510 | 0 | const auto getXY = [this, useForwardDirection](std::array<double, 3> &vX, |
511 | 0 | std::array<double, 3> &vY) { |
512 | 0 | if (useForwardDirection) { |
513 | 0 | const unsigned char *v1_geom = static_cast<const unsigned char *>( |
514 | 0 | sqlite3_column_blob(select_stmt_, 0)); |
515 | 0 | const unsigned char *v2_geom = static_cast<const unsigned char *>( |
516 | 0 | sqlite3_column_blob(select_stmt_, 1)); |
517 | 0 | const unsigned char *v3_geom = static_cast<const unsigned char *>( |
518 | 0 | sqlite3_column_blob(select_stmt_, 2)); |
519 | 0 | constexpr int SIZEOF_GPKG_POINT_PREFIX = 8 + 1 + 4; |
520 | 0 | constexpr int SIZEOF_GPKG_POINT = |
521 | 0 | SIZEOF_GPKG_POINT_PREFIX + 2 * static_cast<int>(sizeof(double)); |
522 | 0 | if (sqlite3_column_bytes(select_stmt_, 0) != SIZEOF_GPKG_POINT || |
523 | 0 | sqlite3_column_bytes(select_stmt_, 1) != SIZEOF_GPKG_POINT || |
524 | 0 | sqlite3_column_bytes(select_stmt_, 2) != SIZEOF_GPKG_POINT) { |
525 | 0 | return false; |
526 | 0 | } |
527 | | |
528 | 0 | memcpy(&vX[0], v1_geom + SIZEOF_GPKG_POINT_PREFIX, sizeof(double)); |
529 | 0 | memcpy(&vY[0], v1_geom + SIZEOF_GPKG_POINT_PREFIX + sizeof(double), |
530 | 0 | sizeof(double)); |
531 | 0 | memcpy(&vX[1], v2_geom + SIZEOF_GPKG_POINT_PREFIX, sizeof(double)); |
532 | 0 | memcpy(&vY[1], v2_geom + SIZEOF_GPKG_POINT_PREFIX + sizeof(double), |
533 | 0 | sizeof(double)); |
534 | 0 | memcpy(&vX[2], v3_geom + SIZEOF_GPKG_POINT_PREFIX, sizeof(double)); |
535 | 0 | memcpy(&vY[2], v3_geom + SIZEOF_GPKG_POINT_PREFIX + sizeof(double), |
536 | 0 | sizeof(double)); |
537 | |
|
538 | 0 | if (!IS_LSB) { |
539 | 0 | swap_words(vX.data(), sizeof(double), vX.size()); |
540 | 0 | swap_words(vY.data(), sizeof(double), vY.size()); |
541 | 0 | } |
542 | 0 | } else { |
543 | 0 | vX[0] = sqlite3_column_double(select_stmt_, 3); |
544 | 0 | vX[1] = sqlite3_column_double(select_stmt_, 4); |
545 | 0 | vX[2] = sqlite3_column_double(select_stmt_, 5); |
546 | 0 | vY[0] = sqlite3_column_double(select_stmt_, 6); |
547 | 0 | vY[1] = sqlite3_column_double(select_stmt_, 7); |
548 | 0 | vY[2] = sqlite3_column_double(select_stmt_, 8); |
549 | 0 | } |
550 | 0 | return true; |
551 | 0 | }; |
552 | | |
553 | | // Get the values at the vertices of the current triangle |
554 | 0 | const auto collectValsVertex = [this, useForwardDirection, &valsVertex1, |
555 | 0 | &valsVertex2, &valsVertex3]() { |
556 | 0 | if (useForwardDirection) { |
557 | 0 | int nextAttrIdx = 3; |
558 | |
|
559 | 0 | if (horizontalTransformation_) { |
560 | | // target_x |
561 | 0 | valsVertex1.push_back(sqlite3_column_double(select_stmt_, 3)); |
562 | | // target_y |
563 | 0 | valsVertex1.push_back(sqlite3_column_double(select_stmt_, 6)); |
564 | | |
565 | | // target_x |
566 | 0 | valsVertex2.push_back(sqlite3_column_double(select_stmt_, 4)); |
567 | | // target_y |
568 | 0 | valsVertex2.push_back(sqlite3_column_double(select_stmt_, 7)); |
569 | | |
570 | | // target_x |
571 | 0 | valsVertex3.push_back(sqlite3_column_double(select_stmt_, 5)); |
572 | | // target_y |
573 | 0 | valsVertex3.push_back(sqlite3_column_double(select_stmt_, 8)); |
574 | |
|
575 | 0 | nextAttrIdx = 9; |
576 | 0 | } |
577 | |
|
578 | 0 | if (verticalTransformation_) { |
579 | 0 | if (sourceZ_) { |
580 | | // source_z |
581 | 0 | valsVertex1.push_back( |
582 | 0 | sqlite3_column_double(select_stmt_, nextAttrIdx)); |
583 | | // target_z |
584 | 0 | valsVertex1.push_back( |
585 | 0 | sqlite3_column_double(select_stmt_, nextAttrIdx + 3)); |
586 | | |
587 | | // source_z |
588 | 0 | valsVertex2.push_back( |
589 | 0 | sqlite3_column_double(select_stmt_, nextAttrIdx + 1)); |
590 | | // target_z |
591 | 0 | valsVertex2.push_back( |
592 | 0 | sqlite3_column_double(select_stmt_, nextAttrIdx + 4)); |
593 | | |
594 | | // source_z |
595 | 0 | valsVertex3.push_back( |
596 | 0 | sqlite3_column_double(select_stmt_, nextAttrIdx + 2)); |
597 | | // target_z |
598 | 0 | valsVertex3.push_back( |
599 | 0 | sqlite3_column_double(select_stmt_, nextAttrIdx + 5)); |
600 | 0 | } else { |
601 | | // offset_z |
602 | 0 | valsVertex1.push_back( |
603 | 0 | sqlite3_column_double(select_stmt_, nextAttrIdx)); |
604 | 0 | valsVertex2.push_back( |
605 | 0 | sqlite3_column_double(select_stmt_, nextAttrIdx + 1)); |
606 | 0 | valsVertex3.push_back( |
607 | 0 | sqlite3_column_double(select_stmt_, nextAttrIdx + 2)); |
608 | 0 | } |
609 | 0 | } |
610 | 0 | } else { |
611 | 0 | int nextAttrIdx = 3; |
612 | |
|
613 | 0 | if (horizontalTransformation_) { |
614 | |
|
615 | 0 | const unsigned char *v1_geom = |
616 | 0 | static_cast<const unsigned char *>( |
617 | 0 | sqlite3_column_blob(select_stmt_, 0)); |
618 | 0 | const unsigned char *v2_geom = |
619 | 0 | static_cast<const unsigned char *>( |
620 | 0 | sqlite3_column_blob(select_stmt_, 1)); |
621 | 0 | const unsigned char *v3_geom = |
622 | 0 | static_cast<const unsigned char *>( |
623 | 0 | sqlite3_column_blob(select_stmt_, 2)); |
624 | 0 | constexpr int SIZEOF_GPKG_POINT_PREFIX = 8 + 1 + 4; |
625 | 0 | constexpr int SIZEOF_GPKG_POINT = |
626 | 0 | SIZEOF_GPKG_POINT_PREFIX + |
627 | 0 | 2 * static_cast<int>(sizeof(double)); |
628 | 0 | if (sqlite3_column_bytes(select_stmt_, 0) != |
629 | 0 | SIZEOF_GPKG_POINT || |
630 | 0 | sqlite3_column_bytes(select_stmt_, 1) != |
631 | 0 | SIZEOF_GPKG_POINT || |
632 | 0 | sqlite3_column_bytes(select_stmt_, 2) != |
633 | 0 | SIZEOF_GPKG_POINT) { |
634 | 0 | return false; |
635 | 0 | } |
636 | 0 | double vX[3], vY[3]; |
637 | 0 | memcpy(&vX[0], v1_geom + SIZEOF_GPKG_POINT_PREFIX, |
638 | 0 | sizeof(double)); |
639 | 0 | memcpy(&vY[0], |
640 | 0 | v1_geom + SIZEOF_GPKG_POINT_PREFIX + sizeof(double), |
641 | 0 | sizeof(double)); |
642 | 0 | memcpy(&vX[1], v2_geom + SIZEOF_GPKG_POINT_PREFIX, |
643 | 0 | sizeof(double)); |
644 | 0 | memcpy(&vY[1], |
645 | 0 | v2_geom + SIZEOF_GPKG_POINT_PREFIX + sizeof(double), |
646 | 0 | sizeof(double)); |
647 | 0 | memcpy(&vX[2], v3_geom + SIZEOF_GPKG_POINT_PREFIX, |
648 | 0 | sizeof(double)); |
649 | 0 | memcpy(&vY[2], |
650 | 0 | v3_geom + SIZEOF_GPKG_POINT_PREFIX + sizeof(double), |
651 | 0 | sizeof(double)); |
652 | |
|
653 | 0 | if (!IS_LSB) { |
654 | 0 | swap_words(vX, sizeof(double), 3); |
655 | 0 | swap_words(vY, sizeof(double), 3); |
656 | 0 | } |
657 | | |
658 | | // source_x |
659 | 0 | valsVertex1.push_back(vX[0]); |
660 | | // source_y |
661 | 0 | valsVertex1.push_back(vY[0]); |
662 | | |
663 | | // source_x |
664 | 0 | valsVertex2.push_back(vX[1]); |
665 | | // source_y |
666 | 0 | valsVertex2.push_back(vY[1]); |
667 | | |
668 | | // source_x |
669 | 0 | valsVertex3.push_back(vX[2]); |
670 | | // source_y |
671 | 0 | valsVertex3.push_back(vY[2]); |
672 | |
|
673 | 0 | nextAttrIdx = 9; |
674 | 0 | } |
675 | | |
676 | 0 | if (verticalTransformation_) { |
677 | 0 | if (sourceZ_) { |
678 | | // source_z |
679 | 0 | valsVertex1.push_back( |
680 | 0 | sqlite3_column_double(select_stmt_, nextAttrIdx)); |
681 | | // target_z |
682 | 0 | valsVertex1.push_back( |
683 | 0 | sqlite3_column_double(select_stmt_, nextAttrIdx + 3)); |
684 | | |
685 | | // source_z |
686 | 0 | valsVertex2.push_back( |
687 | 0 | sqlite3_column_double(select_stmt_, nextAttrIdx + 1)); |
688 | | // target_z |
689 | 0 | valsVertex2.push_back( |
690 | 0 | sqlite3_column_double(select_stmt_, nextAttrIdx + 4)); |
691 | | |
692 | | // source_z |
693 | 0 | valsVertex3.push_back( |
694 | 0 | sqlite3_column_double(select_stmt_, nextAttrIdx + 2)); |
695 | | // target_z |
696 | 0 | valsVertex3.push_back( |
697 | 0 | sqlite3_column_double(select_stmt_, nextAttrIdx + 5)); |
698 | 0 | } else { |
699 | | // offset_z |
700 | 0 | valsVertex1.push_back( |
701 | 0 | sqlite3_column_double(select_stmt_, nextAttrIdx)); |
702 | 0 | valsVertex2.push_back( |
703 | 0 | sqlite3_column_double(select_stmt_, nextAttrIdx + 1)); |
704 | 0 | valsVertex3.push_back( |
705 | 0 | sqlite3_column_double(select_stmt_, nextAttrIdx + 2)); |
706 | 0 | } |
707 | 0 | } |
708 | 0 | } |
709 | 0 | return true; |
710 | 0 | }; |
711 | | |
712 | | // Try to use last triangle |
713 | 0 | if (cachedForwardDirection_ == forwardDirection && |
714 | 0 | !cachedValsVertex1_.empty() && |
715 | 0 | computeLambdas(cachedVerticesX_, cachedVerticesY_, |
716 | 0 | /* checkWithinTriangle = */ true)) { |
717 | 0 | valsVertex1 = cachedValsVertex1_; |
718 | 0 | valsVertex2 = cachedValsVertex2_; |
719 | 0 | valsVertex3 = cachedValsVertex3_; |
720 | 0 | return true; |
721 | 0 | } |
722 | | |
723 | 0 | constexpr double EPSILON = 1e-10; |
724 | |
|
725 | 0 | sqlite3_reset(select_stmt_); |
726 | 0 | if (useForwardDirection) { |
727 | 0 | sqlite3_bind_double(select_stmt_, 1, x - EPSILON); |
728 | 0 | sqlite3_bind_double(select_stmt_, 2, x + EPSILON); |
729 | 0 | sqlite3_bind_double(select_stmt_, 3, y - EPSILON); |
730 | 0 | sqlite3_bind_double(select_stmt_, 4, y + EPSILON); |
731 | 0 | } else { |
732 | | // Take into account the shift between source and target horizontal |
733 | | // coordinates when searching in the reverse direction. |
734 | 0 | sqlite3_bind_double(select_stmt_, 1, x - max_shift_x_ - EPSILON); |
735 | 0 | sqlite3_bind_double(select_stmt_, 2, x - min_shift_x_ + EPSILON); |
736 | 0 | sqlite3_bind_double(select_stmt_, 3, y - max_shift_y_ - EPSILON); |
737 | 0 | sqlite3_bind_double(select_stmt_, 4, y - min_shift_y_ + EPSILON); |
738 | 0 | } |
739 | | |
740 | | // Iterate over triangles whose bounding box intersecs the point of interest |
741 | 0 | while (sqlite3_step(select_stmt_) == SQLITE_ROW) { |
742 | 0 | std::array<double, 3> vX, vY; |
743 | 0 | if (!getXY(vX, vY)) |
744 | 0 | return false; |
745 | | |
746 | 0 | if (computeLambdas(vX, vY, /* checkWithinTriangle = */ true)) { |
747 | 0 | const bool status = collectValsVertex(); |
748 | 0 | if (status) { |
749 | 0 | cachedForwardDirection_ = forwardDirection; |
750 | 0 | cachedVerticesX_ = vX; |
751 | 0 | cachedVerticesY_ = vY; |
752 | 0 | cachedValsVertex1_ = valsVertex1; |
753 | 0 | cachedValsVertex2_ = valsVertex2; |
754 | 0 | cachedValsVertex3_ = valsVertex3; |
755 | 0 | } |
756 | 0 | return status; |
757 | 0 | } |
758 | 0 | } |
759 | | |
760 | 0 | if (fallbackStrategy_ == FALLBACK_NONE) { |
761 | 0 | return false; |
762 | 0 | } |
763 | | |
764 | 0 | const double x_search = std::clamp( |
765 | 0 | x + (useForwardDirection ? 0 : -(min_shift_x_ + max_shift_x_) * 0.5), |
766 | 0 | min_x_, max_x_); |
767 | 0 | const double y_search = std::clamp( |
768 | 0 | y + (useForwardDirection ? 0 : -(min_shift_y_ + max_shift_y_) * 0.5), |
769 | 0 | min_y_, max_y_); |
770 | 0 | double closest_sq_dist = std::numeric_limits<double>::infinity(); |
771 | 0 | double closest_dist = std::numeric_limits<double>::infinity(); |
772 | 0 | std::array<double, 3> closest_vX{0, 0, 0}; |
773 | 0 | std::array<double, 3> closest_vY{0, 0, 0}; |
774 | | |
775 | | // Initiate the search radius from the density of points |
776 | | // and double it if there is no match. |
777 | 0 | const double bbox_w = max_x_ - min_x_; |
778 | 0 | const double bbox_h = max_y_ - min_y_; |
779 | 0 | double radius = sqrt(bbox_w * bbox_h / numVertices_); |
780 | 0 | constexpr int MAX_ITERS = 20; |
781 | 0 | for (int iter = 0; iter <= MAX_ITERS && radius <= std::max(bbox_w, bbox_h); |
782 | 0 | ++iter) { |
783 | 0 | sqlite3_reset(select_stmt_); |
784 | 0 | sqlite3_bind_double(select_stmt_, 1, x_search - radius); |
785 | 0 | sqlite3_bind_double(select_stmt_, 2, x_search + radius); |
786 | 0 | sqlite3_bind_double(select_stmt_, 3, y_search - radius); |
787 | 0 | sqlite3_bind_double(select_stmt_, 4, y_search + radius); |
788 | | |
789 | | // Iterate over triangles in the current area of search |
790 | 0 | while (sqlite3_step(select_stmt_) == SQLITE_ROW) { |
791 | 0 | std::array<double, 3> vX, vY; |
792 | 0 | if (!getXY(vX, vY)) |
793 | 0 | return false; |
794 | | |
795 | | // don't check this triangle if the query point plusminus the |
796 | | // currently closest found distance is outside the triangle's |
797 | | // bounding box |
798 | 0 | if (x + closest_dist < std::min(vX[0], std::min(vX[1], vX[2])) || |
799 | 0 | x - closest_dist > std::max(vX[0], std::max(vX[1], vX[2])) || |
800 | 0 | y + closest_dist < std::min(vY[0], std::min(vY[1], vY[2])) || |
801 | 0 | y - closest_dist > std::max(vY[0], std::min(vY[1], vY[2]))) { |
802 | 0 | continue; |
803 | 0 | } |
804 | 0 | const double dist12 = squared_distance(vX[0], vY[0], vX[1], vY[1]); |
805 | 0 | const double dist23 = squared_distance(vX[1], vY[1], vX[2], vY[2]); |
806 | 0 | const double dist13 = squared_distance(vX[0], vY[0], vX[2], vY[2]); |
807 | 0 | if (dist12 < EPSILON || dist23 < EPSILON || dist13 < EPSILON) { |
808 | | // do not use degenerate triangles |
809 | 0 | continue; |
810 | 0 | } |
811 | | |
812 | 0 | bool isClosest = false; |
813 | 0 | if (fallbackStrategy_ == FALLBACK_NEAREST_SIDE) { |
814 | | // we don't know whether the points of the triangle are given |
815 | | // clockwise or counter-clockwise, so we have to check the |
816 | | // distance of the point to all three sides of the triangle |
817 | 0 | double sq_dist = sq_distance_point_segment( |
818 | 0 | x, y, vX[0], vY[0], vX[1], vY[1], dist12); |
819 | 0 | if (sq_dist < closest_sq_dist) { |
820 | 0 | closest_sq_dist = sq_dist; |
821 | 0 | isClosest = true; |
822 | 0 | } |
823 | 0 | sq_dist = sq_distance_point_segment(x, y, vX[1], vY[1], vX[2], |
824 | 0 | vY[2], dist23); |
825 | 0 | if (sq_dist < closest_sq_dist) { |
826 | 0 | closest_sq_dist = sq_dist; |
827 | 0 | isClosest = true; |
828 | 0 | } |
829 | 0 | sq_dist = sq_distance_point_segment(x, y, vX[0], vY[0], vX[2], |
830 | 0 | vY[2], dist13); |
831 | 0 | if (sq_dist < closest_sq_dist) { |
832 | 0 | closest_sq_dist = sq_dist; |
833 | 0 | isClosest = true; |
834 | 0 | } |
835 | 0 | } else { |
836 | 0 | assert(fallbackStrategy_ == FALLBACK_NEAREST_CENTROID); |
837 | 0 | const double c_x = (vX[0] + vX[1] + vX[2]) / 3.0; |
838 | 0 | const double c_y = (vY[0] + vY[1] + vY[2]) / 3.0; |
839 | 0 | const double sq_dist = squared_distance(x, y, c_x, c_y); |
840 | 0 | if (sq_dist < closest_sq_dist) { |
841 | 0 | closest_sq_dist = sq_dist; |
842 | 0 | isClosest = true; |
843 | 0 | } |
844 | 0 | } |
845 | 0 | if (isClosest) { |
846 | 0 | closest_dist = sqrt(closest_sq_dist); |
847 | 0 | closest_vX = vX; |
848 | 0 | closest_vY = vY; |
849 | 0 | valsVertex1.clear(); |
850 | 0 | valsVertex2.clear(); |
851 | 0 | valsVertex3.clear(); |
852 | 0 | if (!collectValsVertex()) |
853 | 0 | return false; |
854 | 0 | } |
855 | 0 | } |
856 | | |
857 | 0 | if (std::isinf(closest_sq_dist)) { |
858 | | // No match: increase search radius |
859 | 0 | radius *= 2; |
860 | 0 | } else { |
861 | 0 | break; |
862 | 0 | } |
863 | 0 | } |
864 | 0 | if (std::isinf(closest_sq_dist)) { |
865 | | // nothing was found due to empty triangle list or only degenerate |
866 | | // triangles |
867 | 0 | return false; |
868 | 0 | } |
869 | | |
870 | 0 | return computeLambdas(closest_vX, closest_vY, |
871 | 0 | /* checkWithinTriangle = */ false); |
872 | 0 | } |
873 | | |
874 | | // --------------------------------------------------------------------------- |
875 | | |
876 | | TINShiftGeopackageEvaluator::TINShiftGeopackageEvaluator( |
877 | | std::unique_ptr<TINShiftGeopackageFile> fileIn) |
878 | 0 | : file_(std::move(fileIn)) {} |
879 | | |
880 | | // --------------------------------------------------------------------------- |
881 | | |
882 | | bool TINShiftGeopackageEvaluator::transform(bool forwardDirection, double x, |
883 | | double y, double z, double &x_out, |
884 | 0 | double &y_out, double &z_out) { |
885 | 0 | double lambda1 = 0; |
886 | 0 | double lambda2 = 0; |
887 | 0 | double lambda3 = 0; |
888 | 0 | std::vector<double> valsVertex1; |
889 | 0 | std::vector<double> valsVertex2; |
890 | 0 | std::vector<double> valsVertex3; |
891 | 0 | if (!file_->findTriangle(x, y, forwardDirection, lambda1, lambda2, lambda3, |
892 | 0 | valsVertex1, valsVertex2, valsVertex3)) |
893 | 0 | return false; |
894 | | |
895 | 0 | x_out = x; |
896 | 0 | y_out = y; |
897 | 0 | z_out = z; |
898 | |
|
899 | 0 | if (file_->hasHorizontalTransformation()) { |
900 | 0 | x_out = lambda1 * valsVertex1[0] + lambda2 * valsVertex2[0] + |
901 | 0 | lambda3 * valsVertex3[0]; |
902 | 0 | y_out = lambda1 * valsVertex1[1] + lambda2 * valsVertex2[1] + |
903 | 0 | lambda3 * valsVertex3[1]; |
904 | 0 | } |
905 | 0 | if (file_->hasVerticalTransformation()) { |
906 | 0 | const int nextAttrIdx = file_->hasHorizontalTransformation() ? 2 : 0; |
907 | 0 | const double sign = forwardDirection ? 1 : -1; |
908 | 0 | if (file_->hasOffsetZ()) { |
909 | 0 | z_out += sign * (lambda1 * valsVertex1[nextAttrIdx] + |
910 | 0 | lambda2 * valsVertex2[nextAttrIdx] + |
911 | 0 | lambda3 * valsVertex3[nextAttrIdx]); |
912 | 0 | } else { |
913 | 0 | const double offset1 = |
914 | 0 | valsVertex1[nextAttrIdx + 1] - valsVertex1[nextAttrIdx]; |
915 | 0 | const double offset2 = |
916 | 0 | valsVertex2[nextAttrIdx + 1] - valsVertex2[nextAttrIdx]; |
917 | 0 | const double offset3 = |
918 | 0 | valsVertex3[nextAttrIdx + 1] - valsVertex3[nextAttrIdx]; |
919 | 0 | z_out += sign * (lambda1 * offset1 + lambda2 * offset2 + |
920 | 0 | lambda3 * offset3); |
921 | 0 | } |
922 | 0 | } |
923 | |
|
924 | 0 | return true; |
925 | 0 | } |
926 | | |
927 | | // --------------------------------------------------------------------------- |
928 | | |
929 | | bool TINShiftGeopackageEvaluator::forward(double x, double y, double z, |
930 | | double &x_out, double &y_out, |
931 | 0 | double &z_out) { |
932 | 0 | return transform(/* forwardDirection = */ true, x, y, z, x_out, y_out, |
933 | 0 | z_out); |
934 | 0 | } |
935 | | |
936 | | // --------------------------------------------------------------------------- |
937 | | |
938 | | bool TINShiftGeopackageEvaluator::inverse(double x, double y, double z, |
939 | | double &x_out, double &y_out, |
940 | 0 | double &z_out) { |
941 | 0 | return transform(/* forwardDirection = */ false, x, y, z, x_out, y_out, |
942 | 0 | z_out); |
943 | 0 | } |
944 | | |
945 | | // --------------------------------------------------------------------------- |