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