Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: import from JetLog #24

Merged
merged 2 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: 0 additions & 6 deletions src/routes/+page.server.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import type { PageServerLoad } from './$types';
import { trpcServer } from '$lib/server/server';
import { db } from '$lib/db';
import dayjs from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import duration from 'dayjs/plugin/duration';

dayjs.extend(customParseFormat);
dayjs.extend(duration);

export const load: PageServerLoad = async (event) => {
await trpcServer.flight.list.ssr(event);
Expand Down