Skip to content
Closed
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
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ VITE_VALHALLA_URL=https://valhalla1.openstreetmap.de
VITE_NOMINATIM_URL=https://nominatim.openstreetmap.org
VITE_TILE_SERVER_URL="https://tile.openstreetmap.org/{z}/{x}/{y}.png"
VITE_CENTER_COORDS="52.51831,13.393707"
VITE_VALHALLA_DEFAULT_STYLE_URL="https://raw.githubusercontent.com/valhalla/valhalla/master/docs/docs/api/tile/default_style.json"
76 changes: 56 additions & 20 deletions src/components/tiles/valhalla-layers-toggle.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ vi.mock('@/stores/common-store', () => ({
),
}));

vi.mock('./valhalla-layers', async (importOriginal) => {
const actual = await importOriginal<typeof import('./valhalla-layers')>();
return {
...actual,
fetchValhallaLayers: vi.fn(() => Promise.resolve(actual.VALHALLA_LAYERS)),
};
});

describe('ValhallaLayersToggle', () => {
beforeEach(() => {
mockMap = createMockMap();
Expand Down Expand Up @@ -101,13 +109,15 @@ describe('ValhallaLayersToggle', () => {
const toggle = screen.getByRole('switch');
await user.click(toggle);

expect(mockMap.addSource).toHaveBeenCalledWith(
VALHALLA_SOURCE_ID,
expect.objectContaining({
type: 'vector',
tiles: expect.any(Array),
})
);
await waitFor(() => {
expect(mockMap.addSource).toHaveBeenCalledWith(
VALHALLA_SOURCE_ID,
expect.objectContaining({
type: 'vector',
tiles: expect.any(Array),
})
);
});
});

it('should add all layers when toggled on', async () => {
Expand All @@ -117,10 +127,12 @@ describe('ValhallaLayersToggle', () => {
const toggle = screen.getByRole('switch');
await user.click(toggle);

expect(mockMap.addLayer).toHaveBeenCalledTimes(VALHALLA_LAYERS.length);
for (const layer of VALHALLA_LAYERS) {
expect(mockMap.addLayer).toHaveBeenCalledWith(layer);
}
await waitFor(() => {
expect(mockMap.addLayer).toHaveBeenCalledTimes(VALHALLA_LAYERS.length);
for (const layer of VALHALLA_LAYERS) {
expect(mockMap.addLayer).toHaveBeenCalledWith(layer);
}
});
});

it('should remove all layers when toggled off', async () => {
Expand All @@ -129,13 +141,24 @@ describe('ValhallaLayersToggle', () => {

const toggle = screen.getByRole('switch');
await user.click(toggle);

await waitFor(() => {
expect(mockMap.addLayer).toHaveBeenCalled();
});

await user.click(toggle);

expect(mockMap.removeLayer).toHaveBeenCalledWith(VALHALLA_EDGES_LAYER_ID);
expect(mockMap.removeLayer).toHaveBeenCalledWith(
VALHALLA_SHORTCUTS_LAYER_ID
);
expect(mockMap.removeLayer).toHaveBeenCalledWith(VALHALLA_NODES_LAYER_ID);
await waitFor(() => {
expect(mockMap.removeLayer).toHaveBeenCalledWith(
VALHALLA_EDGES_LAYER_ID
);
expect(mockMap.removeLayer).toHaveBeenCalledWith(
VALHALLA_SHORTCUTS_LAYER_ID
);
expect(mockMap.removeLayer).toHaveBeenCalledWith(
VALHALLA_NODES_LAYER_ID
);
});
});

it('should remove source when toggled off', async () => {
Expand All @@ -144,9 +167,16 @@ describe('ValhallaLayersToggle', () => {

const toggle = screen.getByRole('switch');
await user.click(toggle);

await waitFor(() => {
expect(mockMap.addSource).toHaveBeenCalled();
});

await user.click(toggle);

expect(mockMap.removeSource).toHaveBeenCalledWith(VALHALLA_SOURCE_ID);
await waitFor(() => {
expect(mockMap.removeSource).toHaveBeenCalledWith(VALHALLA_SOURCE_ID);
});
});

it('should not add source if already exists', async () => {
Expand All @@ -158,6 +188,10 @@ describe('ValhallaLayersToggle', () => {
const toggle = screen.getByRole('switch');
await user.click(toggle);

await waitFor(() => {
expect(mockMap.addLayer).toHaveBeenCalled();
});

expect(mockMap.addSource).not.toHaveBeenCalled();
});

Expand All @@ -172,9 +206,11 @@ describe('ValhallaLayersToggle', () => {
const toggle = screen.getByRole('switch');
await user.click(toggle);

expect(mockMap.addLayer).toHaveBeenCalledTimes(
VALHALLA_LAYERS.length - 1
);
await waitFor(() => {
expect(mockMap.addLayer).toHaveBeenCalledTimes(
VALHALLA_LAYERS.length - 1
);
});
});

it('should update checked state when toggled', async () => {
Expand Down
21 changes: 16 additions & 5 deletions src/components/tiles/valhalla-layers-toggle.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useRef } from 'react';
import { useMap } from 'react-map-gl/maplibre';
import { useCommonStore } from '@/stores/common-store';
import { Switch } from '@/components/ui/switch';
import { Label } from '@/components/ui/label';
import type { LayerSpecification } from 'maplibre-gl';
import {
VALHALLA_SOURCE_ID,
VALHALLA_LAYERS,
fetchValhallaLayers,
getValhallaSourceSpec,
} from './valhalla-layers';

export const ValhallaLayersToggle = () => {
const { mainMap } = useMap();
const mapReady = useCommonStore((state) => state.mapReady);
const [enabled, setEnabled] = useState(false);
const layersRef = useRef<LayerSpecification[]>([]);
const toggleIdRef = useRef(0);

useEffect(() => {
if (!mainMap) return;
Expand All @@ -31,23 +34,31 @@ export const ValhallaLayersToggle = () => {
};
}, [mainMap]);

const handleToggle = (checked: boolean) => {
const handleToggle = async (checked: boolean) => {
if (!mainMap || !mapReady) return;

const map = mainMap.getMap();
const currentToggleId = ++toggleIdRef.current;
setEnabled(checked);

if (checked) {
const layers = await fetchValhallaLayers();

// If the toggle was flipped again while we were fetching, bail out.
if (toggleIdRef.current !== currentToggleId) return;

layersRef.current = layers;

if (!map.getSource(VALHALLA_SOURCE_ID)) {
map.addSource(VALHALLA_SOURCE_ID, getValhallaSourceSpec());
}
for (const layer of VALHALLA_LAYERS) {
for (const layer of layers) {
if (!map.getLayer(layer.id)) {
map.addLayer(layer);
}
}
} else {
for (const layer of VALHALLA_LAYERS) {
for (const layer of layersRef.current) {
if (map.getLayer(layer.id)) {
map.removeLayer(layer.id);
}
Expand Down
91 changes: 91 additions & 0 deletions src/components/tiles/valhalla-layers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ import {
VALHALLA_SHORTCUTS_LAYER,
VALHALLA_NODES_LAYER,
VALHALLA_LAYERS,
VALHALLA_DEFAULT_STYLE_URL,
getValhallaTileUrl,
getValhallaSourceSpec,
fetchValhallaLayers,
_resetLayerCache,
} from './valhalla-layers';

vi.mock('@/utils/base-url', () => ({
Expand Down Expand Up @@ -194,4 +197,92 @@ describe('valhalla-layers', () => {
expect(spec.scheme).toBe('xyz');
});
});

describe('VALHALLA_DEFAULT_STYLE_URL', () => {
it('should point to the upstream raw GitHub URL', () => {
expect(VALHALLA_DEFAULT_STYLE_URL).toBe(
'https://raw.githubusercontent.com/valhalla/valhalla/master/docs/docs/api/tile/default_style.json'
);
});
});

describe('fetchValhallaLayers', () => {
const mockRemoteLayers = [
{
id: 'edges',
type: 'line',
source: 'valhalla',
'source-layer': 'edges',
paint: {},
},
{
id: 'shortcuts',
type: 'line',
source: 'valhalla',
'source-layer': 'shortcuts',
paint: {},
},
{
id: 'nodes',
type: 'circle',
source: 'valhalla',
'source-layer': 'nodes',
paint: {},
},
];

beforeEach(() => {
vi.restoreAllMocks();
_resetLayerCache();
});

it('should fetch and adapt layers from remote style', async () => {
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ layers: mockRemoteLayers }),
})
);

const layers = await fetchValhallaLayers();

expect(layers).toHaveLength(3);
expect(layers[0]!.id).toBe(VALHALLA_EDGES_LAYER_ID);
expect(layers[1]!.id).toBe(VALHALLA_SHORTCUTS_LAYER_ID);
expect(layers[2]!.id).toBe(VALHALLA_NODES_LAYER_ID);
// All layers should reference the app's source ID
for (const layer of layers) {
expect((layer as Record<string, unknown>).source).toBe(
VALHALLA_SOURCE_ID
);
}
});

it('should fall back to hardcoded layers on fetch failure', async () => {
vi.stubGlobal(
'fetch',
vi.fn().mockRejectedValue(new Error('Network error'))
);

const layers = await fetchValhallaLayers();

expect(layers).toHaveLength(3);
expect(layers[0]!.id).toBe(VALHALLA_EDGES_LAYER_ID);
});

it('should fall back when response is not ok', async () => {
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue({
ok: false,
status: 404,
})
);

const layers = await fetchValhallaLayers();

expect(layers).toHaveLength(3);
});
});
});
Loading