Weather routing

From Rosetta Code
Weather routing is a draft programming task. It is not yet considered ready to be promoted as a complete task, for reasons that should be found in its talk page.

The weather routing problem has the following parts:

  • a predicted surface wind direction and speed, at increments of longitude, latitude, and time
  • an expected surface current direction and speed, at increments of longitude, latitude, and time
  • 'polar data' describing maximum speed of a sailboat at points of sail for a given speed of wind over water
  • regions for sailing (the open ocean) and not (the land, shallows, restricted areas, etc.)
  • a starting location and time, and a destination

Given the above information and a specific path, progress and arrival time are determined. The weather routing problem, conversely, is to determine the path which results in the earliest arrival time.


Julia

Brute force optimization search, practical for shorter path lengths, but would require a better algorithm for paths over twice this size. <lang julia>module SailingPolars

using DelimitedFiles

export SailingPolar, SurfaceParameters, getpolardata, deg2rad, rad2deg, cartesian2polar export polar2cartesian, haversine, inverse_haversine, boatspeed, bestvectorspeed export sailingspeed, sailsegmenttime

"""

   Structure to represent a polar CSV file's data.

Contains a matrix, speeds, of sailing speeds indexed by wind velocity and angle of boat to wind winds is a list of wind speeds degrees is a list of angles in degrees of direction relative to the wind Note 0.0 degrees is directly into the wind, 180 degrees is directly downwind. """ struct SailingPolar

   winds::Vector{Float32}
   degrees::Vector{Float32}
   speeds::Matrix{Float32} # speeds[wind direction degrees, windspeed knots]

end

"""

   struct SurfaceParameters

Structure that represents wind and surface current direction and velocity for a given position Angles in degrees, velocities in knots """ struct SurfaceParameters

   winddeg::Float32
   windkts::Float32
   currentdeg::Float32
   currentkts::Float32

end

""" function getpolardata(filename)

Read a sailing polar CSV file and return a SailingPolar containg the file data.

A sailing polar file is a CSV file, with ';' used as the comma separator instead of a comma. The first line of file contains labels for the wind velocities that make up columns, and the first entry of each row makes up a column of angle of sailing direction from wind in degrees """ function getpolardata(filename)

   datacells, headercells = readdlm(filename, ';', header=true)
   winds = map(x -> parse(Float32, x), headercells[2:end])
   degrees = datacells[:, 1]
   speeds = datacells[:, 2:end]
   return SailingPolar(winds, degrees, speeds)

end


const R = 6372800 # Earth's approximate radius in meters

"""

   deg2rad(deg)

Convert degrees to radians """ deg2rad(deg) = (deg * π / 180.0 + 2π) % 2π

"""

   rad2deg(rad)

Convert radians to degrees """ rad2deg(rad) = (rad * (180.0 / π) + 360.0) % 360.0

"""

   cartesian2polard(x, y)

Convert x, y coordinates to polar coordinates with angle in degrees """ cartesian2polard(x, y) = sqrt(x * x + y * y), atand(x, y)

"""

   polard2cartesian(r, deg)

Convert polar coordinates in degrees to cartesian x, y coordinates """ polard2cartesian(r, deg) = r .* sincosd(deg)

"""

   function haversine(lat1, lon1, lat2, lon2)

Calculate the haversine function for two points on the Earth's surface.

Given two latitude, longitude pairs in degrees for a point on the Earth, get distance in meters and the initial direction of travel in degrees for movement from point 1 to point 2. """ function haversine(lat1, lon1, lat2, lon2)

   dlat = lat2 - lat1
   dlon = lon2 - lon1
   a = sind(dlat / 2)^2 + cosd(lat1) * cosd(lat2) * sind(dlon / 2)^2
   c = 2.0 * asind(sqrt(a))
   theta = atand(sind(dlon) * cosd(lat2),
       cosd(lat1) * sind(lat2) - sind(lat1) * cosd(lat2) * cosd(dlon))
   theta = (theta + 360) % 360
   return R * c * 0.5399565, theta

end

