Skip to content

Commit 39139d5

Browse files
authored
Merge pull request #69 from Navigraph/feat/navigraph-leaflet-layer
2 parents ef9e7e0 + c4ee897 commit 39139d5

File tree

7 files changed

+140
-1
lines changed

7 files changed

+140
-1
lines changed

.changeset/strong-meals-grin.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@navigraph/charts": minor
3+
---
4+
5+
Added `NavigraphTileLayer`, a Leaflet `TileLayer` that implements Navigraph enroute tiles.
6+
The aim with this extension is to help developers by making the following features available:
7+
8+
- Switching of source between VFR, IFR and world map
9+
- Changing of the theme between day and night
10+
- Automatic handling of credential expiry, with useful hints in the log when something does not look quite right.

packages/auth/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
export { CancelToken, navigraphRequest, isAxiosError } from "./lib/navigraphRequest";
2-
export { default as getAuth } from "./lib/getAuth";
2+
export { default as getAuth, type NavigraphAuth } from "./lib/getAuth";
33
export type * from "./flows/device-flow";
44
export type { User, UserCallback, Unsubscribe } from "./internals/user";

packages/auth/src/lib/getAuth.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export default function getAuth({ keys, storage }: AuthParameters = {}) {
5656
const initPromise = loadPersistedCredentials();
5757

5858
return {
59+
/** Adds a callback that is called whenever the signe-in user changes. */
5960
onAuthStateChanged: (callback: UserCallback, initialNotify = true): Unsubscribe => {
6061
const promise = INITIALIZED ? Promise.resolve() : initPromise;
6162

@@ -73,3 +74,5 @@ export default function getAuth({ keys, storage }: AuthParameters = {}) {
7374
isInitialized: () => INITIALIZED,
7475
};
7576
}
77+
78+
export type NavigraphAuth = ReturnType<typeof getAuth>;

packages/charts/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,5 +37,11 @@
3737
"dependencies": {
3838
"@navigraph/auth": "2.4.0",
3939
"@navigraph/app": "1.3.3"
40+
},
41+
"peerDependencies": {
42+
"leaflet": "^1.9.4"
43+
},
44+
"devDependencies": {
45+
"@types/leaflet": "^1.9.3"
4046
}
4147
}

packages/charts/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from "./api/types";
22
export * from "./api/chartTypeCodes";
33
export * from "./util";
4+
export * from "./lib/NavigraphTileLayer";
45

56
export { getChartsAPI } from "./lib/getChartsAPI";
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { Logger, getDefaultAppDomain } from "@navigraph/app";
2+
import { NavigraphAuth } from "@navigraph/auth";
3+
import { TileLayer, Coords, DoneCallback } from "leaflet";
4+
5+
export enum NavigraphRasterSource {
6+
"IFR HIGH" = "ifr.hi",
7+
"IFR LOW" = "ifr.lo",
8+
"VFR" = "vfr",
9+
"WORLD" = "world",
10+
}
11+
12+
export type RasterTheme = "DAY" | "NIGHT";
13+
14+
function getNavigraphTileURL(
15+
source: keyof typeof NavigraphRasterSource = "VFR",
16+
theme: RasterTheme = "DAY",
17+
retina = false
18+
) {
19+
return `https://enroute-bitmap.charts.api-v2.${getDefaultAppDomain()}/styles/${NavigraphRasterSource[source]}.${theme.toLowerCase()}/{z}/{x}/{y}${retina ? "@2x" : "{r}"}.png` // prettier-ignore
20+
}
21+
22+
export interface PresetConfig {
23+
source: keyof typeof NavigraphRasterSource;
24+
theme: RasterTheme;
25+
forceRetina?: boolean;
26+
}
27+
28+
/**
29+
* A Leaflet tile layer that renders Navigraph enroute charts.
30+
* @example
31+
* ```ts
32+
* const navigraphLayer = new NavigraphTileLayer(auth, { source: "IFR HIGH", theme: "NIGHT" });
33+
* navigraphLayer.addTo(map);
34+
*
35+
* navigraphLayer.setPreset({ source: "IFR LOW", theme: "DAY" });
36+
* ```
37+
*/
38+
export class NavigraphTileLayer extends TileLayer {
39+
/** A list of tiles that has failed to load since the last successful tile load. */
40+
protected FAILED_TILES = new Set<string>();
41+
42+
/** Indicates whether map tiles failed to load due to authentication being invalid or missing. */
43+
private isMissingAuth = false;
44+
45+
constructor(public auth: NavigraphAuth, public preset: PresetConfig = { source: "VFR", theme: "DAY" }) {
46+
super(getNavigraphTileURL(preset.source, preset.theme, preset.forceRetina));
47+
48+
auth.onAuthStateChanged((user) => {
49+
if (this.isMissingAuth && user) {
50+
this.redraw();
51+
this.isMissingAuth = false;
52+
}
53+
});
54+
55+
if (!this.auth.isInitialized()) {
56+
Logger.warning(
57+
"NavigraphLayer was created before Navigraph Auth was initialized. Tiles may fail to load until a user is signed in."
58+
);
59+
}
60+
}
61+
62+
/**
63+
* Changes the preset that the map is rendering. Automatically rerenders the map.
64+
* @param preset The base style of the map tiles.
65+
* @param theme The color theme of the map tiles.
66+
* @example
67+
* ```ts
68+
* navigraphLayer.setPreset({ source: "IFR HIGH", theme: "NIGHT" });
69+
* ```
70+
*/
71+
public setPreset(preset: PresetConfig) {
72+
this.preset = preset;
73+
const newUrl = getNavigraphTileURL(preset.source, preset.theme, preset.forceRetina);
74+
this.setUrl(newUrl);
75+
}
76+
77+
protected createTile(coords: Coords, done: DoneCallback): HTMLElement {
78+
const url = this.getTileUrl(coords);
79+
const img = document.createElement("img");
80+
81+
img.onerror = () => {
82+
Logger.debug("Failed to load tile!");
83+
84+
this.isMissingAuth = this.auth.getUser() === null;
85+
const tileHasFailedBefore = this.FAILED_TILES.has(url);
86+
87+
if (tileHasFailedBefore || this.isMissingAuth) return;
88+
89+
Logger.debug("Refreshing auth and tile...");
90+
this.FAILED_TILES.add(url);
91+
this.auth
92+
.getUser(true)
93+
.then(() => (img.src = url))
94+
.catch(() => (this.isMissingAuth = true));
95+
};
96+
97+
img.onload = () => {
98+
done(undefined, img);
99+
this.FAILED_TILES.clear();
100+
Logger.debug("Loaded tile successfully!");
101+
};
102+
103+
img.src = url;
104+
105+
return img;
106+
}
107+
}

yarn.lock

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2538,6 +2538,11 @@
25382538
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
25392539
integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==
25402540

2541+
"@types/geojson@*":
2542+
version "7946.0.10"
2543+
resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.10.tgz#6dfbf5ea17142f7f9a043809f1cd4c448cb68249"
2544+
integrity sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==
2545+
25412546
"@types/glob@^7.1.1":
25422547
version "7.2.0"
25432548
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb"
@@ -2616,6 +2621,13 @@
26162621
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
26172622
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
26182623

2624+
"@types/leaflet@^1.9.3":
2625+
version "1.9.3"
2626+
resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.9.3.tgz#7aac302189eb3aa283f444316167995df42a5467"
2627+
integrity sha512-Caa1lYOgKVqDkDZVWkto2Z5JtVo09spEaUt2S69LiugbBpoqQu92HYFMGUbYezZbnBkyOxMNPXHSgRrRY5UyIA==
2628+
dependencies:
2629+
"@types/geojson" "*"
2630+
26192631
"@types/minimatch@*":
26202632
version "5.1.2"
26212633
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca"

0 commit comments

Comments
 (0)