~ihabunek/triglav

807966dcfe01331f22daea2699b70545fff06bc7 — Ivan Habunek a month ago d0a3b6c master
Import HPS tracks data set, add basic UI
A assets/css/_button.scss => assets/css/_button.scss +38 -0
@@ 0,0 1,38 @@
.button {
  align-items: center;
  background-color: whitesmoke;
  border-radius: 3px;
  border: 1px solid gray;
  color: black;
  cursor: pointer;
  display: inline-flex;
  font-size: 0.9rem;
  height: 2.4rem;
  line-height: 1;
  margin-bottom: 1rem;
  padding: 0 1em;
  text-decoration: none;

  &:hover {
    background-color: Gainsboro;
  }

  &[disabled] {
    cursor: not-allowed;
  }
}

.button-group {
  margin-bottom: 1rem;
  display: flex;
  flex-wrap: nowrap;
  align-items: stretch;

  .button {
    margin: 0;
    margin-right: 1px;
    margin-bottom: 1px;
    font-size: 0.9rem;
    flex: 0 0 auto;
  }
}

A assets/css/_form.scss => assets/css/_form.scss +11 -0
@@ 0,0 1,11 @@
form {
  margin-bottom: 1rem;
}

select {
  background-color: white;
  border-radius: 3px;
  border: 1px solid gray;
  height: 2.4rem;
  padding: 0 0.5rem;
}

A assets/css/_utils.scss => assets/css/_utils.scss +22 -0
@@ 0,0 1,22 @@
.tag { color: DimGray; white-space: nowrap; }

.no-margin { margin: 0; }
.no-wrap { white-space: nowrap; }
.inline-block { display: inline-block; }
.hidden { visibility: hidden; }
.w-full { width: 100%; }

.bg-red { background-color: rgba($red, 0.2); }
.bg-green { background-color: rgba($green, 0.2); }

.text-bold { font-weight: bold; }
.text-smaller { font-size: 0.9rem; }

.text-gray { color: DimGray; }
.text-red { color: $red; }
.text-green { color: $green; }

.text-left { text-align: left }
.text-center { text-align: center }
.text-right { text-align: right }
.text-justify { text-align: justify }

M assets/css/app.scss => assets/css/app.scss +5 -28
@@ 1,11 1,13 @@
@import "reset";

// Palette

$red: FireBrick;
$orange: DarkOrange;
$green: ForestGreen;

@import "reset";
@import "button";
@import "form";
@import "utils";

html {
  font-family: monospace;
  font-size: 14px;


@@ 181,28 183,3 @@ ul.breadcrumbs {
    padding: 0 .75rem;
  }
}

// Utils

.tag { color: DimGray; white-space: nowrap; }

.no-margin { margin: 0; }
.no-wrap { white-space: nowrap; }
.inline-block { display: inline-block; }
.hidden { visibility: hidden; }
.w-full { width: 100%; }

.bg-red { background-color: rgba($red, 0.2); }
.bg-green { background-color: rgba($green, 0.2); }

.text-bold { font-weight: bold; }
.text-smaller { font-size: 0.9rem; }

.text-gray { color: DimGray; }
.text-red { color: $red; }
.text-green { color: $green; }

.text-left { text-align: left }
.text-center { text-align: center }
.text-right { text-align: right }
.text-justify { text-align: justify }

A lib/mix/tasks/triglav/import_hps.ex => lib/mix/tasks/triglav/import_hps.ex +13 -0
@@ 0,0 1,13 @@
defmodule Mix.Tasks.Triglav.ImportHps do
  use Mix.Task
  alias Triglav.Import.Hps

  @shortdoc "Imports track data from HPS"

  @impl Mix.Task
  def run(_args) do
    Application.put_env(:triglav, :repo_only, true)
    {:ok, _} = Application.ensure_all_started(:triglav)
    Hps.import_tracks() |> IO.inspect()
  end
end

A lib/triglav/hps.ex => lib/triglav/hps.ex +31 -0
@@ 0,0 1,31 @@
defmodule Triglav.Hps do
  alias Triglav.Repo
  alias Triglav.Schemas.Hps.Track
  import Ecto.Query

  def get_track(id) do
    Repo.get(Track, id)
  end

  def list_tracks(opts \\ []) do
    region = Keyword.get(opts, :region)

    from(t in Track)
    |> maybe_filter_by_region(region)
    |> Repo.all()
  end

  defp maybe_filter_by_region(query, nil), do: query

  defp maybe_filter_by_region(query, region),
    do: where(query, [t], t.region == ^region)

  def track_count_by_region() do
    from(t in Track,
      group_by: :region,
      order_by: [asc: :region],
      select: {t.region, count()}
    )
    |> Repo.all()
  end
