Skip to content

Commit 9efbbb4

Browse files
authored
APP-8726 Add google maps to MapLibre (#632)
1 parent 2e1d9e5 commit 9efbbb4

File tree

9 files changed

+200
-12
lines changed

9 files changed

+200
-12
lines changed

packages/blocks/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@viamrobotics/prime-blocks",
3-
"version": "0.1.8",
3+
"version": "0.1.9",
44
"repository": {
55
"type": "git",
66
"url": "https://github.com/viamrobotics/prime.git",

packages/blocks/src/lib/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,12 @@ export {
2828
cartesianToLngLat,
2929
cartesianToMercator,
3030
} from './maplibre/math';
31-
export { GeoPose, Waypoint } from './maplibre/types';
31+
export {
32+
GeoPose,
33+
Waypoint,
34+
type MapProvider,
35+
MapProviders,
36+
} from './maplibre/types';
3237

3338
// Slam components
3439
export { default as SlamMap2D } from './slam-map-2d/index.svelte';
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { describe, it, expect, vi } from 'vitest';
2+
3+
// Mock maplibre-gl module to avoid URL.createObjectURL issues
4+
vi.mock('maplibre-gl', () => ({
5+
LngLat: class {
6+
constructor(
7+
public lng: number,
8+
public lat: number
9+
) {}
10+
},
11+
Map: vi.fn(),
12+
NavigationControl: vi.fn(),
13+
GeolocateControl: vi.fn(),
14+
FullscreenControl: vi.fn(),
15+
ScaleControl: vi.fn(),
16+
}));
17+
18+
import { getStyleSpecification } from '../style';
19+
import { MapProviders } from '../types';
20+
21+
describe('MapLibre Provider', () => {
22+
describe('getStyle', () => {
23+
it('should return OpenStreetMap style for open-streets provider', () => {
24+
const style = getStyleSpecification('open-street');
25+
26+
expect(style.version).toBe(8);
27+
expect(style.sources).toHaveProperty('osm');
28+
expect(style.sources).toHaveProperty('satellite');
29+
expect(style.layers).toHaveLength(2);
30+
});
31+
32+
it('should return Google Maps style for google-maps provider with valid API key', () => {
33+
const style = getStyleSpecification(
34+
MapProviders.googleMaps,
35+
'test_api_key'
36+
);
37+
38+
expect(style.version).toBe(8);
39+
expect(style.sources).toHaveProperty('google-maps');
40+
expect(style.sources).toHaveProperty('satellite');
41+
expect(style.layers).toHaveLength(2);
42+
});
43+
44+
it('should fallback to OpenStreetMap when Google Maps API key is not configured', () => {
45+
const style = getStyleSpecification(MapProviders.googleMaps);
46+
47+
expect(style.version).toBe(8);
48+
expect(style.sources).toHaveProperty('osm');
49+
expect(style.sources).toHaveProperty('satellite');
50+
});
51+
});
52+
});

packages/blocks/src/lib/maplibre/index.svelte

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@
66
Children will mount once the map is fully loaded.
77
88
```svelte
9-
<MapLibre>
9+
<MapLibre mapProvider="open-street">
10+
<MapLibreMarker lngLat={{ lng: 0, lat: 0 }} />
11+
</MapLibre>
12+
```
13+
14+
```svelte
15+
<MapLibre mapProvider="google-maps" mapProviderKey="mycoolkey">
1016
<MapLibreMarker lngLat={{ lng: 0, lat: 0 }} />
1117
</MapLibre>
1218
```
@@ -18,8 +24,9 @@ import 'maplibre-gl/dist/maplibre-gl.css';
1824
import { onMount, tick } from 'svelte';
1925
import { provideMapContext } from './hooks';
2026
import { Map, type MapOptions } from 'maplibre-gl';
21-
import { style } from './style';
27+
import { getStyleSpecification } from './style';
2228
import { LngLat } from '$lib';
29+
import { MapProviders, type MapProvider } from './types';
2330
2431
/** The minimum camera pitch. */
2532
export let minPitch = 0;
@@ -49,6 +56,20 @@ export let map: Map | undefined = undefined;
4956
5057
export let options: Partial<MapOptions> | undefined = undefined;
5158
59+
/**
60+
* The map provider to use.
61+
*
62+
* @default 'open-street'
63+
*/
64+
export let mapProvider: MapProvider = MapProviders.openStreet;
65+
66+
/**
67+
* The API key for the map provider.
68+
*
69+
* @default undefined
70+
*/
71+
export let mapProviderKey: string | undefined = undefined;
72+
5273
/** Fired after the map has been created. */
5374
export let onCreate: undefined | ((map: Map) => void) = undefined;
5475
@@ -109,7 +130,7 @@ onMount(() => {
109130
map = new Map({
110131
antialias: true,
111132
container,
112-
style,
133+
style: getStyleSpecification(mapProvider, mapProviderKey),
113134
center,
114135
zoom,
115136
minPitch,
@@ -136,6 +157,7 @@ $: map?.setMinPitch(minPitch);
136157
$: map?.setMaxPitch(maxPitch);
137158
$: map?.setZoom(zoom);
138159
$: map?.setCenter(center);
160+
$: map?.setStyle(getStyleSpecification(mapProvider, mapProviderKey));
139161
</script>
140162

141163
{#if created}

packages/blocks/src/lib/maplibre/style.ts

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,66 @@
11
import type { StyleSpecification } from 'maplibre-gl';
2+
import { MapProviders, type MapProvider } from './types';
23

3-
export const style: StyleSpecification = {
4+
const tileSize = 256;
5+
const maxzoom = 20;
6+
7+
const getGoogleMapsStyle = (apiKey: string): StyleSpecification => ({
8+
version: 8,
9+
sources: {
10+
[MapProviders.googleMaps]: {
11+
type: 'raster',
12+
tiles: [
13+
`https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}&key=${apiKey}`,
14+
],
15+
tileSize,
16+
attribution: '&copy; Google Maps',
17+
maxzoom,
18+
},
19+
satellite: {
20+
type: 'raster',
21+
tiles: [
22+
`https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}&key=${apiKey}`,
23+
],
24+
tileSize: 256,
25+
attribution: '&copy; Google Maps',
26+
maxzoom,
27+
},
28+
},
29+
layers: [
30+
{
31+
id: MapProviders.googleMaps,
32+
type: 'raster',
33+
source: MapProviders.googleMaps,
34+
},
35+
{
36+
id: 'satellite',
37+
type: 'raster',
38+
source: 'satellite',
39+
layout: {
40+
visibility: 'none',
41+
},
42+
},
43+
],
44+
});
45+
46+
const getOpenStreetMapStyle = (): StyleSpecification => ({
447
version: 8,
548
sources: {
649
osm: {
750
type: 'raster',
851
tiles: ['https://a.tile.openstreetmap.org/{z}/{x}/{y}.png'],
9-
tileSize: 256,
52+
tileSize,
1053
attribution: '&copy; OpenStreetMap Contributors',
11-
maxzoom: 19,
54+
maxzoom,
1255
},
1356
satellite: {
1457
type: 'raster',
1558
tiles: [
1659
'https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryOnly/MapServer/tile/{z}/{y}/{x}',
1760
],
18-
tileSize: 256,
61+
tileSize,
1962
attribution: '&copy; USGS National Map Services',
20-
maxzoom: 19,
63+
maxzoom,
2164
},
2265
},
2366
layers: [
@@ -35,4 +78,23 @@ export const style: StyleSpecification = {
3578
},
3679
},
3780
],
81+
});
82+
83+
export const getStyleSpecification = (
84+
provider: MapProvider,
85+
apiKey?: string
86+
): StyleSpecification => {
87+
switch (provider) {
88+
case MapProviders.googleMaps: {
89+
if (!apiKey) {
90+
// eslint-disable-next-line no-console
91+
console.warn('Google Maps API key is required');
92+
return getOpenStreetMapStyle();
93+
}
94+
return getGoogleMapsStyle(apiKey);
95+
}
96+
case MapProviders.openStreet: {
97+
return getOpenStreetMapStyle();
98+
}
99+
}
38100
};

packages/blocks/src/lib/maplibre/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
// eslint-disable-next-line max-classes-per-file
22
import { LngLat } from 'maplibre-gl';
33

4+
export const MapProviders = {
5+
openStreet: 'open-street',
6+
googleMaps: 'google-maps',
7+
} as const;
8+
9+
export type MapProvider = (typeof MapProviders)[keyof typeof MapProviders];
10+
411
export class GeoPose extends LngLat {
512
rotation: number;
613
constructor(lng: number, lat: number, rotation: number) {

packages/blocks/src/lib/navigation-map/components/map.svelte

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import type { Map } from 'maplibre-gl';
44
import {
55
MapLibre,
66
type GeoPose,
7+
type MapProvider,
8+
MapProviders,
79
NavigationControls,
810
SatelliteControls,
911
FollowControls,
@@ -20,6 +22,12 @@ import type { Obstacle } from '../types';
2022
/** The Geo-pose of a robot base. */
2123
export let baseGeoPose: GeoPose | undefined = undefined;
2224
25+
/** The map provider to use. */
26+
export let mapProvider: MapProvider = MapProviders.openStreet;
27+
28+
/** The API key for the map provider. */
29+
export let mapProviderKey: string | undefined = undefined;
30+
2331
const minPitch = 0;
2432
const maxPitch = 60;
2533
@@ -42,6 +50,8 @@ let didHoverTooltip = Boolean(
4250
{minPitch}
4351
maxPitch={$view === '3D' ? maxPitch : minPitch}
4452
minZoom={6}
53+
{mapProvider}
54+
{mapProviderKey}
4555
bind:map
4656
>
4757
<NavigationControls showZoom={false} />

packages/blocks/src/lib/navigation-map/index.svelte

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,12 @@ import {
3131
tab as tabStore,
3232
tabs as tabsStore,
3333
} from './stores';
34-
import type { GeoPose, Waypoint } from '$lib';
34+
import {
35+
type GeoPose,
36+
type Waypoint,
37+
type MapProvider,
38+
MapProviders,
39+
} from '$lib';
3540
3641
/** The map environment. "debug" assumes the robot is on and connected. */
3742
export let environment: 'debug' | 'configure' = 'debug';
@@ -60,6 +65,12 @@ export let tabs: NavigationTabType[] = [
6065
/** The pose (Lng,Lat) and rotation of a base. */
6166
export let baseGeoPose: GeoPose | undefined = undefined;
6267
68+
/** The map provider to use. */
69+
export let mapProvider: MapProvider = MapProviders.openStreet;
70+
71+
/** The API key for the map provider. */
72+
export let mapProviderKey: string | undefined = undefined;
73+
6374
export let onUpdate: (payload: Obstacle[]) => void;
6475
6576
$: $tabStore = tab;
@@ -73,6 +84,8 @@ $: $envStore = environment;
7384
<Map
7485
bind:map
7586
{baseGeoPose}
87+
{mapProvider}
88+
{mapProviderKey}
7689
{onUpdate}
7790
on:add-waypoint
7891
on:delete-waypoint

packages/blocks/src/routes/+page.svelte

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
<script lang="ts">
22
import pointcloudUrl from '$lib/assets/pointcloud.pcd?url';
33
import motionPath from '$lib/assets/cbirrtsmooth800.txt?raw';
4-
import { MapLibre, NavigationControls, SlamMap2D } from '$lib';
4+
import {
5+
MapLibre,
6+
NavigationControls,
7+
SatelliteControls,
8+
SlamMap2D,
9+
} from '$lib';
510
import NavigationMap from './navigation-map.svelte';
611
import FollowingMarker from './following-marker.svelte';
712
@@ -53,5 +58,17 @@ const path = () =>
5358
</div>
5459
</div>
5560

61+
<div class="px-12">
62+
<div class="relative aspect-video w-full border border-gray-200 pt-0">
63+
<MapLibre
64+
mapProvider="google-maps"
65+
mapProviderKey={import.meta.env.VITE_GOOGLE_MAPS_API_KEY}
66+
>
67+
<NavigationControls />
68+
<SatelliteControls />
69+
</MapLibre>
70+
</div>
71+
</div>
72+
5673
<FollowingMarker />
5774
</div>

0 commit comments

Comments
 (0)