mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-09-02 16:52:31 +00:00
progress
This commit is contained in:
@@ -19,7 +19,7 @@
|
|||||||
<div class="grow relative">
|
<div class="grow relative">
|
||||||
<Menu />
|
<Menu />
|
||||||
<Toolbar />
|
<Toolbar />
|
||||||
<Map class="h-full" />
|
<Map class="h-full {$verticalFileView ? '' : 'horizontal'}" />
|
||||||
<LayerControl />
|
<LayerControl />
|
||||||
<GPXLayers />
|
<GPXLayers />
|
||||||
<Toaster richColors />
|
<Toaster richColors />
|
||||||
@@ -34,9 +34,11 @@
|
|||||||
<ElevationProfile />
|
<ElevationProfile />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="shrink-0">
|
||||||
{#if $verticalFileView}
|
{#if $verticalFileView}
|
||||||
<FileList orientation="vertical" recursive={true} class="w-60" />
|
<FileList orientation="vertical" recursive={true} class="w-60" />
|
||||||
{/if}
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
|
@@ -21,7 +21,7 @@
|
|||||||
easing: () => 1
|
easing: () => 1
|
||||||
};
|
};
|
||||||
|
|
||||||
const { distanceUnits } = settings;
|
const { distanceUnits, verticalFileView } = settings;
|
||||||
let scaleControl = new mapboxgl.ScaleControl({
|
let scaleControl = new mapboxgl.ScaleControl({
|
||||||
unit: $distanceUnits
|
unit: $distanceUnits
|
||||||
});
|
});
|
||||||
@@ -116,6 +116,10 @@
|
|||||||
$: if ($map) {
|
$: if ($map) {
|
||||||
scaleControl.setUnit($distanceUnits);
|
scaleControl.setUnit($distanceUnits);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: if ($map && !$verticalFileView) {
|
||||||
|
$map.resize();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div {...$$restProps}>
|
<div {...$$restProps}>
|
||||||
@@ -205,11 +209,11 @@
|
|||||||
@apply overflow-hidden;
|
@apply overflow-hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-ctrl-bottom-left) {
|
.horizontal :global(.mapboxgl-ctrl-bottom-left) {
|
||||||
@apply bottom-[42px];
|
@apply bottom-[42px];
|
||||||
}
|
}
|
||||||
|
|
||||||
div :global(.mapboxgl-ctrl-bottom-right) {
|
.horizontal :global(.mapboxgl-ctrl-bottom-right) {
|
||||||
@apply bottom-[42px];
|
@apply bottom-[42px];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -23,7 +23,8 @@
|
|||||||
Thermometer,
|
Thermometer,
|
||||||
Sun,
|
Sun,
|
||||||
Moon,
|
Moon,
|
||||||
Rows3
|
Rows3,
|
||||||
|
Layers3
|
||||||
} from 'lucide-svelte';
|
} from 'lucide-svelte';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -31,18 +32,18 @@
|
|||||||
exportAllFiles,
|
exportAllFiles,
|
||||||
exportSelectedFiles,
|
exportSelectedFiles,
|
||||||
triggerFileInput,
|
triggerFileInput,
|
||||||
selectFiles,
|
|
||||||
createFile
|
createFile
|
||||||
} from '$lib/stores';
|
} 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 { derived } from 'svelte/store';
|
||||||
import { canUndo, canRedo, dbUtils, fileObservers, settings } from '$lib/db';
|
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 { resetMode, setMode, systemPrefersMode } from 'mode-watcher';
|
||||||
|
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from 'svelte-i18n';
|
||||||
import { anySelectedLayer } from './layer-control/utils';
|
|
||||||
import { defaultOverlays } from '$lib/assets/layers';
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
distanceUnits,
|
distanceUnits,
|
||||||
@@ -88,6 +89,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let layerSettingsOpen = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="absolute top-2 left-0 right-0 z-20 flex flex-row justify-center pointer-events-none">
|
<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} />
|
<Shortcut key="Z" ctrl={true} shift={true} />
|
||||||
</Menubar.Item>
|
</Menubar.Item>
|
||||||
<Menubar.Separator />
|
<Menubar.Separator />
|
||||||
<Menubar.Item on:click={() => $selectFiles.selectAllFiles()}>
|
<Menubar.Item on:click={selectAll}>
|
||||||
<span class="w-4 mr-1"></span>
|
<span class="w-4 mr-1"></span>
|
||||||
{$_('menu.select_all')}
|
{$_('menu.select_all')}
|
||||||
<Shortcut key="A" ctrl={true} />
|
<Shortcut key="A" ctrl={true} />
|
||||||
@@ -265,6 +268,11 @@
|
|||||||
</Menubar.RadioGroup>
|
</Menubar.RadioGroup>
|
||||||
</Menubar.SubContent>
|
</Menubar.SubContent>
|
||||||
</Menubar.Sub>
|
</Menubar.Sub>
|
||||||
|
<Menubar.Separator />
|
||||||
|
<Menubar.Item on:click={() => (layerSettingsOpen = true)}>
|
||||||
|
<Layers3 size="16" class="mr-1" />
|
||||||
|
{$_('menu.layers')}
|
||||||
|
</Menubar.Item>
|
||||||
</Menubar.Content>
|
</Menubar.Content>
|
||||||
</Menubar.Menu>
|
</Menubar.Menu>
|
||||||
</Menubar.Root>
|
</Menubar.Root>
|
||||||
@@ -283,6 +291,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<LayerControlSettings bind:open={layerSettingsOpen} />
|
||||||
|
|
||||||
<svelte:window
|
<svelte:window
|
||||||
on:keydown={(e) => {
|
on:keydown={(e) => {
|
||||||
if (e.key === 'n' && (e.metaKey || e.ctrlKey)) {
|
if (e.key === 'n' && (e.metaKey || e.ctrlKey)) {
|
||||||
@@ -315,7 +325,7 @@
|
|||||||
}
|
}
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
} else if (e.key === 'a' && (e.metaKey || e.ctrlKey)) {
|
} else if (e.key === 'a' && (e.metaKey || e.ctrlKey)) {
|
||||||
$selectFiles.selectAllFiles();
|
selectAll();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
} else if (e.key === 'F1') {
|
} else if (e.key === 'F1') {
|
||||||
switchBasemaps();
|
switchBasemaps();
|
||||||
|
@@ -20,6 +20,6 @@
|
|||||||
scrollbarYClasses={orientation === 'vertical' ? '' : ''}
|
scrollbarYClasses={orientation === 'vertical' ? '' : ''}
|
||||||
>
|
>
|
||||||
<div class="flex {orientation === 'vertical' ? 'flex-col' : 'flex-row'} {$$props.class ?? ''}">
|
<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>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
import type { Readable } from 'svelte/store';
|
import type { Readable } from 'svelte/store';
|
||||||
import FileListNodeContent from './FileListNodeContent.svelte';
|
import FileListNodeContent from './FileListNodeContent.svelte';
|
||||||
import FileListNodeLabel from './FileListNodeLabel.svelte';
|
import FileListNodeLabel from './FileListNodeLabel.svelte';
|
||||||
import { getContext } from 'svelte';
|
import { afterUpdate, getContext } from 'svelte';
|
||||||
import { type ListItem, type ListTrackItem } from './FileList';
|
import { type ListItem, type ListTrackItem } from './FileList';
|
||||||
|
|
||||||
export let node:
|
export let node:
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
let recursive = getContext<boolean>('recursive');
|
let recursive = getContext<boolean>('recursive');
|
||||||
|
|
||||||
let label =
|
$: label =
|
||||||
node instanceof GPXFile
|
node instanceof GPXFile
|
||||||
? node.metadata.name
|
? node.metadata.name
|
||||||
: node instanceof Track
|
: node instanceof Track
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
import { GPXFile, Track, Waypoint, type AnyGPXTreeElement, type GPXTreeElement } from 'gpx';
|
import { GPXFile, Track, Waypoint, type AnyGPXTreeElement, type GPXTreeElement } from 'gpx';
|
||||||
import { afterUpdate, getContext, onDestroy, onMount } from 'svelte';
|
import { afterUpdate, getContext, onDestroy, onMount } from 'svelte';
|
||||||
import Sortable from 'sortablejs/Sortable';
|
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 { get, type Readable } from 'svelte/store';
|
||||||
import FileListNodeStore from './FileListNodeStore.svelte';
|
import FileListNodeStore from './FileListNodeStore.svelte';
|
||||||
import FileListNode from './FileListNode.svelte';
|
import FileListNode from './FileListNode.svelte';
|
||||||
@@ -50,16 +50,17 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { fileOrder } = settings;
|
||||||
function syncFileOrder() {
|
function syncFileOrder() {
|
||||||
if (sortableLevel !== 'file') {
|
if (!sortable || sortableLevel !== 'file') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*Object.keys(buttons).forEach((fileId) => {
|
if ($fileOrder.length !== $fileObservers.size) {
|
||||||
if (!get(fileObservers).has(fileId)) {
|
// Files were added or removed
|
||||||
delete buttons[fileId];
|
fileOrder.set(sortable.toArray());
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
});*/
|
|
||||||
|
|
||||||
const currentOrder = sortable.toArray();
|
const currentOrder = sortable.toArray();
|
||||||
if (currentOrder.length !== $fileOrder.length) {
|
if (currentOrder.length !== $fileOrder.length) {
|
||||||
@@ -74,8 +75,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { fileOrder } = settings;
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
sortable = Sortable.create(container, {
|
sortable = Sortable.create(container, {
|
||||||
group: {
|
group: {
|
||||||
@@ -109,13 +108,19 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$: if ($fileOrder && sortable) {
|
$: if ($fileOrder) {
|
||||||
syncFileOrder();
|
syncFileOrder();
|
||||||
}
|
}
|
||||||
|
|
||||||
afterUpdate(() => {
|
afterUpdate(() => {
|
||||||
|
if (sortableLevel === 'file') {
|
||||||
syncFileOrder();
|
syncFileOrder();
|
||||||
// TODO: update selection if files are removed
|
Object.keys(elements).forEach((fileId) => {
|
||||||
|
if (!get(fileObservers).has(fileId)) {
|
||||||
|
delete elements[fileId];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const unsubscribe = selection.subscribe(($selection) => {
|
const unsubscribe = selection.subscribe(($selection) => {
|
||||||
|
@@ -4,6 +4,21 @@ import { fileObservers } from "$lib/db";
|
|||||||
|
|
||||||
export const selection = writable<SelectionTreeType>(new SelectionTreeType(new ListRootItem()));
|
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() {
|
export function selectAll() {
|
||||||
selection.update(($selection) => {
|
selection.update(($selection) => {
|
||||||
get(fileObservers).forEach((_file, fileId) => {
|
get(fileObservers).forEach((_file, fileId) => {
|
||||||
|
95
website/src/lib/components/gpx-layer/DistanceMarkers.ts
Normal file
95
website/src/lib/components/gpx-layer/DistanceMarkers.ts
Normal 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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@@ -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 { settings, type GPXFileWithStatistics } from "$lib/db";
|
||||||
import { get, type Readable } from "svelte/store";
|
import { get, type Readable } from "svelte/store";
|
||||||
import mapboxgl from "mapbox-gl";
|
import mapboxgl from "mapbox-gl";
|
||||||
import { currentWaypoint, waypointPopup } from "./WaypointPopup";
|
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;
|
let defaultOpacity = 1;
|
||||||
|
|
||||||
const colors = [
|
const colors = [
|
||||||
@@ -37,7 +39,7 @@ function decrementColor(color: string) {
|
|||||||
colorCount[color]--;
|
colorCount[color]--;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { directionMarkers, distanceMarkers, distanceUnits } = settings;
|
const { directionMarkers } = settings;
|
||||||
|
|
||||||
export class GPXLayer {
|
export class GPXLayer {
|
||||||
map: mapboxgl.Map;
|
map: mapboxgl.Map;
|
||||||
@@ -45,6 +47,7 @@ export class GPXLayer {
|
|||||||
file: Readable<GPXFileWithStatistics | undefined>;
|
file: Readable<GPXFileWithStatistics | undefined>;
|
||||||
layerColor: string;
|
layerColor: string;
|
||||||
markers: mapboxgl.Marker[] = [];
|
markers: mapboxgl.Marker[] = [];
|
||||||
|
selected: ListItem[] = [];
|
||||||
unsubscribe: Function[] = [];
|
unsubscribe: Function[] = [];
|
||||||
|
|
||||||
updateBinded: () => void = this.update.bind(this);
|
updateBinded: () => void = this.update.bind(this);
|
||||||
@@ -56,13 +59,17 @@ export class GPXLayer {
|
|||||||
this.file = file;
|
this.file = file;
|
||||||
this.layerColor = getColor();
|
this.layerColor = getColor();
|
||||||
this.unsubscribe.push(file.subscribe(this.updateBinded));
|
this.unsubscribe.push(file.subscribe(this.updateBinded));
|
||||||
this.unsubscribe.push(directionMarkers.subscribe(this.updateBinded));
|
this.unsubscribe.push(selection.subscribe($selection => {
|
||||||
this.unsubscribe.push(distanceMarkers.subscribe(this.updateBinded));
|
let selected = $selection.getChild(fileId)?.getSelected() || [];
|
||||||
this.unsubscribe.push(distanceUnits.subscribe(() => {
|
if (selected.length !== this.selected.length || selected.some((item, index) => item !== this.selected[index])) {
|
||||||
if (get(distanceMarkers)) {
|
this.selected = selected;
|
||||||
this.update();
|
this.update();
|
||||||
|
if (this.selected.length > 0) {
|
||||||
|
this.moveToFront();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
this.unsubscribe.push(directionMarkers.subscribe(this.updateBinded));
|
||||||
|
|
||||||
this.map.on('style.load', this.updateBinded);
|
this.map.on('style.load', this.updateBinded);
|
||||||
}
|
}
|
||||||
@@ -74,7 +81,6 @@ export class GPXLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
let source = this.map.getSource(this.fileId);
|
let source = this.map.getSource(this.fileId);
|
||||||
if (source) {
|
if (source) {
|
||||||
source.setData(this.getGeoJSON());
|
source.setData(this.getGeoJSON());
|
||||||
@@ -132,43 +138,6 @@ export class GPXLayer {
|
|||||||
this.map.removeLayer(this.fileId + '-direction');
|
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
|
} catch (e) { // No reliable way to check if the map is ready to add sources and layers
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -212,9 +181,6 @@ export class GPXLayer {
|
|||||||
if (this.map.getLayer(this.fileId + '-direction')) {
|
if (this.map.getLayer(this.fileId + '-direction')) {
|
||||||
this.map.removeLayer(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)) {
|
if (this.map.getLayer(this.fileId)) {
|
||||||
this.map.removeLayer(this.fileId);
|
this.map.removeLayer(this.fileId);
|
||||||
}
|
}
|
||||||
@@ -238,9 +204,6 @@ export class GPXLayer {
|
|||||||
if (this.map.getLayer(this.fileId + '-direction')) {
|
if (this.map.getLayer(this.fileId + '-direction')) {
|
||||||
this.map.moveLayer(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) {
|
selectOnClick(e: any) {
|
||||||
@@ -248,9 +211,9 @@ export class GPXLayer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (e.originalEvent.shiftKey) {
|
if (e.originalEvent.shiftKey) {
|
||||||
get(selectFiles).addSelect(this.fileId);
|
addSelect(this.fileId);
|
||||||
} else {
|
} else {
|
||||||
get(selectFiles).select(this.fileId);
|
select(this.fileId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,6 +227,8 @@ export class GPXLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let data = file.toGeoJSON();
|
let data = file.toGeoJSON();
|
||||||
|
|
||||||
|
let trackIndex = 0, segmentIndex = 0;
|
||||||
for (let feature of data.features) {
|
for (let feature of data.features) {
|
||||||
if (!feature.properties) {
|
if (!feature.properties) {
|
||||||
feature.properties = {};
|
feature.properties = {};
|
||||||
@@ -277,44 +242,18 @@ export class GPXLayer {
|
|||||||
if (!feature.properties.opacity) {
|
if (!feature.properties.opacity) {
|
||||||
feature.properties.opacity = defaultOpacity;
|
feature.properties.opacity = defaultOpacity;
|
||||||
}
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
segmentIndex++;
|
||||||
|
if (segmentIndex >= file.trk[trackIndex].trkseg.length) {
|
||||||
|
segmentIndex = 0;
|
||||||
|
trackIndex++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: 'FeatureCollection',
|
|
||||||
features
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toPointerCursor() {
|
function toPointerCursor() {
|
||||||
|
@@ -4,7 +4,9 @@
|
|||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import WaypointPopup from './WaypointPopup.svelte';
|
import WaypointPopup from './WaypointPopup.svelte';
|
||||||
import { fileObservers } from '$lib/db';
|
import { fileObservers } from '$lib/db';
|
||||||
import { selection } from '$lib/components/file-list/Selection';
|
import { DistanceMarkers } from './DistanceMarkers';
|
||||||
|
|
||||||
|
let distanceMarkers: DistanceMarkers;
|
||||||
|
|
||||||
$: if ($map && $fileObservers) {
|
$: if ($map && $fileObservers) {
|
||||||
gpxLayers.update(($layers) => {
|
gpxLayers.update(($layers) => {
|
||||||
@@ -25,13 +27,9 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$: $selection.forEach((item) => {
|
$: if ($map) {
|
||||||
let fileId = item.getFileId();
|
distanceMarkers = new DistanceMarkers(get(map));
|
||||||
// TODO move more precise selection to front?
|
|
||||||
if ($gpxLayers.has(fileId)) {
|
|
||||||
$gpxLayers.get(fileId)?.moveToFront();
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<WaypointPopup />
|
<WaypointPopup />
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import CustomControl from '$lib/components/custom-control/CustomControl.svelte';
|
import CustomControl from '$lib/components/custom-control/CustomControl.svelte';
|
||||||
import LayerTree from './LayerTree.svelte';
|
import LayerTree from './LayerTree.svelte';
|
||||||
import LayerControlSettings from './LayerControlSettings.svelte';
|
|
||||||
|
|
||||||
import { Separator } from '$lib/components/ui/separator';
|
import { Separator } from '$lib/components/ui/separator';
|
||||||
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
|
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
|
||||||
@@ -121,10 +120,6 @@
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<Separator class="w-full" />
|
|
||||||
<div class="p-2">
|
|
||||||
<LayerControlSettings />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,29 +1,23 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import LayerTree from './LayerTree.svelte';
|
import LayerTree from './LayerTree.svelte';
|
||||||
|
|
||||||
import { Button } from '$lib/components/ui/button';
|
|
||||||
import { Separator } from '$lib/components/ui/separator';
|
import { Separator } from '$lib/components/ui/separator';
|
||||||
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
|
import { ScrollArea } from '$lib/components/ui/scroll-area/index.js';
|
||||||
import * as Sheet from '$lib/components/ui/sheet';
|
import * as Sheet from '$lib/components/ui/sheet';
|
||||||
import * as Accordion from '$lib/components/ui/accordion';
|
import * as Accordion from '$lib/components/ui/accordion';
|
||||||
|
|
||||||
import { Settings } from 'lucide-svelte';
|
|
||||||
|
|
||||||
import { basemapTree, overlayTree } from '$lib/assets/layers';
|
import { basemapTree, overlayTree } from '$lib/assets/layers';
|
||||||
import { settings } from '$lib/db';
|
import { settings } from '$lib/db';
|
||||||
|
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from 'svelte-i18n';
|
||||||
|
|
||||||
const { selectedBasemapTree, selectedOverlayTree } = settings;
|
const { selectedBasemapTree, selectedOverlayTree } = settings;
|
||||||
|
|
||||||
|
export let open: boolean;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Sheet.Root>
|
<Sheet.Root bind:open>
|
||||||
<Sheet.Trigger class="w-full">
|
<Sheet.Trigger class="hidden" />
|
||||||
<Button variant="ghost" class="w-full px-1 py-1.5">
|
|
||||||
<Settings size="18" class="mr-2" />
|
|
||||||
{$_('layers.manage')}
|
|
||||||
</Button>
|
|
||||||
</Sheet.Trigger>
|
|
||||||
<Sheet.Content>
|
<Sheet.Content>
|
||||||
<Sheet.Header class="h-full">
|
<Sheet.Header class="h-full">
|
||||||
<Sheet.Title>{$_('layers.settings')}</Sheet.Title>
|
<Sheet.Title>{$_('layers.settings')}</Sheet.Title>
|
||||||
|
@@ -12,7 +12,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form>
|
<form>
|
||||||
<fieldset class="min-w-64">
|
<fieldset class="min-w-64 mb-1">
|
||||||
<CollapsibleTree nohover={true}>
|
<CollapsibleTree nohover={true}>
|
||||||
<LayerTreeNode {name} node={layerTree} bind:selected {multiple} bind:checked />
|
<LayerTreeNode {name} node={layerTree} bind:selected {multiple} bind:checked />
|
||||||
</CollapsibleTree>
|
</CollapsibleTree>
|
||||||
|
@@ -26,11 +26,11 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col gap-0.5">
|
<div class="flex flex-col gap-[3px]">
|
||||||
{#each Object.keys(node) as id}
|
{#each Object.keys(node) as id}
|
||||||
{#if typeof node[id] == 'boolean'}
|
{#if typeof node[id] == 'boolean'}
|
||||||
{#if node[id]}
|
{#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}
|
{#if multiple}
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id="{name}-{id}"
|
id="{name}-{id}"
|
||||||
|
@@ -352,10 +352,12 @@ export const dbUtils = {
|
|||||||
applyToFiles([id], callback);
|
applyToFiles([id], callback);
|
||||||
},
|
},
|
||||||
applyToSelection: (callback: (file: WritableDraft<GPXFile>) => GPXFile) => {
|
applyToSelection: (callback: (file: WritableDraft<GPXFile>) => GPXFile) => {
|
||||||
|
// TODO
|
||||||
applyToFiles(get(selection).forEach(fileId), callback);
|
applyToFiles(get(selection).forEach(fileId), callback);
|
||||||
},
|
},
|
||||||
duplicateSelection: () => {
|
duplicateSelection: () => {
|
||||||
applyGlobal((draft) => {
|
applyGlobal((draft) => {
|
||||||
|
// TODO
|
||||||
let ids = getFileIds(get(settings.fileOrder).length);
|
let ids = getFileIds(get(settings.fileOrder).length);
|
||||||
get(settings.fileOrder).forEach((fileId, index) => {
|
get(settings.fileOrder).forEach((fileId, index) => {
|
||||||
if (get(selection).has(fileId)) {
|
if (get(selection).has(fileId)) {
|
||||||
@@ -371,12 +373,16 @@ export const dbUtils = {
|
|||||||
},
|
},
|
||||||
deleteSelection: () => {
|
deleteSelection: () => {
|
||||||
applyGlobal((draft) => {
|
applyGlobal((draft) => {
|
||||||
get(selection).forEach((item) => {
|
selection.update(($selection) => {
|
||||||
|
$selection.forEach((item) => {
|
||||||
if (item instanceof ListFileItem) {
|
if (item instanceof ListFileItem) {
|
||||||
draft.delete(item.getId());
|
draft.delete(item.getId());
|
||||||
}
|
}
|
||||||
// TODO: Implement deletion of tracks, segments, waypoints
|
// TODO: Implement deletion of tracks, segments, waypoints
|
||||||
});
|
});
|
||||||
|
$selection.clear();
|
||||||
|
return $selection;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
deleteAllFiles: () => {
|
deleteAllFiles: () => {
|
||||||
|
@@ -31,6 +31,7 @@
|
|||||||
"light": "Light",
|
"light": "Light",
|
||||||
"dark": "Dark",
|
"dark": "Dark",
|
||||||
"system": "System",
|
"system": "System",
|
||||||
|
"layers": "Map layers...",
|
||||||
"distance_markers": "Show distance markers",
|
"distance_markers": "Show distance markers",
|
||||||
"direction_markers": "Show direction markers",
|
"direction_markers": "Show direction markers",
|
||||||
"about": "About",
|
"about": "About",
|
||||||
@@ -109,7 +110,6 @@
|
|||||||
"structure_tooltip": "Manage the file structure"
|
"structure_tooltip": "Manage the file structure"
|
||||||
},
|
},
|
||||||
"layers": {
|
"layers": {
|
||||||
"manage": "Manage layers",
|
|
||||||
"settings": "Layer settings",
|
"settings": "Layer settings",
|
||||||
"settings_help": "Select the map layers you want to show in the interface, add custom ones, and adjust their settings.",
|
"settings_help": "Select the map layers you want to show in the interface, add custom ones, and adjust their settings.",
|
||||||
"selection": "Layer selection",
|
"selection": "Layer selection",
|
||||||
|
Reference in New Issue
Block a user