Saturday, December 31, 2022

Sunday, December 18, 2022

What happens to your Bitcoin when you die?

When someone dies, to access their bank account, the executor goes to the bank, shows them the relevant documentation and gets access to the account. Bitcoin works differently because there is no central authority. The owner of the Bitcoins is responsible for the security of their wallet. There are two identifiers associated with a wallet, the public part which anyone can have access to and lets you send and receive bitcoins. It also means anyone can see the transactions in and out of that wallet. Then there is a private key, that only the owner of the wallet should have access to. If anyone else gets access to it, they can steal your Bitcoins.

Which is where things get tricky. When you're alive, you want to keep the private key as secret as possible, so you may decide not to share it with anyone and store it somewhere only you know about. But if you get hit by the proverbial bus tomorrow, those bitcoins are essentially gone if your family have no idea how to access them. 

Let me illustrate with a story. My brother Andy was an early adopter of Bitcoin. In March 2013, he invested a small amount for a few Bitcoins. Then in December 2014, he moved them to another wallet, printed out the details to access that wallet and gave the hard copy to my other brother Simon for safe keeping. All good so far. In June 2014, he moved some Bitcoins back to the original wallet. I'm not sure what he did with those Bitcoins but the original wallet seems to be what he was using for everyday transactions. I'm not sure how much of them he spent, but he mostly seemed to purchase other cryptocurrencies with them.

Jump forward to November 2020. We are in the middle of a pandemic, lots of people locked in their houses have started dabbling in cryptocurrencies, the Bitcoin price has started to take off and Andy moves his remaining Bitcoins to another wallet. I'm not sure why he moved them but in December he moved them again into another wallet. This time it's a bit clearer why. About $17,000 in cryptocurrencies were stolen from his Guarda account, so I think he was spooked by that and moved the Bitcoins in case that wallet had been compromised*.

On to November 2021 and Andy's collection of Bitcoins is now worth about 1,000 times what he originally paid for them. By any reckoning, that is a phenomenal investment. But Andy continues to hold them (or hodl them as the true believers say). 

Onto June 2022. At the start of the month, Bitcoin suffers a big fall in its price and Andy gets a letter from HMRC suggesting he may owe them some tax. Andy had been out of the country for most of the last ten years, but there were some years he would be considered a UK resident. We'll never know which of these, if any, were the cause but on 19th June Andy killed himself. 

After we dealt with the immediate stuff that needed handling (thanks to the British Embassy in Belgrade), we had to sort out Andy's estate. We had the hard copy for what we thought was his Bitcoin holding but when I opened up the wallet, I found it was empty. I could follow the transactions to where the Bitcoins ended up but had no means to access them**. We had his laptop and I was able to get into that***. I looked around there but couldn't find anything. We were able to access some of his email accounts (he had several) since we could reset his password but we weren't able to get into ProtonMail since no recovery option had been set up. 

His Gmail account included a cryptic email sent in January 2021 titled 'KP2' that contained something that looked like a private key. But it wasn't...

Eventually I got lucky and discovered a text file of passwords in his GMail account. Except they weren't full passwords, parts of them were replaced with asterisks and underscores and it was several years old. But now I knew how he stored his passwords, I'd previously been looking for some kind of password manager on his laptop. 

After staring at those partial passwords I realised that they were all kind of similar, a word followed by a number and ending with the service name. And some of those asterisked words looked familiar, they were all places he'd visited. I tried this theory out with a few of the passwords and was able to access a few accounts. I still couldn't access ProtonMail, which was the one I was keen to look at because that seemed to be the email account he was using for important stuff and there was no way to get into it other than knowing the correct password.

I went back to the laptop. This time I found what I was looking for almost immediately. It was the password file, but this was a much more recent version. There were two email passwords suffixed with (PKP1) and two email passwords suffixed with (PKP2). I guessed that meant he'd emailed the private key to himself in two separate emails. Fortunately the latest password file meant I was able to access ProtonMail and find the other part of the private key there. Doubly fortunate because the other email account he had sent it to no longer existed (CTemplar). I tried the two parts together and I had access to his wallet.

