MapRun Score Optimal Route Planning

October 5th, 2025

I decided to test out Copilot on another MapRun-related challenge: planning the optimal route for a score event. Our Summer League events are, more often than not, planned using OpenOrienteeringMap. This uses OpenStreetMap data for the base map. The format is usually a 45-minute urban score event, using MapRun’s ScoreNxx scoring system. The aim was to take the KML file that describes an event, and determine the best route to take to maximise the score. As a constraint, I would specify the maximum distance that the route should cover (i.e., how fast the competitor was expected to run).

Travelling Salesperson

Having been given Python as the implementation language, Copilot made short work of the first task: extracting the control coordinates from the KML file and converting them into latitude and longitude. When instructed to find the shortest path that included all of the controls, it first used the Haversine formula to calculate the distance between each pair of controls (taking into account the curvature of the earth). It then called the python-tsp (Travelling Salesperson Problems) package to find the shortest route.

With the number of controls in a typical event being around 30 or so, a dynamic programming algorithm took too long. Simulated annealing (a metaheuristic algorithm) returned in a reasonable length of time. Some judicious weighting was needed to persuade the algorithm to start and finish in the correct places. Copilot wrote more code than it needed, as this library also includes a function for calculating great circle distances.

OpenStreetMap Routing

Of course, we’re not typically interested in straight-line distance between controls. Copilot correctly identified OSMnx as the go-to library here. It will download a section of the OpenStreetMap network data. Copilot initially just wanted to base this on the midpoint of the coordinates and some arbitrary radius. It was eventually persuaded to use a bounding box containing all of the coordinates. There is then a convenient nearest_nodes function to find the nearest point on the network for any given coordinate. The shortest_path function from the NetworkX package was then used to calculate the shortest route between each pair of controls. As before, it used TSP to calculate the optimal route to visit all of the controls.

OSMnx has a convenient plot_graph_routes function that draws the network and one or more routes. This showed up some oddities due to the placement of the controls. The nodes in the OSM network are only where lines terminate, whereas a control might be on a bend in a path halfway between the two ends. I had Copilot switch from using nearest_nodes, to use nearest_edges instead. It would then remove the edge and replace it with two edges that joined at the location of the control. It then took me a while to work out that as edges are directional, I needed to do the same thing for the edge going in the other direction.

The Orienteering Problem

As a planner, this solution could already give some useful insight. Unlike the local night league, we try to ensure that nobody can get easily get all of the controls in the time available. This means even the faster runners have to think about what controls they might leave out. What you really want, though, is for the optimal route to change significantly depending on how far you run, forcing competitors to commit early. Rather than a travelling salesman, we needed a solution to what the academic literature unimaginatively calls “the orienteering problem”. It was at this point that Copilot decided to switch to using Google OR-Tools.

OR-Tools has a significantly more complicated interface. Copilot took a fair amount of prompting to come up with working code, but got there in the end. The following is an example of the output for one of this year’s events. Thankfully, the proposed route didn’t use the M3 motorway that divides the area in two!

Limitations

I’ve placed the final code, such as it is, on GitHub. As the README states, there are still some limitations. From an orienteering perspective, the most significant is that the routes always follow linear features, i.e., there are no shortcuts across open areas. In hillier locations, I’m sure the fact that it doesn’t account for climb would lead to sub-optimal routes.

From a technical perspective, as I discovered when trying it on another event, there are edges in the OSM network that don’t have associated lines. The current splitting logic fails when it encounters one of these. I’m sure that’s fixable.

Summer Orienteering in Slovenia and Italy

August 16th, 2025

We decided to spurn the Scottish 6-Days for our orienteering holiday this year, and instead went south to the OOcup. The event moves around, but this year was taking place on the Slovenia/Italy border. We flew Easyjet to Venice and then hired a car to drive the 200km to Kranjska Gora where we had rented an apartment. (I now know much more about cross-border hire charges than I ever wanted to. For the record, Enterprise was around £25 for the week.)

