Skip to content

Commit 710f99a

Browse files
authored
feat(ag-ui): AdvancedMarkerElement + Map ID for the App-mode map (#745)
* feat(ag-ui): plumb GOOGLE_MAPS_MAP_ID through the env mechanism * feat(ag-ui): load Maps marker library for advanced markers * feat(ag-ui): migrate to AdvancedMarkerElement + cloud dark map style * docs(ag-ui): document GOOGLE_MAPS_API_KEY + GOOGLE_MAPS_MAP_ID (App mode map)
1 parent 9b93953 commit 710f99a

7 files changed

Lines changed: 78 additions & 40 deletions

File tree

examples/ag-ui/angular/README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<!-- SPDX-License-Identifier: MIT -->
2+
# AG-UI Itinerary example (Angular)
3+
4+
A trip-planner demo where the agent edits a live itinerary UI over the
5+
[AG-UI](https://github.com/cacheplane/angular-agent-framework) transport. It has a
6+
chat/panel layout and an **App mode** that swaps the panel for a full-bleed
7+
Google Map cockpit.
8+
9+
## Google Maps (App mode)
10+
11+
App mode renders a Google Map, which needs a Maps JavaScript API key. The key is
12+
read from the repo-root `.env` at build time (via `scripts/inject-env.mjs`, which
13+
writes a gitignored `src/environments/generated-keys.local.ts`). Add to the root
14+
`.env`:
15+
16+
```
17+
GOOGLE_MAPS_API_KEY=your_api_key_here
18+
```
19+
20+
Without a key, App mode's map stays disabled (the rest of the demo still works).
21+
22+
### Dark map theme (optional)
23+
24+
App-mode markers use Google's **Advanced Markers**, which require the map to have
25+
a **Map ID** — and a map with a Map ID takes its styling from a **cloud-based map
26+
style** (the inline JSON dark theme no longer applies). To get the dark theme:
27+
28+
1. In the [Google Cloud Console](https://console.cloud.google.com/google/maps-apis/studio/maps),
29+
create a **vector Map ID** and a **dark map style**, and associate them.
30+
2. Add the id to the root `.env`:
31+
32+
```
33+
GOOGLE_MAPS_MAP_ID=your_map_id_here
34+
```
35+
36+
Without `GOOGLE_MAPS_MAP_ID`, the demo falls back to Google's `DEMO_MAP_ID` and
37+
renders a **light** map — markers and routes still work, only the dark styling is
38+
absent.

examples/ag-ui/angular/scripts/inject-env.mjs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@ function readDotEnv() {
2020

2121
const env = { ...readDotEnv(), ...process.env };
2222
const key = env.GOOGLE_MAPS_API_KEY ?? '';
23+
const mapId = env.GOOGLE_MAPS_MAP_ID ?? '';
2324

2425
const targetPath = resolve(__dirname, '../src/environments/generated-keys.local.ts');
2526
const contents = `// SPDX-License-Identifier: MIT
2627
// AUTO-GENERATED by scripts/inject-env.mjs. Do not edit by hand.
2728
export const GENERATED_KEYS = {
2829
googleMaps: ${JSON.stringify(key)},
30+
googleMapsMapId: ${JSON.stringify(mapId)},
2931
} as const;
3032
`;
3133
writeFileSync(targetPath, contents);
32-
console.log(`[inject-env] wrote generated-keys.local.ts (key length: ${key.length})`);
34+
console.log(`[inject-env] wrote generated-keys.local.ts (key length: ${key.length}, mapId: ${mapId ? 'set' : 'unset'})`);

examples/ag-ui/angular/src/app/google-maps-loader.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export class GoogleMapsLoader {
5050
}
5151

5252
const script = doc.createElement('script');
53-
script.src = `https://maps.googleapis.com/maps/api/js?key=${encodeURIComponent(key)}&libraries=geocoding`;
53+
script.src = `https://maps.googleapis.com/maps/api/js?key=${encodeURIComponent(key)}&libraries=geocoding,marker`;
5454
script.async = true;
5555
script.setAttribute('data-google-maps', '');
5656
script.addEventListener('load', () => this.loaded.set(true));

examples/ag-ui/angular/src/app/map-canvas.component.ts

Lines changed: 33 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -10,32 +10,18 @@ import {
1010
viewChild,
1111
viewChildren,
1212
} from '@angular/core';
13-
import { GoogleMap, MapInfoWindow, MapMarker, MapPolyline } from '@angular/google-maps';
13+
import { GoogleMap, MapInfoWindow, MapAdvancedMarker, MapPolyline } from '@angular/google-maps';
1414
import { ItineraryStop, ItineraryStore } from './itinerary-store';
1515
import { GoogleMapsLoader } from './google-maps-loader';
16-
17-
const DARK_STYLE: google.maps.MapTypeStyle[] = [
18-
{ elementType: 'geometry', stylers: [{ color: '#1d2c4d' }] },
19-
{ elementType: 'labels.text.fill', stylers: [{ color: '#8ec3b9' }] },
20-
{ elementType: 'labels.text.stroke', stylers: [{ color: '#1a3646' }] },
21-
{ featureType: 'administrative.country', elementType: 'geometry.stroke', stylers: [{ color: '#4b6878' }] },
22-
{ featureType: 'landscape.man_made', elementType: 'geometry.stroke', stylers: [{ color: '#334e87' }] },
23-
{ featureType: 'landscape.natural', elementType: 'geometry', stylers: [{ color: '#023e58' }] },
24-
{ featureType: 'poi', elementType: 'geometry', stylers: [{ color: '#283d6a' }] },
25-
{ featureType: 'road', elementType: 'geometry', stylers: [{ color: '#304a7d' }] },
26-
{ featureType: 'road', elementType: 'labels.text.fill', stylers: [{ color: '#98a5be' }] },
27-
{ featureType: 'transit', elementType: 'geometry', stylers: [{ color: '#283d6a' }] },
28-
{ featureType: 'water', elementType: 'geometry', stylers: [{ color: '#0e1626' }] },
29-
{ featureType: 'water', elementType: 'labels.text.fill', stylers: [{ color: '#4e6d70' }] },
30-
];
16+
import { environment } from '../environments/environment';
3117

3218
const DAY_COLORS = ['#ef4444', '#3b82f6', '#10b981', '#f59e0b', '#8b5cf6', '#ec4899', '#06b6d4', '#84cc16'];
3319
const PARIS_CENTER: google.maps.LatLngLiteral = { lat: 48.8566, lng: 2.3522 };
3420

3521
@Component({
3622
selector: 'app-map-canvas',
3723
standalone: true,
38-
imports: [GoogleMap, MapInfoWindow, MapMarker, MapPolyline],
24+
imports: [GoogleMap, MapInfoWindow, MapAdvancedMarker, MapPolyline],
3925
changeDetection: ChangeDetectionStrategy.OnPush,
4026
template: `
4127
<!-- Render <google-map> ONLY after the Maps API has loaded. Its constructor
@@ -49,12 +35,13 @@ const PARIS_CENTER: google.maps.LatLngLiteral = { lat: 48.8566, lng: 2.3522 };
4935
[zoom]="zoom()"
5036
[options]="mapOptions"
5137
>
52-
@for (s of stopsWithCoords(); track s.id) {
53-
<map-marker
38+
@for (m of markerViews(); track m.id) {
39+
<map-advanced-marker
5440
#marker
55-
[position]="{ lat: s.lat!, lng: s.lng! }"
56-
[options]="markerOptions(s)"
57-
(mapClick)="onMarkerClick(s)"
41+
[position]="m.position"
42+
[content]="m.content"
43+
[title]="m.stop.place"
44+
(mapClick)="onMarkerClick(m.stop)"
5845
/>
5946
}
6047
@for (line of polylines(); track line.day) {
@@ -92,20 +79,36 @@ export class MapCanvasComponent {
9279
protected readonly loader = inject(GoogleMapsLoader);
9380
protected readonly center = signal<google.maps.LatLngLiteral>(PARIS_CENTER);
9481
protected readonly zoom = signal<number>(12);
82+
// A mapId is REQUIRED for advanced markers; a mapId map ignores inline JSON
83+
// `styles`, so the dark theme is a cloud-based map style tied to this id.
84+
// DEMO_MAP_ID lets a fresh clone run (light map) with no Console setup.
85+
protected readonly mapId = environment.googleMapsMapId || 'DEMO_MAP_ID';
9586
protected readonly mapOptions: google.maps.MapOptions = {
96-
styles: DARK_STYLE,
87+
mapId: this.mapId,
9788
disableDefaultUI: true,
9889
zoomControl: true,
9990
clickableIcons: false,
10091
};
10192

10293
private readonly infoWindow = viewChild(MapInfoWindow);
103-
private readonly markers = viewChildren(MapMarker);
94+
private readonly markers = viewChildren(MapAdvancedMarker);
10495

10596
protected readonly stopsWithCoords = computed(() =>
10697
this.store.stops().filter((s) => s.lat != null && s.lng != null),
10798
);
10899

100+
/** One marker view per coord'd stop. The pin <div> is built here (not in a
101+
* template method) so it is recreated only when stops change — not on every
102+
* change-detection pass (e.g. focus pans), which would cause flicker. */
103+
protected readonly markerViews = computed(() =>
104+
this.stopsWithCoords().map((s) => ({
105+
id: s.id,
106+
stop: s,
107+
position: { lat: s.lat!, lng: s.lng! },
108+
content: this.makePin(s.day),
109+
})),
110+
);
111+
109112
protected readonly focused = computed(
110113
() => this.store.stops().find((s) => s.id === this.store.focusedStopId()) ?? null,
111114
);
@@ -146,20 +149,12 @@ export class MapCanvasComponent {
146149
.map(([day, stops]) => ({ day, path: stops.map((s) => ({ lat: s.lat!, lng: s.lng! })) }));
147150
});
148151

149-
protected markerOptions(s: ItineraryStop): google.maps.MarkerOptions {
150-
return {
151-
icon: {
152-
// Numeric literal for google.maps.SymbolPath.CIRCLE (=0) — avoids an
153-
// eager google.* value read (defense-in-depth alongside the loader gate).
154-
path: 0,
155-
fillColor: DAY_COLORS[(s.day - 1) % DAY_COLORS.length],
156-
fillOpacity: 1,
157-
strokeColor: '#fff',
158-
strokeWeight: 2,
159-
scale: 8,
160-
},
161-
title: s.place,
162-
};
152+
private makePin(day: number): HTMLElement {
153+
const el = document.createElement('div');
154+
el.style.cssText =
155+
'width:16px;height:16px;border-radius:50%;border:2px solid #fff;' +
156+
`box-shadow:0 1px 3px rgba(0,0,0,.4);background:${DAY_COLORS[(day - 1) % DAY_COLORS.length]};`;
157+
return el;
163158
}
164159

165160
protected polylineOptions(day: number): google.maps.PolylineOptions {

examples/ag-ui/angular/src/environments/environment.development.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ export const environment = {
77
telemetry: { enabled: false, sampleRate: 1 },
88
license: undefined as string | undefined,
99
googleMapsApiKey: GENERATED_KEYS.googleMaps,
10+
googleMapsMapId: GENERATED_KEYS.googleMapsMapId,
1011
};

examples/ag-ui/angular/src/environments/environment.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ export const environment = {
77
telemetry: { enabled: false, sampleRate: 1 },
88
license: undefined as string | undefined,
99
googleMapsApiKey: GENERATED_KEYS.googleMaps,
10+
googleMapsMapId: GENERATED_KEYS.googleMapsMapId,
1011
};

examples/ag-ui/angular/src/environments/generated-keys.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@
44
// Ships empty (CI has no key); local/preview builds get the real value.
55
export const GENERATED_KEYS = {
66
googleMaps: '',
7+
googleMapsMapId: '',
78
} as const;

0 commit comments

Comments
 (0)