fix tools

This commit is contained in:
vcoppe
2025-10-18 16:10:08 +02:00
parent 9fa8fe5767
commit c59cd66141
60 changed files with 1289 additions and 1161 deletions

View File

@@ -544,7 +544,9 @@
<svelte:window
on:keydown={(e) => {
let targetInput =
e.target.tagName === 'INPUT' ||
e &&
e.target &&
(e.target.tagName === 'INPUT' ||
e.target.tagName === 'TEXTAREA' ||
e.target.tagName === 'SELECT' ||
e.target.role === 'combobox' ||
@@ -552,7 +554,7 @@
e.target.role === 'menu' ||
e.target.role === 'menuitem' ||
e.target.role === 'menuitemradio' ||
e.target.role === 'menuitemcheckbox';
e.target.role === 'menuitemcheckbox');
if (e.key === '+' && (e.metaKey || e.ctrlKey)) {
createFile();

View File

@@ -10,14 +10,7 @@ import {
ListFileItem,
ListRootItem,
} from '$lib/components/file-list/file-list';
import {
getClosestLinePoint,
getElevation,
resetCursor,
setGrabbingCursor,
setPointerCursor,
setScissorsCursor,
} from '$lib/utils';
import { getClosestLinePoint, getElevation } from '$lib/utils';
import { selectedWaypoint } from '$lib/components/toolbar/tools/waypoint/waypoint';
import { MapPin, Square } from 'lucide-static';
import { getSymbolKey, symbols } from '$lib/assets/symbols';
@@ -28,6 +21,7 @@ import { currentTool, Tool } from '$lib/components/toolbar/tools';
import { fileActionManager } from '$lib/logic/file-action-manager';
import { fileActions } from '$lib/logic/file-actions';
import { splitAs } from '$lib/components/toolbar/tools/scissors/scissors';
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
const colors = [
'#ff0000',
@@ -335,12 +329,12 @@ export class GPXLayer {
e.stopPropagation();
});
marker.on('dragstart', () => {
setGrabbingCursor();
mapCursor.notify(MapCursorState.WAYPOINT_DRAGGING, true);
marker.getElement().style.cursor = 'grabbing';
waypointPopup?.hide();
});
marker.on('dragend', (e) => {
resetCursor();
mapCursor.notify(MapCursorState.WAYPOINT_DRAGGING, false);
marker.getElement().style.cursor = '';
getElevation([marker._waypoint]).then((ele) => {
fileActionManager.applyToFile(this.fileId, (file) => {
@@ -431,14 +425,15 @@ export class GPXLayer {
new ListTrackSegmentItem(this.fileId, trackIndex, segmentIndex)
)
) {
setScissorsCursor();
mapCursor.notify(MapCursorState.SCISSORS, true);
} else {
setPointerCursor();
mapCursor.notify(MapCursorState.LAYER_HOVER, true);
}
}
layerOnMouseLeave() {
resetCursor();
mapCursor.notify(MapCursorState.SCISSORS, false);
mapCursor.notify(MapCursorState.LAYER_HOVER, false);
}
layerOnMouseMove(e: any) {

View File

@@ -1,4 +1,4 @@
import { resetCursor, setCrosshairCursor } from '$lib/utils';
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
import type mapboxgl from 'mapbox-gl';
export class GoogleRedirect {
@@ -13,7 +13,7 @@ export class GoogleRedirect {
if (this.enabled) return;
this.enabled = true;
setCrosshairCursor();
mapCursor.notify(MapCursorState.STREET_VIEW_CROSSHAIR, true);
this.map.on('click', this.openStreetView);
}
@@ -21,11 +21,11 @@ export class GoogleRedirect {
if (!this.enabled) return;
this.enabled = false;
resetCursor();
mapCursor.notify(MapCursorState.STREET_VIEW_CROSSHAIR, false);
this.map.off('click', this.openStreetView);
}
openStreetView(e) {
openStreetView(e: mapboxgl.MapMouseEvent) {
window.open(
`https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=${e.lngLat.lat},${e.lngLat.lng}`
);

View File

@@ -1,7 +1,7 @@
import mapboxgl, { type LayerSpecification, type VectorSourceSpecification } from 'mapbox-gl';
import { Viewer, type ViewerBearingEvent } from 'mapillary-js/dist/mapillary.module';
import 'mapillary-js/dist/mapillary.css';
import { resetCursor, setPointerCursor } from '$lib/utils';
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
const mapillarySource: VectorSourceSpecification = {
type: 'vector',
@@ -140,10 +140,10 @@ export class MapillaryLayer {
this.viewer.resize();
this.viewer.moveTo(e.features[0].properties.id);
setPointerCursor();
mapCursor.notify(MapCursorState.MAPILLARY_HOVER, true);
}
onMouseLeave() {
resetCursor();
mapCursor.notify(MapCursorState.MAPILLARY_HOVER, false);
}
}

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { Button } from '$lib/components/ui/button';
import * as Tooltip from '$lib/components/ui/tooltip/index.js';
import { tool, Tool } from '$lib/components/toolbar/tools';
import { currentTool, Tool } from '$lib/components/toolbar/tools';
import type { Snippet } from 'svelte';
let {
@@ -15,10 +15,10 @@
} = $props();
function toggleTool() {
if (tool.current === itemTool) {
tool.current = null;
if ($currentTool === itemTool) {
$currentTool = null;
} else {
tool.current = itemTool;
$currentTool = itemTool;
}
}
</script>
@@ -30,7 +30,7 @@
<Button
{...props}
variant="ghost"
class="h-[26px] px-1 py-1.5 {tool.current === itemTool ? 'bg-accent' : ''}"
class="h-[26px] px-1 py-1.5 {$currentTool === itemTool ? 'bg-accent' : ''}"
onclick={toggleTool}
aria-label={label}
>

View File

@@ -1,68 +1,64 @@
<script lang="ts">
import { Tool, tool } from '$lib/components/toolbar/tools';
import { Tool, currentTool } from '$lib/components/toolbar/tools';
import * as Card from '$lib/components/ui/card';
import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
import Scissors from '$lib/components/toolbar/tools/scissors/Scissors.svelte';
import Waypoint from '$lib/components/toolbar/tools/waypoint/Waypoint.svelte';
import Time from '$lib/components/toolbar/tools/Time.svelte';
// import Time from '$lib/components/toolbar/tools/Time.svelte';
import Merge from '$lib/components/toolbar/tools/Merge.svelte';
import Extract from '$lib/components/toolbar/tools/Extract.svelte';
import Elevation from '$lib/components/toolbar/tools/Elevation.svelte';
import Extract from '$lib/components/toolbar/tools/Extract.svelte';
import Clean from '$lib/components/toolbar/tools/Clean.svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import RoutingControlPopup from '$lib/components/toolbar/tools/routing/RoutingControlPopup.svelte';
import { onMount } from 'svelte';
import mapboxgl from 'mapbox-gl';
import { settings } from '$lib/logic/settings';
let {
popupElement,
popup,
class: className = '',
}: {
popupElement: HTMLDivElement;
popup: mapboxgl.Popup;
class: string;
} = $props();
const { minimizeRoutingMenu } = settings;
onMount(() => {
popup = new mapboxgl.Popup({
let popupElement: HTMLDivElement | undefined = $state(undefined);
let popup: mapboxgl.Popup | undefined = $derived.by(() => {
if (!popupElement) {
return undefined;
}
let popup = new mapboxgl.Popup({
closeButton: false,
maxWidth: undefined,
});
popup.setDOMContent(popupElement);
popupElement.classList.remove('hidden');
return popup;
});
</script>
{#if tool.current !== null}
{#if $currentTool !== null}
<div class="translate-x-1 h-full animate-in animate-out {className}">
<div class="rounded-md shadow-md pointer-events-auto">
<Card.Root class="rounded-md border-none">
<Card.Content class="p-2.5">
{#if tool.current === Tool.ROUTING}
<Routing
{popup}
{popupElement}
bind:minimized={minimizeRoutingMenu.value}
/>
{:else if tool.current === Tool.SCISSORS}
{#if $currentTool === Tool.ROUTING}
<Routing {popup} {popupElement} bind:minimized={$minimizeRoutingMenu} />
{:else if $currentTool === Tool.SCISSORS}
<Scissors />
{:else if tool.current === Tool.WAYPOINT}
{:else if $currentTool === Tool.WAYPOINT}
<Waypoint />
{:else if tool.current === Tool.TIME}
<Time />
{:else if tool.current === Tool.MERGE}
<!-- {:else if $currentTool === Tool.TIME}
<Time /> -->
{:else if $currentTool === Tool.MERGE}
<Merge />
{:else if tool.current === Tool.ELEVATION}
{:else if $currentTool === Tool.ELEVATION}
<Elevation />
{:else if tool.current === Tool.EXTRACT}
{:else if $currentTool === Tool.EXTRACT}
<Extract />
{:else if tool.current === Tool.CLEAN}
{:else if $currentTool === Tool.CLEAN}
<Clean />
{:else if tool.current === Tool.REDUCE}
{:else if $currentTool === Tool.REDUCE}
<Reduce />
{/if}
</Card.Content>
@@ -73,8 +69,8 @@
<svelte:window
on:keydown={(e) => {
if (tool.current !== null && e.key === 'Escape') {
tool.current = null;
if ($currentTool !== null && e.key === 'Escape') {
$currentTool = null;
}
}}
/>

View File

@@ -13,12 +13,13 @@
import Help from '$lib/components/Help.svelte';
import { i18n } from '$lib/i18n.svelte';
import { onDestroy, onMount } from 'svelte';
import { getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils';
import { getURLForLanguage } from '$lib/utils';
import { Trash2 } from '@lucide/svelte';
import { map } from '$lib/components/map/map';
import type { GeoJSONSource } from 'mapbox-gl';
import { selection } from '$lib/logic/selection';
import { fileActions } from '$lib/logic/file-actions';
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
let props: {
class?: string;
@@ -30,10 +31,10 @@
let rectangleCoordinates: mapboxgl.LngLat[] = $state([]);
$effect(() => {
if (map.value) {
if ($map) {
if (rectangleCoordinates.length != 2) {
if (map.value.getLayer('rectangle')) {
map.value.removeLayer('rectangle');
if ($map.getLayer('rectangle')) {
$map.removeLayer('rectangle');
}
} else {
let data: GeoJSON.Feature = {
@@ -52,17 +53,17 @@
},
properties: {},
};
let source: GeoJSONSource | undefined = map.value.getSource('rectangle');
let source: GeoJSONSource | undefined = $map.getSource('rectangle');
if (source) {
source.setData(data);
} else {
map.value.addSource('rectangle', {
$map.addSource('rectangle', {
type: 'geojson',
data: data,
});
}
if (!map.value.getLayer('rectangle')) {
map.value.addLayer({
if (!$map.getLayer('rectangle')) {
$map.addLayer({
id: 'rectangle',
type: 'fill',
source: 'rectangle',
@@ -93,39 +94,39 @@
}
onMount(() => {
if (map.value) {
setCrosshairCursor(map.value.getCanvas());
map.value.on('mousedown', onMouseDown);
map.value.on('mousemove', onMouseMove);
map.value.on('mouseup', onMouseUp);
map.value.on('touchstart', onMouseDown);
map.value.on('touchmove', onMouseMove);
map.value.on('touchend', onMouseUp);
map.value.dragPan.disable();
if ($map) {
mapCursor.notify(MapCursorState.TOOL_WITH_CROSSHAIR, true);
$map.on('mousedown', onMouseDown);
$map.on('mousemove', onMouseMove);
$map.on('mouseup', onMouseUp);
$map.on('touchstart', onMouseDown);
$map.on('touchmove', onMouseMove);
$map.on('touchend', onMouseUp);
$map.dragPan.disable();
}
});
onDestroy(() => {
if (map.value) {
resetCursor(map.value.getCanvas());
map.value.off('mousedown', onMouseDown);
map.value.off('mousemove', onMouseMove);
map.value.off('mouseup', onMouseUp);
map.value.off('touchstart', onMouseDown);
map.value.off('touchmove', onMouseMove);
map.value.off('touchend', onMouseUp);
map.value.dragPan.enable();
if ($map) {
mapCursor.notify(MapCursorState.TOOL_WITH_CROSSHAIR, false);
$map.off('mousedown', onMouseDown);
$map.off('mousemove', onMouseMove);
$map.off('mouseup', onMouseUp);
$map.off('touchstart', onMouseDown);
$map.off('touchmove', onMouseMove);
$map.off('touchend', onMouseUp);
$map.dragPan.enable();
if (map.value.getLayer('rectangle')) {
map.value.removeLayer('rectangle');
if ($map.getLayer('rectangle')) {
$map.removeLayer('rectangle');
}
if (map.value.getSource('rectangle')) {
map.value.removeSource('rectangle');
if ($map.getSource('rectangle')) {
$map.removeSource('rectangle');
}
}
});
let validSelection = $derived(selection.value.size > 0);
let validSelection = $derived($selection.size > 0);
</script>
<div class="flex flex-col gap-3 w-full max-w-80 items-center {props.class ?? ''}">

View File

@@ -12,7 +12,7 @@
class?: string;
} = $props();
let validSelection = $derived(selection.value.size > 0);
let validSelection = $derived($selection.size > 0);
</script>
<div class="flex flex-col gap-3 w-full max-w-80 {props.class ?? ''}">
@@ -21,8 +21,8 @@
class="whitespace-normal h-fit"
disabled={!validSelection}
onclick={async () => {
if (map.value) {
fileActions.addElevationToSelection(map.value);
if ($map) {
fileActions.addElevationToSelection($map);
}
}}
>

View File

@@ -20,8 +20,8 @@
} = $props();
let validSelection = $derived(
selection.value.size > 0 &&
selection.value.getSelected().every((item) => {
$selection.size > 0 &&
$selection.getSelected().every((item) => {
if (
item instanceof ListWaypointsItem ||
item instanceof ListWaypointItem ||

View File

@@ -16,20 +16,20 @@
import { Group } from '@lucide/svelte';
import { getURLForLanguage } from '$lib/utils';
import Shortcut from '$lib/components/Shortcut.svelte';
import { gpxStatistics } from '$lib/stores';
import { selection } from '$lib/logic/selection';
import { fileStateCollection } from '$lib/logic/file-state';
import { fileActions } from '$lib/logic/file-actions';
import { gpxStatistics } from '$lib/logic/statistics';
let props: {
class?: string;
} = $props();
let canMergeTraces = $derived.by(() => {
if (selection.value.size > 1) {
if ($selection.size > 1) {
return true;
} else if (selection.value.size === 1) {
let selected = selection.value.getSelected()[0];
} else if ($selection.size === 1) {
let selected = $selection.getSelected()[0];
if (selected instanceof ListFileItem) {
let file = fileStateCollection.getFile(selected.getFileId());
if (file) {
@@ -47,8 +47,8 @@
});
let canMergeContents = $derived(
selection.value.size > 1 &&
selection.value
$selection.size > 1 &&
$selection
.getSelected()
.some((item) => item instanceof ListFileItem || item instanceof ListTrackItem)
);

View File

@@ -1,195 +0,0 @@
<script lang="ts">
import { Label } from '$lib/components/ui/label/index.js';
import { Button } from '$lib/components/ui/button';
import { Slider } from '$lib/components/ui/slider';
import {
ListItem,
ListRootItem,
ListTrackSegmentItem,
} from '$lib/components/file-list/file-list';
import Help from '$lib/components/Help.svelte';
import { Funnel } from '@lucide/svelte';
import { i18n } from '$lib/i18n.svelte';
import WithUnits from '$lib/components/WithUnits.svelte';
import { map } from '$lib/components/map/map';
import { onDestroy } from 'svelte';
import { ramerDouglasPeucker, TrackPoint, type SimplifiedTrackPoint } from 'gpx';
import { getURLForLanguage } from '$lib/utils';
import type { GeoJSONSource } from 'mapbox-gl';
import { selection } from '$lib/logic/selection';
import { fileActions } from '$lib/logic/file-actions';
let props: { class?: string } = $props();
let sliderValue = $state([50]);
let maxPoints = $state(0);
let currentPoints = $state(0);
const minTolerance = 0.1;
const maxTolerance = 10000;
let validSelection = $derived(
selection.value.hasAnyChildren(new ListRootItem(), true, ['waypoints'])
);
let tolerance = $derived(
minTolerance * 2 ** (sliderValue[0] / (100 / Math.log2(maxTolerance / minTolerance)))
);
let simplified = new Map<string, [ListItem, number, SimplifiedTrackPoint[]]>();
let unsubscribes = new Map<string, () => void>();
function update() {
maxPoints = 0;
currentPoints = 0;
let data: GeoJSON.FeatureCollection = {
type: 'FeatureCollection',
features: [],
};
simplified.forEach(([item, maxPts, points], itemFullId) => {
maxPoints += maxPts;
let current = points.filter(
(point) => point.distance === undefined || point.distance >= tolerance
);
currentPoints += current.length;
data.features.push({
type: 'Feature',
geometry: {
type: 'LineString',
coordinates: current.map((point) => [
point.point.getLongitude(),
point.point.getLatitude(),
]),
},
properties: {},
});
});
if (map.value) {
let source: GeoJSONSource | undefined = map.value.getSource('simplified');
if (source) {
source.setData(data);
} else {
map.value.addSource('simplified', {
type: 'geojson',
data: data,
});
}
if (!map.value.getLayer('simplified')) {
map.value.addLayer({
id: 'simplified',
type: 'line',
source: 'simplified',
paint: {
'line-color': 'white',
'line-width': 3,
},
});
} else {
map.value.moveLayer('simplified');
}
}
}
// $effect(() => {
// if ($fileObservers) {
// unsubscribes.forEach((unsubscribe, fileId) => {
// if (!$fileObservers.has(fileId)) {
// unsubscribe();
// unsubscribes.delete(fileId);
// }
// });
// $fileObservers.forEach((fileStore, fileId) => {
// if (!unsubscribes.has(fileId)) {
// let unsubscribe = derived([fileStore, selection], ([fs, sel]) => [
// fs,
// sel,
// ]).subscribe(([fs, sel]) => {
// if (fs) {
// fs.file.forEachSegment((segment, trackIndex, segmentIndex) => {
// let segmentItem = new ListTrackSegmentItem(
// fileId,
// trackIndex,
// segmentIndex
// );
// if (sel.hasAnyParent(segmentItem)) {
// let statistics = fs.statistics.getStatisticsFor(segmentItem);
// simplified.set(segmentItem.getFullId(), [
// segmentItem,
// statistics.local.points.length,
// ramerDouglasPeucker(statistics.local.points, minTolerance),
// ]);
// update();
// } else if (simplified.has(segmentItem.getFullId())) {
// simplified.delete(segmentItem.getFullId());
// update();
// }
// });
// }
// });
// unsubscribes.set(fileId, unsubscribe);
// }
// });
// }
// });
$effect(() => {
if (tolerance) {
update();
}
});
onDestroy(() => {
if (map.value) {
if (map.value.getLayer('simplified')) {
map.value.removeLayer('simplified');
}
if (map.value.getSource('simplified')) {
map.value.removeSource('simplified');
}
}
unsubscribes.forEach((unsubscribe) => unsubscribe());
simplified.clear();
});
function reduce() {
let itemsAndPoints = new Map<ListItem, TrackPoint[]>();
simplified.forEach(([item, maxPts, points], itemFullId) => {
itemsAndPoints.set(
item,
points
.filter((point) => point.distance === undefined || point.distance >= tolerance)
.map((point) => point.point)
);
});
fileActions.reduce(itemsAndPoints);
}
</script>
<div class="flex flex-col gap-3 w-full max-w-80 {props.class ?? ''}">
<div class="p-2">
<Slider bind:value={sliderValue} min={0} max={100} step={1} type="multiple" />
</div>
<Label class="flex flex-row justify-between">
<span>{i18n._('toolbar.reduce.tolerance')}</span>
<WithUnits value={tolerance / 1000} type="distance" decimals={4} class="font-normal" />
</Label>
<Label class="flex flex-row justify-between">
<span>{i18n._('toolbar.reduce.number_of_points')}</span>
<span class="font-normal">{currentPoints}/{maxPoints}</span>
</Label>
<Button variant="outline" disabled={!validSelection} onclick={reduce}>
<Funnel size="16" class="mr-1" />
{i18n._('toolbar.reduce.button')}
</Button>
<Help link={getURLForLanguage(i18n.lang, '/help/toolbar/minify')}>
{#if validSelection}
{i18n._('toolbar.reduce.help')}
{:else}
{i18n._('toolbar.reduce.help_no_selection')}
{/if}
</Help>
</div>

View File

@@ -0,0 +1,63 @@
<script lang="ts">
import { Label } from '$lib/components/ui/label/index.js';
import { Button } from '$lib/components/ui/button';
import { Slider } from '$lib/components/ui/slider';
import { ListItem, ListRootItem } from '$lib/components/file-list/file-list';
import Help from '$lib/components/Help.svelte';
import { Funnel } from '@lucide/svelte';
import { i18n } from '$lib/i18n.svelte';
import WithUnits from '$lib/components/WithUnits.svelte';
import { onDestroy } from 'svelte';
import { getURLForLanguage } from '$lib/utils';
import { selection } from '$lib/logic/selection';
import { minTolerance, ReducedGPXLayerCollection, tolerance } from './reduce';
let props: { class?: string } = $props();
let sliderValue = $state([50]);
let maxPoints = $state(0);
let currentPoints = $state(0);
const maxTolerance = 10000;
let validSelection = $derived(
$selection.hasAnyChildren(new ListRootItem(), true, ['waypoints'])
);
let reducedLayers = new ReducedGPXLayerCollection();
$effect(() => {
tolerance.set(
minTolerance * 2 ** (sliderValue[0] / (100 / Math.log2(maxTolerance / minTolerance)))
);
});
onDestroy(() => {
reducedLayers.destroy();
});
</script>
<div class="flex flex-col gap-3 w-full max-w-80 {props.class ?? ''}">
<div class="p-2">
<Slider bind:value={sliderValue} min={0} max={100} step={1} type="multiple" />
</div>
<Label class="flex flex-row justify-between">
<span>{i18n._('toolbar.reduce.tolerance')}</span>
<WithUnits value={$tolerance / 1000} type="distance" decimals={4} class="font-normal" />
</Label>
<Label class="flex flex-row justify-between">
<span>{i18n._('toolbar.reduce.number_of_points')}</span>
<span class="font-normal">{currentPoints}/{maxPoints}</span>
</Label>
<Button variant="outline" disabled={!validSelection} onclick={() => reducedLayers.reduce()}>
<Funnel size="16" class="mr-1" />
{i18n._('toolbar.reduce.button')}
</Button>
<Help link={getURLForLanguage(i18n.lang, '/help/toolbar/minify')}>
{#if validSelection}
{i18n._('toolbar.reduce.help')}
{:else}
{i18n._('toolbar.reduce.help_no_selection')}
{/if}
</Help>
</div>

View File

@@ -0,0 +1,187 @@
import { ListItem, ListTrackSegmentItem } from '$lib/components/file-list/file-list';
import { map } from '$lib/components/map/map';
import { fileActions } from '$lib/logic/file-actions';
import { GPXFileStateCollectionObserver, type GPXFileState } from '$lib/logic/file-state';
import { selection } from '$lib/logic/selection';
import { ramerDouglasPeucker, TrackPoint, type SimplifiedTrackPoint } from 'gpx';
import type { GeoJSONSource } from 'mapbox-gl';
import { get, writable } from 'svelte/store';
export const minTolerance = 0.1;
export class ReducedGPXLayer {
private _fileState: GPXFileState;
private _updateSimplified: (
itemId: string,
data: [ListItem, number, SimplifiedTrackPoint[]]
) => void;
private _unsubscribes: (() => void)[] = [];
constructor(
fileState: GPXFileState,
updateSimplified: (itemId: string, data: [ListItem, number, SimplifiedTrackPoint[]]) => void
) {
this._fileState = fileState;
this._updateSimplified = updateSimplified;
this._unsubscribes.push(this._fileState.subscribe(() => this.update()));
}
update() {
const file = this._fileState.file;
const stats = this._fileState.statistics;
if (!file || !stats) {
return;
}
file.forEachSegment((segment, trackIndex, segmentIndex) => {
let segmentItem = new ListTrackSegmentItem(file._data.id, trackIndex, segmentIndex);
let statistics = stats.getStatisticsFor(segmentItem);
this._updateSimplified(segmentItem.getFullId(), [
segmentItem,
statistics.local.points.length,
ramerDouglasPeucker(statistics.local.points, minTolerance),
]);
});
}
destroy() {
this._unsubscribes.forEach((unsubscribe) => unsubscribe());
}
}
export const tolerance = writable<number>(0);
export class ReducedGPXLayerCollection {
private _layers: Map<string, ReducedGPXLayer> = new Map();
private _simplified: Map<string, [ListItem, number, SimplifiedTrackPoint[]]>;
private _fileStateCollectionOberver: GPXFileStateCollectionObserver;
private _updateSimplified = this.updateSimplified.bind(this);
private _unsubscribes: (() => void)[] = [];
constructor() {
this._layers = new Map();
this._simplified = new Map();
this._fileStateCollectionOberver = new GPXFileStateCollectionObserver(
(fileId, fileState) => {
this._layers.set(fileId, new ReducedGPXLayer(fileState, this._updateSimplified));
},
(fileId) => {
this._layers.get(fileId)?.destroy();
this._layers.delete(fileId);
},
() => {
this._layers.forEach((layer) => layer.destroy());
this._layers.clear();
}
);
this._unsubscribes.push(selection.subscribe(() => this.update()));
this._unsubscribes.push(tolerance.subscribe(() => this.update()));
}
updateSimplified(itemId: string, data: [ListItem, number, SimplifiedTrackPoint[]]) {
this._simplified.set(itemId, data);
if (get(selection).hasAnyParent(data[0])) {
this.update();
}
}
removeSimplified(itemId: string) {
if (this._simplified.delete(itemId)) {
this.update();
}
}
update() {
let maxPoints = 0;
let currentPoints = 0;
let data: GeoJSON.FeatureCollection = {
type: 'FeatureCollection',
features: [],
};
this._simplified.forEach(([item, maxPts, points], itemFullId) => {
if (!get(selection).hasAnyParent(item)) {
return;
}
maxPoints += maxPts;
let current = points.filter(
(point) => point.distance === undefined || point.distance >= get(tolerance)
);
currentPoints += current.length;
data.features.push({
type: 'Feature',
geometry: {
type: 'LineString',
coordinates: current.map((point) => [
point.point.getLongitude(),
point.point.getLatitude(),
]),
},
properties: {},
});
});
const map_ = get(map);
if (!map_) {
return;
}
let source: GeoJSONSource | undefined = map_.getSource('simplified');
if (source) {
source.setData(data);
} else {
map_.addSource('simplified', {
type: 'geojson',
data: data,
});
}
if (!map_.getLayer('simplified')) {
map_.addLayer({
id: 'simplified',
type: 'line',
source: 'simplified',
paint: {
'line-color': 'white',
'line-width': 3,
},
});
} else {
map_.moveLayer('simplified');
}
}
reduce() {
let itemsAndPoints = new Map<ListItem, TrackPoint[]>();
this._simplified.forEach(([item, maxPts, points], itemFullId) => {
itemsAndPoints.set(
item,
points
.filter(
(point) => point.distance === undefined || point.distance >= get(tolerance)
)
.map((point) => point.point)
);
});
fileActions.reduce(itemsAndPoints);
}
destroy() {
this._fileStateCollectionOberver.destroy();
this._unsubscribes.forEach((unsubscribe) => unsubscribe());
const map_ = get(map);
if (!map_) {
return;
}
if (map_.getLayer('simplified')) {
map_.removeLayer('simplified');
}
if (map_.getSource('simplified')) {
map_.removeSource('simplified');
}
}
}

View File

@@ -21,9 +21,8 @@
SquareArrowUpLeft,
SquareArrowOutDownRight,
} from '@lucide/svelte';
import { brouterProfiles } from '$lib/components/toolbar/tools/routing/utils.svelte';
import { brouterProfiles } from '$lib/components/toolbar/tools/routing/routing';
import { i18n } from '$lib/i18n.svelte';
// import { RoutingControls } from './RoutingControls';
import { slide } from 'svelte/transition';
import {
ListFileItem,
@@ -32,14 +31,16 @@
ListTrackSegmentItem,
type ListItem,
} from '$lib/components/file-list/file-list';
import { getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils';
import { getURLForLanguage } from '$lib/utils';
import { onDestroy, onMount } from 'svelte';
import { TrackPoint } from 'gpx';
import { settings } from '$lib/logic/settings';
import { map } from '$lib/components/map/map';
import { fileStateCollection } from '$lib/logic/file-state';
import { fileStateCollection, GPXFileStateCollectionObserver } from '$lib/logic/file-state';
import { selection } from '$lib/logic/selection';
import { fileActions, getFileIds, newGPXFile } from '$lib/logic/file-actions';
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
import { RoutingControls, routingControls } from './RoutingControls';
let {
minimized = $bindable(false),
@@ -55,34 +56,9 @@
class?: string;
} = $props();
let selectedItem: ListItem | null = null;
const { privateRoads, routing, routingProfile } = settings;
// $: if (map && popup && popupElement) {
// // remove controls for deleted files
// routingControls.forEach((controls, fileId) => {
// if (!$fileObservers.has(fileId)) {
// controls.destroy();
// routingControls.delete(fileId);
// if (selectedItem && selectedItem.getFileId() === fileId) {
// selectedItem = null;
// }
// } else if ($map !== controls.map) {
// controls.updateMap($map);
// }
// });
// // add controls for new files
// fileStateCollection.files.forEach((file, fileId) => {
// if (!routingControls.has(fileId)) {
// routingControls.set(
// fileId,
// new RoutingControls($map, fileId, file, popup, popupElement)
// );
// }
// });
// }
let fileStateCollectionObserver: GPXFileStateCollectionObserver;
let validSelection = $derived(
$selection.hasAnyChildren(new ListRootItem(), true, ['waypoints'])
@@ -101,21 +77,44 @@
]);
file._data.id = getFileIds(1)[0];
fileActions.add(file);
// selectFileWhenLoaded(file._data.id);
selection.selectFileWhenLoaded(file._data.id);
}
}
onMount(() => {
// setCrosshairCursor();
$map?.on('click', createFileWithPoint);
if ($map && popup && popupElement) {
fileStateCollectionObserver = new GPXFileStateCollectionObserver(
(fileId, fileState) => {
routingControls.set(
fileId,
new RoutingControls(fileId, fileState, popup, popupElement)
);
},
(fileId) => {
const controls = routingControls.get(fileId);
if (controls) {
controls.destroy();
routingControls.delete(fileId);
}
},
() => {
routingControls.forEach((controls) => controls.destroy());
routingControls.clear();
}
);
mapCursor.notify(MapCursorState.TOOL_WITH_CROSSHAIR, true);
$map.on('click', createFileWithPoint);
}
});
onDestroy(() => {
// resetCursor();
$map?.off('click', createFileWithPoint);
if ($map) {
fileStateCollectionObserver.destroy();
// routingControls.forEach((controls) => controls.destroy());
// routingControls.clear();
mapCursor.notify(MapCursorState.TOOL_WITH_CROSSHAIR, false);
$map.off('click', createFileWithPoint);
}
});
</script>
@@ -130,7 +129,7 @@
<div class="flex flex-col gap-3">
<Label class="flex flex-row justify-between items-center gap-2">
<span class="flex flex-row items-center gap-1">
{#if routing.value}
{#if $routing}
<Route size="16" />
{:else}
<RouteOff size="16" />
@@ -138,28 +137,28 @@
{i18n._('toolbar.routing.use_routing')}
</span>
<Tooltip label={i18n._('toolbar.routing.use_routing_tooltip')}>
<Switch class="scale-90" bind:checked={routing.value} />
<Switch class="scale-90" bind:checked={$routing} />
<Shortcut slot="extra" key="F5" />
</Tooltip>
</Label>
{#if routing.value}
{#if $routing}
<div class="flex flex-col gap-3" in:slide>
<Label class="flex flex-row justify-between items-center gap-2">
<span class="shrink-0 flex flex-row items-center gap-1">
{#if routingProfile.value.includes('bike') || routingProfile.value.includes('motorcycle')}
{#if $routingProfile.includes('bike') || $routingProfile.includes('motorcycle')}
<Bike size="16" />
{:else if routingProfile.value.includes('foot')}
{:else if $routingProfile.includes('foot')}
<Footprints size="16" />
{:else if routingProfile.value.includes('water')}
{:else if $routingProfile.includes('water')}
<Waves size="16" />
{:else if routingProfile.value.includes('railway')}
{:else if $routingProfile.includes('railway')}
<TrainFront size="16" />
{/if}
{i18n._('toolbar.routing.activity')}
</span>
<Select.Root type="single" bind:value={routingProfile.value}>
<Select.Root type="single" bind:value={$routingProfile}>
<Select.Trigger class="h-8 grow">
{i18n._(`toolbar.routing.activities.${routingProfile.value}`)}
{i18n._(`toolbar.routing.activities.${$routingProfile}`)}
</Select.Trigger>
<Select.Content>
{#each Object.keys(brouterProfiles) as profile}
@@ -177,7 +176,7 @@
<TriangleAlert size="16" />
{i18n._('toolbar.routing.allow_private')}
</span>
<Switch class="scale-90" bind:checked={privateRoads.value} />
<Switch class="scale-90" bind:checked={$privateRoads} />
</Label>
</div>
{/if}
@@ -218,9 +217,9 @@
if (start !== undefined) {
const lastFileId = selected[selected.length - 1].getFileId();
// routingControls
// .get(lastFileId)
// ?.appendAnchorWithCoordinates(start.getCoordinates());
routingControls
.get(lastFileId)
?.appendAnchorWithCoordinates(start.getCoordinates());
}
}
}

View File

@@ -7,7 +7,11 @@
import { i18n } from '$lib/i18n.svelte';
export let element: HTMLElement;
let {
element = $bindable(),
}: {
element: HTMLElement | undefined;
} = $props();
</script>
<div bind:this={element} class="hidden">
@@ -17,7 +21,7 @@
<Button
class="w-full px-2 py-1 h-6 justify-start"
variant="ghost"
onclick={() => element.dispatchEvent(new CustomEvent('change-start'))}
onclick={() => element?.dispatchEvent(new CustomEvent('change-start'))}
>
<CirclePlay size="16" class="mr-1" />
{i18n._('toolbar.routing.start_loop_here')}
@@ -26,7 +30,7 @@
<Button
class="w-full px-2 py-1 h-6 justify-start"
variant="ghost"
onclick={() => element.dispatchEvent(new CustomEvent('delete'))}
onclick={() => element?.dispatchEvent(new CustomEvent('delete'))}
>
<Trash2 size="16" class="mr-1" />
{i18n._('menu.delete')}

View File

@@ -1,17 +1,25 @@
import { distance, type Coordinates, TrackPoint, TrackSegment, Track, projectedPoint } from 'gpx';
import { get, writable, type Readable } from 'svelte/store';
import mapboxgl from 'mapbox-gl';
import { route } from './utils.svelte';
import { route } from './routing';
import { toast } from 'svelte-sonner';
import {
ListFileItem,
ListTrackItem,
ListTrackSegmentItem,
} from '$lib/components/file-list/file-list';
import { getClosestLinePoint, resetCursor, setGrabbingCursor } from '$lib/utils';
import { getClosestLinePoint } from '$lib/utils';
import type { GPXFileWithStatistics } from '$lib/logic/statistics-tree';
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
import { settings } from '$lib/logic/settings';
import { selection } from '$lib/logic/selection';
import { currentTool, Tool } from '$lib/components/toolbar/tools';
import { streetViewEnabled } from '$lib/components/map/street-view-control/utils';
import { fileActionManager } from '$lib/logic/file-action-manager';
import { i18n } from '$lib/i18n.svelte';
import { map } from '$lib/components/map/map';
// const { streetViewSource } = settings;
const { streetViewSource } = settings;
export const canChangeStart = writable(false);
function stopPropagation(e: any) {
@@ -20,7 +28,6 @@ function stopPropagation(e: any) {
export class RoutingControls {
active: boolean = false;
map: mapboxgl.Map;
fileId: string = '';
file: Readable<GPXFileWithStatistics | undefined>;
anchors: AnchorWithMarker[] = [];
@@ -39,13 +46,11 @@ export class RoutingControls {
appendAnchorBinded: (e: mapboxgl.MapMouseEvent) => void = this.appendAnchor.bind(this);
constructor(
map: mapboxgl.Map,
fileId: string,
file: Readable<GPXFileWithStatistics | undefined>,
popup: mapboxgl.Popup,
popupElement: HTMLElement
) {
this.map = map;
this.fileId = fileId;
this.file = file;
this.popup = popup;
@@ -88,12 +93,17 @@ export class RoutingControls {
}
add() {
const map_ = get(map);
if (!map_) {
return;
}
this.active = true;
this.map.on('move', this.toggleAnchorsForZoomLevelAndBoundsBinded);
this.map.on('click', this.appendAnchorBinded);
this.map.on('mousemove', this.fileId, this.showTemporaryAnchorBinded);
this.map.on('click', this.fileId, stopPropagation);
map_.on('move', this.toggleAnchorsForZoomLevelAndBoundsBinded);
map_.on('click', this.appendAnchorBinded);
map_.on('mousemove', this.fileId, this.showTemporaryAnchorBinded);
map_.on('click', this.fileId, stopPropagation);
this.fileUnsubscribe = this.file.subscribe(this.updateControls.bind(this));
}
@@ -141,25 +151,26 @@ export class RoutingControls {
}
remove() {
const map_ = get(map);
if (!map_) {
return;
}
this.active = false;
for (let anchor of this.anchors) {
anchor.marker.remove();
}
this.map.off('move', this.toggleAnchorsForZoomLevelAndBoundsBinded);
this.map.off('click', this.appendAnchorBinded);
this.map.off('mousemove', this.fileId, this.showTemporaryAnchorBinded);
this.map.off('click', this.fileId, stopPropagation);
this.map.off('mousemove', this.updateTemporaryAnchorBinded);
map_.off('move', this.toggleAnchorsForZoomLevelAndBoundsBinded);
map_.off('click', this.appendAnchorBinded);
map_.off('mousemove', this.fileId, this.showTemporaryAnchorBinded);
map_.off('click', this.fileId, stopPropagation);
map_.off('mousemove', this.updateTemporaryAnchorBinded);
this.temporaryAnchor.marker.remove();
this.fileUnsubscribe();
}
updateMap(map: mapboxgl.Map) {
this.map = map;
}
createAnchor(
point: TrackPoint,
segment: TrackSegment,
@@ -186,13 +197,13 @@ export class RoutingControls {
marker.on('dragstart', (e) => {
this.lastDragEvent = Date.now();
setGrabbingCursor();
mapCursor.notify(MapCursorState.TRACKPOINT_DRAGGING, true);
element.classList.remove('cursor-pointer');
element.classList.add('cursor-grabbing');
});
marker.on('dragend', (e) => {
this.lastDragEvent = Date.now();
resetCursor();
mapCursor.notify(MapCursorState.TRACKPOINT_DRAGGING, false);
element.classList.remove('cursor-grabbing');
element.classList.add('cursor-pointer');
this.moveAnchor(anchor);
@@ -255,19 +266,24 @@ export class RoutingControls {
}
toggleAnchorsForZoomLevelAndBounds() {
const map_ = get(map);
if (!map_) {
return;
}
// Show markers only if they are in the current zoom level and bounds
this.shownAnchors.splice(0, this.shownAnchors.length);
let center = this.map.getCenter();
let bottomLeft = this.map.unproject([0, this.map.getCanvas().height]);
let topRight = this.map.unproject([this.map.getCanvas().width, 0]);
let center = map_.getCenter();
let bottomLeft = map_.unproject([0, map_.getCanvas().height]);
let topRight = map_.unproject([map_.getCanvas().width, 0]);
let diagonal = bottomLeft.distanceTo(topRight);
let zoom = this.map.getZoom();
let zoom = map_.getZoom();
this.anchors.forEach((anchor) => {
anchor.inZoom = anchor.point._data.zoom <= zoom;
if (anchor.inZoom && center.distanceTo(anchor.marker.getLngLat()) < diagonal) {
anchor.marker.addTo(this.map);
anchor.marker.addTo(map_);
this.shownAnchors.push(anchor);
} else {
anchor.marker.remove();
@@ -276,6 +292,11 @@ export class RoutingControls {
}
showTemporaryAnchor(e: any) {
const map_ = get(map);
if (!map_) {
return;
}
if (this.temporaryAnchor.marker.getElement().classList.contains('cursor-grabbing')) {
// Do not not change the source point if it is already being dragged
return;
@@ -305,25 +326,30 @@ export class RoutingControls {
lat: e.lngLat.lat,
lon: e.lngLat.lng,
});
this.temporaryAnchor.marker.setLngLat(e.lngLat).addTo(this.map);
this.temporaryAnchor.marker.setLngLat(e.lngLat).addTo(map_);
this.map.on('mousemove', this.updateTemporaryAnchorBinded);
map_.on('mousemove', this.updateTemporaryAnchorBinded);
}
updateTemporaryAnchor(e: any) {
const map_ = get(map);
if (!map_) {
return;
}
if (this.temporaryAnchor.marker.getElement().classList.contains('cursor-grabbing')) {
// Do not hide if it is being dragged, and stop listening for mousemove
this.map.off('mousemove', this.updateTemporaryAnchorBinded);
map_.off('mousemove', this.updateTemporaryAnchorBinded);
return;
}
if (
e.point.dist(this.map.project(this.temporaryAnchor.point.getCoordinates())) > 20 ||
e.point.dist(map_.project(this.temporaryAnchor.point.getCoordinates())) > 20 ||
this.temporaryAnchorCloseToOtherAnchor(e)
) {
// Hide if too far from the layer
this.temporaryAnchor.marker.remove();
this.map.off('mousemove', this.updateTemporaryAnchorBinded);
map_.off('mousemove', this.updateTemporaryAnchorBinded);
return;
}
@@ -331,8 +357,13 @@ export class RoutingControls {
}
temporaryAnchorCloseToOtherAnchor(e: any) {
const map_ = get(map);
if (!map_) {
return false;
}
for (let anchor of this.shownAnchors) {
if (e.point.dist(this.map.project(anchor.marker.getLngLat())) < 10) {
if (e.point.dist(map_.project(anchor.marker.getLngLat())) < 10) {
return true;
}
}
@@ -482,7 +513,7 @@ export class RoutingControls {
});
if (minInfo.trackIndex !== -1) {
dbUtils.applyToFile(this.fileId, (file) =>
fileActionManager.applyToFile(this.fileId, (file) =>
file.replaceTrackPoints(
minInfo.trackIndex,
minInfo.segmentIndex,
@@ -506,12 +537,12 @@ export class RoutingControls {
if (previousAnchor === null && nextAnchor === null) {
// Only one point, remove it
dbUtils.applyToFile(this.fileId, (file) =>
fileActionManager.applyToFile(this.fileId, (file) =>
file.replaceTrackPoints(anchor.trackIndex, anchor.segmentIndex, 0, 0, [])
);
} else if (previousAnchor === null) {
// First point, remove trackpoints until nextAnchor
dbUtils.applyToFile(this.fileId, (file) =>
fileActionManager.applyToFile(this.fileId, (file) =>
file.replaceTrackPoints(
anchor.trackIndex,
anchor.segmentIndex,
@@ -522,7 +553,7 @@ export class RoutingControls {
);
} else if (nextAnchor === null) {
// Last point, remove trackpoints from previousAnchor
dbUtils.applyToFile(this.fileId, (file) => {
fileActionManager.applyToFile(this.fileId, (file) => {
let segment = file.getSegment(anchor.trackIndex, anchor.segmentIndex);
file.replaceTrackPoints(
anchor.trackIndex,
@@ -558,7 +589,7 @@ export class RoutingControls {
).global.speed.moving;
let segment = anchor.segment;
dbUtils.applyToFile(this.fileId, (file) => {
fileActionManager.applyToFile(this.fileId, (file) => {
file.replaceTrackPoints(
anchor.trackIndex,
anchor.segmentIndex,
@@ -590,7 +621,7 @@ export class RoutingControls {
async appendAnchorWithCoordinates(coordinates: Coordinates) {
// Add a new anchor to the end of the last segment
let selected = getOrderedSelection();
let selected = selection.getOrderedSelection();
if (selected.length === 0 || selected[selected.length - 1].getFileId() !== this.fileId) {
return;
}
@@ -605,7 +636,7 @@ export class RoutingControls {
newPoint._data.zoom = 0;
if (!lastAnchor) {
dbUtils.applyToFile(this.fileId, (file) => {
fileActionManager.applyToFile(this.fileId, (file) => {
let trackIndex = file.trk.length > 0 ? file.trk.length - 1 : 0;
if (item instanceof ListTrackItem || item instanceof ListTrackSegmentItem) {
trackIndex = item.getTrackIndex();
@@ -686,7 +717,7 @@ export class RoutingControls {
if (anchors.length === 1) {
// Only one anchor, update the point in the segment
dbUtils.applyToFile(this.fileId, (file) =>
fileActionManager.applyToFile(this.fileId, (file) =>
file.replaceTrackPoints(anchors[0].trackIndex, anchors[0].segmentIndex, 0, 0, [
new TrackPoint({
attributes: targetCoordinates[0],
@@ -701,13 +732,13 @@ export class RoutingControls {
response = await route(targetCoordinates);
} catch (e: any) {
if (e.message.includes('from-position not mapped in existing datafile')) {
toast.error(get(_)('toolbar.routing.error.from'));
toast.error(i18n._('toolbar.routing.error.from'));
} else if (e.message.includes('via1-position not mapped in existing datafile')) {
toast.error(get(_)('toolbar.routing.error.via'));
toast.error(i18n._('toolbar.routing.error.via'));
} else if (e.message.includes('to-position not mapped in existing datafile')) {
toast.error(get(_)('toolbar.routing.error.to'));
toast.error(i18n._('toolbar.routing.error.to'));
} else if (e.message.includes('Time-out')) {
toast.error(get(_)('toolbar.routing.error.timeout'));
toast.error(i18n._('toolbar.routing.error.timeout'));
} else {
toast.error(e.message);
}
@@ -797,7 +828,7 @@ export class RoutingControls {
}
}
dbUtils.applyToFile(this.fileId, (file) =>
fileActionManager.applyToFile(this.fileId, (file) =>
file.replaceTrackPoints(
anchors[0].trackIndex,
anchors[0].segmentIndex,
@@ -818,6 +849,8 @@ export class RoutingControls {
}
}
export const routingControls: Map<string, RoutingControls> = new Map();
type Anchor = {
segment: TrackSegment;
trackIndex: number;

View File

@@ -2,6 +2,7 @@ import type { Coordinates } from 'gpx';
import { TrackPoint, distance } from 'gpx';
import { settings } from '$lib/logic/settings';
import { getElevation } from '$lib/utils';
import { get } from 'svelte/store';
const { routing, routingProfile, privateRoads } = settings;
@@ -17,8 +18,8 @@ export const brouterProfiles: { [key: string]: string } = {
};
export function route(points: Coordinates[]): Promise<TrackPoint[]> {
if (routing.value) {
return getRoute(points, brouterProfiles[routingProfile.value], privateRoads.value);
if (get(routing)) {
return getRoute(points, brouterProfiles[get(routingProfile)], get(privateRoads));
} else {
return getIntermediatePoints(points);
}

View File

@@ -7,16 +7,16 @@
import { Slider } from '$lib/components/ui/slider';
import * as Select from '$lib/components/ui/select';
import { Separator } from '$lib/components/ui/separator';
import { gpxStatistics, slicedGPXStatistics } from '$lib/stores';
import { map } from '$lib/components/map/map';
import { get } from 'svelte/store';
import { i18n } from '$lib/i18n.svelte';
import { onDestroy, tick } from 'svelte';
import { Crop } from '@lucide/svelte';
import { dbUtils } from '$lib/db';
import { SplitControls } from './split-controls';
import { getURLForLanguage } from '$lib/utils';
import { selection } from '$lib/logic/selection';
import { fileActions } from '$lib/logic/file-actions';
import { gpxStatistics, slicedGPXStatistics } from '$lib/logic/statistics';
let props: {
class?: string;
@@ -26,16 +26,16 @@
let canCrop = $state(false);
$effect(() => {
if (map.current) {
if ($map) {
if (splitControls) {
splitControls.destroy();
}
splitControls = new SplitControls(map.current);
splitControls = new SplitControls($map);
}
});
let validSelection = $derived(
selection.value.hasAnyChildren(new ListRootItem(), true, ['waypoints']) &&
$selection.hasAnyChildren(new ListRootItem(), true, ['waypoints']) &&
$gpxStatistics.local.points.length > 0
);
@@ -120,7 +120,7 @@
<Button
variant="outline"
disabled={!validSelection || !canCrop}
onclick={() => dbUtils.cropSelection(sliderValues[0], sliderValues[1])}
onclick={() => fileActions.cropSelection(sliderValues[0], sliderValues[1])}
>
<Crop size="16" class="mr-1" />{i18n._('toolbar.scissors.crop')}
</Button>
@@ -129,9 +129,9 @@
<span class="shrink-0">
{i18n._('toolbar.scissors.split_as')}
</span>
<Select.Root bind:value={splitAs.current} type="single">
<Select.Root bind:value={$splitAs} type="single">
<Select.Trigger class="h-8 w-fit grow">
{i18n._('gpx.' + splitAs)}
{i18n._('gpx.' + $splitAs)}
</Select.Trigger>
<Select.Content>
{#each Object.values(SplitType) as splitType}

View File

@@ -1,12 +1,14 @@
import { TrackPoint, TrackSegment } from 'gpx';
import mapboxgl from 'mapbox-gl';
import { dbUtils, getFile } from '$lib/db';
import { ListTrackSegmentItem } from '$lib/components/file-list/file-list';
import { gpxStatistics } from '$lib/stores';
import { tool, Tool } from '$lib/components/toolbar/tools';
import { currentTool, Tool } from '$lib/components/toolbar/tools';
import { splitAs } from '$lib/components/toolbar/tools/scissors/scissors';
import { Scissors } from 'lucide-static';
import { selection } from '$lib/logic/selection';
import { gpxStatistics } from '$lib/logic/statistics';
import { get } from 'svelte/store';
import { fileStateCollection } from '$lib/logic/file-state';
import { fileActions } from '$lib/logic/file-actions';
export class SplitControls {
active: boolean = false;
@@ -22,13 +24,12 @@ export class SplitControls {
this.map = map;
this.unsubscribes.push(gpxStatistics.subscribe(this.addIfNeeded.bind(this)));
$effect(() => {
tool.current, selection.value, this.addIfNeeded.bind(this);
});
this.unsubscribes.push(currentTool.subscribe(this.addIfNeeded.bind(this)));
this.unsubscribes.push(selection.subscribe(this.addIfNeeded.bind(this)));
}
addIfNeeded() {
let scissors = tool.current === Tool.SCISSORS;
let scissors = get(currentTool) === Tool.SCISSORS;
if (!scissors) {
if (this.active) {
this.remove();
@@ -54,12 +55,12 @@ export class SplitControls {
// Update the markers when the files change
let controlIndex = 0;
selection.applyToOrderedSelectedItemsFromFile((fileId, level, items) => {
let file = getFile(fileId);
let file = fileStateCollection.getFile(fileId);
if (file) {
file.forEachSegment((segment, trackIndex, segmentIndex) => {
if (
selection.value.hasAnyParent(
get(selection).hasAnyParent(
new ListTrackSegmentItem(fileId, trackIndex, segmentIndex)
)
) {
@@ -163,8 +164,8 @@ export class SplitControls {
marker.getElement().addEventListener('click', (e) => {
e.stopPropagation();
dbUtils.split(
splitAs.current,
fileActions.split(
get(splitAs),
control.fileId,
control.trackIndex,
control.segmentIndex,

View File

@@ -6,16 +6,16 @@
import * as Select from '$lib/components/ui/select';
import { i18n } from '$lib/i18n.svelte';
import { ListWaypointItem } from '$lib/components/file-list/file-list';
import { dbUtils, fileObservers, getFile, settings, type GPXFileWithStatistics } from '$lib/db';
import { get } from 'svelte/store';
import Help from '$lib/components/Help.svelte';
import { onDestroy, onMount } from 'svelte';
import { map } from '$lib/stores';
import { getURLForLanguage, resetCursor, setCrosshairCursor } from '$lib/utils';
import { onDestroy, onMount, untrack } from 'svelte';
import { getURLForLanguage } from '$lib/utils';
import { MapPin, CircleX, Save } from '@lucide/svelte';
import { getSymbolKey, symbols } from '$lib/assets/symbols';
import { selection } from '$lib/logic/selection';
import { selectedWaypoint } from './waypoint';
import { fileActions } from '$lib/logic/file-actions';
import { map } from '$lib/components/map/map';
import { mapCursor, MapCursorState } from '$lib/logic/map-cursor';
let props: {
class?: string;
@@ -24,20 +24,46 @@
let name = $state('');
let description = $state('');
let link = $state('');
let symbolKey = $state('');
let sym = $state('');
let longitude = $state(0);
let latitude = $state(0);
let symbolKey = $derived(getSymbolKey(sym));
let canCreate = $derived(selection.value.size > 0);
let canCreate = $derived($selection.size > 0);
function resetWaypointData() {
let sortedSymbols = $derived(
Object.entries(symbols).sort((a, b) => {
return i18n
._(`gpx.symbol.${a[0]}`)
.localeCompare(i18n._(`gpx.symbol.${b[0]}`), i18n.lang);
})
);
$effect(() => {
if ($selectedWaypoint) {
const wpt = $selectedWaypoint[0];
untrack(() => {
name = wpt.name ?? '';
description = wpt.desc ?? '';
if (wpt.cmt !== undefined && wpt.cmt !== wpt.desc) {
description += '\n\n' + wpt.cmt;
}
link = wpt.link?.attributes?.href ?? '';
sym = wpt.sym ?? '';
longitude = parseFloat(wpt.getLongitude().toFixed(6));
latitude = parseFloat(wpt.getLatitude().toFixed(6));
});
} else {
untrack(() => {
name = '';
description = '';
link = '';
symbolKey = '';
sym = '';
longitude = 0;
latitude = 0;
});
}
});
function createOrUpdateWaypoint() {
if (typeof latitude === 'string') {
@@ -49,7 +75,7 @@
latitude = parseFloat(latitude.toFixed(6));
longitude = parseFloat(longitude.toFixed(6));
dbUtils.addOrUpdateWaypoint(
fileActions.addOrUpdateWaypoint(
{
attributes: {
lat: latitude,
@@ -59,7 +85,7 @@
desc: description.length > 0 ? description : undefined,
cmt: description.length > 0 ? description : undefined,
link: link.length > 0 ? { attributes: { href: link } } : undefined,
sym: symbols[symbolKey]?.value ?? '',
sym: sym,
},
selectedWaypoint.wpt && selectedWaypoint.fileId
? new ListWaypointItem(selectedWaypoint.fileId, selectedWaypoint.wpt._data.index)
@@ -67,7 +93,6 @@
);
selectedWaypoint.reset();
resetWaypointData();
}
function setCoordinates(e: any) {
@@ -75,22 +100,18 @@
longitude = e.lngLat.lng.toFixed(6);
}
let sortedSymbols = $derived(
Object.entries(symbols).sort((a, b) => {
return i18n
._(`gpx.symbol.${a[0]}`)
.localeCompare(i18n._(`gpx.symbol.${b[0]}`), i18n.lang);
})
);
onMount(() => {
map.value?.on('click', setCoordinates);
// setCrosshairCursor();
if ($map) {
$map.on('click', setCoordinates);
mapCursor.notify(MapCursorState.TOOL_WITH_CROSSHAIR, true);
}
});
onDestroy(() => {
map.value?.off('click', setCoordinates);
// resetCursor();
if ($map) {
$map.off('click', setCoordinates);
mapCursor.notify(MapCursorState.TOOL_WITH_CROSSHAIR, false);
}
});
</script>
@@ -101,25 +122,25 @@
bind:value={name}
id="name"
class="font-semibold h-8"
disabled={!canCreate && !selectedWaypoint.wpt}
disabled={!canCreate && !$selectedWaypoint}
/>
<Label for="description">{i18n._('menu.metadata.description')}</Label>
<Textarea
bind:value={description}
id="description"
disabled={!canCreate && !selectedWaypoint.wpt}
disabled={!canCreate && !$selectedWaypoint}
/>
<Label for="symbol">{i18n._('toolbar.waypoint.icon')}</Label>
<Select.Root bind:value={symbolKey} type="single">
<Select.Root bind:value={sym} type="single">
<Select.Trigger
id="symbol"
class="w-full h-8"
disabled={!canCreate && !selectedWaypoint.wpt}
disabled={!canCreate && !$selectedWaypoint}
>
{#if symbolKey in symbols}
{#if symbolKey}
{i18n._(`gpx.symbol.${symbolKey}`)}
{:else}
{symbolKey}
{sym}
{/if}
</Select.Trigger>
<Select.Content class="max-h-60 overflow-y-scroll">
@@ -127,11 +148,8 @@
<Select.Item value={symbol.value}>
<span>
{#if symbol.icon}
<svelte:component
this={symbol.icon}
size="14"
class="inline-block align-sub mr-0.5"
/>
{@const Component = symbol.icon}
<Component size="14" class="inline-block align-sub mr-0.5" />
{:else}
<span class="w-4 inline-block"></span>
{/if}
@@ -146,7 +164,7 @@
bind:value={link}
id="link"
class="h-8"
disabled={!canCreate && !selectedWaypoint.wpt}
disabled={!canCreate && !$selectedWaypoint}
/>
<div class="flex flex-row gap-2">
<div class="grow">
@@ -159,7 +177,7 @@
min={-90}
max={90}
class="text-xs h-8"
disabled={!canCreate && !selectedWaypoint.wpt}
disabled={!canCreate && !$selectedWaypoint}
/>
</div>
<div class="grow">
@@ -172,7 +190,7 @@
min={-180}
max={180}
class="text-xs h-8"
disabled={!canCreate && !selectedWaypoint.wpt}
disabled={!canCreate && !$selectedWaypoint}
/>
</div>
</div>
@@ -180,11 +198,11 @@
<div class="flex flex-row gap-2 items-center">
<Button
variant="outline"
disabled={!canCreate && !selectedWaypoint.wpt}
disabled={!canCreate && !$selectedWaypoint}
class="grow whitespace-normal h-fit"
onclick={createOrUpdateWaypoint}
>
{#if selectedWaypoint.wpt}
{#if $selectedWaypoint}
<Save size="16" class="mr-1 shrink-0" />
{i18n._('menu.metadata.save')}
{:else}
@@ -192,18 +210,12 @@
{i18n._('toolbar.waypoint.create')}
{/if}
</Button>
<Button
variant="outline"
onclick={() => {
selectedWaypoint.reset();
resetWaypointData();
}}
>
<Button variant="outline" onclick={() => selectedWaypoint.reset()}>
<CircleX size="16" />
</Button>
</div>
<Help link={getURLForLanguage(i18n.lang, '/help/toolbar/poi')}>
{#if selectedWaypoint.wpt || canCreate}
{#if $selectedWaypoint || canCreate}
{i18n._('toolbar.waypoint.help')}
{:else}
{i18n._('toolbar.waypoint.help_no_selection')}

View File

@@ -7,6 +7,7 @@ import { get, writable, type Writable } from 'svelte/store';
export class WaypointSelection {
private _selection: Writable<[Waypoint, string] | undefined>;
private _fileUnsubscribe: (() => void) | undefined;
constructor() {
this._selection = writable(undefined);
@@ -18,18 +19,40 @@ export class WaypointSelection {
});
}
subscribe(
run: (value: [Waypoint, string] | undefined) => void,
invalidate?: (value?: [Waypoint, string] | undefined) => void
) {
return this._selection.subscribe(run, invalidate);
}
set(value: [Waypoint, string] | undefined) {
this._selection.set(value);
}
update() {
if (this._fileUnsubscribe) {
this._fileUnsubscribe();
this._fileUnsubscribe = undefined;
}
this._selection.update(() => {
if (get(settings.treeFileView) && get(selection).size === 1) {
let item = get(selection).getSelected()[0];
if (item instanceof ListWaypointItem) {
let file = fileStateCollection.getFile(item.getFileId());
let waypoint = file?.wpt[item.getWaypointIndex()];
let fileState = fileStateCollection.getFileState(item.getFileId());
if (fileState) {
let first = true;
this._fileUnsubscribe = fileState.subscribe(() => {
if (first) first = false;
else this.update();
});
let waypoint = fileState.file?.wpt[item.getWaypointIndex()];
if (waypoint) {
return [waypoint, item.getFileId()];
}
}
}
}
return undefined;
});
}
@@ -47,34 +70,6 @@ export class WaypointSelection {
const selection = get(this._selection);
return selection ? selection[1] : undefined;
}
// TODO update the waypoint data if the file changes
// function updateWaypointData(fileStore: GPXFileWithStatistics | undefined) {
// if (selectedWaypoint.wpt) {
// if (fileStore) {
// if ($selectedWaypoint[0]._data.index < fileStore.file.wpt.length) {
// $selectedWaypoint[0] = fileStore.file.wpt[$selectedWaypoint[0]._data.index];
// name = $selectedWaypoint[0].name ?? '';
// description = $selectedWaypoint[0].desc ?? '';
// if (
// $selectedWaypoint[0].cmt !== undefined &&
// $selectedWaypoint[0].cmt !== $selectedWaypoint[0].desc
// ) {
// description += '\n\n' + $selectedWaypoint[0].cmt;
// }
// link = $selectedWaypoint[0].link?.attributes?.href ?? '';
// let symbol = $selectedWaypoint[0].sym ?? '';
// symbolKey = getSymbolKey(symbol) ?? symbol ?? '';
// longitude = parseFloat($selectedWaypoint[0].getLongitude().toFixed(6));
// latitude = parseFloat($selectedWaypoint[0].getLatitude().toFixed(6));
// } else {
// selectedWaypoint.reset();
// }
// } else {
// selectedWaypoint.reset();
// }
// }
// }
}
export const selectedWaypoint = new WaypointSelection();

View File

@@ -4,7 +4,7 @@ title: Minify
<script>
import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>

View File

@@ -4,7 +4,7 @@ title: Minimitzar
<script>
import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>

View File

@@ -4,7 +4,7 @@ title: Zjednodušit
<script>
import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>

View File

@@ -4,7 +4,7 @@ title: Minify
<script>
import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>

View File

@@ -4,7 +4,7 @@ title: Minimieren
<script>
import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>

View File

@@ -4,7 +4,7 @@ title: Minify
<script>
import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>

View File

@@ -4,7 +4,7 @@ title: Minify
<script>
import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>

View File

@@ -4,7 +4,7 @@ title: Minimizar
<script>
import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>

View File

@@ -4,7 +4,7 @@ title: Txikiagotu
<script>
import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>

View File

@@ -4,7 +4,7 @@ title: Minify
<script>
import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>

View File

@@ -4,7 +4,7 @@ title: Minifier
<script>
import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>

View File

@@ -4,7 +4,7 @@ title: Minify
<script>
import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>

View File

@@ -4,7 +4,7 @@ title: Minimalizálás
<script>
import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>

View File

@@ -4,7 +4,7 @@ title: Minimizza
<script>
import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>

View File

@@ -4,7 +4,7 @@ title: Minify
<script>
import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>

View File

@@ -4,7 +4,7 @@ title: Minify
<script>
import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>

View File

@@ -4,7 +4,7 @@ title: Minify
<script>
import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>

View File

@@ -4,7 +4,7 @@ title: Verkleinen
<script>
import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>

View File

@@ -4,7 +4,7 @@ title: Minify
<script>
import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>

View File

@@ -4,7 +4,7 @@ title: Minify
<script>
import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>

View File

@@ -4,7 +4,7 @@ title: Minificar
<script>
import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>

View File

@@ -4,7 +4,7 @@ title: Minify
<script>
import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>

View File

@@ -4,7 +4,7 @@ title: Minify
<script>
import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>

View File

@@ -4,7 +4,7 @@ title: Minify
<script>
import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>

View File

@@ -4,7 +4,7 @@ title: Minify
<script>
import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>

View File

@@ -4,7 +4,7 @@ title: Minimera
<script>
import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>

View File

@@ -4,7 +4,7 @@ title: Küçült
<script>
import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>

View File

@@ -4,7 +4,7 @@ title: Minify
<script>
import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>

View File

@@ -4,7 +4,7 @@ title: Minify
<script>
import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>

View File

@@ -4,7 +4,7 @@ title: 精简 GPS 点数量
<script>
import { Funnel } from '@lucide/svelte';
import Reduce from '$lib/components/toolbar/tools/Reduce.svelte';
import Reduce from '$lib/components/toolbar/tools/reduce/Reduce.svelte';
import DocsNote from '$lib/components/docs/DocsNote.svelte';
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -151,6 +151,7 @@ export class GPXFileStateCollectionObserver {
private _onFileAdded: GPXFileStateCallback;
private _onFileRemoved: (fileId: string) => void;
private _onDestroy: () => void;
private _unsubscribe: () => void;
constructor(
onFileAdded: GPXFileStateCallback,
@@ -162,7 +163,7 @@ export class GPXFileStateCollectionObserver {
this._onFileRemoved = onFileRemoved;
this._onDestroy = onDestroy;
fileStateCollection.subscribe((files) => {
this._unsubscribe = fileStateCollection.subscribe((files) => {
this._fileIds.forEach((fileId) => {
if (!files.has(fileId)) {
this._onFileRemoved(fileId);
@@ -180,5 +181,6 @@ export class GPXFileStateCollectionObserver {
destroy() {
this._onDestroy();
this._unsubscribe();
}
}

View File

@@ -0,0 +1,55 @@
import { map } from '$lib/components/map/map';
import { get, writable, type Writable } from 'svelte/store';
export enum MapCursorState {
DEFAULT,
LAYER_HOVER,
WAYPOINT_DRAGGING,
TRACKPOINT_DRAGGING,
TOOL_WITH_CROSSHAIR,
SCISSORS,
MAPILLARY_HOVER,
STREET_VIEW_CROSSHAIR,
}
const scissorsCursor = `url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" version="1.1"><path d="M 3.200 3.200 C 0.441 5.959, 2.384 9.516, 7 10.154 C 10.466 10.634, 10.187 13.359, 6.607 13.990 C 2.934 14.637, 1.078 17.314, 2.612 19.750 C 4.899 23.380, 10 21.935, 10 17.657 C 10 16.445, 12.405 13.128, 15.693 9.805 C 18.824 6.641, 21.066 3.732, 20.674 3.341 C 20.283 2.950, 18.212 4.340, 16.072 6.430 C 12.019 10.388, 10 10.458, 10 6.641 C 10 2.602, 5.882 0.518, 3.200 3.200 M 4.446 5.087 C 3.416 6.755, 5.733 8.667, 7.113 7.287 C 8.267 6.133, 7.545 4, 6 4 C 5.515 4, 4.816 4.489, 4.446 5.087 M 14 14.813 C 14 16.187, 19.935 21.398, 20.667 20.667 C 21.045 20.289, 20.065 18.634, 18.490 16.990 C 15.661 14.036, 14 13.231, 14 14.813 M 4.446 17.087 C 3.416 18.755, 5.733 20.667, 7.113 19.287 C 8.267 18.133, 7.545 16, 6 16 C 5.515 16, 4.816 16.489, 4.446 17.087" stroke="black" stroke-width="1.2" fill="white" fill-rule="evenodd"/></svg>') 12 12, auto`;
const cursorStyles = {
[MapCursorState.DEFAULT]: 'default',
[MapCursorState.LAYER_HOVER]: 'pointer',
[MapCursorState.WAYPOINT_DRAGGING]: 'grabbing',
[MapCursorState.TRACKPOINT_DRAGGING]: 'grabbing',
[MapCursorState.TOOL_WITH_CROSSHAIR]: 'crosshair',
[MapCursorState.SCISSORS]: scissorsCursor,
[MapCursorState.MAPILLARY_HOVER]: 'pointer',
[MapCursorState.STREET_VIEW_CROSSHAIR]: 'crosshair',
};
export class MapCursor {
private _states: Writable<Set<MapCursorState>>;
constructor() {
this._states = writable(new Set());
this._states.subscribe((states) => {
let state = states.entries().reduce((max, entry) => {
return entry[0] > max ? entry[0] : max;
}, MapCursorState.DEFAULT);
let canvas = get(map)?.getCanvas();
if (canvas) {
canvas.style.cursor = cursorStyles[state];
}
});
}
notify(cursorState: MapCursorState, isActive: boolean) {
this._states.update((states) => {
if (isActive) {
states.add(cursorState);
} else {
states.delete(cursorState);
}
return states;
});
}
}
export const mapCursor = new MapCursor();

View File

@@ -14,6 +14,7 @@ import { settings } from '$lib/logic/settings';
import type { GPXFile } from 'gpx';
import { get, writable, type Readable, type Writable } from 'svelte/store';
import { SelectionTreeType } from '$lib/logic/selection-tree';
import { tick } from 'svelte';
export class Selection {
private _selection: Writable<SelectionTreeType>;
@@ -100,6 +101,15 @@ export class Selection {
});
}
selectFileWhenLoaded(fileId: string) {
const unsubscribe = fileStateCollection.subscribe((files) => {
if (files.has(fileId)) {
this.selectFile(fileId);
unsubscribe();
}
});
}
set(items: ListItem[]) {
this._selection.update(($selection) => {
$selection.clear();

View File

@@ -7,6 +7,9 @@ import {
ListWaypointsItem,
} from '$lib/components/file-list/file-list';
import { get, writable, type Writable } from 'svelte/store';
import { settings } from '$lib/logic/settings';
const { fileOrder } = settings;
export class SelectedGPXStatistics {
private _statistics: Writable<GPXStatistics>;
@@ -22,6 +25,7 @@ export class SelectedGPXStatistics {
this._statistics = writable(new GPXStatistics());
this._files = new Map();
selection.subscribe(() => this.update());
fileOrder.subscribe(() => this.update());
}
subscribe(run: (value: GPXStatistics) => void, invalidate?: (value?: GPXStatistics) => void) {

View File

@@ -24,17 +24,6 @@
// export const routingControls: Map<string, RoutingControls> = new Map();
// export function selectFileWhenLoaded(fileId: string) {
// const unsubscribe = fileObservers.subscribe((files) => {
// if (files.has(fileId)) {
// tick().then(() => {
// selectFile(fileId);
// });
// unsubscribe();
// }
// });
// }
// export const allHidden = writable(false);
// export function updateAllHidden() {

View File

@@ -126,34 +126,6 @@ export function getElevation(
);
}
let previousCursors: string[] = [];
export function setCursor(canvas: HTMLCanvasElement, cursor: string) {
previousCursors.push(canvas.style.cursor);
canvas.style.cursor = cursor;
}
export function resetCursor(canvas: HTMLCanvasElement) {
canvas.style.cursor = previousCursors.pop() ?? '';
}
export function setPointerCursor(canvas: HTMLCanvasElement) {
setCursor(canvas, 'pointer');
}
export function setGrabbingCursor(canvas: HTMLCanvasElement) {
setCursor(canvas, 'grabbing');
}
export function setCrosshairCursor(canvas: HTMLCanvasElement) {
setCursor(canvas, 'crosshair');
}
export const scissorsCursor = `url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" version="1.1"><path d="M 3.200 3.200 C 0.441 5.959, 2.384 9.516, 7 10.154 C 10.466 10.634, 10.187 13.359, 6.607 13.990 C 2.934 14.637, 1.078 17.314, 2.612 19.750 C 4.899 23.380, 10 21.935, 10 17.657 C 10 16.445, 12.405 13.128, 15.693 9.805 C 18.824 6.641, 21.066 3.732, 20.674 3.341 C 20.283 2.950, 18.212 4.340, 16.072 6.430 C 12.019 10.388, 10 10.458, 10 6.641 C 10 2.602, 5.882 0.518, 3.200 3.200 M 4.446 5.087 C 3.416 6.755, 5.733 8.667, 7.113 7.287 C 8.267 6.133, 7.545 4, 6 4 C 5.515 4, 4.816 4.489, 4.446 5.087 M 14 14.813 C 14 16.187, 19.935 21.398, 20.667 20.667 C 21.045 20.289, 20.065 18.634, 18.490 16.990 C 15.661 14.036, 14 13.231, 14 14.813 M 4.446 17.087 C 3.416 18.755, 5.733 20.667, 7.113 19.287 C 8.267 18.133, 7.545 16, 6 16 C 5.515 16, 4.816 16.489, 4.446 17.087" stroke="black" stroke-width="1.2" fill="white" fill-rule="evenodd"/></svg>') 12 12, auto`;
export function setScissorsCursor(canvas: HTMLCanvasElement) {
setCursor(canvas, scissorsCursor);
}
export function isMac() {
return navigator.userAgent.toUpperCase().indexOf('MAC') >= 0;
}

View File

@@ -3,7 +3,7 @@
import DocsContainer from '$lib/components/docs/DocsContainer.svelte';
import Logo from '$lib/components/Logo.svelte';
// import ElevationProfile from '$lib/components/ElevationProfile.svelte';
// import GPXStatistics from '$lib/components/GPXStatistics.svelte';
import GPXStatistics from '$lib/components/GPXStatistics.svelte';
// import Routing from '$lib/components/toolbar/tools/routing/Routing.svelte';
import {
BookOpenText,
@@ -20,7 +20,7 @@
import { exampleGPXFile } from '$lib/assets/example';
import { writable } from 'svelte/store';
// import Toolbar from '$lib/components/toolbar/Toolbar.svelte';
// import { tool, Tool } from '$lib/components/toolbar/utils.svelte';
import { currentTool, Tool } from '$lib/components/toolbar/tools';
import { onDestroy, onMount } from 'svelte';
let {
@@ -38,19 +38,19 @@
let additionalDatasets = writable(['speed', 'atemp']);
let elevationFill = writable<'slope' | 'surface' | undefined>(undefined);
// onMount(() => {
// tool.current = Tool.SCISSORS;
// });
onMount(() => {
$currentTool = Tool.SCISSORS;
});
// $effect(() => {
// if (tool.current !== Tool.SCISSORS) {
// tool.current = Tool.SCISSORS;
// }
// });
$effect(() => {
if ($currentTool !== Tool.SCISSORS) {
$currentTool = Tool.SCISSORS;
}
});
// onDestroy(() => {
// tool.current = null;
// });
onDestroy(() => {
$currentTool = null;
});
</script>
<div class="space-y-24 my-24">
@@ -199,12 +199,12 @@
</div>
<div class="flex flex-col items-center">
<div class="h-10 w-fit">
<!-- <GPXStatistics
<GPXStatistics
{gpxStatistics}
{slicedGPXStatistics}
panelSize={192}
orientation={'horizontal'}
/> -->
/>
</div>
</div>
</div>

View File

@@ -5,7 +5,7 @@
import GPXStatistics from '$lib/components/GPXStatistics.svelte';
import Map from '$lib/components/map/Map.svelte';
import Menu from '$lib/components/Menu.svelte';
// import Toolbar from '$lib/components/toolbar/Toolbar.svelte';
import Toolbar from '$lib/components/toolbar/Toolbar.svelte';
import StreetViewControl from '$lib/components/map/street-view-control/StreetViewControl.svelte';
import LayerControl from '$lib/components/map/layer-control/LayerControl.svelte';
// import CoordinatesPopup from '$lib/components/map/CoordinatesPopup.svelte';
@@ -101,7 +101,7 @@
<div
class="absolute top-0 bottom-0 left-0 z-20 flex flex-col justify-center pointer-events-none"
>
<!-- <Toolbar /> -->
<Toolbar />
</div>
<Map class="h-full {$treeFileView ? '' : 'horizontal'}" />
<StreetViewControl />