We were straight into the orienteering the next day, with a 45-minute drive in the direction of Ljubljana. The area was a mixture of large karst depressions and then over a steep slope into rock-strewn terrain. I was incredibly slow in the latter, but my biggest mistake was probably missing an absolutely enormous depression! On the way back, we joined the queue of tourists for the famous Lake Bled.

We’d entered Emma late, and due to transport constraints on the second day (a fleet of minibuses ferrying competitors to the start in convoy), she’d been unable to get an entry. It was probably a good one to miss, as it rained the whole time I was out. A shame, as the Alpine terrain on the Italian side of the border would have been lovely in the sunshine.

The next two days involved getting a bus across the border. On the first, we were grateful for the large, if somewhat crowded marquee, that offered shelter from the rain when we arrived. Thankfully, when we returned the next day, the sun was out again. The area was pretty physical, and I failed to break 10 min/kms on my 6km course (as well as getting stung by a wasp!). On the way back, we stopped at Zelenci, the beauty spot that graced the cover of our Lonely Planet guide.

The last day was, dare I say it, more like the terrain back home. That showed in my result, which was the best of the week: 14th, pulling me up to 25th overall. We then had one more day in Slovenia. Christine and Duncan hired bikes, while I went for a walk. Emma and I then took the free bus to a nearby waterfall.

We relocated to Venice for our last day. We left our stuff at the hotel near the airport and then took the bus into the city. Unfortunately, the thunder that had been rumbling around the plain turned into a torrential downpour, and we were soaked by the time the bus arrived. Still, it meant the streets were nowhere near as busy as they might have been! We’d all dried out sufficiently to enjoy a final dinner beside one of the canals.

The OOCup was a great event, but I’m not sure we’ll be following it to Killarney next year, having been on holiday there in 2024. Christine has plans to make the most of Duncan finishing school early after exams to head to Swiss O Week…

More photos on Flickr.

MapRun League Results Generator

August 2nd, 2025

Southampton Orienteering Club has what is now an annual MapRun league. A few years ago, I wrote a tool to scrape the results for each event, allocate points (only your first attempt counts, and it must be in a specific time window), and publish some HTML results. For example, those from this year (which I might just happen to have won!). For some reason lost in the mists of time, it was written in Node, but I decided that I would rewrite it in Golang before sharing it with the world on GitHub.

When I say “I would rewrite it”, this is 2025, so what I really mean is that I would get some AI to rewrite it for me. In this case, Copilot. It did a pretty good job of it on the first attempt, including some new requirements I’d added about extracting configuration into a separate file. There was one logic error that I had to correct, and then for some reason, it had decided not to bother providing the code to generate the emojis for podium places. To be fair, it even told me it hadn’t done this, and offered to finish the job.

The only subsequent changes were to switch to Kong for CLI parsing (not that there is much) and then the addition of a GitHub workflow to publish release binaries (although how useful they are without signing is debateable). Will anyone else use it? Who knows. Did I manage to push to GitHub the password used to publish results to the SOC website? Perhaps. Have I now changed that password? Definitely!

Dartmoor DofE

July 26th, 2025

Duncan’s Silver Duke of Edinburgh expedition was on Dartmoor, and I was on the hook for driving half the group down early on Saturday morning, and back on Monday. It seemed to make sense to stay down there and make a long weekend of it.

The journey there was remarkably painless, and once they’d been briefed by the leaders, they set off north from Bennett’s Cross. I was due to meet my uncle in Bovey Tracey for lunch, but had time for a quick circular walk past Grimspound. In addition to the obligatory Dartmoor ponies, I also spotted a fox and some llamas (the latter captive!).

My accommodation for the weekend was YHA Okehampton. It hadn’t really dawned on me how far this was from my lunch destination, until Google decided to route me via the outskirts of Exeter.

On Sunday, I headed up on to Okehampton Ranges. The weather was lovely to begin with, but at my furthest point, the windblown rain arrived. The next hour was fairly unpleasant, but it eventually dried out and I was treated to sunshine again as I returned over High Willhays and Yes Tor. After a quick fish and chip supper, I went to watch Jurassic World Rebirth at Okehampton Cinema. I’m not sure that I’ve ever been to the cinema on my own before!

