Skip to content

Commit 2b8254b

Browse files
committed
Merge branch 'chore/ui-improvements' of https://github.com/Navigraph/msfs-navdata-interface into chore/ui-improvements
2 parents 448e70a + d51e5cb commit 2b8254b

File tree

10 files changed

+243
-87
lines changed

10 files changed

+243
-87
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ If you bundle outdated navigation data in your aircraft and you want this module
3636

3737
## Where is the Navigation Data Stored?
3838

39-
The default location for navigation data is `work/NavigationData`. If you have bundled navigation data, its located in the `NavigationData` folder in the root of your project.
39+
The default location for navigation data is `work/NavigationData`. If you have bundled navigation data, its located in the `NavigationData` folder in the root of your project. (although it gets copied into the `work` directory at runtime)
4040

4141
## Building the Sample Aircraft
4242

examples/gauge/Components/InterfaceSample.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@
55
position: relative;
66
}
77

8+
.loading-container {
9+
display: flex;
10+
justify-content: center;
11+
align-items: center;
12+
width: 100%;
13+
height: 100%;
14+
font-size: x-large;
15+
text-align: center;
16+
}
17+
818
.button {
919
width: 150px;
1020
height: 50px;

examples/gauge/Components/InterfaceSample.tsx

Lines changed: 52 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
FSComponent,
66
MappedSubject,
77
Subject,
8+
SubscribableMapFunctions,
89
VNode,
910
} from "@microsoft/msfs-sdk"
1011
import {
@@ -36,6 +37,8 @@ export class InterfaceSample extends DisplayComponent<InterfaceSampleProps> {
3637
private readonly sqlInputRef = FSComponent.createRef<Input>()
3738
private readonly executeSqlButtonRef = FSComponent.createRef<HTMLButtonElement>()
3839
private readonly outputRef = FSComponent.createRef<HTMLPreElement>()
40+
private readonly loadingRef = FSComponent.createRef<HTMLDivElement>()
41+
private readonly authContainerRef = FSComponent.createRef<HTMLDivElement>()
3942

4043
private readonly navigationDataStatus = Subject.create<NavigationDataStatus | null>(null)
4144

@@ -94,65 +97,75 @@ export class InterfaceSample extends DisplayComponent<InterfaceSampleProps> {
9497

9598
public render(): VNode {
9699
return (
97-
<div class="auth-container">
98-
<div class="horizontal">
99-
<div class="vertical">
100-
<h4>Step 1 - Sign in</h4>
101-
<div ref={this.textRef}>Loading</div>
102-
<div ref={this.loginButtonRef} class="button" />
103-
<div ref={this.navigationDataTextRef} />
104-
<img ref={this.qrCodeRef} class="qr-code" />
105-
</div>
106-
<div class="vertical">
107-
<h4>Step 2 - Select Database</h4>
108-
<Dropdown ref={this.dropdownRef} />
109-
<div ref={this.downloadButtonRef} class="button">
110-
Download
100+
<>
101+
<div class="loading-container" ref={this.loadingRef}>
102+
Waiting for navigation data interface to initialize... If building for the first time, this may take a few
103+
minutes
104+
</div>
105+
<div class="auth-container" ref={this.authContainerRef} style={{ display: "none" }}>
106+
<div class="horizontal">
107+
<div class="vertical">
108+
<h4>Step 1 - Sign in</h4>
109+
<div ref={this.textRef}>Loading</div>
110+
<div ref={this.loginButtonRef} class="button" />
111+
<div ref={this.navigationDataTextRef} />
112+
<img ref={this.qrCodeRef} class="qr-code" />
113+
</div>
114+
<div class="vertical">
115+
<h4>Step 2 - Select Database</h4>
116+
<Dropdown ref={this.dropdownRef} />
117+
<div ref={this.downloadButtonRef} class="button">
118+
Download
119+
</div>
120+
{this.renderDatabaseStatus()}
111121
</div>
112-
{this.renderDatabaseStatus()}
113122
</div>
114-
</div>
115123

116-
<h4 style="text-align: center;">Step 3 - Query the database</h4>
117-
<div class="horizontal">
118-
<div class="vertical">
119-
<Input ref={this.icaoInputRef} value="ESSA" class="text-field" />
120-
<div ref={this.executeIcaoButtonRef} class="button">
121-
Fetch Airport
122-
</div>
123-
<div style="height:30px;"></div>
124-
<Input
125-
ref={this.sqlInputRef}
126-
textarea
127-
value="SELECT airport_name FROM tbl_airports WHERE airport_identifier = 'ESSA'"
128-
class="text-field"
129-
/>
130-
<div ref={this.executeSqlButtonRef} class="button">
131-
Execute SQL
124+
<h4 style="text-align: center;">Step 3 - Query the database</h4>
125+
<div class="horizontal">
126+
<div class="vertical">
127+
<Input ref={this.icaoInputRef} value="ESSA" class="text-field" />
128+
<div ref={this.executeIcaoButtonRef} class="button">
129+
Fetch Airport
130+
</div>
131+
<div style="height:30px;"></div>
132+
<Input
133+
ref={this.sqlInputRef}
134+
textarea
135+
value="SELECT airport_name FROM tbl_airports WHERE airport_identifier = 'ESSA'"
136+
class="text-field"
137+
/>
138+
<div ref={this.executeSqlButtonRef} class="button">
139+
Execute SQL
140+
</div>
132141
</div>
142+
<pre ref={this.outputRef} id="output">
143+
The output of the query will show up here
144+
</pre>
133145
</div>
134-
<pre ref={this.outputRef} id="output">
135-
The output of the query will show up here
136-
</pre>
137146
</div>
138-
</div>
147+
</>
139148
)
140149
}
141150

142151
public onAfterRender(node: VNode): void {
143152
super.onAfterRender(node)
144153

145-
this.loginButtonRef.instance.addEventListener("click", () => this.handleClick())
146-
this.downloadButtonRef.instance.addEventListener("click", () => this.handleDownloadClick())
147-
148154
// Populate status when ready
149155
this.navigationDataInterface.onReady(() => {
150156
this.navigationDataInterface
151157
.get_navigation_data_install_status()
152158
.then(status => this.navigationDataStatus.set(status))
153159
.catch(e => console.error(e))
160+
161+
// show the auth container
162+
this.authContainerRef.instance.style.display = "block"
163+
this.loadingRef.instance.style.display = "none"
154164
})
155165

166+
this.loginButtonRef.instance.addEventListener("click", () => this.handleClick())
167+
this.downloadButtonRef.instance.addEventListener("click", () => this.handleDownloadClick())
168+
156169
this.executeIcaoButtonRef.instance.addEventListener("click", () => {
157170
console.time("query")
158171
this.navigationDataInterface

src/database/src/database.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ impl Database {
7676
pub fn open_connection(&mut self, path: String) -> Result<(), Box<dyn Error>> {
7777
// We have to open with flags because the SQLITE_OPEN_CREATE flag with the default open causes the file to
7878
// be overwritten
79-
let flags = OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_URI | OpenFlags::SQLITE_OPEN_NO_MUTEX;
79+
let flags = OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_URI | OpenFlags::SQLITE_OPEN_NO_MUTEX;
8080
let conn = Connection::open_with_flags(path, flags)?;
8181
self.database = Some(conn);
8282

src/test/setup.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ let wasmFunctionTable: WebAssembly.Table // The table of callback functions in t
157157
* Maps request ids to a tuple of the returned data's pointer, and the data's size
158158
*/
159159
const promiseResults = new Map<bigint, [number, number]>()
160-
const failedRequests: bigint[] = [];
160+
const failedRequests: bigint[] = []
161161

162162
wasmInstance = new WebAssembly.Instance(wasmModule, {
163163
wasi_snapshot_preview1: wasiSystem.wasiImport,
@@ -223,20 +223,20 @@ wasmInstance = new WebAssembly.Instance(wasmModule, {
223223
func(requestId, 200, ctx)
224224
})
225225
.catch(err => {
226-
failedRequests.push(requestId);
226+
failedRequests.push(requestId)
227227
})
228228

229229
return requestId
230230
},
231231
fsNetworkHttpRequestGetState: (requestId: bigint) => {
232-
if(failedRequests.includes(requestId)) {
232+
if (failedRequests.includes(requestId)) {
233233
return 4 // FS_NETWORK_HTTP_REQUEST_STATE_FAILED
234234
}
235-
if(promiseResults.has(requestId)) {
235+
if (promiseResults.has(requestId)) {
236236
return 3 // FS_NETWORK_HTTP_REQUEST_STATE_DATA_READY
237237
}
238238
return 2 // FS_NETWORK_HTTP_REQUEST_STATE_WAITING_FOR_DATA
239-
}
239+
},
240240
},
241241
}) as WasmInstance
242242

@@ -290,6 +290,15 @@ beforeAll(async () => {
290290
throw new Error("Please specify the env var `NAVIGATION_DATA_SIGNED_URL`")
291291
}
292292

293+
// Utility function to convert onReady to a promise
294+
const waitForReady = (navDataInterface: NavigraphNavigationDataInterface): Promise<void> => {
295+
return new Promise((resolve, _reject) => {
296+
navDataInterface.onReady(() => resolve())
297+
})
298+
}
299+
300+
await waitForReady(navigationDataInterface)
301+
293302
await navigationDataInterface.download_navigation_data(downloadUrl)
294303
}, 30000)
295304

src/wasm/src/consts.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
pub const NAVIGATION_DATA_DEFAULT_LOCATION: &str = ".\\NavigationData";
2-
pub const NAVIGATION_DATA_DOWNLOADED_LOCATION: &str = "\\work/NavigationData";
2+
pub const NAVIGATION_DATA_WORK_LOCATION: &str = "\\work/NavigationData";
3+
pub const NAVIGATION_DATA_INTERNAL_CONFIG_LOCATION: &str = "\\work/navigraph_navigation_data_interface_config.json";

src/wasm/src/dispatcher.rs

Lines changed: 72 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::{cell::RefCell, error::Error, path::Path, rc::Rc};
1+
use std::{cell::RefCell, path::Path, rc::Rc};
22

33
use msfs::{commbus::*, network::NetworkRequestState, sys::sGaugeDrawData, MSFSEvent};
44
use navigation_database::database::Database;
@@ -11,7 +11,7 @@ use crate::{
1111
functions::{CallFunction, FunctionResult, FunctionStatus, FunctionType},
1212
params,
1313
},
14-
meta,
14+
meta::{self, InternalState},
1515
network_helper::NetworkHelper,
1616
util::{self, path_exists},
1717
};
@@ -114,39 +114,86 @@ impl<'a> Dispatcher<'a> {
114114
self.downloader.acknowledge_download();
115115
}
116116
}
117-
118117
fn load_database(&mut self) {
119118
println!("[NAVIGRAPH] Loading database");
120-
// First check if we have a database in the downloaded location
121-
let found_downloaded = self
122-
.set_database_if_exists(consts::NAVIGATION_DATA_DOWNLOADED_LOCATION)
123-
.is_ok();
124-
125-
if found_downloaded {
126-
println!("[NAVIGRAPH] Loaded database from downloaded location");
127-
return;
128-
}
129119

130-
// If we didn't find a database in the downloaded location, check the default location
131-
let found_default = self
132-
.set_database_if_exists(consts::NAVIGATION_DATA_DEFAULT_LOCATION)
133-
.is_ok();
134-
135-
if !found_default {
136-
println!("[NAVIGRAPH] No database found in default location, not loading any database");
120+
// Go through logic to determine which database to load
121+
122+
// Are we bundled? None means we haven't installed anything yet
123+
let is_bundled = meta::get_internal_state()
124+
.map(|internal_state| Some(internal_state.is_bundled))
125+
.unwrap_or(None);
126+
127+
// Get the installed cycle (if it exists)
128+
let installed_cycle = match meta::get_installed_cycle_from_json(
129+
&Path::new(consts::NAVIGATION_DATA_WORK_LOCATION).join("cycle.json"),
130+
) {
131+
Ok(cycle) => Some(cycle.cycle),
132+
Err(_) => None,
133+
};
134+
135+
// Get the bundled cycle (if it exists)
136+
let bundled_cycle = match meta::get_installed_cycle_from_json(
137+
&Path::new(consts::NAVIGATION_DATA_DEFAULT_LOCATION).join("cycle.json"),
138+
) {
139+
Ok(cycle) => Some(cycle.cycle),
140+
Err(_) => None,
141+
};
142+
143+
// Determine if we are bundled ONLY and the bundled cycle is newer than the installed (old bundled) cycle
144+
let bundled_updated = if is_bundled.is_some() && is_bundled.unwrap() {
145+
if installed_cycle.is_some() && bundled_cycle.is_some() {
146+
bundled_cycle.unwrap() > installed_cycle.unwrap()
147+
} else {
148+
false
149+
}
150+
} else {
151+
false
152+
};
153+
154+
// If there is no addon config, we can assume that we need to copy the bundled database to the work location
155+
let need_to_copy = is_bundled.is_none();
156+
157+
// If we are bundled and the installed cycle is older than the bundled cycle, we need to copy the bundled database to the work location. Or if we haven't installed anything yet, we need to copy the bundled database to the work location
158+
if bundled_updated || need_to_copy {
159+
match util::copy_files_to_folder(
160+
&Path::new(consts::NAVIGATION_DATA_DEFAULT_LOCATION),
161+
&Path::new(consts::NAVIGATION_DATA_WORK_LOCATION),
162+
) {
163+
Ok(_) => {
164+
// Set the internal state to bundled
165+
let res = meta::set_internal_state(InternalState { is_bundled: true });
166+
if let Err(e) = res {
167+
println!("[NAVIGRAPH] Failed to set internal state: {}", e);
168+
}
169+
},
170+
Err(e) => {
171+
println!(
172+
"[NAVIGRAPH] Failed to copy database from default location to work location: {}",
173+
e
174+
);
175+
return;
176+
},
177+
}
137178
}
138-
}
139179

140-
fn set_database_if_exists(&mut self, path: &str) -> Result<(), Box<dyn Error>> {
141-
if path_exists(&Path::new(path)) {
142-
self.database.set_active_database(path.to_owned())
180+
// Finally, set the active database
181+
if path_exists(&Path::new(consts::NAVIGATION_DATA_WORK_LOCATION)) {
182+
match self.database.set_active_database(consts::NAVIGATION_DATA_WORK_LOCATION.to_owned()) {
183+
Ok(_) => {
184+
println!("[NAVIGRAPH] Loaded database");
185+
},
186+
Err(e) => {
187+
println!("[NAVIGRAPH] Failed to load database: {}", e);
188+
},
189+
}
143190
} else {
144-
Err("Path does not exist".into())
191+
println!("[NAVIGRAPH] Failed to load database: there is no installed database");
145192
}
146193
}
147194

148195
fn on_download_finish(&mut self) {
149-
match navigation_database::util::find_sqlite_file(consts::NAVIGATION_DATA_DOWNLOADED_LOCATION) {
196+
match navigation_database::util::find_sqlite_file(consts::NAVIGATION_DATA_WORK_LOCATION) {
150197
Ok(path) => {
151198
match self.database.set_active_database(path) {
152199
Ok(_) => {},

src/wasm/src/download/downloader.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use crate::{
77
dispatcher::{Dispatcher, Task, TaskStatus},
88
download::zip_handler::{BatchReturn, ZipFileHandler},
99
json_structs::{events, params},
10+
meta::{self, InternalState},
1011
};
1112

1213
pub struct DownloadOptions {
@@ -63,6 +64,11 @@ impl NavigationDataDownloader {
6364
borrowed_task.status = TaskStatus::Success(None);
6465
}
6566
self.download_status.replace(DownloadStatus::Downloaded);
67+
// Update the internal state
68+
let res = meta::set_internal_state(InternalState { is_bundled: false });
69+
if let Err(e) = res {
70+
println!("[NAVIGRAPH] Failed to set internal state: {}", e);
71+
}
6672
},
6773
Ok(BatchReturn::MoreFilesToDelete) => {
6874
self.download_status.replace(DownloadStatus::CleaningDestination);
@@ -181,7 +187,7 @@ impl NavigationDataDownloader {
181187
return;
182188
}
183189

184-
let path = PathBuf::from(consts::NAVIGATION_DATA_DOWNLOADED_LOCATION);
190+
let path = PathBuf::from(consts::NAVIGATION_DATA_WORK_LOCATION);
185191

186192
// Check the data from the request
187193
let data = request.data();

0 commit comments

Comments
 (0)