From c89b5b099857735c7d31fb0acb0744a55d9b466f Mon Sep 17 00:00:00 2001 From: Daniel Nitsikopoulos Date: Tue, 25 Apr 2023 15:56:41 +1000 Subject: [PATCH] Add workouts --- Gemfile | 3 ++ Gemfile.lock | 6 +++ app/actions/workouts/create.rb | 27 ++++++++++ app/actions/workouts/index.rb | 13 +++++ app/commands/workouts/create.rb | 19 +++++++ app/repos/workout_repo.rb | 11 ++++ app/templates/workouts/index.html.slim | 12 +++++ app/views/workouts/index.rb | 18 +++++++ config/providers/geo.rb | 5 ++ config/routes.rb | 7 +++ db/migrate/20230424120318_create_workouts.rb | 12 +++++ lib/adamantium/geo/gpx_parser.rb | 51 +++++++++++++++++++ .../persistence/relations/workouts.rb | 13 +++++ 13 files changed, 197 insertions(+) create mode 100644 app/actions/workouts/create.rb create mode 100644 app/actions/workouts/index.rb create mode 100644 app/commands/workouts/create.rb create mode 100644 app/repos/workout_repo.rb create mode 100644 app/templates/workouts/index.html.slim create mode 100644 app/views/workouts/index.rb create mode 100644 config/providers/geo.rb create mode 100644 db/migrate/20230424120318_create_workouts.rb create mode 100644 lib/adamantium/geo/gpx_parser.rb create mode 100644 lib/adamantium/persistence/relations/workouts.rb diff --git a/Gemfile b/Gemfile index 9e716cf..3c1ab6d 100644 --- a/Gemfile +++ b/Gemfile @@ -23,6 +23,9 @@ gem "puma" gem "rake" gem "slim" gem "builder" +gem "georuby" +gem "gnuplot" +gem "matrix" gem "httparty" gem "redcarpet" diff --git a/Gemfile.lock b/Gemfile.lock index d6f72b9..7db3323 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -137,6 +137,8 @@ GEM ffi (>= 1.0.0) rake formatador (1.1.0) + georuby (2.5.2) + gnuplot (2.6.2) guard (2.18.0) formatador (>= 0.2.4) listen (>= 2.7, < 4.0) @@ -232,6 +234,7 @@ GEM net-imap net-pop net-smtp + matrix (0.4.2) method_source (1.0.0) mime-types (3.4.1) mime-types-data (~> 3.2015) @@ -411,6 +414,8 @@ DEPENDENCIES dry-matcher dry-monads dry-types + georuby + gnuplot guard-puma (~> 0.8) hanami (~> 2.0.0) hanami-controller (~> 2.0.0) @@ -422,6 +427,7 @@ DEPENDENCIES httparty lastfm (~> 1.27) mail + matrix ogpr pg pinboard! diff --git a/app/actions/workouts/create.rb b/app/actions/workouts/create.rb new file mode 100644 index 0000000..02a7497 --- /dev/null +++ b/app/actions/workouts/create.rb @@ -0,0 +1,27 @@ +module Adamantium + module Actions + module Workouts + class Create < Action + include Deps["geo.gpx_parser", "commands.workouts.create"] + + def handle(req, res) + tempfile = Tempfile.new(%w/path .gpx/) + tempfile.write req.params[:file] + tempfile.rewind + + gpxfile = gpx_parser.call(file: tempfile) + + if gpxfile.success? + create.call(**gpxfile.value!) + res.status = 201 + else + res.status = 500 + end + ensure + tempfile.close + tempfile.unlink + end + end + end + end +end diff --git a/app/actions/workouts/index.rb b/app/actions/workouts/index.rb new file mode 100644 index 0000000..0054abc --- /dev/null +++ b/app/actions/workouts/index.rb @@ -0,0 +1,13 @@ +module Adamantium + module Actions + module Workouts + class Index < Action + include Deps["views.workouts.index"] + + def handle(req, res) + res.render index + end + end + end + end +end diff --git a/app/commands/workouts/create.rb b/app/commands/workouts/create.rb new file mode 100644 index 0000000..0363577 --- /dev/null +++ b/app/commands/workouts/create.rb @@ -0,0 +1,19 @@ + +require "securerandom" +require "dry/monads" +require "filemagic" + +module Adamantium + module Commands + module Workouts + class Create < Command + include Deps["repos.workout_repo"] + include Dry::Monads[:result] + + def call(svg:, distance:) + workout_repo.create(path: svg, distance: distance, published_at: Time.now) + end + end + end + end +end diff --git a/app/repos/workout_repo.rb b/app/repos/workout_repo.rb new file mode 100644 index 0000000..4d7346d --- /dev/null +++ b/app/repos/workout_repo.rb @@ -0,0 +1,11 @@ +module Adamantium + module Repos + class WorkoutRepo < Adamantium::Repo[:workouts] + commands :create, update: :by_pk + + def list + workouts.order(:published_at).to_a + end + end + end +end \ No newline at end of file diff --git a/app/templates/workouts/index.html.slim b/app/templates/workouts/index.html.slim new file mode 100644 index 0000000..e832b72 --- /dev/null +++ b/app/templates/workouts/index.html.slim @@ -0,0 +1,12 @@ +div class="mb-12 prose dark:prose-invert max-w-prose mx-auto text-gray-800 dark:text-gray-200" + h1 Hikes + +div class="max-w-prose mx-auto" + - workouts_by_year.each do |year, workouts| + h3= year + h1= "#{(workouts.map{|w| w.distance }.sum / 1000).round} km" + div class="grid grid-cols-3 gap-4 mb-4 max-w-prose mx-auto" + - workouts.each do |workout| + == workout.path + +div class="max-w-screen-md mx-auto border-t-4 border-solid border-gray-400 dark:border-gray-600" diff --git a/app/views/workouts/index.rb b/app/views/workouts/index.rb new file mode 100644 index 0000000..7432467 --- /dev/null +++ b/app/views/workouts/index.rb @@ -0,0 +1,18 @@ +module Adamantium + module Views + module Workouts + class Index < View + include Deps["repos.workout_repo"] + + expose :workouts_by_year do + workout_repo + .list + .group_by { |wo| + wo.published_at.year + } + .sort + end + end + end + end +end diff --git a/config/providers/geo.rb b/config/providers/geo.rb new file mode 100644 index 0000000..132b316 --- /dev/null +++ b/config/providers/geo.rb @@ -0,0 +1,5 @@ +Hanami.app.register_provider :geo, namespace: true do + start do + register "gpx_parser", Adamantium::Geo::GpxParser.new + end +end diff --git a/config/routes.rb b/config/routes.rb index 28dfc26..402dee9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -19,6 +19,7 @@ module Adamantium get "/post/top_tracks/:slug", to: "posts.top_tracks" get "/post/:slug", to: "posts.show" get "/posts", to: "posts.index" + get "/posts/archive/:year", to: "posts.archive" get "/bookmarks", to: "bookmarks.index" get "/bookmarks/metadata/:id", to: "bookmarks.metadata" @@ -28,6 +29,7 @@ module Adamantium get "/places", to: "places.index" get "/statuses", to: "statuses.index" + get "/tags", to: "tags.index" get "/tagged/:slug", to: "tags.show" get "/key", to: "key.show" if Hanami.app.settings.micropub_pub_key @@ -35,6 +37,11 @@ module Adamantium get "/feeds/rss", to: "feeds.rss" get "/feeds/statuses_rss", to: "feeds.statuses_rss" + get "/more", to: "more.index" + + get "/hikes", to: "workouts.index" + post "/workouts", to: "workouts.create" + get "/:slug", to: "pages.show" redirect "deploying-a-hanami-app-to-fly-io", to: "/post/deploying-a-hanami-20-app-to-flyio" diff --git a/db/migrate/20230424120318_create_workouts.rb b/db/migrate/20230424120318_create_workouts.rb new file mode 100644 index 0000000..dc1588e --- /dev/null +++ b/db/migrate/20230424120318_create_workouts.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +ROM::SQL.migration do + change do + create_table :workouts do + primary_key :id + column :path, :text, null: false + column :distance, :float, null: false + column :published_at, :timestamp + end + end +end diff --git a/lib/adamantium/geo/gpx_parser.rb b/lib/adamantium/geo/gpx_parser.rb new file mode 100644 index 0000000..28fe2b9 --- /dev/null +++ b/lib/adamantium/geo/gpx_parser.rb @@ -0,0 +1,51 @@ +require "geo_ruby" +require "geo_ruby/gpx" +require "gnuplot" +require "dry/monads" + +module Adamantium + module Geo + class GpxParser + include Dry::Monads[:result] + + def call(file:) + gpxfile = GeoRuby::Gpx4r::GpxFile.open(file.path) + + x = gpxfile.as_line_string.points.flat_map { |p| p.x } + y = gpxfile.as_line_string.points.flat_map { |p| p.y } + + maxlat = y.max + minlat = y.min + + maxlon = x.max + minlon = x.min + + latdiff = maxlat - minlat + londiff = maxlon - minlon + + svg = Gnuplot.open do |gp| + Gnuplot::Plot.new(gp) do |plot| + plot.arbitrary_lines << "unset border" + plot.arbitrary_lines << "unset xtics" + plot.arbitrary_lines << "unset ytics" + plot.arbitrary_lines << "set size ratio -1" + plot.arbitrary_lines << "set yrange [#{minlat}:#{maxlat}]" if latdiff >= londiff # portrait + plot.arbitrary_lines << "set xrange [#{minlon}:#{maxlon}]" if latdiff < londiff # landscape + plot.arbitrary_lines << "set term svg" + plot.data << Gnuplot::DataSet.new([x, y]) do |ds| + ds.with = "lines" + ds.linewidth = 4 + ds.linecolor = 'rgb "#84cc16"' + ds.notitle + end + end + end + + svg.gsub!('width="600" height="480"', 'width="100%" height="100%"') + # svg.gsub!('viewBox="0 0 600 480"', 'viewBox="0 0 100% 100%"') + + Success({svg: svg, distance: gpxfile.as_line_string.spherical_distance}) + end + end + end +end diff --git a/lib/adamantium/persistence/relations/workouts.rb b/lib/adamantium/persistence/relations/workouts.rb new file mode 100644 index 0000000..9cd5db1 --- /dev/null +++ b/lib/adamantium/persistence/relations/workouts.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Adamantium + module Persistence + module Relations + class Workouts < ROM::Relation[:sql] + schema :workouts, infer: true + + auto_struct(true) + end + end + end +end