2024-05-02 19:51:08 +02:00
|
|
|
import Dexie, { liveQuery } from 'dexie';
|
2024-05-22 16:05:31 +02:00
|
|
|
import { GPXFile, GPXStatistics, Track } from 'gpx';
|
2024-05-16 13:27:12 +02:00
|
|
|
import { enableMapSet, enablePatches, applyPatches, type Patch, type WritableDraft, castDraft, Immer } from 'immer';
|
2024-05-02 19:51:08 +02:00
|
|
|
import { writable, get, derived, type Readable, type Writable } from 'svelte/store';
|
2024-05-22 16:05:31 +02:00
|
|
|
import { initTargetMapBounds, updateTargetMapBounds } from './stores';
|
2024-05-04 15:10:30 +02:00
|
|
|
import { mode } from 'mode-watcher';
|
2024-05-05 18:59:09 +02:00
|
|
|
import { defaultBasemap, defaultBasemapTree, defaultOverlayTree, defaultOverlays } from './assets/layers';
|
2024-05-22 16:05:31 +02:00
|
|
|
import { selection } from '$lib/components/file-list/Selection';
|
|
|
|
import { ListFileItem, ListItem, type ListLevel } from '$lib/components/file-list/FileList';
|
2024-05-02 19:51:08 +02:00
|
|
|
|
2024-05-07 18:14:47 +02:00
|
|
|
enableMapSet();
|
|
|
|
enablePatches();
|
|
|
|
|
2024-05-16 13:27:12 +02:00
|
|
|
const noFreezeImmer = new Immer({ autoFreeze: false }); // Do not freeze files that are not concerned by an update, otherwise cannot assign anchors for them
|
|
|
|
|
2024-05-02 19:51:08 +02:00
|
|
|
class Database extends Dexie {
|
|
|
|
|
2024-05-03 15:59:34 +02:00
|
|
|
fileids!: Dexie.Table<string, string>;
|
2024-05-07 18:14:47 +02:00
|
|
|
files!: Dexie.Table<GPXFile, string>;
|
2024-05-07 13:19:02 +02:00
|
|
|
patches!: Dexie.Table<{ patch: Patch[], inversePatch: Patch[], index: number }, number>;
|
2024-05-02 19:51:08 +02:00
|
|
|
settings!: Dexie.Table<any, string>;
|
|
|
|
|
|
|
|
constructor() {
|
2024-05-03 15:59:34 +02:00
|
|
|
super("Database", {
|
|
|
|
cache: 'immutable'
|
|
|
|
});
|
2024-05-02 19:51:08 +02:00
|
|
|
this.version(1).stores({
|
2024-05-03 15:59:34 +02:00
|
|
|
fileids: ',&fileid',
|
|
|
|
files: '',
|
|
|
|
patches: ',patch',
|
|
|
|
settings: ''
|
2024-05-02 19:51:08 +02:00
|
|
|
});
|
2024-05-03 15:59:34 +02:00
|
|
|
this.files.add
|
2024-05-02 19:51:08 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const db = new Database();
|
|
|
|
|
2024-05-05 18:59:09 +02:00
|
|
|
// Wrap Dexie live queries in a Svelte store to avoid triggering the query for every subscriber, and updates to the store are pushed to the DB
|
2024-05-21 22:37:52 +02:00
|
|
|
function dexieSettingStore<T>(setting: string, initial: T): Writable<T> {
|
2024-05-05 18:59:09 +02:00
|
|
|
let store = writable(initial);
|
|
|
|
liveQuery(() => db.settings.get(setting)).subscribe(value => {
|
|
|
|
if (value !== undefined) {
|
|
|
|
store.set(value);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return {
|
|
|
|
subscribe: store.subscribe,
|
|
|
|
set: (value: any) => db.settings.put(value, setting),
|
|
|
|
update: (callback: (value: any) => any) => {
|
|
|
|
let newValue = callback(get(store));
|
|
|
|
db.settings.put(newValue, setting);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2024-05-06 15:52:11 +02:00
|
|
|
// Wrap Dexie live queries in a Svelte store to avoid triggering the query for every subscriber, and updates to the store are pushed to the DB
|
|
|
|
function dexieUninitializedSettingStore(setting: string, initial: any): Writable<any> {
|
|
|
|
let store = writable(undefined);
|
|
|
|
liveQuery(() => db.settings.get(setting)).subscribe(value => {
|
|
|
|
if (value !== undefined) {
|
|
|
|
store.set(value);
|
|
|
|
} else {
|
|
|
|
store.set(initial);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
return {
|
|
|
|
subscribe: store.subscribe,
|
|
|
|
set: (value: any) => db.settings.put(value, setting),
|
|
|
|
update: (callback: (value: any) => any) => {
|
|
|
|
let newValue = callback(get(store));
|
|
|
|
db.settings.put(newValue, setting);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2024-05-05 18:59:09 +02:00
|
|
|
export const settings = {
|
2024-05-22 16:05:31 +02:00
|
|
|
distanceUnits: dexieSettingStore<'metric' | 'imperial'>('distanceUnits', 'metric'),
|
2024-05-05 18:59:09 +02:00
|
|
|
velocityUnits: dexieSettingStore('velocityUnits', 'speed'),
|
|
|
|
temperatureUnits: dexieSettingStore('temperatureUnits', 'celsius'),
|
2024-05-22 16:05:31 +02:00
|
|
|
verticalFileView: dexieSettingStore<boolean>('fileView', false),
|
2024-05-05 18:59:09 +02:00
|
|
|
mode: dexieSettingStore('mode', (() => {
|
|
|
|
let currentMode: string | undefined = get(mode);
|
|
|
|
if (currentMode === undefined) {
|
|
|
|
currentMode = 'system';
|
|
|
|
}
|
|
|
|
return currentMode;
|
|
|
|
})()),
|
|
|
|
routing: dexieSettingStore('routing', true),
|
|
|
|
routingProfile: dexieSettingStore('routingProfile', 'bike'),
|
|
|
|
privateRoads: dexieSettingStore('privateRoads', false),
|
|
|
|
currentBasemap: dexieSettingStore('currentBasemap', defaultBasemap),
|
|
|
|
previousBasemap: dexieSettingStore('previousBasemap', defaultBasemap),
|
|
|
|
selectedBasemapTree: dexieSettingStore('selectedBasemapTree', defaultBasemapTree),
|
2024-05-06 15:52:11 +02:00
|
|
|
currentOverlays: dexieUninitializedSettingStore('currentOverlays', defaultOverlays),
|
2024-05-05 18:59:09 +02:00
|
|
|
previousOverlays: dexieSettingStore('previousOverlays', defaultOverlays),
|
|
|
|
selectedOverlayTree: dexieSettingStore('selectedOverlayTree', defaultOverlayTree),
|
2024-05-08 12:35:31 +02:00
|
|
|
directionMarkers: dexieSettingStore('directionMarkers', false),
|
|
|
|
distanceMarkers: dexieSettingStore('distanceMarkers', false),
|
2024-05-21 22:37:52 +02:00
|
|
|
fileOrder: dexieSettingStore<string[]>('fileOrder', []),
|
2024-05-05 18:59:09 +02:00
|
|
|
};
|
|
|
|
|
2024-05-04 14:27:12 +02:00
|
|
|
// Wrap Dexie live queries in a Svelte store to avoid triggering the query for every subscriber
|
|
|
|
function dexieStore<T>(querier: () => T | Promise<T>, initial?: T): Readable<T> {
|
|
|
|
let store = writable<T>(initial);
|
2024-05-03 15:59:34 +02:00
|
|
|
liveQuery(querier).subscribe(value => {
|
|
|
|
if (value !== undefined) {
|
2024-05-04 14:27:12 +02:00
|
|
|
store.set(value);
|
2024-05-03 15:59:34 +02:00
|
|
|
}
|
|
|
|
});
|
2024-05-02 19:51:08 +02:00
|
|
|
return {
|
2024-05-04 14:27:12 +02:00
|
|
|
subscribe: store.subscribe,
|
2024-05-03 15:59:34 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2024-05-22 16:05:31 +02:00
|
|
|
export class GPXStatisticsTree {
|
|
|
|
level: ListLevel;
|
|
|
|
statistics: {
|
|
|
|
[key: number]: GPXStatisticsTree | GPXStatistics;
|
|
|
|
} = {};
|
|
|
|
|
|
|
|
constructor(element: GPXFile | Track) {
|
|
|
|
if (element instanceof GPXFile) {
|
|
|
|
this.level = 'file';
|
|
|
|
element.children.forEach((child, index) => {
|
|
|
|
this.statistics[index] = new GPXStatisticsTree(child);
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
this.level = 'track';
|
|
|
|
element.children.forEach((child, index) => {
|
|
|
|
this.statistics[index] = child.getStatistics();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
getStatisticsFor(item: ListItem): GPXStatistics {
|
|
|
|
let statistics = new GPXStatistics();
|
|
|
|
let id = item.getIdAtLevel(this.level);
|
|
|
|
if (id === undefined || id === 'waypoints') {
|
|
|
|
Object.keys(this.statistics).forEach(key => {
|
|
|
|
if (this.statistics[key] instanceof GPXStatistics) {
|
|
|
|
statistics.mergeWith(this.statistics[key]);
|
|
|
|
} else {
|
|
|
|
statistics.mergeWith(this.statistics[key].getStatisticsFor(item));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
let child = this.statistics[id];
|
|
|
|
if (child instanceof GPXStatistics) {
|
|
|
|
statistics.mergeWith(child);
|
|
|
|
} else if (child !== undefined) {
|
|
|
|
statistics.mergeWith(child.getStatisticsFor(item));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return statistics;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
export type GPXFileWithStatistics = { file: GPXFile, statistics: GPXStatisticsTree };
|
2024-05-08 21:31:54 +02:00
|
|
|
|
2024-05-04 14:27:12 +02:00
|
|
|
// Wrap Dexie live queries in a Svelte store to avoid triggering the query for every subscriber, also takes care of the conversion to a GPXFile object
|
2024-05-15 11:47:42 +02:00
|
|
|
function dexieGPXFileStore(id: string): Readable<GPXFileWithStatistics> & { destroy: () => void } {
|
2024-05-08 21:31:54 +02:00
|
|
|
let store = writable<GPXFileWithStatistics>(undefined);
|
2024-05-15 11:47:42 +02:00
|
|
|
let query = liveQuery(() => db.files.get(id)).subscribe(value => {
|
2024-05-03 15:59:34 +02:00
|
|
|
if (value !== undefined) {
|
2024-05-04 14:27:12 +02:00
|
|
|
let gpx = new GPXFile(value);
|
2024-05-22 16:05:31 +02:00
|
|
|
|
|
|
|
let statistics = new GPXStatisticsTree(gpx);
|
2024-05-15 11:47:42 +02:00
|
|
|
if (!fileState.has(id)) { // Update the map bounds for new files
|
2024-05-22 16:05:31 +02:00
|
|
|
updateTargetMapBounds(statistics.getStatisticsFor(new ListFileItem(id)).global.bounds);
|
2024-05-08 21:31:54 +02:00
|
|
|
}
|
2024-05-22 16:05:31 +02:00
|
|
|
|
2024-05-15 11:47:42 +02:00
|
|
|
fileState.set(id, gpx);
|
2024-05-08 21:31:54 +02:00
|
|
|
store.set({
|
|
|
|
file: gpx,
|
|
|
|
statistics
|
|
|
|
});
|
2024-05-02 19:51:08 +02:00
|
|
|
}
|
2024-05-03 15:59:34 +02:00
|
|
|
});
|
|
|
|
return {
|
2024-05-09 00:02:27 +02:00
|
|
|
subscribe: store.subscribe,
|
2024-05-15 11:47:42 +02:00
|
|
|
destroy: () => {
|
|
|
|
fileState.delete(id);
|
|
|
|
query.unsubscribe();
|
|
|
|
}
|
2024-05-03 15:59:34 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2024-05-04 14:27:12 +02:00
|
|
|
// Add/update the files to the database
|
2024-05-07 18:14:47 +02:00
|
|
|
function updateDbFiles(files: (GPXFile | undefined)[], add: boolean = false) {
|
|
|
|
let filteredFiles = files.filter(file => file !== undefined) as GPXFile[];
|
2024-05-03 15:59:34 +02:00
|
|
|
let fileIds = filteredFiles.map(file => file._data.id);
|
|
|
|
if (add) {
|
|
|
|
return db.transaction('rw', db.fileids, db.files, async () => {
|
|
|
|
await db.fileids.bulkAdd(fileIds, fileIds);
|
|
|
|
await db.files.bulkAdd(filteredFiles, fileIds);
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
return db.files.bulkPut(filteredFiles, fileIds);
|
2024-05-02 19:51:08 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-05-04 14:27:12 +02:00
|
|
|
// Delete the files with the given ids from the database
|
|
|
|
function deleteDbFiles(fileIds: string[]) {
|
2024-05-03 15:59:34 +02:00
|
|
|
return db.transaction('rw', db.fileids, db.files, async () => {
|
|
|
|
await db.fileids.bulkDelete(fileIds);
|
|
|
|
await db.files.bulkDelete(fileIds);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-05-04 14:27:12 +02:00
|
|
|
// Commit the changes to the file state to the database
|
2024-05-07 18:14:47 +02:00
|
|
|
function commitFileStateChange(newFileState: ReadonlyMap<string, GPXFile>, patch: Patch[]) {
|
2024-05-03 15:59:34 +02:00
|
|
|
if (newFileState.size > fileState.size) {
|
2024-05-04 14:27:12 +02:00
|
|
|
return updateDbFiles(getChangedFileIds(patch).map((fileId) => newFileState.get(fileId)), true);
|
2024-05-03 15:59:34 +02:00
|
|
|
} else if (newFileState.size === fileState.size) {
|
2024-05-04 14:27:12 +02:00
|
|
|
return updateDbFiles(getChangedFileIds(patch).map((fileId) => newFileState.get(fileId)));
|
2024-05-03 15:59:34 +02:00
|
|
|
} else {
|
2024-05-04 14:27:12 +02:00
|
|
|
return deleteDbFiles(getChangedFileIds(patch));
|
2024-05-03 15:59:34 +02:00
|
|
|
}
|
2024-05-02 19:51:08 +02:00
|
|
|
}
|
|
|
|
|
2024-05-09 00:02:27 +02:00
|
|
|
export const fileObservers: Writable<Map<string, Readable<GPXFileWithStatistics | undefined> & { destroy: () => void }>> = writable(new Map());
|
2024-05-03 17:37:34 +02:00
|
|
|
const fileState: Map<string, GPXFile> = new Map(); // Used to generate patches
|
2024-05-02 19:51:08 +02:00
|
|
|
|
2024-05-04 14:27:12 +02:00
|
|
|
// Observe the file ids in the database, and maintain a map of file observers for the corresponding files
|
2024-05-03 15:59:34 +02:00
|
|
|
liveQuery(() => db.fileids.toArray()).subscribe(dbFileIds => {
|
2024-05-02 19:51:08 +02:00
|
|
|
// Find new files to observe
|
2024-05-07 12:36:54 +02:00
|
|
|
let newFiles = dbFileIds.filter(id => !get(fileObservers).has(id)).sort((a, b) => parseInt(a.split('-')[1]) - parseInt(b.split('-')[1]));
|
2024-05-02 19:51:08 +02:00
|
|
|
// Find deleted files to stop observing
|
2024-05-03 15:59:34 +02:00
|
|
|
let deletedFiles = Array.from(get(fileObservers).keys()).filter(id => !dbFileIds.find(fileId => fileId === id));
|
2024-05-08 21:31:54 +02:00
|
|
|
|
|
|
|
if (newFiles.length > 0) { // Reset the target map bounds when new files are added
|
|
|
|
initTargetMapBounds(fileState.size === 0);
|
|
|
|
}
|
|
|
|
|
2024-05-02 19:51:08 +02:00
|
|
|
// Update the store
|
|
|
|
if (newFiles.length > 0 || deletedFiles.length > 0) {
|
|
|
|
fileObservers.update($files => {
|
|
|
|
newFiles.forEach(id => {
|
2024-05-15 11:47:42 +02:00
|
|
|
$files.set(id, dexieGPXFileStore(id));
|
2024-05-02 19:51:08 +02:00
|
|
|
});
|
|
|
|
deletedFiles.forEach(id => {
|
2024-05-09 00:02:27 +02:00
|
|
|
$files.get(id)?.destroy();
|
2024-05-02 19:51:08 +02:00
|
|
|
$files.delete(id);
|
|
|
|
});
|
|
|
|
return $files;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2024-05-03 15:59:34 +02:00
|
|
|
const patchIndex: Readable<number> = dexieStore(() => db.settings.get('patchIndex'), -1);
|
2024-05-07 15:09:44 +02:00
|
|
|
const patchMinMaxIndex: Readable<{ min: number, max: number }> = dexieStore(() => db.patches.orderBy(':id').keys().then(keys => {
|
|
|
|
if (keys.length === 0) {
|
|
|
|
return { min: 0, max: 0 };
|
|
|
|
} else {
|
|
|
|
return { min: keys[0], max: keys[keys.length - 1] + 1 };
|
|
|
|
}
|
|
|
|
}), { min: 0, max: 0 });
|
2024-05-07 13:19:02 +02:00
|
|
|
export const canUndo: Readable<boolean> = derived([patchIndex, patchMinMaxIndex], ([$patchIndex, $patchMinMaxIndex]) => $patchIndex >= $patchMinMaxIndex.min);
|
2024-05-07 15:09:44 +02:00
|
|
|
export const canRedo: Readable<boolean> = derived([patchIndex, patchMinMaxIndex], ([$patchIndex, $patchMinMaxIndex]) => $patchIndex < $patchMinMaxIndex.max - 1);
|
2024-05-02 19:51:08 +02:00
|
|
|
|
2024-05-04 14:27:12 +02:00
|
|
|
// Helper function to apply a callback to the global file state
|
|
|
|
function applyGlobal(callback: (files: Map<string, GPXFile>) => void) {
|
2024-05-16 13:27:12 +02:00
|
|
|
const [newFileState, patch, inversePatch] = noFreezeImmer.produceWithPatches(fileState, callback);
|
2024-05-02 19:51:08 +02:00
|
|
|
|
2024-05-04 14:27:12 +02:00
|
|
|
storePatches(patch, inversePatch);
|
2024-05-02 19:51:08 +02:00
|
|
|
|
2024-05-03 15:59:34 +02:00
|
|
|
return commitFileStateChange(newFileState, patch);
|
2024-05-02 19:51:08 +02:00
|
|
|
}
|
|
|
|
|
2024-05-04 14:27:12 +02:00
|
|
|
// Helper function to apply a callback to multiple files
|
2024-05-15 15:30:02 +02:00
|
|
|
function applyToFiles(fileIds: string[], callback: (file: WritableDraft<GPXFile>) => GPXFile) {
|
2024-05-16 13:27:12 +02:00
|
|
|
const [newFileState, patch, inversePatch] = noFreezeImmer.produceWithPatches(fileState, (draft) => {
|
2024-05-02 19:51:08 +02:00
|
|
|
fileIds.forEach((fileId) => {
|
2024-05-03 15:59:34 +02:00
|
|
|
let file = draft.get(fileId);
|
|
|
|
if (file) {
|
2024-05-15 15:30:02 +02:00
|
|
|
draft.set(fileId, castDraft(callback(file)));
|
2024-05-03 15:59:34 +02:00
|
|
|
}
|
2024-05-02 19:51:08 +02:00
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2024-05-04 14:27:12 +02:00
|
|
|
storePatches(patch, inversePatch);
|
2024-05-02 19:51:08 +02:00
|
|
|
|
2024-05-03 15:59:34 +02:00
|
|
|
return commitFileStateChange(newFileState, patch);
|
2024-05-02 19:51:08 +02:00
|
|
|
}
|
|
|
|
|
2024-05-07 15:09:44 +02:00
|
|
|
const MAX_PATCHES = 100;
|
2024-05-04 14:27:12 +02:00
|
|
|
// Store the new patches in the database
|
|
|
|
async function storePatches(patch: Patch[], inversePatch: Patch[]) {
|
2024-05-03 15:59:34 +02:00
|
|
|
if (get(patchIndex) !== undefined) {
|
2024-05-07 13:19:02 +02:00
|
|
|
db.patches.where(':id').above(get(patchIndex)).delete(); // Delete all patches after the current patch to avoid redoing them
|
2024-05-07 15:09:44 +02:00
|
|
|
let minmax = get(patchMinMaxIndex);
|
|
|
|
if (minmax.max - minmax.min + 1 > MAX_PATCHES) {
|
|
|
|
db.patches.where(':id').belowOrEqual(get(patchMinMaxIndex).max - MAX_PATCHES).delete();
|
|
|
|
}
|
2024-05-03 15:59:34 +02:00
|
|
|
}
|
|
|
|
db.transaction('rw', db.patches, db.settings, async () => {
|
2024-05-07 13:19:02 +02:00
|
|
|
let index = get(patchIndex) + 1;
|
2024-05-03 15:59:34 +02:00
|
|
|
await db.patches.put({
|
|
|
|
patch,
|
2024-05-07 13:19:02 +02:00
|
|
|
inversePatch,
|
|
|
|
index
|
|
|
|
}, index);
|
|
|
|
await db.settings.put(index, 'patchIndex');
|
2024-05-02 19:51:08 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-05-04 14:27:12 +02:00
|
|
|
// Apply a patch to the file state
|
2024-05-02 19:51:08 +02:00
|
|
|
function applyPatch(patch: Patch[]) {
|
|
|
|
let newFileState = applyPatches(fileState, patch);
|
2024-05-03 15:59:34 +02:00
|
|
|
return commitFileStateChange(newFileState, patch);
|
|
|
|
}
|
|
|
|
|
2024-05-04 14:27:12 +02:00
|
|
|
// Get the file ids of the files that have changed in the patch
|
2024-05-07 18:14:47 +02:00
|
|
|
function getChangedFileIds(patch: Patch[]): string[] {
|
2024-05-15 11:47:42 +02:00
|
|
|
let changedFileIds = new Set<string>();
|
2024-05-02 19:51:08 +02:00
|
|
|
for (let p of patch) {
|
2024-05-07 18:14:47 +02:00
|
|
|
changedFileIds.add(p.path[0]);
|
2024-05-02 19:51:08 +02:00
|
|
|
}
|
2024-05-07 18:14:47 +02:00
|
|
|
return Array.from(changedFileIds);
|
2024-05-02 19:51:08 +02:00
|
|
|
}
|
|
|
|
|
2024-05-04 14:27:12 +02:00
|
|
|
// Generate unique file ids, different from the ones in the database
|
2024-05-03 15:59:34 +02:00
|
|
|
function getFileIds(n: number) {
|
|
|
|
let ids = [];
|
|
|
|
for (let index = 0; ids.length < n; index++) {
|
2024-05-02 19:51:08 +02:00
|
|
|
let id = `gpx-${index}`;
|
|
|
|
if (!get(fileObservers).has(id)) {
|
2024-05-03 15:59:34 +02:00
|
|
|
ids.push(id);
|
2024-05-02 19:51:08 +02:00
|
|
|
}
|
|
|
|
}
|
2024-05-03 15:59:34 +02:00
|
|
|
return ids;
|
2024-05-02 19:51:08 +02:00
|
|
|
}
|
|
|
|
|
2024-05-04 14:27:12 +02:00
|
|
|
// Helper functions for file operations
|
2024-05-02 19:51:08 +02:00
|
|
|
export const dbUtils = {
|
|
|
|
add: (file: GPXFile) => {
|
2024-05-03 15:59:34 +02:00
|
|
|
file._data.id = getFileIds(1)[0];
|
|
|
|
return applyGlobal((draft) => {
|
2024-05-02 19:51:08 +02:00
|
|
|
draft.set(file._data.id, file);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
addMultiple: (files: GPXFile[]) => {
|
2024-05-03 15:59:34 +02:00
|
|
|
return applyGlobal((draft) => {
|
|
|
|
let ids = getFileIds(files.length);
|
|
|
|
files.forEach((file, index) => {
|
|
|
|
file._data.id = ids[index];
|
2024-05-02 19:51:08 +02:00
|
|
|
draft.set(file._data.id, file);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
},
|
2024-05-15 15:30:02 +02:00
|
|
|
applyToFile: (id: string, callback: (file: WritableDraft<GPXFile>) => GPXFile) => {
|
2024-05-02 19:51:08 +02:00
|
|
|
applyToFiles([id], callback);
|
|
|
|
},
|
2024-05-22 16:05:31 +02:00
|
|
|
applyToSelection: (callback: (file: WritableDraft<GPXFile>) => GPXFile) => {
|
|
|
|
applyToFiles(get(selection).forEach(fileId), callback);
|
2024-05-02 19:51:08 +02:00
|
|
|
},
|
2024-05-22 16:05:31 +02:00
|
|
|
duplicateSelection: () => {
|
2024-05-02 19:51:08 +02:00
|
|
|
applyGlobal((draft) => {
|
2024-05-21 22:37:52 +02:00
|
|
|
let ids = getFileIds(get(settings.fileOrder).length);
|
|
|
|
get(settings.fileOrder).forEach((fileId, index) => {
|
2024-05-22 16:05:31 +02:00
|
|
|
if (get(selection).has(fileId)) {
|
2024-05-02 19:51:08 +02:00
|
|
|
let file = draft.get(fileId);
|
|
|
|
if (file) {
|
|
|
|
let clone = file.clone();
|
2024-05-03 15:59:34 +02:00
|
|
|
clone._data.id = ids[index];
|
2024-05-02 19:51:08 +02:00
|
|
|
draft.set(clone._data.id, clone);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
},
|
2024-05-22 16:05:31 +02:00
|
|
|
deleteSelection: () => {
|
2024-05-02 19:51:08 +02:00
|
|
|
applyGlobal((draft) => {
|
2024-05-22 16:05:31 +02:00
|
|
|
get(selection).forEach((item) => {
|
|
|
|
if (item instanceof ListFileItem) {
|
|
|
|
draft.delete(item.getId());
|
|
|
|
}
|
|
|
|
// TODO: Implement deletion of tracks, segments, waypoints
|
2024-05-02 19:51:08 +02:00
|
|
|
});
|
|
|
|
});
|
|
|
|
},
|
|
|
|
deleteAllFiles: () => {
|
|
|
|
applyGlobal((draft) => {
|
|
|
|
draft.clear();
|
|
|
|
});
|
|
|
|
},
|
2024-05-04 14:27:12 +02:00
|
|
|
// undo-redo
|
|
|
|
undo: () => {
|
|
|
|
if (get(canUndo)) {
|
|
|
|
let index = get(patchIndex);
|
|
|
|
db.patches.get(index).then(patch => {
|
|
|
|
if (patch) {
|
|
|
|
applyPatch(patch.inversePatch);
|
|
|
|
db.settings.put(index - 1, 'patchIndex');
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
},
|
|
|
|
redo: () => {
|
|
|
|
if (get(canRedo)) {
|
|
|
|
let index = get(patchIndex) + 1;
|
|
|
|
db.patches.get(index).then(patch => {
|
|
|
|
if (patch) {
|
|
|
|
applyPatch(patch.patch);
|
|
|
|
db.settings.put(index, 'patchIndex');
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
2024-05-05 18:59:09 +02:00
|
|
|
}
|