This commit is contained in:
vcoppe
2025-10-05 19:34:05 +02:00
parent 1cc07901f6
commit 0733562c0d
70 changed files with 2641 additions and 2968 deletions

View File

@@ -0,0 +1,206 @@
import { db, type Database } from '$lib/db';
import { liveQuery } from 'dexie';
import type { GPXFile } from 'gpx';
import { applyPatches, produceWithPatches, type Patch, type WritableDraft } from 'immer';
import { fileStateCollection, type GPXFileStateCollection } from '$lib/logic/file-state.svelte';
const MAX_PATCHES = 100;
export class FileActionManager {
private _db: Database;
private _files: Map<string, GPXFile>;
private _patchIndex: number;
private _patchMinIndex: number;
private _patchMaxIndex: number;
constructor(db: Database, fileStateCollection: GPXFileStateCollection) {
this._db = db;
this._files = $derived.by(() => {
let files = new Map<string, GPXFile>();
fileStateCollection.files.forEach((state, id) => {
if (state.file) {
files.set(id, state.file);
}
});
return files;
});
this._patchIndex = $state(-1);
this._patchMinIndex = $state(0);
this._patchMaxIndex = $state(0);
liveQuery(() => db.settings.get('patchIndex')).subscribe((value) => {
if (value !== undefined) {
this._patchIndex = value;
}
});
liveQuery(() =>
(db.patches.orderBy(':id').keys() as Promise<number[]>).then((keys) => {
if (keys.length === 0) {
return { min: 0, max: 0 };
} else {
return { min: keys[0], max: keys[keys.length - 1] + 1 };
}
})
).subscribe((value) => {
this._patchMinIndex = value.min;
this._patchMaxIndex = value.max;
});
}
async store(patch: Patch[], inversePatch: Patch[]) {
this._db.patches.where(':id').above(this._patchIndex).delete(); // Delete all patches after the current patch to avoid redoing them
if (this._patchMaxIndex - this._patchMinIndex + 1 > MAX_PATCHES) {
this._db.patches
.where(':id')
.belowOrEqual(this._patchMaxIndex - MAX_PATCHES)
.delete();
}
this._db.transaction('rw', this._db.patches, this._db.settings, async () => {
let index = this._patchIndex + 1;
await this._db.patches.put(
{
patch,
inversePatch,
index,
},
index
);
await this._db.settings.put(index, 'patchIndex');
});
}
get canUndo(): boolean {
return this._patchIndex >= this._patchMinIndex;
}
get canRedo(): boolean {
return this._patchIndex < this._patchMaxIndex - 1;
}
undo() {
if (this.canUndo) {
this._db.patches.get(this._patchIndex).then((patch) => {
if (patch) {
this.apply(patch.inversePatch);
this._db.settings.put(this._patchIndex - 1, 'patchIndex');
}
});
}
}
redo() {
if (this.canRedo) {
this._db.patches.get(this._patchIndex + 1).then((patch) => {
if (patch) {
this.apply(patch.patch);
this._db.settings.put(this._patchIndex + 1, 'patchIndex');
}
});
}
}
apply(patch: Patch[]) {
let newFiles = applyPatches(this._files, patch);
return this.commitFileStateChange(newFiles, patch);
}
commitFileStateChange(newFiles: ReadonlyMap<string, GPXFile>, patch: Patch[]) {
let changedFileIds = getChangedFileIds(patch);
let updatedFileIds: string[] = [],
deletedFileIds: string[] = [];
changedFileIds.forEach((id) => {
if (newFiles.has(id)) {
updatedFileIds.push(id);
} else {
deletedFileIds.push(id);
}
});
let updatedFiles = updatedFileIds
.map((id) => newFiles.get(id))
.filter((file) => file !== undefined) as GPXFile[];
updatedFileIds = updatedFiles.map((file) => file._data.id);
// updateSelection(updatedFiles, deletedFileIds);
// @ts-ignore
return db.transaction('rw', db.fileids, db.files, async () => {
if (updatedFileIds.length > 0) {
await this._db.fileids.bulkPut(updatedFileIds, updatedFileIds);
await this._db.files.bulkPut(updatedFiles, updatedFileIds);
}
if (deletedFileIds.length > 0) {
await this._db.fileids.bulkDelete(deletedFileIds);
await this._db.files.bulkDelete(deletedFileIds);
}
});
}
applyGlobal(callback: (files: Map<string, GPXFile>) => void) {
const [newFileCollection, patch, inversePatch] = produceWithPatches(this._files, callback);
this.store(patch, inversePatch);
return this.commitFileStateChange(newFileCollection, patch);
}
applyToFiles(fileIds: string[], callback: (file: WritableDraft<GPXFile>) => void) {
const [newFileCollection, patch, inversePatch] = produceWithPatches(
this._files,
(draft) => {
fileIds.forEach((fileId) => {
let file = draft.get(fileId);
if (file) {
callback(file);
}
});
}
);
this.store(patch, inversePatch);
return this.commitFileStateChange(newFileCollection, patch);
}
applyToFile(fileId: string, callback: (file: WritableDraft<GPXFile>) => void) {
return this.applyToFiles([fileId], callback);
}
applyEachToFilesAndGlobal(
fileIds: string[],
callbacks: ((file: WritableDraft<GPXFile>, context?: any) => void)[],
globalCallback: (files: Map<string, GPXFile>, context?: any) => void,
context?: any
) {
const [newFileCollection, patch, inversePatch] = produceWithPatches(
this._files,
(draft) => {
fileIds.forEach((fileId, index) => {
let file = draft.get(fileId);
if (file) {
callbacks[index](file, context);
}
});
globalCallback(draft, context);
}
);
this.store(patch, inversePatch);
return this.commitFileStateChange(newFileCollection, patch);
}
}
// Get the file ids of the files that have changed in the patch
function getChangedFileIds(patch: Patch[]): string[] {
let changedFileIds = new Set<string>();
for (let p of patch) {
changedFileIds.add(p.path[0] as string);
}
return Array.from(changedFileIds);
}
export const fileActionManager = new FileActionManager(db, fileStateCollection);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,128 @@
import { updateAnchorPoints } from '$lib/components/toolbar/tools/routing/Simplify';
import { db, type Database } from '$lib/db';
import { liveQuery } from 'dexie';
import { GPXFile } from 'gpx';
import { GPXStatisticsTree, type GPXFileWithStatistics } from '$lib/logic/statistics';
import { settings } from '$lib/logic/settings.svelte';
// Observe a single file from the database, and maintain its statistics
class GPXFileState {
private _file: GPXFileWithStatistics | undefined;
private _subscription: { unsubscribe: () => void } | undefined;
constructor(db: Database, fileId: string) {
this._file = $state(undefined);
let first = true;
this._subscription = liveQuery(() => db.files.get(fileId)).subscribe((value) => {
if (value !== undefined) {
let file = new GPXFile(value);
updateAnchorPoints(file);
let statistics = new GPXStatisticsTree(file);
if (first) {
// Update the map bounds for new files
// updateTargetMapBounds(
// id,
// statistics.getStatisticsFor(new ListFileItem(id)).global.bounds
// );
first = false;
}
this._file = { file, statistics };
// if (get(selection).hasAnyChildren(new ListFileItem(id))) {
// updateAllHidden();
// }
}
});
}
destroy() {
this._subscription?.unsubscribe();
this._subscription = undefined;
this._file = undefined;
}
get file(): GPXFile | undefined {
return this._file?.file;
}
get statistics(): GPXStatisticsTree | undefined {
return this._file?.statistics;
}
}
// Observe the file ids in the database, and maintain a map of file states for the corresponding files
export class GPXFileStateCollection {
private _db: Database;
private _files: Map<string, GPXFileState>;
constructor(db: Database) {
this._db = db;
this._files = $state(new Map());
}
initialize(fitBounds: boolean) {
let initialize = true;
liveQuery(() => this._db.fileids.toArray()).subscribe((dbFileIds) => {
if (initialize) {
// if (fitBounds && dbFileIds.length > 0) {
// initTargetMapBounds(dbFileIds);
// }
initialize = false;
}
// Find new files to observe
let newFiles = dbFileIds
.filter((id) => !this._files.has(id))
.sort((a, b) => parseInt(a.split('-')[1]) - parseInt(b.split('-')[1]));
// Find deleted files to stop observing
let deletedFiles = Array.from(this._files.keys()).filter(
(id) => !dbFileIds.find((fileId) => fileId === id)
);
if (newFiles.length > 0 || deletedFiles.length > 0) {
// Update the map of file states
let files = new Map(this._files);
newFiles.forEach((id) => {
files.set(id, new GPXFileState(this._db, id));
});
deletedFiles.forEach((id) => {
files.get(id)?.destroy();
files.delete(id);
});
this._files = files;
// Update the file order
let fileOrder = settings.fileOrder.value.filter((id) => !deletedFiles.includes(id));
newFiles.forEach((id) => {
if (!fileOrder.includes(id)) {
fileOrder.push(id);
}
});
settings.fileOrder.value = fileOrder;
}
});
}
get files(): ReadonlyMap<string, GPXFileState> {
return this._files;
}
get size(): number {
return this._files.size;
}
getFile(fileId: string): GPXFile | undefined {
let fileState = this._files.get(fileId);
return fileState?.file;
}
getStatistics(fileId: string): GPXStatisticsTree | undefined {
let fileState = this._files.get(fileId);
return fileState?.statistics;
}
}
// Collection of all file states
export const fileStateCollection = new GPXFileStateCollection(db);

