mirror of
https://github.com/gpxstudio/gpx.studio.git
synced 2025-11-04 13:31:13 +00:00
fix tools
This commit is contained in:
@@ -544,15 +544,17 @@
|
||||
<svelte:window
|
||||
on:keydown={(e) => {
|
||||
let targetInput =
|
||||
e.target.tagName === 'INPUT' ||
|
||||
e.target.tagName === 'TEXTAREA' ||
|
||||
e.target.tagName === 'SELECT' ||
|
||||
e.target.role === 'combobox' ||
|
||||
e.target.role === 'radio' ||
|
||||
e.target.role === 'menu' ||
|
||||
e.target.role === 'menuitem' ||
|
||||
e.target.role === 'menuitemradio' ||
|
||||
e.target.role === 'menuitemcheckbox';
|
||||
e &&
|
||||
e.target &&
|
||||
(e.target.tagName === 'INPUT' ||
|
||||
e.target.tagName === 'TEXTAREA' ||
|
||||
e.target.tagName === 'SELECT' ||
|
||||
e.target.role === 'combobox' ||
|
||||
e.target.role === 'radio' ||
|
||||
e.target.role === 'menu' ||
|
||||
e.target.role === 'menuitem' ||
|
||||
e.target.role === 'menuitemradio' ||
|
||||
e.target.role === 'menuitemcheckbox');
|
||||
|
||||
if (e.key === '+' && (e.metaKey || e.ctrlKey)) {
|
||||
createFile();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}`
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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 ?? ''}">
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
187
website/src/lib/components/toolbar/tools/reduce/reduce.ts
Normal file
187
website/src/lib/components/toolbar/tools/reduce/reduce.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
name = '';
|
||||
description = '';
|
||||
link = '';
|
||||
symbolKey = '';
|
||||
longitude = 0;
|
||||
latitude = 0;
|
||||
}
|
||||
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 = '';
|
||||
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')}
|
||||
|
||||
@@ -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,15 +19,37 @@ 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()];
|
||||
if (waypoint) {
|
||||
return [waypoint, item.getFileId()];
|
||||
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()];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
55
website/src/lib/logic/map-cursor.ts
Normal file
55
website/src/lib/logic/map-cursor.ts
Normal 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();
|
||||
@@ -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();
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
Reference in New Issue
Block a user