@@ -37,6 +37,15 @@ public partial class ChatterMapDirect : ComponentBase, IAsyncDisposable
3737 private DotNetObjectReference < ChatterMapDirect > ? dotNetObjectRef ;
3838 private List < ClusterGroup > tourClusters = new ( ) ;
3939
40+ // External tour grouping support - allows caller to provide AI-powered grouping
41+ [ Parameter ] public Func < IEnumerable < ViewerLocationEvent > , Task < IEnumerable < ExternalTourGroup > > > ? OnTourGroupRequested { get ; set ; }
42+
43+ // Event to listen for external tour restart requests
44+ [ Parameter ] public EventCallback OnTourRestartRequested { get ; set ; }
45+
46+ // External tour groups when using OnTourGroupRequested
47+ private List < ExternalTourGroup > externalTourGroups = new ( ) ;
48+
4049 // NEW: Aggregation structures
4150 private readonly Dictionary < ( double lat , double lng ) , AggregateLocation > aggregatedMarkers = new ( ) ;
4251 private readonly Dictionary < Guid , ( double lat , double lng ) > locationKeyIndex = new ( ) ;
@@ -348,48 +357,90 @@ private async Task StartMapTour()
348357 locationsList = locationsList . Take ( 200 ) . ToList ( ) ;
349358 }
350359
351- // Use the tour service to generate clusters
352- tourClusters = TourService . GenerateClusters ( locationsList ) ;
360+ object [ ] tourStops ;
353361
354- // Log clustering results
355- Console . WriteLine ( $ "Generated { tourClusters . Count } clusters from { locationsList . Count } locations") ;
356- foreach ( var cluster in tourClusters )
362+ // Check if external tour grouping is available (e.g., TourGroupingService with AI region names)
363+ if ( OnTourGroupRequested != null )
357364 {
358- Console . WriteLine ( $ " Cluster: { cluster . Count } locations, center ({ cluster . CenterLatitude : F2} , { cluster . CenterLongitude : F2} ), avg distance: { cluster . AverageDistanceFromCenter : F0} km") ;
359- }
365+ Console . WriteLine ( $ "Using external tour grouping for { locationsList . Count } locations") ;
366+ var externalGroups = await OnTourGroupRequested ( locationsList ) ;
367+ externalTourGroups = externalGroups . ToList ( ) ;
360368
361- // Use the tour service to arrange clusters
362- var arrangedClusters = TourService . ArrangeClustersByDistance ( tourClusters ) ;
369+ Console . WriteLine ( $ "External grouping returned { externalTourGroups . Count } groups") ;
370+ foreach ( var group in externalTourGroups )
371+ {
372+ Console . WriteLine ( $ " Group: { group . RegionName } - { group . LocationCount } locations, zoom { group . OptimalZoomLevel } ") ;
373+ }
363374
364- if ( arrangedClusters . Count > 15 )
365- {
366- arrangedClusters = arrangedClusters . Take ( 15 ) . ToList ( ) ;
367- }
375+ if ( externalTourGroups . Count > 15 )
376+ {
377+ externalTourGroups = externalTourGroups . Take ( 15 ) . ToList ( ) ;
378+ }
368379
369- var tourStops = arrangedClusters . Select ( cluster => new
380+ tourStops = externalTourGroups . Select ( group => new
381+ {
382+ lat = Math . Round ( group . CenterLatitude , 6 ) ,
383+ lng = Math . Round ( group . CenterLongitude , 6 ) ,
384+ zoom = Math . Min ( group . OptimalZoomLevel , MaxZoom ) ,
385+ description = group . RegionName ,
386+ locationCount = group . LocationCount ,
387+ locations = group . Locations . Take ( 10 ) . Select ( loc => new
388+ {
389+ description = Truncate ( loc . LocationDescription , 50 ) ,
390+ lat = Math . Round ( ( double ) loc . Latitude , 6 ) ,
391+ lng = Math . Round ( ( double ) loc . Longitude , 6 ) ,
392+ userType = loc . UserType ,
393+ service = loc . Service == "YouTube" ? "YouTube" : "Twitch"
394+ } ) . ToArray ( )
395+ } ) . ToArray ( ) ;
396+
397+ // Clear internal clusters since we're using external groups
398+ tourClusters . Clear ( ) ;
399+ }
400+ else
370401 {
371- lat = Math . Round ( cluster . CenterLatitude , 6 ) ,
372- lng = Math . Round ( cluster . CenterLongitude , 6 ) ,
373- zoom = TourService . DetermineZoomLevel ( cluster , MaxZoom ) ,
374- description = TourService . GetLocationDescription ( cluster ) ,
375- locationCount = cluster . Locations . Count ,
376- locations = cluster . Locations . Take ( 10 ) . Select ( loc => new
402+ // Use the built-in tour service to generate clusters
403+ tourClusters = TourService . GenerateClusters ( locationsList ) ;
404+
405+ // Log clustering results
406+ Console . WriteLine ( $ "Generated { tourClusters . Count } clusters from { locationsList . Count } locations" ) ;
407+ foreach ( var cluster in tourClusters )
377408 {
378- description = Truncate ( loc . LocationDescription , 50 ) ,
379- lat = Math . Round ( ( double ) loc . Latitude , 6 ) ,
380- lng = Math . Round ( ( double ) loc . Longitude , 6 ) ,
381- userType = loc . UserType ,
382- service = loc . Service == "YouTube" ? "YouTube" : "Twitch"
383- } ) . ToArray ( )
384- } ) . ToArray ( ) ;
409+ Console . WriteLine ( $ " Cluster: { cluster . Count } locations, center ({ cluster . CenterLatitude : F2} , { cluster . CenterLongitude : F2} ), avg distance: { cluster . AverageDistanceFromCenter : F0} km") ;
410+ }
385411
386- // Log tour stops with zoom levels
387- Console . WriteLine ( $ "Tour will visit { tourStops . Length } stops:") ;
388- for ( int i = 0 ; i < tourStops . Length ; i ++ )
389- {
390- Console . WriteLine ( $ " Stop { i + 1 } : { tourStops [ i ] . description } - { tourStops [ i ] . locationCount } locations, zoom level { tourStops [ i ] . zoom } ") ;
412+ // Use the tour service to arrange clusters
413+ var arrangedClusters = TourService . ArrangeClustersByDistance ( tourClusters ) ;
414+
415+ if ( arrangedClusters . Count > 15 )
416+ {
417+ arrangedClusters = arrangedClusters . Take ( 15 ) . ToList ( ) ;
418+ }
419+
420+ tourStops = arrangedClusters . Select ( cluster => new
421+ {
422+ lat = Math . Round ( cluster . CenterLatitude , 6 ) ,
423+ lng = Math . Round ( cluster . CenterLongitude , 6 ) ,
424+ zoom = TourService . DetermineZoomLevel ( cluster , MaxZoom ) ,
425+ description = TourService . GetLocationDescription ( cluster ) ,
426+ locationCount = cluster . Locations . Count ,
427+ locations = cluster . Locations . Take ( 10 ) . Select ( loc => new
428+ {
429+ description = Truncate ( loc . LocationDescription , 50 ) ,
430+ lat = Math . Round ( ( double ) loc . Latitude , 6 ) ,
431+ lng = Math . Round ( ( double ) loc . Longitude , 6 ) ,
432+ userType = loc . UserType ,
433+ service = loc . Service == "YouTube" ? "YouTube" : "Twitch"
434+ } ) . ToArray ( )
435+ } ) . ToArray ( ) ;
436+
437+ // Clear external groups since we're using internal clustering
438+ externalTourGroups . Clear ( ) ;
391439 }
392440
441+ // Log tour stops with zoom levels
442+ Console . WriteLine ( $ "Tour will visit { tourStops . Length } stops") ;
443+
393444 await mapModule . InvokeVoidAsync ( "startTour" , ( object ) tourStops ) ;
394445 IsTourActive = true ;
395446 StateHasChanged ( ) ;
@@ -434,16 +485,35 @@ public async Task OnTourStatusChanged(bool isActive, int currentIndex, int total
434485 {
435486 try
436487 {
437- if ( isActive && currentIndex > 0 && currentIndex <= tourClusters . Count )
488+ if ( isActive && currentIndex > 0 )
438489 {
439- var currentCluster = tourClusters [ currentIndex - 1 ] ;
440- CurrentTourLocation = new ViewerLocationEvent (
441- ( decimal ) Math . Round ( currentCluster . CenterLatitude , 6 ) ,
442- ( decimal ) Math . Round ( currentCluster . CenterLongitude , 6 ) ,
443- TourService . GetLocationDescription ( currentCluster ) )
490+ // Check if using external tour groups or internal clusters
491+ if ( externalTourGroups . Count > 0 && currentIndex <= externalTourGroups . Count )
492+ {
493+ var currentGroup = externalTourGroups [ currentIndex - 1 ] ;
494+ CurrentTourLocation = new ViewerLocationEvent (
495+ ( decimal ) Math . Round ( currentGroup . CenterLatitude , 6 ) ,
496+ ( decimal ) Math . Round ( currentGroup . CenterLongitude , 6 ) ,
497+ currentGroup . RegionName )
498+ {
499+ Id = Guid . NewGuid ( )
500+ } ;
501+ }
502+ else if ( tourClusters . Count > 0 && currentIndex <= tourClusters . Count )
503+ {
504+ var currentCluster = tourClusters [ currentIndex - 1 ] ;
505+ CurrentTourLocation = new ViewerLocationEvent (
506+ ( decimal ) Math . Round ( currentCluster . CenterLatitude , 6 ) ,
507+ ( decimal ) Math . Round ( currentCluster . CenterLongitude , 6 ) ,
508+ TourService . GetLocationDescription ( currentCluster ) )
509+ {
510+ Id = Guid . NewGuid ( )
511+ } ;
512+ }
513+ else
444514 {
445- Id = Guid . NewGuid ( )
446- } ;
515+ CurrentTourLocation = null ;
516+ }
447517 }
448518 else
449519 {
@@ -777,6 +847,7 @@ public async ValueTask DisposeAsync()
777847 TourLocations . Clear ( ) ;
778848 PendingLocationQueue . Clear ( ) ;
779849 tourClusters . Clear ( ) ;
850+ externalTourGroups . Clear ( ) ;
780851 aggregatedMarkers . Clear ( ) ;
781852 locationKeyIndex . Clear ( ) ;
782853 userLocationIndex . Clear ( ) ;
@@ -788,6 +859,31 @@ public async ValueTask DisposeAsync()
788859 }
789860 }
790861
862+ /// <summary>
863+ /// Restarts the tour by stopping any active tour, clearing caches, and starting fresh.
864+ /// Can be called externally to force a tour restart with new groupings.
865+ /// </summary>
866+ public async Task RestartTourAsync ( )
867+ {
868+ Console . WriteLine ( "🔄 RestartTourAsync called - restarting tour with fresh groupings" ) ;
869+
870+ // Stop current tour if active
871+ if ( IsTourActive )
872+ {
873+ await StopMapTour ( ) ;
874+ }
875+
876+ // Clear cached tour data
877+ tourClusters . Clear ( ) ;
878+ externalTourGroups . Clear ( ) ;
879+
880+ // Start new tour if we have locations
881+ if ( TourLocations . Count > 0 )
882+ {
883+ await StartMapTour ( ) ;
884+ }
885+ }
886+
791887 /// <summary>
792888 /// Optional custom icon provider for marker icons.
793889 /// If not provided, default icon logic will be used.
0 commit comments