View File

@@ -1,40 +0,0 @@
import { updateAnchorPoints } from '$lib/components/toolbar/tools/routing/Simplify';
import { GPXStatisticsTree, type Database } from '$lib/db';
import { liveQuery } from 'dexie';
import { GPXFile } from 'gpx';
class GPXFileState {
private _db: Database;
private _file: GPXFile | undefined;
constructor(db: Database, fileId: string, file: GPXFile) {
this._db = db;
this._file = $state(undefined);
liveQuery(() => db.files.get(fileId)).subscribe((value) => {
if (value !== undefined) {
let gpx = new GPXFile(value);
updateAnchorPoints(gpx);
let statistics = new GPXStatisticsTree(gpx);
if (!fileState.has(id)) {
// Update the map bounds for new files
updateTargetMapBounds(
id,
statistics.getStatisticsFor(new ListFileItem(id)).global.bounds
);
}
fileState.set(id, gpx);
store.set({
file: gpx,
statistics,
});
if (get(selection).hasAnyChildren(new ListFileItem(id))) {
updateAllHidden();
}
}
});
}
}

View File

@@ -1,4 +1,3 @@
import { get, writable } from 'svelte/store';
import {
ListFileItem,
ListItem,
@@ -9,223 +8,187 @@ import {
ListLevel,
sortItems,
ListWaypointsItem,
moveItems,
} from './FileList';
import { fileObservers, getFile, getFileIds, settings } from '$lib/db';
} from '$lib/components/file-list/file-list';
import { SelectionTreeType } from '$lib/logic/selection';
import { fileStateCollection } from '$lib/logic/file-state.svelte';
import { settings } from '$lib/logic/settings.svelte';
import type { GPXFile } from 'gpx';
export class SelectionTreeType {
item: ListItem;
selected: boolean;
children: {
[key: string | number]: SelectionTreeType;
};
size: number = 0;
export class Selection {
private _selection: SelectionTreeType;
private _copied: ListItem[] | undefined;
private _cut: boolean;
constructor(item: ListItem) {
this.item = item;
this.selected = false;
this.children = {};
constructor() {
this._selection = $state(new SelectionTreeType(new ListRootItem()));
this._copied = $state(undefined);
this._cut = $state(false);
}
clear() {
this.selected = false;
for (let key in this.children) {
this.children[key].clear();
}
this.size = 0;
get value(): SelectionTreeType {
return this._selection;
}
_setOrToggle(item: ListItem, value?: boolean) {
if (item.level === this.item.level) {
let newSelected = value === undefined ? !this.selected : value;
if (this.selected !== newSelected) {
this.selected = newSelected;
this.size += this.selected ? 1 : -1;
}
} else {
let id = item.getIdAtLevel(this.item.level);
if (id !== undefined) {
if (!this.children.hasOwnProperty(id)) {
this.children[id] = new SelectionTreeType(this.item.extend(id));
}
this.size -= this.children[id].size;
this.children[id]._setOrToggle(item, value);
this.size += this.children[id].size;
}
}
selectItem(item: ListItem) {
let selection = new SelectionTreeType(new ListRootItem());
selection.set(item, true);
this._selection = selection;
}
set(item: ListItem, value: boolean) {
this._setOrToggle(item, value);
selectFile(fileId: string) {
this.selectItem(new ListFileItem(fileId));
}
toggle(item: ListItem) {
this._setOrToggle(item);
addSelectItem(item: ListItem) {
this._selection.toggle(item);
}
has(item: ListItem): boolean {
if (item.level === this.item.level) {
return this.selected;
} else {
let id = item.getIdAtLevel(this.item.level);
if (id !== undefined) {
if (this.children.hasOwnProperty(id)) {
return this.children[id].has(item);
}
}
}
return false;
addSelectFile(fileId: string) {
this.addSelectItem(new ListFileItem(fileId));
}
hasAnyParent(item: ListItem, self: boolean = true): boolean {
if (
this.selected &&
this.item.level <= item.level &&
(self || this.item.level < item.level)
) {
return this.selected;
}
let id = item.getIdAtLevel(this.item.level);
if (id !== undefined) {
if (this.children.hasOwnProperty(id)) {
return this.children[id].hasAnyParent(item, self);
}
}
return false;
}
hasAnyChildren(item: ListItem, self: boolean = true, ignoreIds?: (string | number)[]): boolean {
if (
this.selected &&
this.item.level >= item.level &&
(self || this.item.level > item.level)
) {
return this.selected;
}
let id = item.getIdAtLevel(this.item.level);
if (id !== undefined) {
if (ignoreIds === undefined || ignoreIds.indexOf(id) === -1) {
if (this.children.hasOwnProperty(id)) {
return this.children[id].hasAnyChildren(item, self, ignoreIds);
}
}
} else {
for (let key in this.children) {
if (ignoreIds === undefined || ignoreIds.indexOf(key) === -1) {
if (this.children[key].hasAnyChildren(item, self, ignoreIds)) {
return true;
}
}
}
}
return false;
}
getSelected(selection: ListItem[] = []): ListItem[] {
if (this.selected) {
selection.push(this.item);
}
for (let key in this.children) {
this.children[key].getSelected(selection);
}
return selection;
}
forEach(callback: (item: ListItem) => void) {
if (this.selected) {
callback(this.item);
}
for (let key in this.children) {
this.children[key].forEach(callback);
}
}
getChild(id: string | number): SelectionTreeType | undefined {
return this.children[id];
}
deleteChild(id: string | number) {
if (this.children.hasOwnProperty(id)) {
this.size -= this.children[id].size;
delete this.children[id];
}
}
}
export const selection = writable<SelectionTreeType>(new SelectionTreeType(new ListRootItem()));
export function selectItem(item: ListItem) {
selection.update(($selection) => {
$selection.clear();
$selection.set(item, true);
return $selection;
});
}
export function selectFile(fileId: string) {
selectItem(new ListFileItem(fileId));
}
export function addSelectItem(item: ListItem) {
selection.update(($selection) => {
$selection.toggle(item);
return $selection;
});
}
export function addSelectFile(fileId: string) {
addSelectItem(new ListFileItem(fileId));
}
export function selectAll() {
selection.update(($selection) => {
selectAll() {
let item: ListItem = new ListRootItem();
$selection.forEach((i) => {
this._selection.forEach((i) => {
item = i;
});
let selection = new SelectionTreeType(new ListRootItem());
if (item instanceof ListRootItem || item instanceof ListFileItem) {
$selection.clear();
get(fileObservers).forEach((_file, fileId) => {
$selection.set(new ListFileItem(fileId), true);
fileStateCollection.files.forEach((_file, fileId) => {
selection.set(new ListFileItem(fileId), true);
});
} else if (item instanceof ListTrackItem) {
let file = getFile(item.getFileId());
let file = fileStateCollection.getFile(item.getFileId());
if (file) {
file.trk.forEach((_track, trackId) => {
$selection.set(new ListTrackItem(item.getFileId(), trackId), true);
selection.set(new ListTrackItem(item.getFileId(), trackId), true);
});
}
} else if (item instanceof ListTrackSegmentItem) {
let file = getFile(item.getFileId());
let file = fileStateCollection.getFile(item.getFileId());
if (file) {
file.trk[item.getTrackIndex()].trkseg.forEach((_segment, segmentId) => {
$selection.set(
selection.set(
new ListTrackSegmentItem(item.getFileId(), item.getTrackIndex(), segmentId),
true
);
});
}
} else if (item instanceof ListWaypointItem) {
let file = getFile(item.getFileId());
let file = fileStateCollection.getFile(item.getFileId());
if (file) {
file.wpt.forEach((_waypoint, waypointId) => {
$selection.set(new ListWaypointItem(item.getFileId(), waypointId), true);
selection.set(new ListWaypointItem(item.getFileId(), waypointId), true);
});
}
}
this._selection = selection;
}
return $selection;
});
set(items: ListItem[]) {
let selection = new SelectionTreeType(new ListRootItem());
items.forEach((item) => {
selection.set(item, true);
});
this._selection = selection;
}
update(updatedFiles: GPXFile[], deletedFileIds: string[]) {
// TODO do it the other way around: get all selected items, and check if they still exist?
// let removedItems: ListItem[] = [];
// applyToOrderedItemsFromFile(selection.value.getSelected(), (fileId, level, items) => {
// let file = updatedFiles.find((file) => file._data.id === fileId);
// if (file) {
// items.forEach((item) => {
// if (item instanceof ListTrackItem) {
// let newTrackIndex = file.trk.findIndex(
// (track) => track._data.trackIndex === item.getTrackIndex()
// );
// if (newTrackIndex === -1) {
// removedItems.push(item);
// }
// } else if (item instanceof ListTrackSegmentItem) {
// let newTrackIndex = file.trk.findIndex(
// (track) => track._data.trackIndex === item.getTrackIndex()
// );
// if (newTrackIndex === -1) {
// removedItems.push(item);
// } else {
// let newSegmentIndex = file.trk[newTrackIndex].trkseg.findIndex(
// (segment) => segment._data.segmentIndex === item.getSegmentIndex()
// );
// if (newSegmentIndex === -1) {
// removedItems.push(item);
// }
// }
// } else if (item instanceof ListWaypointItem) {
// let newWaypointIndex = file.wpt.findIndex(
// (wpt) => wpt._data.index === item.getWaypointIndex()
// );
// if (newWaypointIndex === -1) {
// removedItems.push(item);
// }
// }
// });
// } else if (deletedFileIds.includes(fileId)) {
// items.forEach((item) => {
// removedItems.push(item);
// });
// }
// });
// if (removedItems.length > 0) {
// selection.update(($selection) => {
// removedItems.forEach((item) => {
// if (item instanceof ListFileItem) {
// $selection.deleteChild(item.getFileId());
// } else {
// $selection.set(item, false);
// }
// });
// return $selection;
// });
// }
}
getOrderedSelection(reverse: boolean = false): ListItem[] {
let selected: ListItem[] = [];
applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
selected.push(...items);
}, reverse);
return selected;
}
copySelection(): boolean {
let selected = this._selection.getSelected();
if (selected.length > 0) {
this._copied = selected;
this._cut = false;
return true;
}
return false;
}
cutSelection() {
if (this.copySelection()) {
this._cut = true;
}
}
resetCopied() {
this._copied = undefined;
this._cut = false;
}
get copied(): ListItem[] | undefined {
return this._copied;
}
get cut(): boolean {
return this._cut;
}
}
export function getOrderedSelection(reverse: boolean = false): ListItem[] {
let selected: ListItem[] = [];
applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
selected.push(...items);
}, reverse);
return selected;
}
export const selection = new Selection();
export function applyToOrderedItemsFromFile(
selectedItems: ListItem[],
@@ -261,115 +224,5 @@ export function applyToOrderedSelectedItemsFromFile(
callback: (fileId: string, level: ListLevel | undefined, items: ListItem[]) => void,
reverse: boolean = true
) {
applyToOrderedItemsFromFile(get(selection).getSelected(), callback, reverse);
}
export const copied = writable<ListItem[] | undefined>(undefined);
export const cut = writable(false);
export function copySelection(): boolean {
let selected = get(selection).getSelected();
if (selected.length > 0) {
copied.set(selected);
cut.set(false);
return true;
}
return false;
}
export function cutSelection() {
if (copySelection()) {
cut.set(true);
}
}
function resetCopied() {
copied.set(undefined);
cut.set(false);
}
export function pasteSelection() {
let fromItems = get(copied);
if (fromItems === undefined || fromItems.length === 0) {
return;
}
let selected = get(selection).getSelected();
if (selected.length === 0) {
selected = [new ListRootItem()];
}
let fromParent = fromItems[0].getParent();
let toParent = selected[selected.length - 1];
let startIndex: number | undefined = undefined;
if (fromItems[0].level === toParent.level) {
if (
toParent instanceof ListTrackItem ||
toParent instanceof ListTrackSegmentItem ||
toParent instanceof ListWaypointItem
) {
startIndex = toParent.getId() + 1;
}
toParent = toParent.getParent();
}
let toItems: ListItem[] = [];
if (toParent.level === ListLevel.ROOT) {
let fileIds = getFileIds(fromItems.length);
fileIds.forEach((fileId) => {
toItems.push(new ListFileItem(fileId));
});
} else {
let toFile = getFile(toParent.getFileId());
if (toFile) {
fromItems.forEach((item, index) => {
if (toParent instanceof ListFileItem) {
if (item instanceof ListTrackItem || item instanceof ListTrackSegmentItem) {
toItems.push(
new ListTrackItem(
toParent.getFileId(),
(startIndex ?? toFile.trk.length) + index
)
);
} else if (item instanceof ListWaypointsItem) {
toItems.push(new ListWaypointsItem(toParent.getFileId()));
} else if (item instanceof ListWaypointItem) {
toItems.push(
new ListWaypointItem(
toParent.getFileId(),
(startIndex ?? toFile.wpt.length) + index
)
);
}
} else if (toParent instanceof ListTrackItem) {
if (item instanceof ListTrackSegmentItem) {
let toTrackIndex = toParent.getTrackIndex();
toItems.push(
new ListTrackSegmentItem(
toParent.getFileId(),
toTrackIndex,
(startIndex ?? toFile.trk[toTrackIndex].trkseg.length) + index
)
);
}
} else if (toParent instanceof ListWaypointsItem) {
if (item instanceof ListWaypointItem) {
toItems.push(
new ListWaypointItem(
toParent.getFileId(),
(startIndex ?? toFile.wpt.length) + index
)
);
}
}
});
}
}
if (fromItems.length === toItems.length) {
moveItems(fromParent, toParent, fromItems, toItems, get(cut));
resetCopied();
}
applyToOrderedItemsFromFile(selection.value.getSelected(), callback, reverse);
}

View File

@@ -0,0 +1,140 @@
import type { ListItem } from '$lib/components/file-list/file-list';
export class SelectionTreeType {
item: ListItem;
selected: boolean;
children: {
[key: string | number]: SelectionTreeType;
};
size: number = 0;
constructor(item: ListItem) {
this.item = item;
this.selected = false;
this.children = {};
}
clear() {
this.selected = false;
for (let key in this.children) {
this.children[key].clear();
}
this.size = 0;
}
_setOrToggle(item: ListItem, value?: boolean) {
if (item.level === this.item.level) {
let newSelected = value === undefined ? !this.selected : value;
if (this.selected !== newSelected) {
this.selected = newSelected;
this.size += this.selected ? 1 : -1;
}
} else {
let id = item.getIdAtLevel(this.item.level);
if (id !== undefined) {
if (!this.children.hasOwnProperty(id)) {
this.children[id] = new SelectionTreeType(this.item.extend(id));
}
this.size -= this.children[id].size;
this.children[id]._setOrToggle(item, value);
this.size += this.children[id].size;
}
}
}
set(item: ListItem, value: boolean) {
this._setOrToggle(item, value);
}
toggle(item: ListItem) {
this._setOrToggle(item);
}
has(item: ListItem): boolean {
if (item.level === this.item.level) {
return this.selected;
} else {
let id = item.getIdAtLevel(this.item.level);
if (id !== undefined) {
if (this.children.hasOwnProperty(id)) {
return this.children[id].has(item);
}
}
}
return false;
}
hasAnyParent(item: ListItem, self: boolean = true): boolean {
if (
this.selected &&
this.item.level <= item.level &&
(self || this.item.level < item.level)
) {
return this.selected;
}
let id = item.getIdAtLevel(this.item.level);
if (id !== undefined) {
if (this.children.hasOwnProperty(id)) {
return this.children[id].hasAnyParent(item, self);
}
}
return false;
}
hasAnyChildren(item: ListItem, self: boolean = true, ignoreIds?: (string | number)[]): boolean {
if (
this.selected &&
this.item.level >= item.level &&
(self || this.item.level > item.level)
) {
return this.selected;
}
let id = item.getIdAtLevel(this.item.level);
if (id !== undefined) {
if (ignoreIds === undefined || ignoreIds.indexOf(id) === -1) {
if (this.children.hasOwnProperty(id)) {
return this.children[id].hasAnyChildren(item, self, ignoreIds);
}
}
} else {
for (let key in this.children) {
if (ignoreIds === undefined || ignoreIds.indexOf(key) === -1) {
if (this.children[key].hasAnyChildren(item, self, ignoreIds)) {
return true;
}
}
}
}
return false;
}
getSelected(selection: ListItem[] = []): ListItem[] {
if (this.selected) {
selection.push(this.item);
}
for (let key in this.children) {
this.children[key].getSelected(selection);
}
return selection;
}
forEach(callback: (item: ListItem) => void) {
if (this.selected) {
callback(this.item);
}
for (let key in this.children) {
this.children[key].forEach(callback);
}
}
getChild(id: string | number): SelectionTreeType | undefined {
return this.children[id];
}
deleteChild(id: string | number) {
if (this.children.hasOwnProperty(id)) {
this.size -= this.children[id].size;
delete this.children[id];
}
}
}

View File

@@ -0,0 +1,46 @@
import { ListItem, ListLevel } from '$lib/components/file-list/file-list';
import { GPXFile, GPXStatistics, type Track } from 'gpx';
export class GPXStatisticsTree {
level: ListLevel;
statistics: {
[key: string]: GPXStatisticsTree | GPXStatistics;
} = {};
constructor(element: GPXFile | Track) {
if (element instanceof GPXFile) {
this.level = ListLevel.FILE;
element.children.forEach((child, index) => {
this.statistics[index] = new GPXStatisticsTree(child);
});
} else {
this.level = ListLevel.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 };