But the point is I got lucky. And I'm an IT professional so have some clue what I'm doing. It could have been very different. We could have quite easily ended up in a situation knowing there was a big chunk of money in a wallet that we'd never be able to access. Andy made an incredible investment that he never benefitted from and it could have meant that no-one benefitted from it, because he didn't tell anyone how to access it. Personally I think cryptocurrencies are a complete scam, but if you think they are the future and own them, make sure the people who will inherit them know how to access them.

* This seems to be a recurring problem on cryptocurrency exchanges, people seem to regularly lose their money due to security breaches or the exchange going bust.

** I do find it odd that wallets are anonymous but the transactions between them are visible to anyone.

*** It's not particularly difficult to hack into a Linux machine, just Google it

Saturday, December 03, 2022

GB station usage data 2021-2022

I've uploaded the latest station usage data for 2021-2022. Traveller numbers have partially recovered from the pandemic, but are still some way from their pre-pandemic levels. Probably not surprising since the data is from April 2021 to March 2022 when life had certainly not returned to normal

Monday, November 28, 2022

Land Registry data for October 2022

The server is slowly churning through the latest house price data from the Land Registry. It looks like the number of sales are still falling but prices are starting to head up. Also interesting to note that it is flats that caused most of the downward movement in prices over the last year, houses continued to increase in value

Friday, October 28, 2022

Land Registry data for September 2022

I'm in the process of uploading the latest house price data to the website. It looks like house prices are on the way up again, although they are still negative on an annual basis. I would guess that increasing interest rates and the cost of living crisis will likely cause prices to fall in the coming months.

One thing I haven't seen mentioned about increasing mortgage rates is how the effect of them is very different depending on where you are in your mortgage life. For example, compare the increasing payments for someone with a 25 year mortgage compared to someone with 5 years remaining on their £100,000 mortgage

Number of years Mortgage rate Percentage increase in payment
2% 6%
25 £424 £644 52%
5 £1,753 £1,933 10%
The person with 5 years left on their mortgage is mostly paying down the loan whilst the person with 25 years left is mostly paying interest so their payments increase much more. Once again the younger generation are getting walloped, this time by maths rather than their government.

Sunday, October 23, 2022

GeoJSON data

I've added GeoJSON versions of some of the KML downloads on the website. If they prove popular, I'll add more as time goes on. Let me know if there is anything you'd particularly like to see.

Friday, September 30, 2022

Land Registry data August 2022

I'm uploading the latest Land Registry data to my website. Sale prices continue to fall but more slowly. And right on cue, the government have reduced Stamp Duty in a bid to get prices moving up again. Unfortunately their other budget changes managed to spook the markets meaning mortgage rates are heading up, so it's quite likely they'll be getting less revenue from Stamp Duty whilst also failing to get an election boost.

Sunday, September 25, 2022

Filter segments by Local Legend

A long time ago, one of the users of my Strava Segment Explorer asked for the ability to filter segments by whether they had a Local Legend. Well, now you can! Most segments still don't have the data required so it may not be currently that useful, but over time it should become more helpful. From my own experimentation, it seems the segments that don't have a Local Legend set are ones nobody has ridden recently or that are flagged as hazardous

Monday, September 19, 2022

Downloadable segment lists

I've just added a new feature to the site. When using the segment explorer, you can download a CSV file of the segments you're viewing. The CSV file will include all the segments I know about in the area, which could be more than what is displayed on the map. 

I've limited the maximum area for which the site will return data, mostly to protect my server from malicious or daft users.

The columns returned are currently just what's displayed in the table. If you need other data, let me know. Also let me know if you have any suggestions or find any problems


Thursday, September 01, 2022

Land Registry data July 2022

