Sitecore

SXA Map component Part 4 Show POI markers for the same coordinates

A group of diverse coworkers working on the computer together

Challenge:

Hi Folks! Given the SXA Map component configured on the page and if we have multiple POI items with the same coordinates, then it shows only one of them on the map. Apparently the last one in the content tree. Let’s explore how we can fix it and show all the POI items information on the marker.

This is the fourth post in the series of an SXA Map component.

  1. SXA Map component
  2. SXA Map component Part 2 With Search results and Location Finder
  3. SXA Map component Part 3 Show distance in POI Marker
  4. SXA Map component Part 4 Show POI markers for the same coordinates
  5. SXA Map component Part 5 Filter locations with Radius Filter or Custom Filter Slider components

Scenarios:

  1. A page has only the SXA Map component with the data source having the selected POI item or POI folder in the POI field. This loads all the data on the page load where Sitecore.XA.Feature.Maps.Repositories.MapsRepository is involved in providing the markers output. (Refer SXA Map component for basic setup.)
  2. A page has the SXA Map component where its data source has an empty POI field, the Location Finder component, and the Search Results component. On click of the POI marker icon, an API is called where Sitecore.XA.Foundation.Variants.Abstractions.Controllers.VariantsApiController is involved in providing the markers output. (Refer SXA Map component Part 2 With Search results and Location Finder for basic setup without the search signature.)

To reproduce the issue, set up 2 pages, one for each scenario above. Once the setup is done, pick up any 2 or more POI items and copy one POI item’s Latitude and Longitude field values to other POI items’ respective fields.

As an example, I have a POI Folder as below. Here POI items “Spoke Steele” and “St Elmo Steak House” have the same coordinates.

Same coordinates POIs items

Same coordinates POIs items

Under the Business Locations POI folder, we have 5 POI items but the map shows only 4 POI item information.

Same coordinates POIs Output - Only one POI marker is shown

Same coordinates POIs Output – Only one POI marker is shown

Solution:

The goal is to show the output of POI items with the same coordinates stacked in one marker delivering all the information.

Check out the Map Extension Repository for the implementation shown in this post.

Sitecore - Understanding Development Approaches: A Sitecore Outlook
Understanding Development Approaches: A Sitecore Outlook

Designing, building, and implementing top-notch experiences not only requires a great deal of planning, strategy, and time – it also requires the right digital experience platform (DXP) and the right development approach for your business needs.

Get the Guide

For the first case, the page has only the SXA Map component. In this case, we can override the Sitecore.XA.Feature.Maps.Repositories.MapsRepository as below.

using Sitecore.Data.Items;
using Sitecore.XA.Feature.Maps.Models;
using Sitecore.XA.Feature.Maps.Repositories;
using Sitecore.XA.Foundation.Geospatial.Services;
using Sitecore.XA.Foundation.SitecoreExtensions.Extensions;
using System.Collections.Generic;
using System.Linq;

namespace CustomSXA.Foundation.MapExtension.Repositories
{
    public class ExtendedNativeMapRepository : MapsRepository
    {
        public ExtendedNativeMapRepository(IMapsProvider mapsProvider) : base(mapsProvider)
        {
        }

        protected override void GetPois(Item item, ICollection<Sitecore.XA.Feature.Maps.Models.Poi> result)
        {
            if (item == null)
                return;
            if (item.InheritsFrom(Sitecore.XA.Foundation.Geospatial.Templates.PoiGroup.ID) || item.InheritsFrom(Sitecore.XA.Foundation.Geospatial.Templates.PoiGroupingItem.ID))
            {
                foreach (Item child in item.GetChildren())
                    this.GetPois(child, result);
            }
            else
            {
                if (!item.InheritsFrom(Sitecore.XA.Foundation.Geospatial.Templates.MyLocationPoi.ID) && !item.InheritsFrom(Sitecore.XA.Foundation.Geospatial.Templates.IPoi.ID))
                    return;

                AddPois(item, result);
            }
        }

