Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 194 additions & 1 deletion website/src/lib/components/Menu.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
import { Button } from '$lib/components/ui/button';
import Logo from '$lib/components/Logo.svelte';
import Shortcut from '$lib/components/Shortcut.svelte';
import { toast } from 'svelte-sonner';
import {
Plus,
Copy,
Download,
Upload,
Undo2,
Redo2,
Trash2,
Expand Down Expand Up @@ -49,14 +51,15 @@
import { editStyle } from '$lib/components/file-list/style/utils.svelte';
import { exportState, ExportState } from '$lib/components/export/utils.svelte';
import { anySelectedLayer } from '$lib/components/map/layer-control/utils';
import { defaultOverlays } from '$lib/assets/layers';
import { defaultOverlays, type CustomLayer } from '$lib/assets/layers';
import LayerControlSettings from '$lib/components/map/layer-control/LayerControlSettings.svelte';
import { ListFileItem, ListTrackItem } from '$lib/components/file-list/file-list';
import Export from '$lib/components/export/Export.svelte';
import { mode, setMode } from 'mode-watcher';
import { i18n } from '$lib/i18n.svelte';
import { languages } from '$lib/languages';
import { getURLForLanguage } from '$lib/utils';
import { get } from 'svelte/store';
import { settings } from '$lib/logic/settings';
import {
createFile,
Expand Down Expand Up @@ -105,6 +108,187 @@
}

let layerSettingsOpen = $state(false);
function exportSettings() {
try {
const settingsData: Record<string, any> = {
version: 1,
timestamp: new Date().toISOString(),
settings: {},
};

settingsData.settings.additionalDatasets = get(settings.additionalDatasets);
settingsData.settings.defaultOpacity = get(settings.defaultOpacity);
settingsData.settings.defaultWidth = get(settings.defaultWidth);
settingsData.settings.directionMarkers = get(settings.directionMarkers);
settingsData.settings.distanceMarkers = get(settings.distanceMarkers);
settingsData.settings.distanceUnits = get(settings.distanceUnits);
settingsData.settings.elevationFill = get(settings.elevationFill);
settingsData.settings.opacities = get(settings.opacities);
settingsData.settings.privateRoads = get(settings.privateRoads);
settingsData.settings.routing = get(settings.routing);
settingsData.settings.routingProfile = get(settings.routingProfile);
settingsData.settings.streetViewSource = get(settings.streetViewSource);
settingsData.settings.temperatureUnits = get(settings.temperatureUnits);
settingsData.settings.velocityUnits = get(settings.velocityUnits);

settingsData.settings.selectedBasemapTree = get(settings.selectedBasemapTree);
settingsData.settings.selectedOverlayTree = get(settings.selectedOverlayTree);
settingsData.settings.selectedOverpassTree = get(settings.selectedOverpassTree);
// import will handle custom layers separately
delete settingsData.settings.selectedBasemapTree.basemaps.custom;
delete settingsData.settings.selectedOverlayTree.overlays.custom;

settingsData.settings.customLayers = get(settings.customLayers);

const json = JSON.stringify(settingsData, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
link.download = `gpx-studio-settings-${timestamp}.json`;
link.href = url;
link.click();
URL.revokeObjectURL(url);

toast.success(i18n._('menu.settings_export.success'));
} catch (error) {
console.error('Export settings failed:', error);
toast.error(i18n._('menu.settings_export.error'));
}
}

function importSettings() {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'application/json,.json';
input.className = 'hidden';
input.onchange = async (event) => {
const target = event.target as HTMLInputElement;
const file = target.files?.[0];
if (!file) return;

try {
const text = await file.text();
const data = JSON.parse(text);

if (!data.version || !data.settings) {
throw new Error('Invalid settings file format');
}

if (data.settings.distanceUnits !== undefined) {
distanceUnits.set(data.settings.distanceUnits);
}
if (data.settings.velocityUnits !== undefined) {
velocityUnits.set(data.settings.velocityUnits);
}
if (data.settings.temperatureUnits !== undefined) {
temperatureUnits.set(data.settings.temperatureUnits);
}
if (data.settings.additionalDatasets !== undefined) {
settings.additionalDatasets.set(data.settings.additionalDatasets);
}
if (data.settings.elevationFill !== undefined) {
settings.elevationFill.set(data.settings.elevationFill);
}
if (data.settings.routing !== undefined) {
settings.routing.set(data.settings.routing);
}
if (data.settings.routingProfile !== undefined) {
settings.routingProfile.set(data.settings.routingProfile);
}
if (data.settings.privateRoads !== undefined) {
settings.privateRoads.set(data.settings.privateRoads);
}
if (data.settings.opacities !== undefined) {
settings.opacities.set(data.settings.opacities);
}
if (data.settings.directionMarkers !== undefined) {
directionMarkers.set(data.settings.directionMarkers);
}
if (data.settings.distanceMarkers !== undefined) {
distanceMarkers.set(data.settings.distanceMarkers);
}
if (data.settings.streetViewSource !== undefined) {
streetViewSource.set(data.settings.streetViewSource);
}
if (data.settings.defaultOpacity !== undefined) {
settings.defaultOpacity.set(data.settings.defaultOpacity);
}
if (data.settings.defaultWidth !== undefined) {
settings.defaultWidth.set(data.settings.defaultWidth);
}
if (data.settings.selectedOverpassTree !== undefined) {
settings.selectedOverpassTree.set(data.settings.selectedOverpassTree);
}
let selectedBasemapTree = get(settings.selectedBasemapTree);
if (data.settings.selectedBasemapTree !== undefined) {
settings.selectedBasemapTree.set(data.settings.selectedBasemapTree);
selectedBasemapTree = data.settings.selectedBasemapTree;
}
let selectedOverlayTree = get(settings.selectedOverlayTree);
if (data.settings.selectedOverlayTree !== undefined) {
settings.selectedOverlayTree.set(data.settings.selectedOverlayTree);
selectedOverlayTree = data.settings.selectedOverlayTree;
}
let customLayers = get(settings.customLayers);
if (data.settings.customLayers !== undefined) {
// Special handling to avoid overwriting existing custom layers
// instead only add new ones and try to avoid duplicates
const duplicationKey = (l: CustomLayer) =>
String(l.layerType) + String(l.name) + l.tileUrls.sort().join(',');

const existingsLayers = new Set();
for (const l of Object.values(get(settings.customLayers))) {
existingsLayers.add(duplicationKey(l));
}

const newLayers: Record<string, CustomLayer> = {};
for (const l of Object.values(data.settings.customLayers) as CustomLayer[]) {
const key = duplicationKey(l);
if (!existingsLayers.has(key)) {
const id = `custom-${Object.keys(newLayers).length + Object.keys(get(settings.customLayers)).length}`;
l.id = id;
newLayers[id] = l;
}
}
console.log('New custom layers to add:', newLayers);

if (Object.keys(newLayers).length > 0) {
customLayers = {
...get(settings.customLayers),
...newLayers,
};
settings.customLayers.set(customLayers);
}
}

// assign new IDs to avoid conflicts
const customBaseTree: Record<string, boolean> = {};
const customOverlayTree: Record<string, boolean> = {};

for (const layer of Object.values(customLayers) as CustomLayer[]) {
if (layer.layerType === 'basemap') {
customBaseTree[layer.id] = true;
} else {
customOverlayTree[layer.id] = true;
}
}
(selectedBasemapTree.basemaps as any).custom = customBaseTree;
settings.selectedBasemapTree.set(selectedBasemapTree);

(selectedOverlayTree.overlays as any).custom = customOverlayTree;
settings.selectedOverlayTree.set(selectedOverlayTree);

toast.success(i18n._('menu.settings_export.import_success'));
} catch (error) {
console.error('Import settings failed:', error);
toast.error(i18n._('menu.settings_export.import_error'));
} finally {
target.value = '';
}
};
input.click();
}
</script>

<div class="absolute md:top-2 left-0 right-0 z-20 flex flex-row justify-center pointer-events-none">
Expand Down Expand Up @@ -497,6 +681,15 @@
<Layers size="16" />
{i18n._('menu.layers')}
</Menubar.Item>
<Menubar.Separator />
<Menubar.Item onclick={exportSettings}>
<Upload size="16" />
{i18n._('menu.settings_export.export')}
</Menubar.Item>
<Menubar.Item onclick={importSettings}>
<Download size="16" />
{i18n._('menu.settings_export.import')}
</Menubar.Item>
</Menubar.Content>
</Menubar.Menu>
</Menubar.Root>
Expand Down
10 changes: 9 additions & 1 deletion website/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,15 @@
"unhide": "Unhide",
"center": "Center",
"open_in": "Open in",
"copy_coordinates": "Copy coordinates"
"copy_coordinates": "Copy coordinates",
"settings_export": {
"export": "Export settings...",
"import": "Import settings...",
"success": "Settings exported successfully!",
"error": "Failed to export settings. Please try again.",
"import_success": "Settings imported successfully!",
"import_error": "Failed to import settings. Please check the file format."
}
},
"toolbar": {
"routing": {
Expand Down