This commit is contained in:
vcoppe
2024-05-23 11:21:57 +02:00
parent f202b6c196
commit 51495e9bd1
16 changed files with 211 additions and 148 deletions

View File

@@ -19,7 +19,7 @@
<div class="grow relative">
<Menu />
<Toolbar />
<Map class="h-full" />
<Map class="h-full {$verticalFileView ? '' : 'horizontal'}" />
<LayerControl />
<GPXLayers />
<Toaster richColors />
@@ -34,9 +34,11 @@
<ElevationProfile />
</div>
</div>
{#if $verticalFileView}
<FileList orientation="vertical" recursive={true} class="w-60" />
{/if}
<div class="shrink-0">
{#if $verticalFileView}
<FileList orientation="vertical" recursive={true} class="w-60" />
{/if}
</div>
</div>
<style lang="postcss">

View File

@@ -21,7 +21,7 @@
easing: () => 1
};
const { distanceUnits } = settings;
const { distanceUnits, verticalFileView } = settings;
let scaleControl = new mapboxgl.ScaleControl({
unit: $distanceUnits
});
@@ -116,6 +116,10 @@
$: if ($map) {
scaleControl.setUnit($distanceUnits);
}
$: if ($map && !$verticalFileView) {
$map.resize();
}
</script>
<div {...$$restProps}>
@@ -205,11 +209,11 @@
@apply overflow-hidden;
}
div :global(.mapboxgl-ctrl-bottom-left) {
.horizontal :global(.mapboxgl-ctrl-bottom-left) {
@apply bottom-[42px];
}
div :global(.mapboxgl-ctrl-bottom-right) {
.horizontal :global(.mapboxgl-ctrl-bottom-right) {
@apply bottom-[42px];
}

View File

@@ -23,7 +23,8 @@
Thermometer,
Sun,
Moon,
Rows3
Rows3,
Layers3
} from 'lucide-svelte';
import {
@@ -31,18 +32,18 @@
exportAllFiles,
exportSelectedFiles,
triggerFileInput,
selectFiles,
createFile
} from '$lib/stores';
import { selection } from '$lib/components/file-list/Selection';
import { selectAll, selection } from '$lib/components/file-list/Selection';
import { derived } from 'svelte/store';
import { canUndo, canRedo, dbUtils, fileObservers, settings } from '$lib/db';
import { anySelectedLayer } from '$lib/components/layer-control/utils';
import { defaultOverlays } from '$lib/assets/layers';
import LayerControlSettings from '$lib/components/layer-control/LayerControlSettings.svelte';
import { resetMode, setMode, systemPrefersMode } from 'mode-watcher';
import { _ } from 'svelte-i18n';
import { anySelectedLayer } from './layer-control/utils';
import { defaultOverlays } from '$lib/assets/layers';
const {
distanceUnits,
@@ -88,6 +89,8 @@
}
}
}
let layerSettingsOpen = false;
</script>
<div class="absolute top-2 left-0 right-0 z-20 flex flex-row justify-center pointer-events-none">
@@ -147,7 +150,7 @@
<Shortcut key="Z" ctrl={true} shift={true} />
</Menubar.Item>
<Menubar.Separator />
<Menubar.Item on:click={() => $selectFiles.selectAllFiles()}>
<Menubar.Item on:click={selectAll}>
<span class="w-4 mr-1"></span>
{$_('menu.select_all')}
<Shortcut key="A" ctrl={true} />
@@ -265,6 +268,11 @@
</Menubar.RadioGroup>
</Menubar.SubContent>
</Menubar.Sub>
<Menubar.Separator />
<Menubar.Item on:click={() => (layerSettingsOpen = true)}>
<Layers3 size="16" class="mr-1" />
{$_('menu.layers')}
</Menubar.Item>
</Menubar.Content>
</Menubar.Menu>
</Menubar.Root>
@@ -283,6 +291,8 @@
</div>
</div>
<LayerControlSettings bind:open={layerSettingsOpen} />
<svelte:window
on:keydown={(e) => {
if (e.key === 'n' && (e.metaKey || e.ctrlKey)) {
@@ -315,7 +325,7 @@
}
e.preventDefault();
} else if (e.key === 'a' && (e.metaKey || e.ctrlKey)) {
$selectFiles.selectAllFiles();
selectAll();
e.preventDefault();
} else if (e.key === 'F1') {
switchBasemaps();

View File

@@ -20,6 +20,6 @@
scrollbarYClasses={orientation === 'vertical' ? '' : ''}
>
<div class="flex {orientation === 'vertical' ? 'flex-col' : 'flex-row'} {$$props.class ?? ''}">
<FileListNode node={$fileObservers} item={new ListRootItem()} />
<FileListNode bind:node={$fileObservers} item={new ListRootItem()} />
</div>
</ScrollArea>

View File

@@ -5,7 +5,7 @@
import type { Readable } from 'svelte/store';
import FileListNodeContent from './FileListNodeContent.svelte';
import FileListNodeLabel from './FileListNodeLabel.svelte';
import { getContext } from 'svelte';
import { afterUpdate, getContext } from 'svelte';
import { type ListItem, type ListTrackItem } from './FileList';
export let node:
@@ -16,7 +16,7 @@
let recursive = getContext<boolean>('recursive');
let label =
$: label =
node instanceof GPXFile
? node.metadata.name
: node instanceof Track

View File

@@ -2,7 +2,7 @@
import { GPXFile, Track, Waypoint, type AnyGPXTreeElement, type GPXTreeElement } from 'gpx';
import { afterUpdate, getContext, onDestroy, onMount } from 'svelte';
import Sortable from 'sortablejs/Sortable';
import { settings, type GPXFileWithStatistics } from '$lib/db';
import { fileObservers, settings, type GPXFileWithStatistics } from '$lib/db';
import { get, type Readable } from 'svelte/store';
import FileListNodeStore from './FileListNodeStore.svelte';
import FileListNode from './FileListNode.svelte';
@@ -50,16 +50,17 @@
});
}
const { fileOrder } = settings;
function syncFileOrder() {
if (sortableLevel !== 'file') {
if (!sortable || sortableLevel !== 'file') {
return;
}
/*Object.keys(buttons).forEach((fileId) => {
if (!get(fileObservers).has(fileId)) {
delete buttons[fileId];
}
});*/
if ($fileOrder.length !== $fileObservers.size) {
// Files were added or removed
fileOrder.set(sortable.toArray());
return;
}
const currentOrder = sortable.toArray();
if (currentOrder.length !== $fileOrder.length) {
@@ -74,8 +75,6 @@
}
}
const { fileOrder } = settings;
onMount(() => {
sortable = Sortable.create(container, {
group: {
@@ -109,13 +108,19 @@
});
});
$: if ($fileOrder && sortable) {
$: if ($fileOrder) {
syncFileOrder();
}
afterUpdate(() => {
syncFileOrder();
// TODO: update selection if files are removed
if (sortableLevel === 'file') {
syncFileOrder();
Object.keys(elements).forEach((fileId) => {
if (!get(fileObservers).has(fileId)) {
delete elements[fileId];
}
});
}
});
const unsubscribe = selection.subscribe(($selection) => {

View File

@@ -4,6 +4,21 @@ import { fileObservers } from "$lib/db";
export const selection = writable<SelectionTreeType>(new SelectionTreeType(new ListRootItem()));
export function select(fileId: string) {
selection.update(($selection) => {
$selection.clear();
$selection.set(new ListFileItem(fileId), true);
return $selection;
});
}
export function addSelect(fileId: string) {
selection.update(($selection) => {
$selection.toggle(new ListFileItem(fileId));
return $selection;
});
}
export function selectAll() {
selection.update(($selection) => {
get(fileObservers).forEach((_file, fileId) => {

View File

@@ -0,0 +1,95 @@
import { settings } from "$lib/db";
import { gpxStatistics } from "$lib/stores";
import { get } from "svelte/store";
const { distanceMarkers, distanceUnits } = settings;
export class DistanceMarkers {
map: mapboxgl.Map;
updateBinded: () => void = this.update.bind(this);
constructor(map: mapboxgl.Map) {
this.map = map;
gpxStatistics.subscribe(this.updateBinded);
distanceMarkers.subscribe(this.updateBinded);
distanceUnits.subscribe(() => {
if (get(distanceMarkers)) {
this.update();
}
});
}
update() {
try {
if (get(distanceMarkers)) {
let distanceSource = this.map.getSource('distance-markers');
if (distanceSource) {
distanceSource.setData(this.getDistanceMarkersGeoJSON());
} else {
this.map.addSource('distance-markers', {
type: 'geojson',
data: this.getDistanceMarkersGeoJSON()
});
}
if (!this.map.getLayer('distance-markers')) {
this.map.addLayer({
id: 'distance-markers',
type: 'symbol',
source: 'distance-markers',
layout: {
'text-field': ['get', 'distance'],
'text-size': 12,
'text-font': ['Open Sans Regular'],
'icon-image': ['get', 'icon'],
'icon-padding': 50,
'icon-allow-overlap': true,
},
paint: {
'text-halo-width': 0.1,
'text-halo-color': 'black'
}
});
} else {
this.map.moveLayer('distance-markers');
}
} else {
if (this.map.getLayer('distance-markers')) {
this.map.removeLayer('distance-markers');
}
}
} catch (e) { // No reliable way to check if the map is ready to add sources and layers
return;
}
}
getDistanceMarkersGeoJSON(): GeoJSON.FeatureCollection {
let statistics = get(gpxStatistics);
let features = [];
let currentTargetDistance = 1;
for (let i = 0; i < statistics.local.distance.length; i++) {
if (statistics.local.distance[i] >= currentTargetDistance * (get(distanceUnits) === 'metric' ? 1 : 1.60934)) {
let distance = currentTargetDistance.toFixed(0);
features.push({
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [statistics.local.points[i].getLongitude(), statistics.local.points[i].getLatitude()]
},
properties: {
distance,
icon: distance.length < 3 ? 'circle-white-2' : 'circle-white-3'
}
} as GeoJSON.Feature);
currentTargetDistance += 1;
}
}
return {
type: 'FeatureCollection',
features
};
}
}

View File

@@ -1,10 +1,12 @@
import { map, selectFiles, currentTool, Tool } from "$lib/stores";
import { map, currentTool, Tool } from "$lib/stores";
import { settings, type GPXFileWithStatistics } from "$lib/db";
import { get, type Readable } from "svelte/store";
import mapboxgl from "mapbox-gl";
import { currentWaypoint, waypointPopup } from "./WaypointPopup";
import { addSelect, select, selection } from "$lib/components/file-list/Selection";
import { ListSegmentItem, type ListItem, ListFileItem, ListTrackItem } from "$lib/components/file-list/FileList";
let defaultWeight = 6;
let defaultWeight = 5;
let defaultOpacity = 1;
const colors = [
@@ -37,7 +39,7 @@ function decrementColor(color: string) {
colorCount[color]--;
}
const { directionMarkers, distanceMarkers, distanceUnits } = settings;
const { directionMarkers } = settings;
export class GPXLayer {
map: mapboxgl.Map;
@@ -45,6 +47,7 @@ export class GPXLayer {
file: Readable<GPXFileWithStatistics | undefined>;
layerColor: string;
markers: mapboxgl.Marker[] = [];
selected: ListItem[] = [];
unsubscribe: Function[] = [];
updateBinded: () => void = this.update.bind(this);
@@ -56,13 +59,17 @@ export class GPXLayer {
this.file = file;
this.layerColor = getColor();
this.unsubscribe.push(file.subscribe(this.updateBinded));
this.unsubscribe.push(directionMarkers.subscribe(this.updateBinded));
this.unsubscribe.push(distanceMarkers.subscribe(this.updateBinded));
this.unsubscribe.push(distanceUnits.subscribe(() => {
if (get(distanceMarkers)) {
this.unsubscribe.push(selection.subscribe($selection => {
let selected = $selection.getChild(fileId)?.getSelected() || [];
if (selected.length !== this.selected.length || selected.some((item, index) => item !== this.selected[index])) {
this.selected = selected;
this.update();
if (this.selected.length > 0) {
this.moveToFront();
}
}
}));
this.unsubscribe.push(directionMarkers.subscribe(this.updateBinded));
this.map.on('style.load', this.updateBinded);
}
@@ -74,7 +81,6 @@ export class GPXLayer {
}
try {
let source = this.map.getSource(this.fileId);
if (source) {
source.setData(this.getGeoJSON());
@@ -132,43 +138,6 @@ export class GPXLayer {
this.map.removeLayer(this.fileId + '-direction');
}
}
if (get(distanceMarkers)) {
let distanceSource = this.map.getSource(this.fileId + '-distance');
if (distanceSource) {
distanceSource.setData(this.getDistanceMarkersGeoJSON());
} else {
this.map.addSource(this.fileId + '-distance', {
type: 'geojson',
data: this.getDistanceMarkersGeoJSON()
});
}
if (!this.map.getLayer(this.fileId + '-distance')) {
this.map.addLayer({
id: this.fileId + '-distance',
type: 'symbol',
source: this.fileId + '-distance',
layout: {
'text-field': ['get', 'distance'],
'text-size': 12,
'text-font': ['Open Sans Regular'],
'icon-image': ['get', 'icon'],
'icon-padding': 50,
'icon-allow-overlap': true,
},
paint: {
'text-halo-width': 0.1,
'text-halo-color': 'black'
}
});
} else {
this.map.moveLayer(this.fileId + '-distance');
}
} else {
if (this.map.getLayer(this.fileId + '-distance')) {
this.map.removeLayer(this.fileId + '-distance');
}
}
} catch (e) { // No reliable way to check if the map is ready to add sources and layers
return;
}
@@ -212,9 +181,6 @@ export class GPXLayer {
if (this.map.getLayer(this.fileId + '-direction')) {
this.map.removeLayer(this.fileId + '-direction');
}
if (this.map.getLayer(this.fileId + '-distance')) {
this.map.removeLayer(this.fileId + '-distance');
}
if (this.map.getLayer(this.fileId)) {
this.map.removeLayer(this.fileId);
}
@@ -238,9 +204,6 @@ export class GPXLayer {
if (this.map.getLayer(this.fileId + '-direction')) {
this.map.moveLayer(this.fileId + '-direction');
}
if (this.map.getLayer(this.fileId + '-distance')) {
this.map.moveLayer(this.fileId + '-distance');
}
}
selectOnClick(e: any) {
@@ -248,9 +211,9 @@ export class GPXLayer {
return;
}
if (e.originalEvent.shiftKey) {
get(selectFiles).addSelect(this.fileId);
addSelect(this.fileId);
} else {
get(selectFiles).select(this.fileId);
select(this.fileId);
}
}
@@ -264,6 +227,8 @@ export class GPXLayer {
}
let data = file.toGeoJSON();
let trackIndex = 0, segmentIndex = 0;
for (let feature of data.features) {
if (!feature.properties) {
feature.properties = {};
@@ -277,43 +242,17 @@ export class GPXLayer {
if (!feature.properties.opacity) {
feature.properties.opacity = defaultOpacity;
}
}
return data;
}
if (get(selection).has(new ListFileItem(this.fileId)) || get(selection).has(new ListTrackItem(this.fileId, trackIndex)) || get(selection).has(new ListSegmentItem(this.fileId, trackIndex, segmentIndex))) {
feature.properties.weight = feature.properties.weight + 2;
}
getDistanceMarkersGeoJSON(): GeoJSON.FeatureCollection {
let statistics = get(this.file)?.statistics;
if (!statistics) {
return {
type: 'FeatureCollection',
features: []
};
}
let features = [];
let currentTargetDistance = 1;
for (let i = 0; i < statistics.local.distance.length; i++) {
if (statistics.local.distance[i] >= currentTargetDistance * (get(distanceUnits) === 'metric' ? 1 : 1.60934)) {
let distance = currentTargetDistance.toFixed(0);
features.push({
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [statistics.local.points[i].getLongitude(), statistics.local.points[i].getLatitude()]
},
properties: {
distance,
icon: distance.length < 3 ? 'circle-white-2' : 'circle-white-3'
}
} as GeoJSON.Feature);
currentTargetDistance += 1;
segmentIndex++;
if (segmentIndex >= file.trk[trackIndex].trkseg.length) {
segmentIndex = 0;
trackIndex++;
}
}
return {
type: 'FeatureCollection',
features
};
return data;
}
}

View File

@@ -4,7 +4,9 @@
import { get } from 'svelte/store';
import WaypointPopup from './WaypointPopup.svelte';
import { fileObservers } from '$lib/db';
import { selection } from '$lib/components/file-list/Selection';
import { DistanceMarkers } from './DistanceMarkers';
let distanceMarkers: DistanceMarkers;
$: if ($map && $fileObservers) {
gpxLayers.update(($layers) => {
@@ -25,13 +27,9 @@
});
}
$: $selection.forEach((item) => {
let fileId = item.getFileId();
// TODO move more precise selection to front?
if ($gpxLayers.has(fileId)) {
$gpxLayers.get(fileId)?.moveToFront();
}
});
$: if ($map) {
distanceMarkers = new DistanceMarkers(get(map));
}
</script>
<WaypointPopup />

View File

@@ -1,7 +1,6 @@
<script lang="ts">
import CustomControl from '$lib/components/custom-control/CustomControl.svelte';
import LayerTree from './LayerTree.svelte';
import LayerControlSettings from './LayerControlSettings.svelte';
import { Separator } from '$lib/components/ui/separator';
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
@@ -121,10 +120,6 @@
/>
{/if}
</div>
<Separator class="w-full" />
<div class="p-2">
<LayerControlSettings />
</div>
</div>
</ScrollArea>
</div>

View File

@@ -1,29 +1,23 @@
<script lang="ts">
import LayerTree from './LayerTree.svelte';
import { Button } from '$lib/components/ui/button';
import { Separator } from '$lib/components/ui/separator';
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
import * as Sheet from '$lib/components/ui/sheet';
import * as Accordion from '$lib/components/ui/accordion';
import { Settings } from 'lucide-svelte';
import { basemapTree, overlayTree } from '$lib/assets/layers';
import { settings } from '$lib/db';
import { _ } from 'svelte-i18n';
const { selectedBasemapTree, selectedOverlayTree } = settings;
export let open: boolean;
</script>
<Sheet.Root>
<Sheet.Trigger class="w-full">
<Button variant="ghost" class="w-full px-1 py-1.5">
<Settings size="18" class="mr-2" />
{$_('layers.manage')}
</Button>
</Sheet.Trigger>
<Sheet.Root bind:open>
<Sheet.Trigger class="hidden" />
<Sheet.Content>
<Sheet.Header class="h-full">
<Sheet.Title>{$_('layers.settings')}</Sheet.Title>

View File

@@ -12,7 +12,7 @@
</script>
<form>
<fieldset class="min-w-64">
<fieldset class="min-w-64 mb-1">
<CollapsibleTree nohover={true}>
<LayerTreeNode {name} node={layerTree} bind:selected {multiple} bind:checked />
</CollapsibleTree>

View File

@@ -26,11 +26,11 @@
});
</script>
<div class="flex flex-col gap-0.5">
<div class="flex flex-col gap-[3px]">
{#each Object.keys(node) as id}
{#if typeof node[id] == 'boolean'}
{#if node[id]}
<div class="flex flex-row items-center gap-2 first:mt-1">
<div class="flex flex-row items-center gap-2 first:mt-0.5 h-4">
{#if multiple}
<Checkbox
id="{name}-{id}"

View File

@@ -352,10 +352,12 @@ export const dbUtils = {
applyToFiles([id], callback);
},
applyToSelection: (callback: (file: WritableDraft<GPXFile>) => GPXFile) => {
// TODO
applyToFiles(get(selection).forEach(fileId), callback);
},
duplicateSelection: () => {
applyGlobal((draft) => {
// TODO
let ids = getFileIds(get(settings.fileOrder).length);
get(settings.fileOrder).forEach((fileId, index) => {
if (get(selection).has(fileId)) {
@@ -371,11 +373,15 @@ export const dbUtils = {
},
deleteSelection: () => {
applyGlobal((draft) => {
get(selection).forEach((item) => {
if (item instanceof ListFileItem) {
draft.delete(item.getId());
}
// TODO: Implement deletion of tracks, segments, waypoints
selection.update(($selection) => {
$selection.forEach((item) => {
if (item instanceof ListFileItem) {
draft.delete(item.getId());
}
// TODO: Implement deletion of tracks, segments, waypoints
});
$selection.clear();
return $selection;
});
});
},

View File

@@ -31,6 +31,7 @@
"light": "Light",
"dark": "Dark",
"system": "System",
"layers": "Map layers...",
"distance_markers": "Show distance markers",
"direction_markers": "Show direction markers",
"about": "About",
@@ -109,7 +110,6 @@
"structure_tooltip": "Manage the file structure"
},
"layers": {
"manage": "Manage layers",
"settings": "Layer settings",
"settings_help": "Select the map layers you want to show in the interface, add custom ones, and adjust their settings.",
"selection": "Layer selection",