Blog

Posts written in September 2024.

  • The Google Maps Distance Matrix API gives us the capability to calculate travel distance and time between multiple locations across different modes of transportation, such as driving walking, or cycling. This is just one of the many other APIs Google provides to allow us to get the most out of location and route related data.

    I needed to use the Google Distance Matrix API (GDMA) to calculate the distance of multiple points of interests (destinations) from one single origin. The dataset of destinations consisted of sixty to one-hundred rows of data containing the following:

    • Title
    • Latitude
    • Longitude

    This dataset would need to be parsed to the GDMA as destinations in order get the information on how far each item was away from the origin. One thing came to light during integration was that the API is limited to only outputting 25 items of distance data per request.

    The limit posed by the GDMA would be fine for the majority of use-cases, but in my case this posed a small problem as I needed to parse the whole dataset of destinations to ensure all points of interests were ordered by the shortest distance.

    The only way I could get around the limits posed by the GDMA was to batch my requests 25 destinations at a time. The dataset of data I would be parsing would never exceed 100 items, so I was fairly confident this would be an adequate approach. However, I cannot be 100% certain what the implications of such an approach would be if you were dealing with thousands of destinations.

    The code below demonstrates a small sample-set of destination data that will be used to calculate distance from a single origin.

    /*
    	Initialise the application functionality.
    */
    const initialise = () => {
    	const destinationData = [
                        {
                          title: "Wimbledon",
                          lat: 51.4273717,
                          long: -0.2444923,
                        },
                        {
                          title: "Westfields Shopping Centre",
                          lat: 51.5067724,
                          long: -0.2289425,
                        },
                        {
                          title: "Sky Garden",
                          lat: 51.3586154,
                          long: -0.9027887,
                        }
                      ];
                      
    	getDistanceFromOrigin("51.7504091", "-1.2888729", destinationData);
    }
    
    /*
    	Processes a list of destinations and outputs distances closest to the origin.
    */
    const getDistanceFromOrigin = (originLat, originLong, destinationData) => {
      const usersMarker = new google.maps.LatLng(originLat, originLong);
      let distanceInfo = [];
      
      if (destinationData.length > 0) {
      	// Segregate dealer locations into batches.
        const destinationBatches = chunkArray(destinationData, 25);
    
        // Make a call to Google Maps in batches.
        const googleMapsRequestPromises = destinationBatches.map(batch => googleMapsDistanceMatrixRequest(usersMarker, batch));
    
        // Iterate through all the aynchronous promises returned by Google Maps batch requests.
        Promise.all(googleMapsRequestPromises).then(responses => {
          const elements = responses.flatMap(item => item.rows).flatMap(item => item.elements);
    
          // Set the distance for each dealer in the dealers data
          elements.map(({ distance, status }, index) => {
            if (status === "OK") {
              destinationData[index].distance = distance.text;
              destinationData[index].distance_value = distance.value;
            }
          });
          
          renderTabularData(destinationData.sort((a, b) => (a.distance_value > b.distance_value ? 1 : -1)));
        })
        .catch(error => {
          console.error("Error calculating distances:", error);
        });
      }
    }
    
    /*
    	Outputs tabular data of distances.
    */
    renderTabularData = (destinationData) => {
    	let tableHtml = "";
      
        tableHtml = `<table>
                        <tr>
                            <th>No.</th>
                            <th>Destination Name</th>
                            <th>Distance</th>
                        </tr>`;
    
    	if (destinationData.length === 0) {
            tableHtml += `<tr colspan="2">
                            <td>No data</td>
                        </tr>`;
      }
      else {
            destinationData.map((item, index) => {
      		        tableHtml += `<tr>
                                    <td>${index+1}</td>
                                    <td>${item.title}</td>
                                    <td>${item.distance}</td>
                                </tr>`;
                });
      }
      
      tableHtml += `</table>`;
      
      document.getElementById("js-destinations").innerHTML = tableHtml;
    }
    
    /*
    	Queries Google API Distance Matrix to get distance information.
    */
    const googleMapsDistanceMatrixRequest = (usersMarker, destinationBatch) => {
      const distanceService = new google.maps.DistanceMatrixService();
      let destinationsLatLong = [];
      
      if (destinationBatch.length === 0) {
      	return;
      }
      
      destinationBatch.map((item, index) => {
        destinationsLatLong.push({
          lat: parseFloat(item.lat),
          lng: parseFloat(item.long),
        });
      });
      
      const request = 
            {
              origins: [usersMarker],
              destinations: destinationsLatLong,
              travelMode: "DRIVING",
            };
    
      return new Promise((resolve, reject) => {
        distanceService.getDistanceMatrix(request, (response, status) => {
          if (status === "OK") {
            resolve(response);
          } 
          else {
            reject(new Error(`Unable to retrieve distances: ${status}`));
          }
        });
      });
    };
    
    /*
    	Takes an array and resizes to specified size.
    */
    const chunkArray = (array, chunkSize) => {
      const chunks = [];
    
      for (let i = 0; i < array.length; i += chunkSize) {
        chunks.push(array.slice(i, i + chunkSize));
      }
    
      return chunks;
    }
    
    /*
    	Load Google Map Distance Data.
    */
    initialise();
    

    The getDistanceFromOrigin() and googleMapsDistanceMatrixRequest() are the key functions that take the list of destinations, batches them into chunks of 25 and returns a tabular list of data. This code can be expanded further to be used alongside visual representation to render each destination as pins on an embedded Google Map, since we have the longitude and latitude points.

    The full working demo can be found via the following link: https://jsfiddle.net/sbhomra/ns2yhfju/. To run this demo, a Google Maps API key needs to be provided, which you will be prompted to enter on load.

  • Published on
    -
    2 min read

    The Silent Blogger

    Everyone has different reasons for blogging. It could be for professional development, knowledge exchange, documenting a personal journey, or just as a form of self-expression. My motive for blogging includes a small portion of each of these reasons, with one major difference: you have to find me.

    I don't go out of my way to promote this small portion of the internet web-sphere that I own. In the past, I experimented with syndicating articles to more prominent blogging media platforms and communities, but it didn't fulfil my expectations or bring any further benefits.

    I've observed that my demeanour mirrors an approach to blogging in that I don't feel the need to go to excessive lengths to disclose my accomplishments or a problems I've solved. This could be due to my age, as I am more comfortable just being myself. I have nothing to prove to anyone.

    A 13th century poet, Rumi, once said:

    In silence, there is eloquence. Stop weaving and see how the pattern improves.

    This quote implies that silence is the source of clarity that allows thoughts to develop naturally and emerge.

    Ever since I stopped the pursuit of recognition and a somewhat futile attempt to force my written words onto others, the natural order has allowed this blog to grow organically. Those who have found me from keyword searches has resulted in better interaction and monetisation (through my Buy Me A Coffee page). Fortunately, since I've made an effort to make this blog as SEO-friendly as possible, my posts appear to perform fairly well across search engines.

    No longer do I stress over feeling the need to write blog posts using the "carrot and stick" approach just to garner more readership. I found I benefit from blogging about the things of interest. It's quality over quantity.

    If you have got this far in this very random admission of silent blogging, you're probably thinking: So what's your point?

    I suppose what I'm trying to say is that it's okay to blog without the expectation of having to promote every single post out to the world in hopes for some recognition. Previously, this was my way of thinking, and I've since realised that I was blogging (for the most part) for the wrong reasons. In one of my posts written in 2019 I was in pursuit to be in the same league as the great bloggers I idolised:

    I look at my blogging heroes like Scott Hanselman, Troy Hunt, Mosh Hamedani and Iris Classon (to name a few) and at times ponder if I will have the ability to churn out great posts on a regular basis with such ease and critical acclaim as they do.

    I've learnt not to be so hard on myself and lessen expectations. When you trade your expectations for appreciation, your whole world changes; even though a sense of achievement feels great, it's far more important to enjoy what you're doing (roughly para-phrasing Tony Robbins here).

    This new perspective has reaffirmed my belief that I have always enjoyed blogging, but being a silent blogger provides a sense of freedom.

  • Published on
    -
    3 min read

    Kentico 13: Cookiebot Blocking Page Builder Scripts

    Cookiebot was added to a Kentico 13 site a few weeks ago resulting in unexpected issues with pages that contained Kentico forms, which led me to believe there is a potential conflict with Kentico Page Builders client-side files.

    As all Kentico Developers are aware, the Page Builder CSS and JavaScript files are required for managing the layout of pages built with widgets as well as the creation and use of Kentico forms consisting of:

    • PageBuilderStyles - consisting CSS files declared in the </head> section of the page code.
    • PageBuilderScripts - consisting of JavaScript files declared before the closing </body> tag.

    In this case, the issue resided with Cookiebot blocking scripts that are generated in code as an extension method or as a Razor Tag Helper.

    <html>
    <body>
        ...
        <!-- Extension Method -->
        @Html.Kentico().PageBuilderScripts()    
        ...
        <!-- Razor Tag Helper -->
        <page-builder-scripts />
        ...
    </body>
    </html>
    

    Depending on the cookie consent given, Kentico Forms either failed on user submission or did not fulfil a specific action, such as, conditional form element visibility or validation.

    The first thing that came to mind was that I needed to configure the Page Builder scripts by allowing it to be ignored by Cookiebot. Cookiebot shouldn't hinder any key site functionality as long as you have configured the consent options correctly to disable cookie blocking for specific client-side scripts via the data-cookieconsent attribute:

    <script data-cookieconsent="ignore">
        // This JavaScript code will run regardless of cookie consent given.
    </script>
    
    <script data-cookieconsent="preferences, statistics, marketing">
        // This JavaScript code will run if consent is given to one or all of options set in "cookieconsent" data attribute.
    </script>
    

    Of course, it's without saying that the data-cookieconsent should be used sparingly - only in situations where you may need the script to execute regardless of consent and have employed alternative ways of ensuring that the cookies are only set after consent has been obtained.

    But how can the Page Builder scripts generated by Kentico be modified to include the cookie consent attribute?

    If I am being honest, the approach I have taken to resolve this issue does not sit quite right with me, as I feel there is a better solution out there I just haven't been able to find...

    Inside the _Layout.cshtml file, I added a conditional statement that checked if the page is in edit mode. If true, the page builder scripts will render normally using the generated output from the Tag Helper. Otherwise, manually output all the scripts from the Tag Helper and assign the data-cookieconsent attribute.

    <html>
    <body>
        ... 
        ...
        @if (Context.Kentico().PageBuilder().EditMode)
        {
            <page-builder-scripts />
        }
        else
        {
            <script src="/_content/Kentico.Content.Web.Rcl/Scripts/jquery-3.5.1.js" data-cookieconsent="ignore"></script>
            <script src="/_content/Kentico.Content.Web.Rcl/Scripts/jquery.unobtrusive-ajax.js" data-cookieconsent="ignore"></script>
            <script type="text/javascript" data-cookieconsent="ignore">
                window.kentico = window.kentico || {};
                window.kentico.builder = {};
                window.kentico.builder.useJQuery = true;
            </script>
            <script src="/Content/Bundles/Public/pageComponents.min.js" data-cookieconsent="ignore"></script>
            <script src="/_content/Kentico.Content.Web.Rcl/Content/Bundles/Public/systemFormComponents.min.js" data-cookieconsent="ignore"></script>
        }
    </body>
    </html>
    

    After the modifications were made, all Kentico Forms were once again fully functional. However, the main disadvantage of this approach is that issues may arise when new hotfixes or major versions are released as the hard-coded script references will require checking.

    If anyone can suggest a better approach to integrating a cookie compliance solution or making modifications to the page builder script output, please leave a comment.

    Useful Information