mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2026-11-01 12:17:18 +00:00
routes continued
This commit is contained in:
1 parent
603b73b99d
commit
68eacc76d1
20 files changed
+307
-85
No files matched your search
@@ -1,49 +0,0 @@
|
||||
import { readable } from 'svelte/store';
|
||||
import { engine, FileStateCollectionObserver, type FileState, type Selection } from '$lib/engine';
|
||||
import { isSelectionHidden } from '$lib/engine/selection-helpers';
|
||||
|
||||
/**
|
||||
* Whether all the selected elements are hidden. It follows the selection and, through the
|
||||
* observer of the files, the state of every file (hiding, editing).
|
||||
*/
|
||||
export const allHidden = readable(false, (set) => {
|
||||
let selection: Selection = { type: 'empty' };
|
||||
const states = new Map<string, FileState>();
|
||||
const unsubscribes = new Map<string, () => void>();
|
||||
|
||||
const update = () => set(isSelectionHidden(selection, states));
|
||||
|
||||
const unsubscribeSelection = engine.selection.subscribe(($selection) => {
|
||||
selection = $selection;
|
||||
update();
|
||||
});
|
||||
const observer = new FileStateCollectionObserver(
|
||||
(files) => {
|
||||
files.forEach((store, fileId) => {
|
||||
unsubscribes.set(
|
||||
fileId,
|
||||
store.subscribe(($file) => {
|
||||
states.set(fileId, $file);
|
||||
update();
|
||||
})
|
||||
);
|
||||
});
|
||||
},
|
||||
(fileId) => {
|
||||
unsubscribes.get(fileId)?.();
|
||||
unsubscribes.delete(fileId);
|
||||
states.delete(fileId);
|
||||
update();
|
||||
},
|
||||
() => {
|
||||
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||
unsubscribes.clear();
|
||||
states.clear();
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribeSelection();
|
||||
observer.destroy();
|
||||
};
|
||||
});
|
||||
@@ -374,7 +374,8 @@ class Engine {
|
||||
|
||||
/**
|
||||
* Converts the selected routes to tracks, or the selected tracks or segments to routes (one
|
||||
* per segment), which are selected. Resolves to `false` when there is nothing to convert.
|
||||
* per segment), which are selected. Selected files with routes only (or tracks only) have all
|
||||
* of them converted. Resolves to `false` when there is nothing to convert.
|
||||
*/
|
||||
convert() {
|
||||
return this.run((b) => b.convert());
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { VisibilityState } from '$lib/engine/file-visibility';
|
||||
import {
|
||||
coversSegments,
|
||||
elementId,
|
||||
fileConversion,
|
||||
hasSelectionWithin,
|
||||
isCovered,
|
||||
isInClipboard,
|
||||
@@ -274,4 +275,41 @@ describe('selection helpers', () => {
|
||||
assert.equal(isSelectionHidden(files('F1', 'F2'), states('F1')), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fileConversion', () => {
|
||||
const segment = (id: string) => ({ id, rev: id, length: 2 });
|
||||
const structure = (routes: string[], tracks: string[]): { structure: FileStructure } => ({
|
||||
structure: {
|
||||
id: 'F',
|
||||
name: 'file',
|
||||
waypointsRev: 'rev',
|
||||
waypoints: [],
|
||||
routes: routes.map((id) => ({ id, segment: segment(`${id}-S`) })),
|
||||
tracks: tracks.map((id) => ({ id, segments: [segment(`${id}-S`)] })),
|
||||
},
|
||||
});
|
||||
const states = new Map([
|
||||
['routes', structure(['R1', 'R2'], [])],
|
||||
['tracks', structure([], ['T1'])],
|
||||
['both', structure(['R1'], ['T1'])],
|
||||
['none', structure([], [])],
|
||||
]);
|
||||
|
||||
it('converts files with tracks only to routes, and routes only to tracks', () => {
|
||||
assert.equal(fileConversion(files('tracks'), states), 'route');
|
||||
assert.equal(fileConversion(files('routes'), states), 'track');
|
||||
});
|
||||
|
||||
it('does not convert files with both, neither, or a mix of the two', () => {
|
||||
assert.equal(fileConversion(files('both'), states), undefined);
|
||||
assert.equal(fileConversion(files('none'), states), undefined);
|
||||
assert.equal(fileConversion(files('routes', 'tracks'), states), undefined);
|
||||
assert.equal(fileConversion(files('unknown'), states), undefined);
|
||||
});
|
||||
|
||||
it('only converts files', () => {
|
||||
assert.equal(fileConversion(empty, states), undefined);
|
||||
assert.equal(fileConversion(tracks('T1'), states), undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -247,3 +247,34 @@ export function isSelectionHidden(
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* What the selected files convert to: `'route'` when they all have tracks only (with segments),
|
||||
* `'track'` when they all have routes only, `undefined` otherwise (other selections, files with
|
||||
* both or neither, or a mix of the two). `states` holds the files by id.
|
||||
*/
|
||||
export function fileConversion(
|
||||
selection: Selection,
|
||||
states: ReadonlyMap<string, { structure: FileStructure }>
|
||||
): 'route' | 'track' | undefined {
|
||||
if (selection.type !== 'file' || selection.fileIds.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const targets = new Set(
|
||||
selection.fileIds.map((fileId) => {
|
||||
const structure = states.get(fileId)?.structure;
|
||||
if (structure === undefined) {
|
||||
return undefined;
|
||||
} else if (structure.routes.length > 0 && structure.tracks.length === 0) {
|
||||
return 'track' as const;
|
||||
} else if (
|
||||
structure.routes.length === 0 &&
|
||||
structure.tracks.some((track) => track.segments.length > 0)
|
||||
) {
|
||||
return 'route' as const;
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
);
|
||||
return targets.size === 1 ? targets.values().next().value : undefined;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { readable, type Readable } from 'svelte/store';
|
||||
import { engine, FileStateCollectionObserver, type FileState, type Selection } from '$lib/engine';
|
||||
import { fileConversion, isSelectionHidden } from '$lib/engine/selection-helpers';
|
||||
|
||||
/**
|
||||
* A value computed from the selection and the states of the files. It follows the selection and,
|
||||
* through the observer of the files, the state of every file (hiding, editing).
|
||||
*/
|
||||
function fromSelectionAndFiles<T>(
|
||||
initial: T,
|
||||
compute: (selection: Selection, states: ReadonlyMap<string, FileState>) => T
|
||||
): Readable<T> {
|
||||
return readable(initial, (set) => {
|
||||
let selection: Selection = { type: 'empty' };
|
||||
const states = new Map<string, FileState>();
|
||||
const unsubscribes = new Map<string, () => void>();
|
||||
|
||||
const update = () => set(compute(selection, states));
|
||||
|
||||
const unsubscribeSelection = engine.selection.subscribe(($selection) => {
|
||||
selection = $selection;
|
||||
update();
|
||||
});
|
||||
const observer = new FileStateCollectionObserver(
|
||||
(files) => {
|
||||
files.forEach((store, fileId) => {
|
||||
unsubscribes.set(
|
||||
fileId,
|
||||
store.subscribe(($file) => {
|
||||
states.set(fileId, $file);
|
||||
update();
|
||||
})
|
||||
);
|
||||
});
|
||||
},
|
||||
(fileId) => {
|
||||
unsubscribes.get(fileId)?.();
|
||||
unsubscribes.delete(fileId);
|
||||
states.delete(fileId);
|
||||
update();
|
||||
},
|
||||
() => {
|
||||
unsubscribes.forEach((unsubscribe) => unsubscribe());
|
||||
unsubscribes.clear();
|
||||
states.clear();
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribeSelection();
|
||||
observer.destroy();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/** Whether all the selected elements are hidden. */
|
||||
export const allHidden = fromSelectionAndFiles(false, isSelectionHidden);
|
||||
|
||||
/** What the selected files convert to, see `fileConversion`. */
|
||||
export const selectedFilesConversion = fromSelectionAndFiles<'route' | 'track' | undefined>(
|
||||
undefined,
|
||||
fileConversion
|
||||
);
|
||||
Reference in new issue
Block a user