On Monday morning, I went for a run out along the Two Castles Trail, returning over the fields past Meldon Reservoir. I had time for a quick side trip to Castle Drogo, before setting off south again to meet Duncan and friends at Shipley Bridge. They were much quieter in the car on the return journey but seemed to have had a good time.

A few more photos over on Flickr.

Creating a Membership List in Drupal 11 with Aggregating Views

July 9th, 2025

I’ve written before about our use of Drupal for the Southampton Orienteering Club website. We’re now on Drupal 11, and my opinions haven’t really changed. Upgrades are still painful, particularly the community modules that we have to leave behind each time. The user experience for creating content also lags behind newer alternatives. We have a significant amount of historical content on the site (not all of it publicly visible), making a move a daunting proposition. In the meantime, as this post demonstrates, we continue to utilise the powerful features that Drupal and its ecosystem offer.

We had a requirement to provide a membership list for use by the club’s members, which would provide names, approximate home location (to facilitate lift sharing), and a contact mechanism. Previously, it fell to the membership secretary to create this list manually; however, given that nearly all members have an account on the website, it felt like there was a better way.

We already had a permission role that was granted to club members (allowing them access to the members’ area), so it was trivial to create a page that listed all of the website users in that role (and limit access to the list to those in that role). Drupal lets you add custom fields to the user profile. We already have fields for forename and surname, to which I added a location field, which we populated from the old membership list.

User profile

Drupal also has a built-in mechanism for users to contact one another. Users can select the user they wish to contact and provide a message, which is then emailed to the recipient with the originating user as the sender. This has the benefit that users see messages where they are most likely to notice them (in their inbox rather than in some additional system), but without having to expose everyone’s email address to everyone else, which was an area of concern. Better still, users can indicate in their profile whether or not they wish to be contactable.

Contact form

So far, so good. We had a list that showed members’ names, locations, and a link to their contact form if they hadn’t disabled it. The last thing we wanted to add to the list was some additional data for each member, highlighting honorary members, any qualifications (e.g., first aider or coach), and any posts they might hold (e.g., secretary or chair).

We already had a Drupal node type to represent a post, which is then linked to multiple users. This was being used to generate the committee page. I decided to extend this to cover the other scenarios. Drupal views allow you to specify reverse relationships, so for each member, it would retrieve all of the ‘posts’ the member held. Unfortunately, it then renders this as if it were an outer join in SQL, with multiple rows in the table for a member, one for each post.

This is where the Views Aggregator Plus module came to the rescue. Once installed, I could select the “Table with aggregation options” format for my Drupal view. Getting the correct settings was then a bit finicky. I had to add a hidden field with the user’s UUID. I then configured the view to group the post holder relationship using the “Enumerate (sort, no dupl.)” function and group the UUID using “Group and compress” as shown in the following screenshot.

Table with aggregation options settings

The module is significantly more powerful than this. It will, for example, allow you to perform operations such as COUNT, MIN, and MAX on the aggregated rows. That’s maybe for another day!

One further tweak was then needed. The table was styled differently from all of the other tables on the site. Rather than try to replicate that styling, I changed the class in modules/contrib/views_aggregator/templates/views-aggregator-results-table.html.twig from table to views-table.

The final list (or at least the important section of it!) then looks something like the following:

Membership list

Stopping the Git CredentialHelperSelector from popping up

June 24th, 2025

Recently, I was plagued by the “CredentialHelperSelector” dialogue popping up multiple times when attempting to pull from a remote Git repository. This was despite repeatedly selecting the option to remember my selection to use manager and various attempts to explicitly set the config helper via the command line.

In the end, the following command was my saviour:

git config -l --show-origin

It showed that the offending credential.helper=helper-selector was specified in the gitconfig file under the Git install (this being Windows). What you then need to know is that credential.helper is a multi-valued list, and so any changes I was making in my user level .gitconfig were additive. This explains why, once an alternative was specified, I could cancel the numerous selector dialogues, and the operation would still complete successfully.

So, how to avoid those annoying pop-ups? Well, if you can edit that system-level gitconfig just remove the offending entry. Unfortunately, on my locked-down system, that wasn’t an option. The answer, then, is this change, available in Git 2.9 onwards. It allows you to specify an empty helper to clear any existing entries in the list. My .gitconfig now contains the following, and the selector is no more!

