OpenCPN Partial API docs
Loading...
Searching...
No Matches
navobj_db.cpp
1/***************************************************************************
2 *
3 * Project: OpenCPN
4 * Purpose: NavObj_dB
5 * Author: David Register
6 *
7 ***************************************************************************
8 * Copyright (C) 2025 by David S. Register *
9 * *
10 * This program is free software; you can redistribute it and/or modify *
11 * it under the terms of the GNU General Public License as published by *
12 * the Free Software Foundation; either version 2 of the License, or *
13 * (at your option) any later version. *
14 * *
15 * This program is distributed in the hope that it will be useful, *
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
18 * GNU General Public License for more details. *
19 * *
20 * You should have received a copy of the GNU General Public License *
21 * along with this program; if not, write to the *
22 * Free Software Foundation, Inc., *
23 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *
24 **************************************************************************/
25#include <cmath>
26#include <memory>
27#include <vector>
28#include <fstream>
29#include <sstream>
30#include <iostream>
31#include <string>
32#include <wx/dir.h>
33
34#include "model/base_platform.h"
35#include "model/navobj_db.h"
36#include "model/navutil_base.h"
37#include "model/notification.h"
39#include "wx/filename.h"
40#include "model/datetime.h"
41#include "model/comm_appmsg_bus.h"
42
43extern BasePlatform* g_BasePlatform;
44extern std::shared_ptr<ObservableListener> ack_listener;
45
46static bool executeSQL(sqlite3* db, const char* sql) {
47 char* errMsg = nullptr;
48 if (sqlite3_exec(db, sql, nullptr, nullptr, &errMsg) != SQLITE_OK) {
49 sqlite3_free(errMsg);
50 return false;
51 }
52 return true;
53}
54
55static bool executeSQL(sqlite3* db, wxString& sql) {
56 return executeSQL(db, sql.ToStdString().c_str());
57}
58
59bool CreateTables(sqlite3* db) {
60 // Track tables
61 const char* create_tables_sql = R"(
62 CREATE TABLE IF NOT EXISTS tracks (
63 guid TEXT PRIMARY KEY,
64 name TEXT,
65 description TEXT,
66 visibility INTEGER,
67 start_string TEXT,
68 end_string TEXT,
69 width INTEGER,
70 style INTEGER,
71 color TEXT,
72 created_at DATETIME DEFAULT CURRENT_TIMESTAMP
73 );
74
75 CREATE TABLE IF NOT EXISTS trk_points (
76 track_guid TEXT NOT NULL,
77 latitude REAL NOT NULL,
78 longitude REAL NOT NULL,
79 timestamp TEXT NOT NULL,
80 point_order INTEGER,
81 FOREIGN KEY (track_guid) REFERENCES tracks(guid) ON DELETE CASCADE
82 );
83
84
85 CREATE TABLE IF NOT EXISTS track_html_links (
86 guid TEXT PRIMARY KEY,
87 track_guid TEXT NOT NULL,
88 html_link TEXT,
89 html_description TEXT,
90 html_type TEXT,
91 FOREIGN KEY (track_guid) REFERENCES tracks(guid) ON DELETE CASCADE
92 );
93
94 )";
95
96 if (!executeSQL(db, create_tables_sql)) return false;
97
98 return true;
99}
100
101bool TrackExists(sqlite3* db, const std::string& track_guid) {
102 const char* sql = "SELECT 1 FROM tracks WHERE guid = ? LIMIT 1";
103 sqlite3_stmt* stmt;
104 bool exists = false;
105
106 if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) == SQLITE_OK) {
107 sqlite3_bind_text(stmt, 1, track_guid.c_str(), -1, SQLITE_STATIC);
108
109 if (sqlite3_step(stmt) == SQLITE_ROW) {
110 exists = true; // found a match
111 }
112
113 sqlite3_finalize(stmt);
114 } else {
115 return false;
116 }
117 return exists;
118}
119
120bool TrackHtmlLinkExists(sqlite3* db, const std::string& link_guid) {
121 const char* sql = "SELECT 1 FROM track_html_links WHERE guid = ? LIMIT 1";
122 sqlite3_stmt* stmt;
123 bool exists = false;
124
125 if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) == SQLITE_OK) {
126 sqlite3_bind_text(stmt, 1, link_guid.c_str(), -1, SQLITE_STATIC);
127
128 if (sqlite3_step(stmt) == SQLITE_ROW) {
129 exists = true; // found a match
130 }
131
132 sqlite3_finalize(stmt);
133 } else {
134 return false;
135 }
136 return exists;
137}
138
139bool DeleteAllCommentsForTrack(sqlite3* db, const std::string& track_guid) {
140 const char* sql = R"(
141 DELETE FROM track_html_links WHERE track_guid = ?
142 )";
143 sqlite3_stmt* stmt;
144 if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) == SQLITE_OK) {
145 sqlite3_bind_text(stmt, 1, track_guid.c_str(), -1, SQLITE_STATIC);
146 if (sqlite3_step(stmt) != SQLITE_DONE) {
147 std::cerr << "Failed to delete comments: " << sqlite3_errmsg(db) << "\n";
148 }
149 sqlite3_finalize(stmt);
150 } else {
151 return false;
152 }
153 return true;
154}
155
156bool InsertTrackPoint(sqlite3* db, const std::string& track_guid, double lat,
157 double lon, const std::string& timestamp, int i_point) {
158 const char* sql = R"(
159 INSERT INTO trk_points (track_guid, latitude, longitude, timestamp, point_order)
160 VALUES (?, ?, ?, ?, ?)
161 )";
162 sqlite3_stmt* stmt;
163
164 if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) == SQLITE_OK) {
165 sqlite3_bind_text(stmt, 1, track_guid.c_str(), -1, SQLITE_TRANSIENT);
166 sqlite3_bind_double(stmt, 2, lat);
167 sqlite3_bind_double(stmt, 3, lon);
168 sqlite3_bind_text(stmt, 4, timestamp.c_str(), -1, SQLITE_TRANSIENT);
169 sqlite3_bind_int(stmt, 5, i_point);
170 sqlite3_step(stmt);
171 sqlite3_finalize(stmt);
172 } else {
173 return false;
174 }
175 return true;
176}
177
178bool InsertTrackHTML(sqlite3* db, const std::string& track_guid,
179 const std::string& link_guid, const std::string& descrText,
180 const std::string& link, const std::string& ltype) {
181 const char* sql = R"(
182 INSERT INTO track_html_links (guid, track_guid, html_link, html_description, html_type)
183 VALUES (?, ?, ?, ?, ?)
184 )";
185 sqlite3_stmt* stmt;
186
187 if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) == SQLITE_OK) {
188 sqlite3_bind_text(stmt, 1, link_guid.c_str(), -1, SQLITE_TRANSIENT);
189 sqlite3_bind_text(stmt, 2, track_guid.c_str(), -1, SQLITE_TRANSIENT);
190 sqlite3_bind_text(stmt, 3, link.c_str(), -1, SQLITE_TRANSIENT);
191 sqlite3_bind_text(stmt, 4, descrText.c_str(), -1, SQLITE_TRANSIENT);
192 sqlite3_bind_text(stmt, 5, ltype.c_str(), -1, SQLITE_TRANSIENT);
193 sqlite3_step(stmt);
194 sqlite3_finalize(stmt);
195 } else {
196 return false;
197 }
198 return true;
199}
200
201void errorLogCallback(void* pArg, int iErrCode, const char* zMsg) {
202 wxString msg =
203 wxString::Format("navobj database error. %d: %s", iErrCode, zMsg);
204 wxLogMessage(msg);
205 auto& noteman = NotificationManager::GetInstance();
206 noteman.AddNotification(NotificationSeverity::kCritical, msg.ToStdString());
207}
208
209NavObj_dB& NavObj_dB::GetInstance() {
210 static NavObj_dB instance;
211 return instance;
212}
213
214NavObj_dB::NavObj_dB() {
215 // Set SQLite per-process config options
216 int ie = sqlite3_config(SQLITE_CONFIG_LOG, errorLogCallback, nullptr);
217
218 // Does dB file exist?
219 wxString db_filename = g_BasePlatform->GetPrivateDataDir() +
220 wxFileName::GetPathSeparator() + "navobj.db";
221 if (!wxFileExists(db_filename)) {
222 // Make a safety backup of current navobj.xml
223 wxString noxml_filename = g_BasePlatform->GetPrivateDataDir() +
224 wxFileName::GetPathSeparator() + "navobj.xml";
225 if (wxFileExists(noxml_filename)) {
226 wxCopyFile(noxml_filename, noxml_filename + ".backup");
227 }
228
229 // Create the new database file navobj.db
230
231 int create_result = sqlite3_open_v2(
232 db_filename.ToStdString().c_str(),
233 &m_db, // sqlite3 **ppDb, /* OUT: SQLite db handle */
234 SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, // int flags, /* Flags */
235 NULL // char *zVfs /* Name of VFS module to use */
236 );
237 if (create_result != SQLITE_OK) {
238 wxLogMessage("Cannot create new navobj.db database file");
239 m_db = nullptr;
240 return;
241 }
242
243 // Create initial database tables
244 CreateTables(m_db);
245
246 // Save/Close the database
247 int close_result = sqlite3_close_v2(m_db);
248 if (close_result != SQLITE_OK) {
249 return;
250 }
251 }
252
253 // Open the existing database file
254 int m_open_result = sqlite3_open_v2(db_filename.ToStdString().c_str(), &m_db,
255 SQLITE_OPEN_READWRITE, NULL);
256 sqlite3_exec(m_db, "PRAGMA foreign_keys = ON;", nullptr, nullptr, nullptr);
257 ImportLegacyNavobj();
258 sqlite3_close_v2(m_db);
259
260 m_open_result = sqlite3_open_v2(db_filename.ToStdString().c_str(), &m_db,
261 SQLITE_OPEN_READWRITE, NULL);
262 sqlite3_exec(m_db, "PRAGMA foreign_keys = ON;", nullptr, nullptr, nullptr);
263}
264
265NavObj_dB::~NavObj_dB() { sqlite3_close_v2(m_db); }
266
267void NavObj_dB::Close() {
268 sqlite3_close_v2(m_db);
269 m_db = nullptr;
270}
271
272bool NavObj_dB::ImportLegacyNavobj() {
273 bool rv = ImportLegacyTracks();
274 return rv;
275}
276
277bool NavObj_dB::ImportLegacyTracks() {
278 auto input_set = new NavObjectCollection1();
279 wxString navobj_filename = g_BasePlatform->GetPrivateDataDir() +
280 wxFileName::GetPathSeparator() + "navobj.xml";
281
282 if (::wxFileExists(navobj_filename) &&
283 input_set->load_file(navobj_filename.ToStdString().c_str()).status ==
284 pugi::xml_parse_status::status_ok) {
285 input_set->LoadAllGPXTrackObjects();
286 }
287
288 std::vector<Track*> tracks_added;
289 // Add all tracks to database
290 for (Track* track_import : g_TrackList) {
291 if (AddNewTrack(track_import)) {
292 tracks_added.push_back(track_import);
293 }
294 }
295
296 // Delete all tracks that were successfully added
297 for (Track* ptrack : tracks_added) {
298 if (ptrack->m_bIsInLayer) continue;
299 g_pRouteMan->DeleteTrack(ptrack);
300 }
301
302 delete input_set;
303 return true;
304}
305
306void NavObj_dB::LoadNavObjects() { LoadAllTracks(); }
307
308bool NavObj_dB::AddNewTrack(Track* track) {
309 if (TrackExists(m_db, track->m_GUID.ToStdString())) return false;
310
311 bool rv = false;
312 char* errMsg = 0;
313 sqlite3_exec(m_db, "BEGIN TRANSACTION", 0, 0, &errMsg);
314
315 // Insert a new track
316 wxString sql = wxString::Format("INSERT INTO tracks (guid) VALUES ('%s')",
317 track->m_GUID.ToStdString().c_str());
318 if (!executeSQL(m_db, sql)) {
319 sqlite3_exec(m_db, "COMMIT", 0, 0, &errMsg);
320 return false;
321 }
322
323 UpdateDBTrackAttributes(track);
324
325 // Add any existing trkpoints
326 for (int i = 0; i < track->GetnPoints(); i++) {
327 auto point = track->GetPoint(i);
328 // Add the bare trkpoint
329 InsertTrackPoint(m_db, track->m_GUID.ToStdString(), point->m_lat,
330 point->m_lon, point->GetTimeString(), i);
331 }
332
333 // Add HTML links to track
334 int NbrOfLinks = track->m_TrackHyperlinkList->GetCount();
335 if (NbrOfLinks > 0) {
336 wxHyperlinkListNode* linknode = track->m_TrackHyperlinkList->GetFirst();
337 while (linknode) {
338 Hyperlink* link = linknode->GetData();
339
340 if (!TrackHtmlLinkExists(m_db, link->GUID)) {
341 InsertTrackHTML(m_db, track->m_GUID.ToStdString(), link->GUID,
342 link->DescrText.ToStdString(), link->Link.ToStdString(),
343 link->LType.ToStdString());
344 }
345 linknode = linknode->GetNext();
346 }
347 }
348 sqlite3_exec(m_db, "COMMIT", 0, 0, &errMsg);
349 rv = true;
350 if (errMsg) rv = false;
351
352 return rv;
353};
354
355bool NavObj_dB::UpdateDBTrackAttributes(Track* track) {
356 const char* sql =
357 "UPDATE tracks SET "
358 "name = ?, "
359 "description = ?, "
360 "visibility = ?, "
361 "start_string = ?, "
362 "end_string = ?, "
363 "width = ?, "
364 "style = ?, "
365 "color = ? "
366 "WHERE guid = ?";
367
368 sqlite3_stmt* stmt;
369 if (sqlite3_prepare_v2(m_db, sql, -1, &stmt, nullptr) == SQLITE_OK) {
370 sqlite3_bind_text(stmt, 1, track->GetName().ToStdString().c_str(),
371 track->GetName().Length(), SQLITE_TRANSIENT);
372 sqlite3_bind_text(stmt, 2, track->m_TrackDescription.ToStdString().c_str(),
373 track->m_TrackDescription.Length(), SQLITE_TRANSIENT);
374 sqlite3_bind_int(stmt, 3, track->m_bVisible);
375 sqlite3_bind_text(stmt, 4, track->m_TrackStartString.ToStdString().c_str(),
376 track->m_TrackStartString.Length(), SQLITE_TRANSIENT);
377 sqlite3_bind_text(stmt, 5, track->m_TrackEndString.ToStdString().c_str(),
378 track->m_TrackEndString.Length(), SQLITE_TRANSIENT);
379 sqlite3_bind_int(stmt, 6, track->m_width);
380 sqlite3_bind_int(stmt, 7,
381 (int)(track->m_style)); // track->m_style.c_str(),
382 sqlite3_bind_text(stmt, 8, track->m_Colour.ToStdString().c_str(), -1,
383 SQLITE_TRANSIENT);
384 sqlite3_bind_text(stmt, 9, track->m_GUID.c_str(), track->m_GUID.size(),
385 SQLITE_TRANSIENT);
386 } else {
387 return false;
388 }
389
390 if (sqlite3_step(stmt) != SQLITE_DONE) {
391 return false;
392 }
393
394 sqlite3_finalize(stmt);
395
396 // Update the HTML links
397 // The list of links is freshly rebuilt when this method is called
398 // So start by deleting all existing bcomments
399 DeleteAllCommentsForTrack(m_db, track->m_GUID.ToStdString());
400
401 // Now add all the links to db
402 int NbrOfLinks = track->m_TrackHyperlinkList->GetCount();
403 if (NbrOfLinks > 0) {
404 wxHyperlinkListNode* linknode = track->m_TrackHyperlinkList->GetFirst();
405 while (linknode) {
406 Hyperlink* link = linknode->GetData();
407
408 if (!TrackHtmlLinkExists(m_db, link->GUID)) {
409 InsertTrackHTML(m_db, track->m_GUID.ToStdString(), link->GUID,
410 link->DescrText.ToStdString(), link->Link.ToStdString(),
411 link->LType.ToStdString());
412 } else {
413 const char* sql =
414 "UPDATE track_html_links SET "
415 "html_link = ?, "
416 "html_description = ?, "
417 "html_type = ? "
418 "WHERE guid = ?";
419 sqlite3_stmt* stmt;
420 if (sqlite3_prepare_v2(m_db, sql, -1, &stmt, nullptr) == SQLITE_OK) {
421 sqlite3_bind_text(stmt, 3, link->Link.ToStdString().c_str(), -1,
422 SQLITE_TRANSIENT);
423 sqlite3_bind_text(stmt, 4, link->DescrText.ToStdString().c_str(), -1,
424 SQLITE_TRANSIENT);
425 sqlite3_bind_text(stmt, 5, link->LType.ToStdString().c_str(), -1,
426 SQLITE_TRANSIENT);
427 }
428 if (sqlite3_step(stmt) != SQLITE_DONE) {
429 return false;
430 }
431 sqlite3_finalize(stmt);
432 }
433
434 linknode = linknode->GetNext();
435 }
436 }
437
438 return true;
439}
440
441bool NavObj_dB::AddTrackPoint(Track* track, TrackPoint* point) {
442 // If track does not yet exist in dB, return
443 if (!TrackExists(m_db, track->m_GUID.ToStdString())) return false;
444
445 // Get next point order
446 int this_point_index = track->GetnPoints();
447
448 // Add the linked point to the dB
449 if (!InsertTrackPoint(m_db, track->m_GUID.ToStdString(), point->m_lat,
450 point->m_lon, point->GetTimeString(),
451 this_point_index - 1))
452 return false;
453
454 return true;
455}
456
457bool NavObj_dB::LoadAllTracks() {
458 const char* sql = R"(
459 SELECT guid, name,
460 description, visibility, start_string, end_string,
461 width, style, color,
462 created_at
463 FROM tracks
464 ORDER BY created_at ASC
465 )";
466
467 sqlite3_stmt* stmt;
468 if (sqlite3_prepare_v2(m_db, sql, -1, &stmt, nullptr) != SQLITE_OK) {
469 return false;
470 }
471
472 while (sqlite3_step(stmt) == SQLITE_ROW) {
473 std::string guid =
474 reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
475 std::string name =
476 reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1));
477 std::string description =
478 reinterpret_cast<const char*>(sqlite3_column_text(stmt, 2));
479 int visibility = sqlite3_column_int(stmt, 3);
480 std::string start_string =
481 reinterpret_cast<const char*>(sqlite3_column_text(stmt, 4));
482 std::string end_string =
483 reinterpret_cast<const char*>(sqlite3_column_text(stmt, 5));
484 int width = sqlite3_column_int(stmt, 6);
485 int style = sqlite3_column_int(stmt, 7);
486 std::string color =
487 reinterpret_cast<const char*>(sqlite3_column_text(stmt, 8));
488 std::string created =
489 reinterpret_cast<const char*>(sqlite3_column_text(stmt, 9));
490
491 Track* new_trk = NULL;
492
493 // Add the trk_points
494 const char* sql = R"(
495 SELECT latitude, longitude, timestamp, point_order
496 FROM trk_points
497 WHERE track_guid = ?
498 ORDER BY point_order ASC
499 )";
500
501 sqlite3_stmt* stmtp;
502 if (sqlite3_prepare_v2(m_db, sql, -1, &stmtp, nullptr) != SQLITE_OK) {
503 return false;
504 }
505
506 sqlite3_bind_text(stmtp, 1, guid.c_str(), -1, SQLITE_STATIC);
507
508 int GPXSeg = 0;
509 while (sqlite3_step(stmtp) == SQLITE_ROW) {
510 if (!new_trk) {
511 new_trk = new Track;
512 new_trk->m_GUID = guid;
513
514 // Set all the track attributes
515 new_trk->SetVisible(visibility == 1);
516 new_trk->SetName(name.c_str());
517 new_trk->m_TrackStartString = start_string.c_str();
518 new_trk->m_TrackEndString = end_string.c_str();
519 new_trk->m_width = width;
520 new_trk->m_style = (wxPenStyle)style;
521 new_trk->m_Colour = color;
522 }
523
524 GPXSeg += 1;
525
526 double latitude = sqlite3_column_double(stmtp, 0);
527 double longitude = sqlite3_column_double(stmtp, 1);
528 std::string timestamp =
529 reinterpret_cast<const char*>(sqlite3_column_text(stmtp, 2));
530 int point_order = sqlite3_column_int(stmtp, 3);
531
532 auto point = new TrackPoint(latitude, longitude, timestamp);
533
534 point->m_GPXTrkSegNo = GPXSeg;
535 new_trk->AddPoint(point);
536 }
537 sqlite3_finalize(stmtp);
538
539 if (new_trk) {
540 new_trk->SetCurrentTrackSeg(GPXSeg);
541
542 // Add the HTML links
543 const char* sqlh = R"(
544 SELECT guid, html_link, html_description, html_type
545 FROM track_html_links
546 WHERE track_guid = ?
547 ORDER BY html_type ASC
548 )";
549
550 sqlite3_stmt* stmt;
551
552 if (sqlite3_prepare_v2(m_db, sqlh, -1, &stmt, nullptr) == SQLITE_OK) {
553 sqlite3_bind_text(stmt, 1, new_trk->m_GUID.ToStdString().c_str(), -1,
554 SQLITE_TRANSIENT);
555
556 while (sqlite3_step(stmt) == SQLITE_ROW) {
557 std::string link_guid =
558 reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
559 std::string link_link =
560 reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1));
561 std::string link_description =
562 reinterpret_cast<const char*>(sqlite3_column_text(stmt, 2));
563 std::string link_type =
564 reinterpret_cast<const char*>(sqlite3_column_text(stmt, 3));
565
566 Hyperlink* h = new Hyperlink();
567 h->DescrText = link_description;
568 h->Link = link_link;
569 h->LType = link_type;
570
571 new_trk->m_TrackHyperlinkList->Append(h);
572 int yyp = 4;
573 }
574
575 sqlite3_finalize(stmt);
576
577 } else {
578 return false;
579 }
580
581 // Insert the track into the global list
582 g_TrackList.push_back(new_trk);
583 // Add the selectable points and segments of the track
584 pSelect->AddAllSelectableTrackSegments(new_trk);
585 }
586 }
587 return true;
588}
589
590bool NavObj_dB::DeleteTrack(Track* track) {
591 std::string track_guid = track->m_GUID.ToStdString();
592 const char* sql = "DELETE FROM tracks WHERE guid = ?";
593 sqlite3_stmt* stmt;
594
595 if (sqlite3_prepare_v2(m_db, sql, -1, &stmt, nullptr) == SQLITE_OK) {
596 sqlite3_bind_text(stmt, 1, track_guid.c_str(), -1, SQLITE_STATIC);
597 if (sqlite3_step(stmt) != SQLITE_DONE) {
598 return false;
599 }
600 sqlite3_finalize(stmt);
601 } else {
602 return false;
603 }
604 return true;
605}
wxString & GetPrivateDataDir()
Return dir path for opencpn.log, etc., respecting -c cli option.
The navobj SQLite container object, a singleton.
Definition navobj_db.h:36
Represents a single point in a track.
Definition track.h:53
Represents a track, which is a series of connected track points.
Definition track.h:111
bool exists(const std::string &name)
Class NavObj_dB.
Class Notification.
Class NotificationManager.