I'm in the process of uploading the latest house sales data for July 2022 to my website. According to the Land Registry, prices are falling on an annual basis but this article caught my eye which claims prices are going up by 10% per year. The Nationwide base their calculations of a different set of data and they probably calculate the numbers differently to me so it's unlikely they would agree completely, but it's still interesting that the direction of travel is completely different. Still, they think the average price of a house is £273,751, my calculations from the Land Registry put it at £266,796, which is in the same ballpark. Maybe they did some funky calculations to remove the effect of the Stamp Duty holiday from last year?

Thursday, August 25, 2022

Friday, August 05, 2022

GB station usage for 2020-2021

I've updated the GB station usage data for 2020-2021. As you may expect, the usage of stations collapsed during this period of time, something to do with a pandemic, you may remember it.

The data came out last November, but I completely forgot about it. I've now added a reminder for this year and subsequent years.

Thursday, July 28, 2022

Land Registry data for June 2022

I'm uploading the latest property data from the Land Registry to my website. Whilst the price of everything else is going through the roof, the price of houses have started going down for the first time in about ten years. The main reason for this is something I hadn't noticed before. Just before the Stamp Duty holiday ended, sales volumes and prices went ballistic. Those figures have now dropped out of the annual price calculations leading to a big drop in prices. The architect of this classic boom and bust could well be our next PM.

Sunday, July 10, 2022

Land Registry data for May 2022

I've uploaded the latest property sales data to my website. House price inflation seems to be bouncing around close to zero, which I guess is why the government is talking about 50 year mortgages... 

Sunday, June 05, 2022

Land Registry property data for April 2022

I've uploaded the latest property data for England and Wales to my website. The annual figures still show sales falling with prices flat and likely to turn negative next month.

Friday, May 27, 2022

UK postcode data update

I've uploaded the latest UK postcode data to my website. Some people have been asking about the ward boundary changes. These are included in the update but are not complete so I would approach with caution. I made some manual changes so that all postcodes have a ward associated but these will be the old wards in some cases.

From the ONSPD user guide

This release of the NHSPD includes the 2022 ward boundary changes. However, some change orders have yet to be included in OS Boundary-Line including The Cumbria (Structural Changes) Order 2022, The North Yorkshire (Structural Changes) Order 2022 and The Somerset (Structural Changes) Order 2022. Therefore, ward codes E05014171–E05014393 are not yet included. Also, Welsh wards W05001616 –W05001626 in Merthyr Tydfil are yet to be included and Sirhowy ward in Blaenau Gwent should be W05001268 rather than W05000764. In Scotland there is a duplicated ward code – S13003134. This should be S13003134 – Stepps, Chryston and Muirhead in North Lanarkshire and S13002608 – An Taobh Siar agus Nis in Na h-Eileanan Siar

Tuesday, May 03, 2022

House price for England and Wales March 2022

I'm currently uploading the latest house price data to my website. I really need to improve the performance of the import, it now takes days to complete, but it should be complete some time soon. 

There's a definite trend appearing in the data since last summer, sales are falling and prices have peaked. Annual house price inflation is still positive but it looks like it will go negative very shortly, unless the government intervene again. It's also noticeable that the price drops are mostly being driven by the price of flats, although I have no insight into why that would be the case.

Wednesday, April 13, 2022

Converting .ashx handlers to .NET 5/6

I've been trying to figure out how to convert my site to .NET 5 or 6 for a long while. The pages are written in PHP (because that was the only thing available to me on my web host at the time) but there are many .ashx handlers that were added when I originally decided to move to ASP.NET but never got round to completing the job. But the .NET Framework isn't going to be improved or added to in the future so moving to .NET 5/6 needs to happen at some point. It's not a simple process, the differences are fairly major, but I think I've finally figured out a path to making the move.

My main concern was that I wanted to have a common code base so I can reuse code and continue to work on my current site whilst I set up the updated one, rather than rewriting the whole thing from scratch. A rewrite would certainly be simpler but the switch over would almost certainly be a disaster.