[credential]
        helper =
        helper = manager

Updating the symbol set and magentic north with OpenOrienteering Mapper

June 15th, 2025

I spend a couple of hours a week hanging around the leisure centre at Fleming Park while Emma swims. For the past month or so, I’ve been using that time to update the orienteering map of the area, ready for the SOC Summer Series event there in August. The fairways of the old golf course are becoming increasingly overgrown, aided by the planting of lots of new trees. I therefore wanted to update the map to the latest sprint specification, ISSprOM 2019-2, so that I could make use of the ‘rough open with scattered bushes’ symbol. Although it hasn’t shifted much since 2016, I thought it was also time to update magnetic north.

The following directions for OpenOrienteering Mapper (OOM) are based on those I received from the club’s mapping officer, Mark Light.

Updating the symbol set

  1. Download and unzip the latest symbol set from the British Orienteering website.
  2. To make your life easier in step 4, delete any unused symbols from the map.
    1. Right-click on the symbol palette and click Select Symbols > Select Unused.
    2. Right-click on any unused symbol in the palette and select Delete.
  3. Select Symbols > Replace symbol set… and select the
    appropriate scale set of icons from the download in step 1. If, as in the case of this map, the scale doesn’t match, you’ll get a warning.
  4. Provide a mapping for each symbol in the old set to the new.
    1. You can use the Symbol mapping dropdown at the bottom of the dialogue to determine whether it matches by textual name or ID number by default.
    2. Work your way down the list, checking where there is no mapping specified. If the old symbol is something custom that you want to carry across, for example, text for a legend, leave the selection as -None-. Similarly, if you’re not sure what it should translate to, just take a note of the number and leave it as -None-.
    3. Click OK.
  5. Map any symbols you were unsure about
    1. Right-click on each symbol in the symbol window and click Select all objects with this symbol.
    2. If you can now work out what they should be mapped to:
      1. Select the new symbol in the symbol window.
      2. Click the Switch symbol icon in the toolbar.
      3. Right-click on the old symbol and select Delete.
  6. Particularly for any custom symbols you’ve carried across, check that they are still visible on the map. It may be that, as with this map, they have been given a colour that is now lower down the colour table than some symbol that appears above them. Either double-click on the symbol and edit it to use the correct colour from the specification, or select View > Color window and re-order the colours so that the symbols reappear.
  7. If, in step 3, you received a warning about the symbol and map scales not matching, now is the time to fix that.
    1. Select Symbols > Scale all symbols….
    2. Enter the scale percentage. For example, when using 1:4,000 symbols on a 1:5,000 map, enter 80%.
    3. Click OK.

Updating magnetic north

  1. Determine the magnetic declination applicable to your map.
    1. Open this website in a browser.
    2. Drag the marker to the location of your map and note the current magnetic declination. OOM will only accept two decimal places, so don’t worry too much about the exact position of the marker.
  2. Ensure that the map is correctly georeferenced with the correct projection, in our case, the Ordnance Survey British Grid (EPSG 27700). These settings can be found under Map > Georeferencing….
  3. If it doesn’t already exist, create a new ‘part’ in OOM for the map furniture (borders, legend, north lines, and anything that shouldn’t change with magnetic north).
    1. Select Map > Add new part….
    2. Name the part Furniture.
    3. Click OK.
    4. A new dropdown appears in the toolbar showing the currently selected part. Select the default part.
    5. Select the items that make up the furniture, either on the map or via their symbols. Select Map > Move selected objects to > Furniture. Repeat until all of the furniture is in the new part.
    6. Under Map > Georeferencing… enter the declination you retrieved in step 1 and click OK. This will rotate all parts of the map to account for the current position of magnetic north.
    7. Now you need to rotate the furniture back.
      1. Select the Furniture part.
      2. If you don’t already have a grid displayed, select View > Show grid.
      3. Select Tools > Rotate objects and rotate the furniture part to align with the grid.

Helm: for better or worse?

June 9th, 2025