        private void AddPois(Item item, ICollection<Sitecore.XA.Feature.Maps.Models.Poi> result)
        {
            Poi newPoi = new Poi(item, GetPoiIcon(item), GetPoiVariant(item));

            Poi foundPoi = result.ToList().Find(it => it.Latitude == newPoi.Latitude && it.Longitude == newPoi.Longitude);
            if (foundPoi != null)
            {
                foundPoi.Html = foundPoi.Html + "<hr/>" + newPoi.Html;
                return;
            }
            result.Add(newPoi);
        }
    }
}

Patch the following configuration.

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <services>
      <register patch:instead="register[@implementationType='Sitecore.XA.Feature.Maps.Repositories.MapsRepository, Sitecore.XA.Feature.Maps']" serviceType="Sitecore.XA.Feature.Maps.Repositories.IMapsRepository, Sitecore.XA.Feature.Maps"
                implementationType="CustomSXA.Foundation.MapExtension.Repositories.ExtendedNativeMapRepository, CustomSXA.Foundation.MapExtension" lifetime="Transient"/>
    </services>
  </sitecore>
</configuration>

Build and deploy the DLL and config file to your webroot.

Same Coordinates POIs Output - First case fix

Same Coordinates POIs Output – First case fix

For the second case, the page has the SXA Map component with Location Finder and Search Results.

Since here an API – /sxa/geoVariants is called on click of the POI marker icon, we can customize its implementation as below.

using System;
using System.Collections.Generic;
using Sitecore.ContentSearch.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Mvc.Common;
using Sitecore.Mvc.Presentation;
using Sitecore.XA.Foundation.Abstractions;
using Sitecore.XA.Foundation.Search.Models;
using Sitecore.XA.Foundation.Search.Services;
using Sitecore.XA.Foundation.SitecoreExtensions.Extensions;
using Sitecore.XA.Foundation.SitecoreExtensions.Repositories;
using Sitecore.XA.Foundation.Variants.Abstractions.Models;
using Sitecore.XA.Foundation.Variants.Abstractions.Services;
using System.Net;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Web.Http;

namespace CustomSXA.Foundation.MapExtension.Controllers
{
    public class ExtendedVariantsApiController : ApiController
    {
        private readonly IVariantRenderingService _variantRenderingService;
        private readonly IContentRepository _contentRepository;
        private readonly IOrderingParametersParserService _parametersParsingService;
        private readonly IContext _context;

        public ExtendedVariantsApiController()
        {}

        public ExtendedVariantsApiController(
          IContentRepository contentRepository,
          IVariantRenderingService variantRenderingService,
          IOrderingParametersParserService parametersParsingService,
          IContext context)
        {
            this._variantRenderingService = variantRenderingService;
            this._contentRepository = contentRepository;
            this._parametersParsingService = parametersParsingService;
            this._context = context;
        }

