Weather routing: Difference between revisions

julia example
No edit summary
(julia example)
Line 10:
 
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.
 
 
=={{header|Julia}}==
<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 SurfaceParameters struct for 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)
 
"""
polard2cartesuan(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
 
# NB: This uses latitude (often considered to be y) first then longitude (often considered to be x).
# This latitude, then longitude ordering is as per ISO 6709 (en.wikipedia.org/wiki/ISO_6709)
 
# Position is a Float32 2-tuple of latitude and longitude in degrees
Position = Point2f0
 
# latitude from Position
lat(p::Position) = p[1]
 
# longitude from Position
lon(p::Position) = p[2]
 
# 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 sequntial 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 # seconds 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, 10_000_000.0, TimedPath(10_000_000.0, [])
for (i, timeframe) in enumerate(rp.timeframe)
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]
for p2 in surround([p1[1], p1[2]], timeframe, rp.obstacleindices)
!rp.allowrepeatvisits && p2 in tpath.path && continue
gp1, gp2 = timeframe[p1[1], p1[2]], timeframe[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
 
#=
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],
])
 
# 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
 
# Vary wind speeds over time.
function mutatetimeslices!(gridpoints)
for (i, x) in enumerate(gridpoints)
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)
mutatetimeslices!(gpoints)
 
const routeprob = RoutingProblem(600.0, fill(gpoints, 200), 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.
<pre style="height:80ex;overflow:scroll;">
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
</pre>
{{out}}
<pre>
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], [9, 5], [9, 4]]
Which has duration 4.0 hours, 39.72104601694747 minutes.
</pre>
4,102

edits