"""

   function inverse_haversine(lat1, lon1, distance, direction)

Calculate an inverse haversine function.

Takes the point of origin in degrees (latitude, longitude), distance in meters, and initial direction in degrees, and returns the latitude and longitude of the endpoint in degrees after traveling the specified distance. """ function inverse_haversine(lat1, lon1, distance, direction)

   lat2 = asind(sind(lat1) * cos(distance / R) + cosd(lat1) * sin(distance / R) * cosd(direction))
   lon2 = lon1 + atand(sind(direction) * sin(distance / R) * cosd(lat1),
                      cos(distance / R) - sind(lat1) * sind(lat2))
   return lat2, lon2

end

"""

   function boatspeed(sp::SailingPolar, pointofsail, windspeed)

Calculate the expected sailing speed in a specified direction in knots, given sailing polar data, a desired point of sail in degrees, and wind speed in knots """ function boatspeed(sp::SailingPolar, pointofsail, windspeed)

   winds, degrees, speeds = sp.winds, sp.degrees, sp.speeds
   udeg = findlast(t -> t <= pointofsail, degrees)
   odeg = findfirst(t -> t >= pointofsail, degrees)
   uvel = findlast(t -> t <= windspeed, winds)
   ovel = findfirst(t -> t >= windspeed, winds)
   if any(t -> t == nothing, [udeg, odeg, uvel, ovel])
       return -1.0
   end
   frac = (odeg == udeg && uvel == ovel) ? 1.0 :
           (odeg == udeg) ? (windspeed - winds[uvel]) / (winds[ovel] - winds[uvel]) :
           (uvel == ovel) ? (pointofsail - degrees[udeg]) / (degrees[odeg] - degrees[udeg]) :
           ((pointofsail - degrees[udeg]) / (degrees[odeg] - degrees[udeg]) +
           (windspeed - winds[uvel]) / (winds[ovel] - winds[uvel])) / 2
   return speeds[udeg, uvel] + frac * (speeds[odeg, ovel] - speeds[udeg, uvel])

end


"""

   sailingspeed(sp::SailingPolar, azimuth, dir, ws)

Calculate the expected net boat speed in a desired direction (azimuth). This is generally different from the actual boat speed in its actual direction. Directions are in degrees (dir is wind direction), velocity of wind (ws) is in knots. """ sailingspeed(sp, azimuth, dir, ws) = boatspeed(sp, dir, ws) * cosd(abs(dir - azimuth))


"""

   function bestvectorspeed(sp::SailingPolar, dirtravel, dirwind, windspeed, dircur, velcur)

Calculate the net direction and velocity of a sailing ship.

Arguments are sailing polar data, direction of travel in degrees from north, wind direction in degrees from north, wind velocity in knots, surface current direction in degrees, and current velocity in knots. """ function bestvectorspeed(sp::SailingPolar, dirtravel, dirwind, windspeed, dircur, velcur)

   pointofsail = (dirtravel - dirwind) % 360.0
   pointofsail = pointofsail < 0 ? pointofsail + 360.0 : pointofsail
   pointofsail = pointofsail > 180.0 ? 360.0 - pointofsail : pointofsail
   VMG = boatspeed(sp, pointofsail, windspeed)
   other, idx = findmax([sailingspeed(sp, pointofsail, x, windspeed) for x in sp.degrees])
   if other > VMG
       pointofsail = sp.degrees[idx]
       VMG = other
   end
   dirchosen = deg2rad(dirwind + pointofsail)
   wx, wy = VMG * sin(dirchosen), VMG * cos(dirchosen)
   curx, cury = velcur * sin(deg2rad(dircur)), velcur * cos(deg2rad(dircur))
   return rad2deg(atan(wy + cury, wx + curx)), sqrt((wx + curx)^2 + (wy + cury)^2)

end

"""

   function sailsegmenttime(sp::SailingPolar, p::SurfaceParameters, lat1, lon1, lat2, lon2)

Calculate the trip time in minutes from (lat1, lon1) to the destination (lat2, lon2). Uses the data in SurfaceParameters for wind and current velocity and direction. """ function sailsegmenttime(sp::SailingPolar, p::SurfaceParameters, lat1, lon1, lat2, lon2)

   distance, dir = haversine(lat1, lon1, lat2, lon2)
   dir2, vel = bestvectorspeed(sp, dir, p.winddeg, p.windkts, p.currentdeg, p.currentkts)
   endlat2, endlon2 = inverse_haversine(lat1, lon1, distance, dir2)
   # minutes/s * m / (knots * (m/s / knot)) = minutes
   return (1 / 60) * distance / (vel * 1.94384)