        [HttpGet]
        [ActionName("renderGeospatial")]
        public RenderVariantResult RenderGeospatialVariant(
          string variantId,
          string itemId,
          string coordinates,
          string ordering,
          string pageId = null)
        {
            Item variantItem = this._contentRepository.GetItem(variantId);
            Item contentItem = this._contentRepository.GetItem(itemId);
            Coordinate coordinate = (Coordinate)coordinates;
            string message = this.ValidateRenderVariantGeospatialParameters(variantId, itemId, coordinates, contentItem, variantItem, coordinate);
            if (!string.IsNullOrEmpty(message))
            {
                Log.Error(message, (object)this);
                throw this.StatusCodeException(HttpStatusCode.BadRequest, message);
            }
            try
            {
                this.SetPageContext(pageId == null ? this._contentRepository.GetItem(itemId) : this._contentRepository.GetItem(pageId));
                if (coordinate == null)
                    return this._variantRenderingService.RenderVariant(variantId, itemId);
                IOrderingParametersParserService parametersParsingService = this._parametersParsingService;
                List<string> sortings = new List<string>();
                sortings.Add(ordering);
                string name = this._context.Site.SiteInfo.Name;
                Unit units = parametersParsingService.GetUnits((IEnumerable<string>)sortings, name);

                //include same coordinates locations
                Item[] sameLocationItems = Sitecore.Context.Database.SelectItems($"{contentItem.Parent.Paths.FullPath}//*[@@templatename ='POI' and @Latitude='{contentItem.Fields["Latitude"]}' and @Longitude='{contentItem.Fields["Longitude"]}']");

                RenderVariantResult finalResult = null;
                foreach (var item in sameLocationItems)
                {
                    Dictionary<string, object> parameters = new Dictionary<string, object>()
                    {
                      {
                        "geospatial",
                        (object) new Geospatial(item, coordinate, units)
                      }
                    };
                    if (finalResult == null)
                        finalResult = this._variantRenderingService.RenderVariantWithParameters(variantItem, contentItem, RendererMode.Html, parameters);
                    else
                    {
                        finalResult.Html += "<hr/>" + this._variantRenderingService.RenderVariantWithParameters(variantItem, item, RendererMode.Html, parameters).Html;
                    }
                }
                return finalResult;
            }
            catch (Exception ex)
            {
                Log.Error(ex.Message, (object)this);
                throw this.StatusCodeException(HttpStatusCode.InternalServerError, ex.Message);
            }
            finally
            {
                this.RevertPageContext();
            }
        }
        protected virtual void SetPageContext(Item contextItem) => ContextService.Get().Push<PageContext>(new PageContext()
        {
            Item = contextItem
        });

        protected virtual void RevertPageContext() => ContextService.Get().Pop<PageContext>();

        protected virtual string ValidateRenderVariantGeospatialParameters(
          string variantId,
          string itemId,
          string coordinates,
          Item item,
          Item variantItem,
          Coordinate parsedCoordinates)
        {
            string originalString = this.ValidateRenderVariantParameters(variantId, itemId, item, variantItem);
            if (parsedCoordinates == null)
                originalString = originalString.AppendNewLine("Coordinates cannot be parsed " + coordinates);
            return originalString;
        }

        protected virtual string ValidateRenderVariantParameters(
          string variantId,
          string itemId,
          Item item,
          Item variantItem)
        {
            string originalString = string.Empty;
            if (item == null)
                originalString = originalString.AppendNewLine("Item not found " + itemId);
            if (variantItem == null)
                originalString = originalString.AppendNewLine("Variant item not found " + variantId);
            return originalString;
        }

        protected virtual HttpResponseException StatusCodeException(
          HttpStatusCode statusCode,
          string message)
        {
            return new HttpResponseException(new HttpResponseMessage()
            {
                StatusCode = statusCode,
                Content = (HttpContent)new ObjectContent(typeof(RenderVariantException), (object)new RenderVariantException(message), (MediaTypeFormatter)System.Web.Http.GlobalConfiguration.Configuration.Formatters.JsonFormatter)
            });
        }
    }
}
using Microsoft.Extensions.DependencyInjection;
using Sitecore.DependencyInjection;
using Sitecore.Pipelines;
using Sitecore.Web;
using Sitecore.XA.Foundation.Multisite;
using System;
using System.Linq;
using System.Web.Http;
using System.Web.Routing;

namespace CustomSXA.Foundation.MapExtension.Pipelines.Initialize
{
    public class InitializeRouting
    {
        public void Process(PipelineArgs args)
        {
            foreach (string virtualFolder in ServiceLocator.ServiceProvider.GetService<ISiteInfoResolver>().Sites.Select<SiteInfo, string>((Func<SiteInfo, string>)(s => s.VirtualFolder.Trim('/'))).Distinct<string>())
            {
                string finalVirtualFolder = virtualFolder.Length > 0 ? virtualFolder + "/" : virtualFolder;

                string key = finalVirtualFolder + "withContextSxaGeospatialVariants";
                RemoveHttpRoute(key);
                RouteTable.Routes.MapHttpRoute(finalVirtualFolder + "withContextSxaGeospatialVariants", finalVirtualFolder + "sxa/geoVariants/{variantId}/{itemId}/{coordinates}/{ordering}/{pageId}", (object)new
                {
                    controller = "ExtendedVariantsApi",
                    action = "renderGeospatial"
                });

                key = finalVirtualFolder + "sxaGeospatialVariants";
                RemoveHttpRoute(key);
                RouteTable.Routes.MapHttpRoute(finalVirtualFolder + "sxaGeospatialVariants", finalVirtualFolder + "sxa/geoVariants/{variantId}/{itemId}/{coordinates}/{ordering}", (object)new
                {
                    controller = "ExtendedVariantsApi",
                    action = "renderGeospatial"
                });
            }
        }

