Skip to content

Commit

Permalink
feat: import from JetLog
Browse files Browse the repository at this point in the history
  • Loading branch information
johanohly committed Sep 14, 2024
1 parent 19745ab commit e87cd0d
Show file tree
Hide file tree
Showing 7 changed files with 161 additions and 8 deletions.
24 changes: 23 additions & 1 deletion docs/docs/features/import.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ sidebar_position: 1

The import feature allows you to import flight data from other sources into AirTrail.
Currently, AirTrail supports importing flights from [MyFlightradar24](https://my.flightradar24.com)
and [App in the Air](https://appintheair.com).
, [App in the Air](https://appintheair.com) and [JetLog](https://github.com/pbogre/jetlog).

## Import flights from MyFlightradar24

Expand Down Expand Up @@ -45,3 +45,25 @@ Once you have the text file, you can import it into AirTrail by following these
5. Click on the "Import" button to start the import process.

After the import process is complete, you will see your flights on the map.

## Import flights from JetLog

:::tip
Make sure the file you are importing is called `jetlog.csv`. If it is not, rename it to `jetlog.csv` before importing.
:::

While logged in to your JetLog account, follow these steps to export your flights:

1. Go to your JetLog instance.
2. Go to the "Settings" page in the top right corner.
3. Click on the "Export to CSV" button to download your flights as a CSV file.

Once you have the CSV file, you can import it into AirTrail by following these steps:

1. Go to the AirTrail application.
2. Go to the settings page.
3. Click on the "Import" tab.
4. Click on the "Choose File" button and select the CSV file you downloaded from JetLog.
5. Click on the "Import" button to start the import process.

After the import process is complete, you will see your flights on the map.
2 changes: 1 addition & 1 deletion src/lib/components/modals/settings/pages/ImportPage.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@

<PageHeader
title="Import"
subtitle="Supported platforms: FlightRadar24, App in the Air"
subtitle="Supported platforms: FlightRadar24, App in the Air, JetLog"
>
<label for="file" class="block">
<Card
Expand Down
5 changes: 4 additions & 1 deletion src/lib/import/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ import { readFile } from '$lib/utils';
import { processFR24File } from '$lib/import/fr24';
import { processAITAFile } from '$lib/import/aita';
import type { CreateFlight } from '$lib/db/types';
import { processJetLogFile } from '$lib/import/jetlog';

export const processFile = async (file: File): Promise<CreateFlight[]> => {
const content = await readFile(file);

if (file.name.endsWith('.csv')) {
if (file.name.includes('jetlog')) {
return processJetLogFile(content);
} else if (file.name.endsWith('.csv')) {
return processFR24File(content);
} else if (file.name.endsWith('.txt')) {
return processAITAFile(content);
Expand Down
122 changes: 122 additions & 0 deletions src/lib/import/jetlog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import type { CreateFlight, Seat } from '$lib/db/types';
import { estimateDuration, parseCsv, toISOString } from '$lib/utils';
import { z } from 'zod';
import { get } from 'svelte/store';
import { page } from '$app/stores';
import { airportFromICAO } from '$lib/utils/data/airports';
import dayjs from 'dayjs';

const JETLOG_FLIGHT_CLASS_MAP: Record<string, Seat['seatClass']> = {
'ClassType.ECONOMY': 'economy',
'ClassType.ECONOMYPLUS': 'economy+',
'ClassType.BUSINESS': 'business',
'ClassType.FIRST': 'first',
'ClassType.PRIVATE': 'private',
};

const nullTransformer = (v: string) => (v === '' ? null : v);

const JetLogFlight = z.object({
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
origin: z.string(),
destination: z.string(),
departure_time: z
.string()
.regex(/^\d{2}:\d{2}$|/)
.transform(nullTransformer),
arrival_time: z
.string()
.regex(/^\d{2}:\d{2}$|/)
.transform(nullTransformer),
arrival_date: z
.string()
.regex(/^\d{4}-\d{2}-\d{2}$|/)
.transform(nullTransformer),
seat: z.enum(['window', 'middle', 'aisle', '']).transform(nullTransformer),
ticket_class: z.string().transform(nullTransformer),
duration: z.string().transform(nullTransformer),
distance: z.string().transform(nullTransformer),
airplane: z.string().transform(nullTransformer),
flight_number: z.string().transform(nullTransformer),
notes: z.string().transform(nullTransformer),
});

export const processJetLogFile = async (input: string) => {
const userId = get(page).data.user?.id;
if (!userId) {
throw new Error('User not found');
}

const [data, error] = parseCsv(input, JetLogFlight);
if (data.length === 0 || error) {
return [];
}

const flights: CreateFlight[] = [];

for (const row of data) {
const from = airportFromICAO(row.origin);
const to = airportFromICAO(row.destination);
if (!from || !to) {
continue;
}

const departure = row.departure_time
? dayjs(`${row.date} ${row.departure_time}`, 'YYYY-MM-DD HH:mm').subtract(
from.tz,
'minutes',
) // convert to UTC (assumes local time)
: null;
const arrival =
row.arrival_time && row.arrival_date
? dayjs(
`${row.arrival_date} ${row.arrival_time}`,
'YYYY-MM-DD HH:mm',
).subtract(to.tz, 'minutes')
: row.arrival_time
? dayjs(
`${row.date} ${row.arrival_time}`,
'YYYY-MM-DD HH:mm',
).subtract(to.tz, 'minutes')
: null;
const duration = row.duration
? +row.duration * 60
: departure && arrival
? arrival.diff(departure, 'seconds')
: estimateDuration(
{ lng: from.lon, lat: from.lat },
{ lng: to.lon, lat: to.lat },
);

const seatClass =
JETLOG_FLIGHT_CLASS_MAP[row.ticket_class ?? 'noop'] ?? null;

flights.push({
date: row.date,
from: from.ICAO,
to: to.ICAO,
departure: departure ? toISOString(departure) : null,
arrival: arrival ? toISOString(arrival) : null,
duration,
flightNumber: row.flight_number
? row.flight_number.substring(0, 10) // limit to 10 characters
: null,
note: row.notes,
airline: null,
aircraft: null,
aircraftReg: null,
flightReason: null,
seats: [
{
userId,
seat: row.seat as Seat['seat'],
seatClass,
seatNumber: null,
guestName: null,
},
],
});
}

return flights;
};
8 changes: 8 additions & 0 deletions src/lib/utils/datetime.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import dayjs from 'dayjs';
import { distanceBetween } from '$lib/utils/distance';
import type { LngLatLike } from 'maplibre-gl';

/**
* Format a Dayjs object to a string in ISO format.
Expand All @@ -11,3 +13,9 @@ export const toISOString = (d: dayjs.Dayjs) => {
export const isUsingAmPm = () => {
return new Date().toLocaleTimeString().match(/am|pm/i) !== null;
};

export const estimateDuration = (from: LngLatLike, to: LngLatLike) => {
const distance = distanceBetween(from, to) / 1000;
const durationHours = distance / 805 + 0.5; // 805 km/h is the average speed of a commercial jet, add 0.5 hours for takeoff and landing
return Math.round(dayjs.duration(durationHours, 'hours').asSeconds());
};
2 changes: 1 addition & 1 deletion src/lib/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export {
prepareVisitedAirports,
} from './data/data';
export { distanceBetween, linearClamped } from './distance';
export { toISOString, isUsingAmPm } from './datetime';
export { toISOString, isUsingAmPm, estimateDuration } from './datetime';
export { calculateBounds } from './latlng';
export { toTitleCase, pluralize } from './string';
export {
Expand Down
6 changes: 2 additions & 4 deletions src/routes/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { trpcServer } from '$lib/server/server';
import { message, setError, superValidate } from 'sveltekit-superforms';
import { zod } from 'sveltekit-superforms/adapters';
import { error, fail } from '@sveltejs/kit';
import { distanceBetween, toISOString } from '$lib/utils';
import { distanceBetween, estimateDuration, toISOString } from '$lib/utils';
import { db } from '$lib/db';
import dayjs from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat';
Expand Down Expand Up @@ -112,9 +112,7 @@ export const actions: Actions = {
// if the airports are the same, the duration can't be calculated
const fromLonLat = { lon: fromAirport.lon, lat: fromAirport.lat };
const toLonLat = { lon: toAirport.lon, lat: toAirport.lat };
const distance = distanceBetween(fromLonLat, toLonLat) / 1000;
const durationHours = distance / 805 + 0.5; // 805 km/h is the average speed of a commercial jet, add 0.5 hours for takeoff and landing
duration = Math.round(dayjs.duration(durationHours, 'hours').asSeconds());
duration = estimateDuration(fromLonLat, toLonLat);
}

const { flightNumber, aircraft, aircraftReg, airline, flightReason, note } =
Expand Down

0 comments on commit e87cd0d

Please sign in to comment.