The first step was to work on assemblies used by the site. The Portability Analyzer helped here in figuring out how portable my code was (although it's not generally too helpful in explaining what to do with APIs that are not available in .NET 5). Fortunately it wasn't too difficult to move .NET Coords to .NET Standard 2, meaning it could be used from old and new .NET. I used the .NET Upgrade Assistant to convert to the latest SDK project format. Luckily there were cross platform versions of the third party assemblies I use.

One other thing I did was rip out Entity Framework, which I've never been able to get along with and move from the Oracle MySql ADO.NET connector to MySqlConnector. Like everything from Oracle on Windows, their MySql connector is a complete dumpster fire.

Once that was done, I decided the next step was to move all my .ashx handler code into a new assembly. So every .ashx file now looks something like

<%@ WebHandler Language="C#" Class="DoogalCode.AdministrativeAreasCSV" %>

I configured that assembly to target .NET 4.8 and .NET 5, since there would have to be differences between them. At this point, the .NET 4.8 assembly compiled fine (although with many warnings about using things from System.Web even though that's perfectly fine in .NET 4.8!) and the .NET 5 assembly failed with a gazillion errors. 

The next step was to build the .NET 5 assembly without errors. I decided the simplest way to do that was to build .NET 5 only versions of all the things the compiler was complaining about. This meant things like HttpContext, HttpRequest, HttpResponse and IHttpHandler. The properties and methods didn't do anything except throw an exception, I didn't want the code to run, just compile. I'd figure out the details of  how to implement those methods as I went through each handler and got them working in .NET 5.

Once I'd done that, I needed a .NET web site. Since my front end is written in PHP, I decided to take a look at PeachPie, which claims to be a PHP compiler for .NET. And I am mightily impressed. I created a new PeachPie project, copied my PHP files across and it just worked. I had my website running in .NET 5 in a few minutes. Admittedly I don't do anything very complicated in my PHP, so I can't be sure it's perfect but it's certainly perfect for my needs.

But I still had a bunch of .ashx handlers to get working in .NET 5. I added a method to my website startup class that let me use my dummy IHttpHandler and HttpContext classes to call into my handlers

private static void MapHandler<THandler>(IApplicationBuilder app, string path) where THandler : IHttpHandler, new()

{

  app.Map(path, (app2) =>

  {

    app2.Run(async context =>

    {

      var handler = new THandler();

      await Task.Run(() =>

      {

        handler.ProcessRequest(new DoogalCode.HttpContext(context));

      });

    });

  });

}

Then for each handler I mapped URL paths to the handler via this method, like so

MapHandler<CountiesCSV>(app, "/CountiesCSV");

Now I could call my handlers and start to fix up the methods in my dummy classes. Which is where I am currently, going through each handler fixing issues as I find them, whilst still being able to work on my live website. Once that is done and has gone live, I may try to figure out how to finally move my PHP code to proper .NET

Addendum - This all worked out really well. I was able to retarget all my classic .NET code to .NET 5, whilst continuing to keep the site running. Switching over to the new website was relatively painless, with little downtime. Moving to .NET 6 was straightforward. I was then able to finally start replacing PHP pages with .NET Razor pages, which I'm currently working through. I've also recently learned that Microsoft are developing their own adapters for System.Web.HttpContext etc to make the transition easier, which I wish I'd known sooner!

Monday, March 28, 2022

House price data for England and Wales February 2022

I've uploaded the latest Land Registry data to my website. I've also added a "Sale Type" column to the data which shows if the sale is a non-standard sale. These include transfers under a power of sale/repossessions, buy-to-lets, transfers to non-private individuals and sales where the property type is classed as ‘Other’. The aggregated data now ignores non-standard sales, although it has not had a major effect.

Monday, February 28, 2022

Distance to the sea for all postcodes

I've added the approximate distance to the sea for every postcode. This is available on the individual postcode pages and as part of the CSV downloads

Sunday, February 20, 2022

Distance to a line

Probably only of use to me, but I've added a page that lets you find the shortest distance between a postcode and a straight line

Distance to a line (doogal.co.uk)

Saturday, February 19, 2022

UK postcode data update

I've uploaded the latest ONS postcode data to my website. The data has passed my sanity tests, but let me know if you spot anything amiss. The CSV downloads now include a UPRNs column, which is a comma separated list of the UPRNs in the postcode

Friday, January 28, 2022

.NET Coords in .NET Core

.NET Coords is now a .NET Standard 2.0 project, meaning it can be used in .NET Framework 4.8 and .NET Core projects

Saturday, January 22, 2022

UPRN data

I've uploaded UPRN (Unique Property Reference Number) data for the UK to my website. They are only currently available as a link from viewing a particular postcode (an example) but do let me know if it would be useful to provide them in some other way.

Unfortunately the freely available data doesn't include the thing that would be make this data exceedingly useful, the address. I doubt that is likely to happen since there are many companies whose business is based on selling that data

One other major problem is the open data includes old UPRNs but no indication that the UPRN is no longer active, hopefully this will be addressed in a future release.

There's more information on this data here which covers other issues with it.

Saturday, January 01, 2022

C# code to generate a segment FIT file

Someone asked about getting hold of the code to generate FIT files from Strava segments. It's not possible to provide standalone code since it's has dependencies on the rest of my website and a Strava library, but here's what I use which may give people an idea of how to use the rather confusing Garmin FIT API


using System;
using System.Collections.Generic;
using System.IO;
using System.Web;
using Dynastream.Fit;
using Newtonsoft.Json.Linq;
using Strava.Common;
using Strava.Segments;
using Strava.Streams;
public static class FitGenerator { private static void AddLeaderEntry(SegmentPointMesg newRecord, int goal, float currentDistance, float totalDistance, byte goalIndex) { if (goal != 0) { var currentGoalTime = (float)Math.Floor(currentDistance / totalDistance * goal); newRecord.SetLeaderTime(goalIndex, currentGoalTime); } } private static SegmentLeaderboardEntryMesg GetLeaderboard(int goal, byte goalIndex, SegmentLeaderboardType type, string name) { var goalLeaderboard = new SegmentLeaderboardEntryMesg(); if (goal != 0) { goalLeaderboard.SetMessageIndex(goalIndex); goalLeaderboard.SetSegmentTime(goal); goalLeaderboard.SetType(type); if (!string.IsNullOrEmpty(name)) goalLeaderboard.SetName(name); } return goalLeaderboard; } private static int LatOrLngToSemicircle(double latOrLng) { return (int)(latOrLng * (Math.Pow(2, 31) / 180)); } private static bool HasPr(List<SegmentStream> pr, Segment seg) { if (pr != null) return true; if (seg.AthleteSegmentStats != null && seg.AthleteSegmentStats.PrElapsedTime != null) return true; return false; } public static byte[] Generate(Segment seg, int goal, string goalName, int rival, string rivalName, int challenger, string challengerName) { List<SegmentStream> altitude = null; try { StravaBaseHandler.TryStravaRequest((client) => { altitude = client.Streams.GetSegmentStream(seg.Id.ToString(), SegmentStreamType.Altitude | SegmentStreamType.LatLng); }); } catch (Exception ex) { throw new Exception("Failed to get stream for segment " + seg.Id, ex); } // get all the streams we are interested in SegmentStream locationStream = null; SegmentStream altitudeStream = null; SegmentStream distanceStream = null; locationStream = SegmentStream.GetStream(altitude, StreamType.LatLng); altitudeStream = SegmentStream.GetStream(altitude, StreamType.Altitude); distanceStream = SegmentStream.GetStream(altitude, StreamType.Distance); // grab user's best time List<SegmentStream> pr = null; var athlete = HttpContext.Current.Request.Cookies["athlete"]; if (athlete != null) { try { pr = StravaBaseHandler.GetSegmentPr(seg.Id.ToString(), athlete.Value); } catch { // do nothing } } // Generate some FIT messages // Every FIT file MUST contain a 'File ID' message as the first message var fileIdMesg = new FileIdMesg(); var records = new List<SegmentPointMesg>(); fileIdMesg.SetType(Dynastream.Fit.File.Segment); fileIdMesg.SetManufacturer(Manufacturer.Strava); // Types defined in the profile are available fileIdMesg.SetProduct(65534); fileIdMesg.SetTimeCreated(new Dynastream.Fit.DateTime(System.DateTime.Now)); fileIdMesg.SetSerialNumber(1); fileIdMesg.SetNumber(1); var fileCreator = new FileCreatorMesg(); fileCreator.SetHardwareVersion(0); fileCreator.SetSoftwareVersion(0); // segment ID var segmentId = new SegmentIdMesg(); segmentId.SetName(seg.Name); segmentId.SetEnabled(Bool.True); if (seg.ActivityType == "Run") segmentId.SetSport(Sport.Running); else segmentId.SetSport(Sport.Cycling); byte prIndex = 0; byte goalIndex = 1; byte rivalIndex = 2; byte challengerIndex = 3; byte komIndex = 4; byte qomIndex = 5; if (!HasPr(pr, seg)) { goalIndex--; rivalIndex--; challengerIndex--; komIndex--; qomIndex--; } if (goal == 0) { rivalIndex--; challengerIndex--; komIndex--; qomIndex--; } if (rival == 0) { challengerIndex--; komIndex--; qomIndex--; } if (challenger == 0) { komIndex--; qomIndex--; } if (seg.KOM() == 0) { qomIndex--; } segmentId.SetSelectionType(SegmentSelectionType.Starred); segmentId.SetUuid(Guid.NewGuid().ToByteArray()); if (HasPr(pr, seg)) segmentId.SetDefaultRaceLeader(prIndex); if (seg.Map.Polyline == "") throw new Exception("The segment has no map data (which is somewhat unexpected), " + "I can't generate the FIT file"); var points = PolylineDecoder.Decode(seg.Map.Polyline); // figure out SW and NE double swLat = 1000; double swLong = 1000; double neLat = -1000; double neLong = -1000; foreach (var pt in points) { swLat = Math.Min(swLat, pt.Latitude); swLong = Math.Min(swLong, pt.Longitude); neLat = Math.Max(neLat, pt.Latitude); neLong = Math.Max(neLong, pt.Longitude); } // segment info var lap = new SegmentLapMesg(); lap.SetUuid(Guid.NewGuid().ToByteArray()); lap.SetTotalDistance(seg.Distance); lap.SetTotalAscent((ushort)seg.TotalElevationGain); lap.SetSwcLat(LatOrLngToSemicircle(swLat)); lap.SetSwcLong(LatOrLngToSemicircle(swLong)); lap.SetNecLat(LatOrLngToSemicircle(neLat)); lap.SetNecLong(LatOrLngToSemicircle(neLong)); lap.SetMessageIndex(1); lap.SetStartPositionLat(LatOrLngToSemicircle(points[0].Latitude)); lap.SetStartPositionLong(LatOrLngToSemicircle(points[0].Longitude)); lap.SetEndPositionLat(LatOrLngToSemicircle(points[points.Count - 1].Latitude)); lap.SetEndPositionLong(LatOrLngToSemicircle(points[points.Count - 1].Longitude)); // goal entry var goalLeaderboard = GetLeaderboard(goal, goalIndex, SegmentLeaderboardType.Goal, goalName); var rivalLeaderboard = GetLeaderboard(rival, rivalIndex, SegmentLeaderboardType.Rival, rivalName); var challengerLeaderboard = GetLeaderboard(challenger, challengerIndex, SegmentLeaderboardType.Challenger, challengerName); var komLeaderboard = GetLeaderboard(seg.KOM(), komIndex, SegmentLeaderboardType.Kom, ""); var qomLeaderboard = GetLeaderboard(seg.QOM(), qomIndex, SegmentLeaderboardType.Qom, ""); SegmentStream prDistanceStream = null; SegmentStream prTimeStream = null; if (pr != null) { prDistanceStream = SegmentStream.GetStream(pr, StreamType.Distance); prTimeStream = SegmentStream.GetStream(pr, StreamType.Time); } SegmentLeaderboardEntryMesg prLeaderboard = null; if (HasPr(pr, seg)) { prLeaderboard = new SegmentLeaderboardEntryMesg(); prLeaderboard.SetMessageIndex(prIndex); var prTime = seg.AthleteSegmentStats.PrElapsedTime; prLeaderboard.SetSegmentTime(prTime); prLeaderboard.SetType(SegmentLeaderboardType.Pr); } for (var i = 0; i < locationStream.Data.Count; i++) { var newRecord = new SegmentPointMesg(); var latLng = locationStream.Data[i]; if (latLng != null) { var intLat = LatOrLngToSemicircle(Convert.ToDouble(((JArray)(latLng)).First)); newRecord.SetPositionLat(intLat); var intLng = LatOrLngToSemicircle(Convert.ToDouble(((JArray)(latLng)).Last)); newRecord.SetPositionLong(intLng); } if (altitudeStream != null) newRecord.SetAltitude(Convert.ToSingle(altitudeStream.Data[i])); var currentDistance = Convert.ToSingle(distanceStream.Data[i]); newRecord.SetDistance(currentDistance); newRecord.SetMessageIndex((ushort)i); if (HasPr(pr, seg)) { if (pr != null) { // add PR time // find index of the PR item that is at or beyond current distance var pos = prDistanceStream.Data.Count - 1; for (var j = 0; j < prDistanceStream.Data.Count; j++) { if (Convert.ToSingle(prDistanceStream.Data[j]) - Convert.ToSingle(prDistanceStream.Data[0]) >= currentDistance) { pos = j; break; } } // find time at that distance if (pos > 0) { var prStartDistance = Convert.ToSingle(prDistanceStream.Data[pos - 1]) - Convert.ToSingle(prDistanceStream.Data[0]); var prEndDistance = Convert.ToSingle(prDistanceStream.Data[pos]) - Convert.ToSingle(prDistanceStream.Data[0]); var fraction = (currentDistance - prStartDistance) / (prEndDistance - prStartDistance); var prTime = Convert.ToSingle(prTimeStream.Data[pos - 1]) + fraction * (Convert.ToSingle(prTimeStream.Data[pos]) - Convert.ToSingle(prTimeStream.Data[pos - 1])); newRecord.SetLeaderTime(prIndex, (float)Math.Floor(prTime - Convert.ToSingle(prTimeStream.Data[0]))); } else { newRecord.SetLeaderTime(prIndex, 0); } } else { AddLeaderEntry(newRecord, seg.AthleteSegmentStats.PrElapsedTime.Value, currentDistance, seg.Distance, prIndex); } } // add goal time AddLeaderEntry(newRecord, goal, currentDistance, seg.Distance, goalIndex); AddLeaderEntry(newRecord, rival, currentDistance, seg.Distance, rivalIndex); AddLeaderEntry(newRecord, challenger, currentDistance, seg.Distance, challengerIndex); AddLeaderEntry(newRecord, seg.KOM(), currentDistance, seg.Distance, komIndex); AddLeaderEntry(newRecord, seg.QOM(), currentDistance, seg.Distance, qomIndex); records.Add(newRecord); } // Create file encode object var encodeDemo = new Encode(ProtocolVersion.V20); using (var fitDest = new MemoryStream()) { // Write our header encodeDemo.Open(fitDest); // Encode each message, a definition message is automatically generated and output if necessary encodeDemo.Write(fileIdMesg); encodeDemo.Write(fileCreator); encodeDemo.Write(segmentId); if (prLeaderboard != null) encodeDemo.Write(prLeaderboard); if (goal != 0) encodeDemo.Write(goalLeaderboard); if (rival != 0) encodeDemo.Write(rivalLeaderboard); if (challenger != 0) encodeDemo.Write(challengerLeaderboard); if (seg.KOM() != 0) encodeDemo.Write(komLeaderboard); if (seg.QOM() != 0) encodeDemo.Write(qomLeaderboard); encodeDemo.Write(lap); encodeDemo.Write(records); encodeDemo.Close(); fitDest.Flush(); fitDest.Position = 0; return fitDest.ToArray(); } } }