A few weeks ago, one of my colleagues at JUXT gave a presentation on Helm, and this started me thinking back over my own experiences with the tool. It appears I already had a lot to say on the subject back in 2018! Since then, I’ve made extensive use of Helm at CloudBees where we had an umbrella chart to deploy the entire SaaS platform, and at R3. It’s that latter experience that I’m going to talk about in this post.

Helm and Corda

The main Helm chart in question is the one for the R3’s Corda DLT, which you can find on GitHub. The corda.net website has, unfortunately, been sunset, but my blog post describing the rationale for using Helm is still available on the Internet Archive. Another article explains how the chart can be used, along with those for Kafka and Postgres, to spin up a complete Corda deployment quickly.

As an aside, it was a conscious decision not to provide a chart that packaged Corda along with those Kafka and PostgreSQL prereqs. The concern was that customers would take this and deploy it to production without thinking about what a production deployment of Kafka or Postgres entails. Not to mention wanting to make it clear that these were not components that we, as a company, were providing support for.

As a cautionary tale: despite its name, the corda-dev-prereqs chart referenced in that last article (which creates a decidedly non-HA deployment of Kafka and PostgreSQL) found itself being deployed in places it shouldn’t have been…

More Go than YAML

Whilst the consumer experience with the Helm chart was pretty good, things weren’t so rosy on the authoring side. The combined novelty of Kubernetes configuration and Go templating was just too much for many developers. While some did engage, ownership of the chart definitely remained with the DevOps team that authored the initial version, rather than the application developers.

The complexity of the chart also ramped up rapidly. With multiple services requiring almost identical configuration, we soon moved from YAML with embedded Go to Go with embedded YAML! That problem is not unique to Helm; I remember having the same issue with JSPs many moons ago.

The lack of typing, combined with the fact that all functions return strings, started to make the chart fragile, particularly without any good testing of the output with different override values.

Two charts are not better than one

If you look at the GitHub repository, you might wonder why most of the logic for the chart sits in a separate library chart (corda-lib) on which the main corda chart depends. What you can’t see is that we had a separate Helm chart for use by paying customers. This was largely identical to the open-source chart, but included some additional configuration overrides. The library chart was an attempt to share as much logic as possible between the two.

What we couldn’t share was the values.yaml itself and the corresponding JSON schema, and as a consequence, there was always a certain amount of double fixing that went on. What we really needed was a first-class mechanism for extending a chart.

Helm hooks

Although there were other niggles, the last issue I’m going to talk about is the use of Helm hooks. Corda has two mechanisms for bootstrapping PostgreSQL and Kafka: an administrator can use the CLI to generate the required SQL and topic definitions, or the chart can automatically perform the setup when the chart is installed. We expected customers to use the former mechanism, at least in production, but the latter was used in most of our development and testing, and by the services team in pilot projects. The automated approach used a pre-install hook to drive a containerised version of the CLI to perform the setup.

So far, so good. We then started to look at using ArgoCD to deploy the chart. ArgoCD doesn’t install Helm charts directly, instead, it renders the template and then applies the Kubernetes configuration. It does have some understanding of Helm hooks, converting them into ArgoCD waves, but it doesn’t distinguish between install and upgrade hooks. This would lead ArgoCD to try to rerun the setup during an upgrade.

Now, here some responsibility must lie with the Corda team, as those setup commands should have been idempotent, but they weren’t. The answer, for us, was to use an alternative to ArgoCD (worth a separate post), but our customers might not have the luxury of that choice.

Summary

Does all of the above mean that I think Helm is a bad choice? As always, it depends. For ‘packaged’ Kubernetes configuration, I still believe it’s a better choice than requiring consumers to understand your YAML sufficiently to be able to apply suitable modifications with Kustomize. In particular, pushing Kustomize is opening up your support organisation to having to deal with customers basically using any arbitrary YAML to deploy your solution.

In the case of Corda, we underinvested in building the skills to make the best of Helm. Fundamentally, though, I’d suggest that we simply outgrew it. If I were still working on its evolution, the next step would undoubtedly have been to implement an operator and write all of that complicated logic in a language that properly supports testing and reuse.