end

M lib/triglav/http.ex => lib/triglav/http.ex +2 -2
@@ 3,8 3,8 @@ defmodule Triglav.Http do

  def get(url) do
    with {:ok, {{'HTTP/1.1', 200, 'OK'}, _headers, body}} <-
           :httpc.request(:get, {to_charlist(url), []}, [], []) do
      {:ok, to_string(body)}
           :httpc.request(:get, {to_charlist(url), []}, [], body_format: :binary) do
      {:ok, body}
    end
  end


A lib/triglav/import/hps.ex => lib/triglav/import/hps.ex +85 -0
@@ 0,0 1,85 @@
defmodule Triglav.Import.Hps do
  @moduledoc """
  Import walking tracks from hps.hr
  """

  alias Ecto.Multi
  alias Triglav.Http
  alias Triglav.Schemas.Hps.Track
  alias Triglav.Repo

  @tracks_url "https://www.hps.hr/karta/csv/tracks.csv"

  def import_tracks() do
    with {:ok, tracks_csv} <- Http.get(@tracks_url) do
      tracks =
        tracks_csv
        |> String.trim()
        |> String.replace_leading("", "")
        |> String.split("\n")
        |> Enum.map(&String.trim/1)
        # Some rows are duplicated
        |> Enum.uniq()
        |> Enum.map(&String.split(&1, ";"))
        |> Enum.map(&parse_track/1)

      Multi.new()
      |> Multi.delete_all(:delete, Track)
      |> Multi.insert_all(:insert, Track, tracks, on_conflict: :raise)
      |> Repo.transaction()
    end
  end

  defp parse_track([
         gpx1,
         _,
         name,
         url,
         region,
         route_no,
         gpx2,
         _,
         _,
         _,
         _,
         length_km,
         height_diff,
         walk_time
       ]) do
    %{
      ref: String.replace_trailing(gpx1, ".gpx", ""),
      gpx1_url: "https://www.hps.hr/karta/gpx/#{gpx1}",
      gpx2_url: "https://info.hps.hr/putovi/www/static/gpx/#{gpx2}",
      length: parse_float(length_km),
      name: name,
      region: region,
      route_no: route_no,
      url: url,
      height_diff: parse_int(height_diff),
      walk_time: parse_duration(walk_time)
    }
  end

  defp parse_float(""), do: nil

  defp parse_float(value) do
    {float, ""} = value |> String.replace(",", ".") |> Float.parse()
    float
  end

  defp parse_int(""), do: nil

  defp parse_int(value) do
    {int, ""} = value |> String.replace(",", ".") |> Integer.parse()
    int
  end

  defp parse_duration(""), do: nil

  defp parse_duration(value) do
    [m, s] = String.split(value, ~r"[:.]")
    {m, ""} = Integer.parse(m)
    {s, ""} = Integer.parse(s)
    60 * m + s
  end
end

A lib/triglav/schemas/hps/track.ex => lib/triglav/schemas/hps/track.ex +30 -0
@@ 0,0 1,30 @@
defmodule Triglav.Schemas.Hps.Track do
  use Ecto.Schema

  @type t() :: %__MODULE__{
          __meta__: Ecto.Schema.Metadata.t(),
          ref: String.t(),
          route_no: String.t(),
          name: String.t(),
          region: String.t(),
          url: String.t(),
          length: Decimal.t(),
          height_diff: integer(),
          walk_time: integer(),
          gpx1_url: String.t(),
          gpx2_url: String.t()
        }

  schema "hps_tracks" do
    field :ref, :string
    field :route_no, :string
    field :name, :string
    field :region, :string
    field :url, :string
    field :length, :decimal
    field :height_diff, :integer
    field :walk_time, :integer
    field :gpx1_url, :string
    field :gpx2_url, :string
  end
end

A lib/triglav_web/controllers/hps/tracks_controller.ex => lib/triglav_web/controllers/hps/tracks_controller.ex +19 -0
@@ 0,0 1,19 @@
defmodule TriglavWeb.Hps.TracksController do
  use TriglavWeb, :controller

  alias Triglav.Hps

  def index(conn, params) do
    region = Map.get(params, "region")
    regions = Hps.track_count_by_region()
    tracks = Hps.list_tracks(region: region)

    render(conn, "index.html", tracks: tracks, region: region, regions: regions)
  end

  def detail(conn, %{"id" => id}) do
    track = Hps.get_track(id)

    render(conn, "detail.html", track: track)
  end
end