        private static void RemoveHttpRoute(string key)
        {
            if (RouteTable.Routes[key] != null)
            {
                RouteTable.Routes.Remove(RouteTable.Routes[key]);
            }
        }
    }
}

Patch the following configuration.

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <initialize>
        <processor patch:after="processor[@type='Sitecore.XA.Foundation.Variants.Abstractions.Pipelines.Initialize.InitializeRouting, Sitecore.XA.Foundation.Variants.Abstractions']" type="CustomSXA.Foundation.MapExtension.Pipelines.Initialize.InitializeRouting, CustomSXA.Foundation.MapExtension" resolve="true" />
      </initialize>
    </pipelines>
    <services>
      <register serviceType="CustomSXA.Foundation.MapExtension.Controllers.ExtendedVariantsApiController, CustomSXA.Foundation.MapExtension" implementationType="CustomSXA.Foundation.MapExtension.Controllers.ExtendedVariantsApiController, CustomSXA.Foundation.MapExtension" lifetime="Transient"/>
    </services>
  </sitecore>
</configuration>

If you have been following the previous posts, then below is the final patch config file.

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <generateScribanContext>
        <processor type="CustomSXA.Foundation.MapExtension.GetGeospatial, CustomSXA.Foundation.MapExtension" resolve="true" />
      </generateScribanContext>
      <initialize>
        <processor patch:after="processor[@type='Sitecore.XA.Foundation.Variants.Abstractions.Pipelines.Initialize.InitializeRouting, Sitecore.XA.Foundation.Variants.Abstractions']" type="CustomSXA.Foundation.MapExtension.Pipelines.Initialize.InitializeRouting, CustomSXA.Foundation.MapExtension" resolve="true" />
      </initialize>
    </pipelines>
    <services>
      <register patch:instead="register[@implementationType='Sitecore.XA.Feature.Maps.Repositories.MapsRepository, Sitecore.XA.Feature.Maps']" serviceType="Sitecore.XA.Feature.Maps.Repositories.IMapsRepository, Sitecore.XA.Feature.Maps"
                implementationType="CustomSXA.Foundation.MapExtension.Repositories.ExtendedNativeMapRepository, CustomSXA.Foundation.MapExtension" lifetime="Transient"/>
      <register serviceType="CustomSXA.Foundation.MapExtension.Controllers.ExtendedVariantsApiController, CustomSXA.Foundation.MapExtension" implementationType="CustomSXA.Foundation.MapExtension.Controllers.ExtendedVariantsApiController, CustomSXA.Foundation.MapExtension" lifetime="Transient"/>
    </services>
  </sitecore>
</configuration>

Build and deploy the DLL and config file to your webroot.

Same Coordinates POIs Output - Second case fix

Same Coordinates POIs Output – Second case fix

Hope this helps!

Check out the SXA Map component Part 5 on how to filter locations with Radius Filter or Custom Filter Slider components.

Happy Sitecore Learning 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Gupta Sandeepkumar

Gupta Sandeepkumar is a Lead Technical Consultant at Perficient. He enjoys research and development work in Sitecore. He has contributed to Sitecore projects based on SXA, ASP.NET MVC, and Web Forms. He has also contributed to Sitecore upgrade projects.

More from this Author

Categories
Follow Us
TwitterLinkedinFacebookYoutubeInstagram