@@ -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' ;
1414import { ItineraryStop , ItineraryStore } from './itinerary-store' ;
1515import { 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
3218const DAY_COLORS = [ '#ef4444' , '#3b82f6' , '#10b981' , '#f59e0b' , '#8b5cf6' , '#ec4899' , '#06b6d4' , '#84cc16' ] ;
3319const 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 {
0 commit comments