Sponsored by AppSignal
Would you like to see your link here? Contact us
Notesclub

Traveling Salesman Problem

tsp.livemd

Traveling Salesman Problem

Mix.install([
  {:maplibre, "~> 0.1.3"},
  {:kino_maplibre, "~> 0.1.7"},
  {:req, "~> 0.3.0"},
  {:geocalc, "~> 0.8"}
])

Convenience Stuff

alias MapLibre, as: Ml

Base Dataset

Define some set of cities:

cities = {
  {12.568337, 55.676098, "Copenhagen"},
  {-122.272743, 37.871593, "Berkeley"},
  {36.821945, -1.292066, "Nairobi"},
  {151.209290, -33.868820, "Sydney"},
  {116.383331, 39.916668, "Beijing"},
  {8.55, 47.36667, "Zurich"},
  {20.22513, 67.85572, "Kiruna"},
  {-122.335167, 47.608013, "Seattle"},
  {-78.878738, 42.880230, "Buffalo"},
  {-79.347015, 43.651070, "Toronto"},
  {114.062996, 22.542883, "Shenzhen"},
  {23.727539, 37.983810, "Athens"},
  {-0.37739, 39.46975, "Valencia"},
  {4.34878000, 50.85045000, "Brussels"},
  {24.945831, 60.192059, "Helsinki"}
}

What does it look like?

markers =
  cities
  |> Tuple.to_list()
  |> Enum.map(fn {long, lat, _name} -> [{long, lat}] end)

map =
  Ml.new(style: :terrain)
  |> Kino.MapLibre.add_markers(markers)
  |> Kino.MapLibre.add_nav_controls(show_compass: false)
  |> Kino.MapLibre.add_nav_controls(show_zoom: false, position: "top-left")

Shortest Path

Create distance map:

distances =
  Enum.map(
    Tuple.to_list(cities),
    fn {long, lat, _name} ->
      Enum.map(
        Tuple.to_list(cities),
        fn {long2, lat2, _name2} ->
          # "#{name} -> #{name2}"
          Geocalc.distance_between([lat, long], [lat2, long2])
        end
      )
      |> List.to_tuple()
    end
  )
  |> List.to_tuple()

Algorithm itself:

defmodule TSP do
  defp lookup(dist, a, b) do
    elem(elem(dist, a), b)
  end

  defp generate_options([], _done) do
    []
  end

  defp generate_options([head | tail], done) do
    [
      {head, done ++ tail}
    ] ++ generate_options(tail, [head | done])
  end

  def tsp(here, [], dist, current_dist, current_path, best_dist, best_path) do
    new_dist = current_dist + lookup(dist, here, 0)

    cond do
      new_dist < best_dist -> {new_dist, current_path ++ [0]}
      true -> {best_dist, best_path}
    end
  end

  def tsp(here, remaining, dist, current_dist, current_path, best_dist, best_path) do
    options = generate_options(remaining, [])

    List.foldl(options, {best_dist, best_path}, fn {next, rest}, {best_dist, best_path} ->
      new_dist = current_dist + lookup(dist, here, next)

      cond do
        new_dist < best_dist ->
          tsp(next, rest, dist, new_dist, current_path ++ [next], best_dist, best_path)

        true ->
          {best_dist, best_path}
      end
    end)
  end
end

Convenient tuple of city names:

citynames =
  cities
  |> Tuple.to_list()
  |> Enum.map(fn {_long, _lat, name} -> name end)
  |> List.to_tuple()

Run solver (this may take some time):

cityindex = 0..(tuple_size(citynames) - 1) |> Enum.to_list()
result = TSP.tsp(hd(cityindex), tl(cityindex), distances, 0, [hd(cityindex)], nil, nil)

Show Result

Result in textual form:

{best_dist, best_path} = result
IO.puts("Shortest loop (total distance: #{best_dist / 1000} km):")
Enum.each(best_path, fn index -> IO.puts("- #{elem(citynames, index)}") end)

Define path as needed by MapLibre:

path =
  Enum.map(
    best_path,
    fn index ->
      {long, lat, _name} = elem(cities, index)
      {long, lat}
    end
  )

Plot on map:

result_map =
  map
  |> Ml.add_source("route",
    type: :geojson,
    data: [
      type: "Feature",
      geometry: [
        type: "LineString",
        coordinates: path
      ]
    ]
  )
  |> Ml.add_layer(
    id: "route",
    type: :line,
    source: "route",
    layout: [
      line_join: "round",
      line_cap: "round"
    ],
    paint: [
      line_color: "#888",
      line_width: 4
    ]
  )

Note: Maplibre does not draw edges across the pacific.