end


end # module


module SailingNavigation

export Position, lat, lon, GridPoint, TimeSlice, TimedPath, closestpoint, surround export RoutingProblem, minimumtimeroute

using GeometryTypes using ..SailingPolars

  1. NB: This uses latitude (often considered to be y) first then longitude (often considered to be x).
  2. This latitude, then longitude ordering is as per ISO 6709 (en.wikipedia.org/wiki/ISO_6709)
  1. Position is a Float32 2-tuple of latitude and longitude in degrees

Position = Point2f0

  1. latitude from Position

lat(p::Position) = p[1]

  1. longitude from Position

lon(p::Position) = p[2]

  1. A GridPoint is a Position with the SurfaceParameters of wind and current at the Position

mutable struct GridPoint

   pt::Position
   sp::SurfaceParameters

end

"""

   TimeSlice

A TimeSlice is a matrix of GridPoints, each Position point with their SurfaceParameters A Vector of TimeSlice can give the surface characteristics for an ocean region over time. """ TimeSlice = Matrix{GridPoint}

"""

   mutable struct RoutingProblem

timeinterval: the seconds duration for each TimeSlice timeframe: a vector of sequential timeslices for the ocean region obstacleindices: the Cartesian indices in each timeslice for

   obstacles, such as land or shoals, where the ship may not go

startindex: the timeslice position for time of starting start: starting location on grid of GridPoints finish: destination / finish location on grid of GridPoints allowrepeatvisits: whether the vessel may overlap its prior path, usually false """ mutable struct RoutingProblem

   timeinterval::Float64 # minutes between timeframe slices
   timeframe::Vector{TimeSlice}
   obstacleindices::Vector{Point2{Int}}
   startindex::Int
   start::Point2{Int}
   finish::Point2{Int}
   allowrepeatvisits::Bool

end

"""

   mutable struct TimedPath

duration: minutes total to travel the path path: vector of Cartesian indices of points in grid for path to travel """ mutable struct TimedPath

   duration::Float64
   path::Vector{Point2{Int}}

end

"""

   closestpoint(p, mat)

Get the closest GridPoint in matrix mat to a given position p. p: Cartesian indices of a Position (latitude, longitude in degrees) in grid of GridPoints mat: matrix of Gridpoints """ closestpoint(p, mat) = findmin(gp -> haversine(p[1], p[2], gp.pt[1], gp.pt[2])[1], mat)[2]