M lib/triglav_web/router.ex => lib/triglav_web/router.ex +5 -0
@@ 27,6 27,11 @@ defmodule TriglavWeb.Router do
      get "/errors/history", ErrorsController, :index
      get "/errors/history/atom", ErrorsController, :atom
    end

    scope "/hps", alias: Hps, as: :hps do
      get "/tracks", TracksController, :index
      get "/tracks/:id", TracksController, :detail
    end
  end

  # Other scopes may use custom stacks.

A lib/triglav_web/templates/hps/tracks/detail.html.eex => lib/triglav_web/templates/hps/tracks/detail.html.eex +32 -0
@@ 0,0 1,32 @@
<main role="main" class="container">
  <h1>HPS Track <%= @track.route_no %>: <%= @track.name %></h1>

  <table>
    <tr>
        <th>Route</th>
        <td><%= @track.route_no %></td>
    </tr>
    <tr>
        <th>Ref</th>
        <td><%= @track.ref %></td>
    </tr>
    <tr>
        <th>Region</th>
        <td><%= @track.region %></td>
    </tr>
    <tr>
        <th>Length</th>
        <td><%= @track.length %> km</td>
    </tr>
    <tr>
        <th>Height difference</th>
        <td><%= @track.height_diff %> m</td>
    </tr>
    <tr>
        <th>Walk time</th>
        <td><%= @track.walk_time %> min</td>
    </tr>
  </table>

  <p>TODO: map</p>
</main>
\ No newline at end of file

A lib/triglav_web/templates/hps/tracks/index.html.eex => lib/triglav_web/templates/hps/tracks/index.html.eex +63 -0
@@ 0,0 1,63 @@
<main role="main" class="container">
  <h1>HPS tracks</h1>

  <p>Tracks loaded from <a href="https://www.hps.hr/">HPS</a>.</p>

  <%= if length(@regions) > 0 do %>
    <form>
      <select name="region">
        <option value="">-- All --</option>
        <%= for {region, count} <- @regions do %>
          <option value="<%= region %>" <%= if @region === region do %>selected="selected"<% end %>>
            <%= region %> (<%= count %>)
          </option>
        <% end %>
      </select>
      <button class="button" type="submit">
        Go
      </button>
      <a href="?" class="button">
        Clear
      </a>
    </form>
  <% end %>

  <%= if length(@tracks) > 0 do %>
    <table>
      <thead>
        <tr>
          <th>Ref</th>
          <th>Route</th>
          <th>Name</th>
          <th>Region</th>
          <th>Length</th>
          <th>Height diff</th>
          <th>Walk time</th>
          <th></th>
          <th></th>
          <th></th>
        </tr>
      </thead>
      <tbody>
        <%= for track <- @tracks do %>
          <tr>
            <td><%= track.ref %></td>
            <td><%= track.route_no %></td>
            <td><a href="<%= Routes.hps_tracks_path(TriglavWeb.Endpoint, :detail, track.id) %>">
              <%= track.name %></a>
            </td>
            <td><%= track.region %></td>
            <td class="text-right"><%= track.length %> km</td>
            <td class="text-right"><%= track.height_diff %> m</td>
            <td class="text-right"><%= track.walk_time %> min</td>
            <td><a href="<%= track.url %>">www</a></td>
            <td><a href="<%= track.gpx1_url %>">GPX1</a></td>
            <td><a href="<%= track.gpx2_url %>">GPX2</a></td>
          </tr>
        <% end %>
      </tbody>
    </table>
  <% else %>
    <p>No tracks found. Run the import script.</p>
  <% end %>
</main>

A lib/triglav_web/views/hps/tracks_view.ex => lib/triglav_web/views/hps/tracks_view.ex +6 -0
@@ 0,0 1,6 @@
defmodule TriglavWeb.Hps.TracksView do
  use TriglavWeb, :view

  def title("index.html", _), do: "HPS Tracks"
  def title("detail.html", %{track: track}), do: "HPS Track #{track.route_no}: #{track.name}"
end

A priv/repo/migrations/20210313075542_create_hps_tracks.exs => priv/repo/migrations/20210313075542_create_hps_tracks.exs +24 -0
@@ 0,0 1,24 @@
defmodule Triglav.Repo.Migrations.CreateHpsTracks do
  use Ecto.Migration

  def up do
    create table("hps_tracks") do
      add :ref, :text
      add :route_no, :text
      add :name, :text
      add :region, :text
      add :url, :text
      add :length, :decimal
      add :height_diff, :integer
      add :walk_time, :integer
      add :gpx1_url, :text
      add :gpx2_url, :text
    end

    create index("hps_tracks", [:ref], unique: true)
  end

  def down do
    drop table("hps_tracks")
  end
end