function surround(p, mat, excluded)

   neighbors = Point2{Int}[(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]
   (xmax, ymax) = size(mat)
   return filter(q -> 1 <= q[1] <= xmax && 1 <= q[2] <= ymax && !(q in excluded),
       [x + p for x in neighbors])

end

"""

   function minimumtimeroute(rp::RoutingProblem, sp::SailingPolar, verbose=false)

Get the route (as a TimedPath) that minimizes time from start to finish for a given RoutingProblem (sea parameters) given sailing polar data (ship parameters). """ function minimumtimeroute(rp::RoutingProblem, sp::SailingPolar, verbose=false)

   timedpaths = [TimedPath(0.0, [rp.start])]
   completed, mintime, minpath = false, 1000.0, TimedPath(1000.0, [])
   for i in 1:1000
       newpaths = TimedPath[]
       verbose && println("Checking ", length(timedpaths), " paths of length ",
           length(timedpaths[1].path) - 1)
       for tpath in timedpaths
           if tpath.path[end] == rp.finish
               completed = true
               push!(newpaths, tpath)
           else
               p1 = tpath.path[end]
               slice = rp.timeframe[div(Int(round(tpath.duration)), 10) %
                                    length(rp.timeframe) + 1]
               for p2 in surround([p1[1], p1[2]], slice, rp.obstacleindices)
                   !rp.allowrepeatvisits && p2 in tpath.path && continue
                   gp1, gp2 = slice[p1[1], p1[2]], slice[p2[1], p2[2]]
                   lat1, lon1, lat2, lon2 = gp1.pt[1], gp1.pt[2], gp2.pt[1], gp2.pt[2]
                   t = sailsegmenttime(sp, gp1.sp, lat1, lon1, lat2, lon2)
                   path = deepcopy(tpath.path)
                   push!(path, p2)
                   push!(newpaths, TimedPath(tpath.duration + t, path))
               end
           end
       end
       timedpaths = unique(newpaths)
       if completed
           mindur = minimum(map(x -> x.duration, timedpaths))
           finished = filter(x -> x.path[end] == rp.finish, timedpaths)
           minfindur, idx = findmin(map(x -> x.duration, finished))
           verbose && println("Current finished minimum: ", finished[idx], ", others $mindur")
           if mindur == minfindur
               minpath = finished[idx]
               break
           end
       end
   end
   return minpath

end

end # module

using GeometryTypes using .SailingNavigation, .SailingPolars

  1. =

The data is selected so the best time path is slightly longer than the shortest length path. The forbidden regions are x, representing land or reef. The allowed sailing points are . and start and finish are S and F.

x . . F . . x . x . . . . . . . x x x . . x x x . . . . . x x x x . x x x . . . x x . x . x . . . x x . x . . . . . x . . x . x . . . . . . x . . . . S . . . . . =# const forbidden = Point2{Int}.([

   [1, 8], [2, 1], [2, 8], [3, 5], [3, 8], [4, 1], [4, 5], [4, 6], [4, 8], [5, 1],
   [5, 5], [5, 6], [5, 8], [6, 3], [6, 4], [6, 5], [6, 6], [6, 8], [6, 9], [7, 1],
   [7, 4], [7, 5], [7, 6], [8, 8], [8, 9], [9, 1], [9, 7], [9, 9],

])

  1. Create regional wind patterns on the map.

function surfacebylongitude(lon)

   return lon < -155.03 ? SurfaceParameters(-5.0, 8, 150, 0.5) :
          lon < -155.99 ? SurfaceParameters(-90.0, 20, 150, 0.4) :
                          SurfaceParameters(180.0, 25, 150, 0.3)

end

  1. Vary wind speeds over time.

function mutatetimeslices!(slices)

   for (i, slice) in enumerate(slices), x in slice
       x.sp = SurfaceParameters(x.sp.winddeg, x.sp.windkts * (1 + 0.002 * i),
           x.sp.currentdeg, x.sp.currentkts)
   end

end


const startpos = Point2{Int}(1, 4) const endpos = Point2{Int}(9, 4) const pmat = [Position(19.78 - 1/60 + i/60, -155.0 - 5/60 + j/60) for i in 0:8, j in 0:8] const gpoints = map(pt -> GridPoint(pt, surfacebylongitude(lon(pt))), pmat) const slices = [deepcopy(gpoints) for _ in 1:200] mutatetimeslices!(slices)

const routeprob = RoutingProblem(10.0, slices, forbidden, 1, startpos, endpos, false) const filename = "polar.csv" const sp = getpolardata(filename) const tp = minimumtimeroute(routeprob, sp)

println("The route taking the least time found was:\n ", tp.path,

   "\nwhich has duration $(div(tp.duration, 60)) hours, $(rem(tp.duration, 60)) minutes.")

</lang> The polar CSV file used for this solution, named polar.csv, is as follows. Note that this is a very detailed polar, chosen to stress the testing of the code. Most polar files are far smaller, with fewer choices for angle and wind speed.

TWA\TWS;0;4;5;6;7;8;9;10;11;12;13;14;15;16;17;18;19;20;21;22;23;24;25;26;27;28;29;30;35;40;60;70
40;0;0.53;0.54;0.49;0.4;0.31;0.21;0.16;0.11;0.08;0.05;0.03;0.02;0.01;0;0;0;0;0;0;0;0;0;0;0;0;0;0;-0.01;-0.05;-0.1;-0.11
41;0;0.61;0.62;0.56;0.47;0.36;0.25;0.19;0.14;0.1;0.07;0.04;0.02;0.01;0.01;0;0;0;0;0;0;0;0;0;0;0;0;0;0;-0.04;-0.09;-0.1
44;0;0.89;0.91;0.82;0.69;0.56;0.42;0.33;0.24;0.18;0.13;0.08;0.05;0.03;0.02;0.01;0.01;0;0;0;0;0;0;0;0;0;0;0;0;-0.02;-0.06;-0.06
45;0;0.99;1.02;0.92;0.78;0.64;0.49;0.39;0.29;0.22;0.15;0.1;0.07;0.04;0.02;0.01;0.01;0;0;0;0;0;0;0;0;0;0;0;0;-0.01;-0.05;-0.05
46;0;1.11;1.14;1.02;0.87;0.73;0.57;0.45;0.35;0.26;0.18;0.13;0.08;0.05;0.03;0.02;0.01;0.01;0;0;0;0;0;0;0;0;0;0;0;-0.01;-0.04;-0.05
47;0;1.23;1.25;1.14;0.97;0.82;0.66;0.53;0.41;0.31;0.22;0.15;0.1;0.07;0.04;0.02;0.02;0.01;0.01;0;0;0;0;0;0;0;0;0;0;-0.01;-0.03;-0.04
48;0;1.37;1.37;1.26;1.08;0.93;0.76;0.61;0.48;0.36;0.26;0.19;0.13;0.08;0.05;0.03;0.02;0.01;0.01;0.01;0;0;0;0;0;0;0;0;0;0;-0.02;-0.03
49;0;1.5;1.5;1.39;1.2;1.05;0.87;0.71;0.56;0.42;0.31;0.22;0.15;0.1;0.07;0.04;0.03;0.02;0.01;0.01;0;0;0;0;0;0;0;0;0;0;-0.02;-0.02
50;0;1.65;1.64;1.52;1.33;1.18;1;0.81;0.65;0.49;0.37;0.26;0.19;0.13;0.08;0.05;0.04;0.03;0.02;0.01;0.01;0;0;0;0;0;0;0;0;0;-0.01;-0.02
51;0;1.79;1.77;1.67;1.46;1.32;1.13;0.92;0.74;0.57;0.43;0.31;0.22;0.15;0.1;0.07;0.05;0.03;0.02;0.02;0.01;0.01;0;0;0;0;0;0;0;0;-0.01;-0.02
53;0;2.1;2.07;1.99;1.76;1.62;1.4;1.14;0.95;0.74;0.57;0.43;0.31;0.22;0.16;0.1;0.08;0.06;0.04;0.03;0.02;0.01;0.01;0.01;0;0;0;0;0;0;-0.01;-0.01
54;0;2.26;2.22;2.16;1.92;1.78;1.55;1.28;1.06;0.84;0.65;0.5;0.37;0.27;0.19;0.13;0.1;0.07;0.06;0.04;0.03;0.02;0.01;0.01;0;0;0;0;0;0;0;-0.01
55;0;2.43;2.39;2.34;2.09;1.95;1.7;1.42;1.18;0.95;0.74;0.57;0.43;0.32;0.23;0.16;0.12;0.09;0.07;0.05;0.04;0.03;0.02;0.01;0.01;0;0;0;0;0;0;-0.01
60;0;3.29;3.33;3.33;3.08;2.93;2.64;2.29;1.98;1.66;1.36;1.1;0.88;0.68;0.53;0.39;0.32;0.26;0.21;0.17;0.13;0.1;0.07;0.05;0.04;0.03;0.02;0.01;0;0;0;0
70;0;5.2;5.53;5.74;5.59;5.5;5.22;4.84;4.46;3.94;3.51;3.08;2.65;2.26;1.9;1.55;1.38;1.22;1.06;0.92;0.78;0.66;0.55;0.46;0.37;0.3;0.24;0.18;0.03;0;0;0
80;0;6.8;7.43;7.97;8.02;8.23;8.34;8.2;7.9;7.37;6.91;6.43;5.9;5.32;4.72;4.12;3.83;3.55;3.25;2.96;2.67;2.4;2.13;1.88;1.65;1.43;1.22;1.04;0.37;0.09;0.01;0
90;0;7.59;8.5;9.4;9.73;10.4;11.16;11.53;11.56;11.3;11.05;10.77;10.44;9.83;9.07;8.34;8;7.65;7.27;6.88;6.46;6.04;5.61;5.15;4.74;4.33;3.88;3.51;1.72;0.67;0.12;0.03
100;0;7.34;8.25;9.16;9.86;10.5;11.95;12.79;13.5;14.02;14.4;14.37;14.5;14.4;13.92;13.52;13.19;12.79;12.51;12.1;11.66;11.22;10.77;10.26;9.72;9.2;8.58;8.01;4.87;2.51;0.7;0.23
110;0;7.09;7.97;8.84;9.74;10.09;11.85;12.75;13.84;14.99;16.02;16.33;17.1;17.83;17.99;18.32;18.14;17.81;17.84;17.6;17.3;17.05;16.83;16.53;16.03;15.59;15.03;14.37;10.26;6.41;2.32;0.86
120;0;6.59;7.42;8.3;9.1;9.56;10.83;11.6;13.1;13.87;14.66;15.75;16.67;17.63;18.43;19.62;20.17;20.6;21.12;21.55;21.75;21.91;22.07;21.9;21.58;21.29;20.92;20.29;16.47;12.03;5.49;2.26
129;0;6.14;6.93;7.83;8.52;9.09;9.89;10.57;12.42;12.87;13.43;15.23;16.16;17.08;18.07;19.48;20.35;21.22;21.93;22.85;23.44;23.98;24.55;24.59;24.55;24.51;24.46;24;21.56;17.75;9.64;4.25
130;0;6.07;6.87;7.76;8.44;9.02;9.8;10.48;12.29;12.73;13.27;15.08;16.03;16.97;17.96;19.36;20.25;21.15;21.88;22.82;23.44;24.03;24.6;24.66;24.68;24.67;24.64;24.24;22;18.33;10.11;4.5
135;0;5.72;6.57;7.36;8.02;8.65;9.38;10.11;11.52;11.97;12.55;13.85;15.31;16.31;17.33;18.54;19.48;20.35;21.28;22.3;23.08;24.09;24.63;24.69;24.78;24.79;24.91;24.82;23.74;20.98;12.39;5.78
136;0;5.66;6.5;7.28;7.93;8.57;9.3;10.04;11.34;11.82;12.42;13.62;15.06;16.17;17.2;18.35;19.29;20.15;21.12;22.15;22.96;24.07;24.6;24.67;24.76;24.75;24.85;24.81;23.98;21.45;12.8;6.03
139;0;5.42;6.31;6.92;7.67;8.34;9.08;9.86;10.86;11.32;12.03;12.99;14.3;15.73;16.76;17.76;18.71;19.53;20.6;21.66;22.54;23.92;24.44;24.53;24.64;24.58;24.65;24.67;24.47;22.68;13.79;6.73
140;0;5.35;6.22;6.79;7.59;8.26;9;9.8;10.72;11.16;11.89;12.79;14.06;15.5;16.62;17.57;18.51;19.32;20.43;21.49;22.4;23.84;24.36;24.46;24.58;24.51;24.57;24.61;24.56;23.02;14.08;6.96
141;0;5.29;6.12;6.67;7.48;8.18;8.93;9.74;10.57;11.02;11.77;12.62;13.82;15.26;16.47;17.38;18.32;19.04;20.28;21.31;22.07;23.53;24;24.21;24.29;24.43;24.48;24.55;24.61;23.33;14.31;7.18
142;0;5.23;6.02;6.57;7.39;8.1;8.86;9.67;10.43;10.88;11.64;12.45;13.59;15.03;16.24;17.14;18.06;18.77;19.98;21.01;21.75;23.18;23.65;23.86;23.95;24.34;24.39;24.48;24.61;23.61;14.54;7.4
143;0;5.16;5.93;6.45;7.3;8;8.78;9.54;10.27;10.75;11.5;12.28;13.36;14.8;16.01;16.9;17.81;18.5;19.69;20.72;21.43;22.84;23.31;23.52;23.61;24.05;24.27;24.41;24.57;23.85;14.8;7.6
144;0;5.09;5.83;6.33;7.23;7.92;8.66;9.41;10.13;10.62;11.39;12.13;13.13;14.57;15.78;16.65;17.56;18.24;19.41;20.43;21.12;22.5;22.97;23.19;23.28;23.73;24.08;24.33;24.49;24.04;15;7.8
145;0;5.02;5.73;6.23;7.15;7.85;8.55;9.28;9.98;10.51;11.27;11.98;12.92;14.35;15.56;16.42;17.31;17.97;19.13;20.14;20.82;22.17;22.64;22.87;22.96;23.42;23.81;24.23;24.41;24.19;15.14;7.98
146;0;4.96;5.64;6.12;7.07;7.77;8.43;9.15;9.84;10.38;11.16;11.83;12.71;14.12;15.35;16.19;17.07;17.72;18.86;19.86;20.51;21.84;22.31;22.56;22.65;23.12;23.48;23.94;24.33;24.3;15.3;8.16
148;0;4.82;5.45;5.91;6.9;7.59;8.21;8.89;9.55;10.14;10.89;11.55;12.29;13.7;14.92;15.74;16.6;17.23;18.32;19.3;19.91;21.2;21.67;21.95;22.05;22.53;22.87;23.38;24.13;24.39;15.52;8.46
149;0;4.76;5.35;5.81;6.78;7.49;8.09;8.78;9.42;10.01;10.76;11.41;12.1;13.48;14.71;15.52;16.36;16.98;18.06;19.03;19.63;20.89;21.37;21.67;21.77;22.26;22.61;23.12;23.98;24.37;15.57;8.58
150;0;4.69;5.26;5.7;6.67;7.37;7.96;8.64;9.26;9.86;10.6;11.24;11.89;13.26;14.48;15.29;16.11;16.73;17.79;18.74;19.33;20.55;21.04;21.37;21.48;21.98;22.34;22.83;23.69;24.11;15.6;8.67
155;0;4.33;4.74;5.16;6.16;6.79;7.33;7.96;8.51;9.15;9.81;10.4;10.85;12.14;13.37;14.1;14.87;15.48;16.42;17.3;17.8;18.88;19.39;19.86;20;20.54;20.97;21.45;22.25;22.77;15.38;8.89
160;0;4.09;4.41;4.83;5.77;6.39;6.94;7.55;8.04;8.67;9.28;9.83;10.24;11.46;12.69;13.39;14.11;14.73;15.6;16.41;16.87;17.85;18.4;18.97;19.15;19.72;20.2;20.65;21.35;21.84;14.95;8.74
162;0;4;4.29;4.69;5.62;6.23;6.77;7.38;7.86;8.48;9.07;9.6;10;11.18;12.42;13.1;13.81;14.43;15.27;16.06;16.5;17.43;18;18.62;18.81;19.39;19.89;20.33;20.99;21.48;14.76;8.61
168;0;3.74;3.93;4.35;5.15;5.75;6.31;6.93;7.34;7.92;8.45;8.95;9.35;10.44;11.68;12.32;12.99;13.63;14.39;15.11;15.5;16.31;16.94;17.7;17.92;18.53;19.08;19.49;19.99;20.46;14.34;8.3
170;0;3.69;3.85;4.27;5.04;5.65;6.22;6.82;7.23;7.8;8.31;8.8;9.22;10.27;11.51;12.15;12.81;13.45;14.19;14.9;15.28;16.06;16.7;17.51;17.73;18.34;18.91;19.31;19.77;20.22;14.24;8.24
174;0;3.57;3.69;4.11;4.83;5.43;6.01;6.62;7;7.55;8.03;8.5;8.93;9.95;11.19;11.81;12.45;13.11;13.81;14.48;14.84;15.57;16.24;17.11;17.35;17.98;18.57;18.95;19.33;19.77;14.03;8.13
180;0;3.51;3.6;4.03;4.71;5.31;5.91;6.51;6.88;7.41;7.88;8.33;8.79;9.78;11.02;11.63;12.26;12.93;13.61;14.26;14.61;15.31;15.99;16.9;17.15;17.79;18.39;18.77;19.09;19.52;13.87;8.07
Output:
The route taking the least time found was:
    Point{2,Int64}[[1, 4], [1, 5], [2, 6], [3, 7], [4, 7], [5, 7], [6, 7], [7, 7], [8, 6], [8, 5], [9, 4]]
which has duration 4.0 hours, 43.697879668707344 minutes.