Add workouts
This commit is contained in:
3
Gemfile
3
Gemfile
@@ -23,6 +23,9 @@ gem "puma"
|
|||||||
gem "rake"
|
gem "rake"
|
||||||
gem "slim"
|
gem "slim"
|
||||||
gem "builder"
|
gem "builder"
|
||||||
|
gem "georuby"
|
||||||
|
gem "gnuplot"
|
||||||
|
gem "matrix"
|
||||||
|
|
||||||
gem "httparty"
|
gem "httparty"
|
||||||
gem "redcarpet"
|
gem "redcarpet"
|
||||||
|
@@ -137,6 +137,8 @@ GEM
|
|||||||
ffi (>= 1.0.0)
|
ffi (>= 1.0.0)
|
||||||
rake
|
rake
|
||||||
formatador (1.1.0)
|
formatador (1.1.0)
|
||||||
|
georuby (2.5.2)
|
||||||
|
gnuplot (2.6.2)
|
||||||
guard (2.18.0)
|
guard (2.18.0)
|
||||||
formatador (>= 0.2.4)
|
formatador (>= 0.2.4)
|
||||||
listen (>= 2.7, < 4.0)
|
listen (>= 2.7, < 4.0)
|
||||||
@@ -232,6 +234,7 @@ GEM
|
|||||||
net-imap
|
net-imap
|
||||||
net-pop
|
net-pop
|
||||||
net-smtp
|
net-smtp
|
||||||
|
matrix (0.4.2)
|
||||||
method_source (1.0.0)
|
method_source (1.0.0)
|
||||||
mime-types (3.4.1)
|
mime-types (3.4.1)
|
||||||
mime-types-data (~> 3.2015)
|
mime-types-data (~> 3.2015)
|
||||||
@@ -411,6 +414,8 @@ DEPENDENCIES
|
|||||||
dry-matcher
|
dry-matcher
|
||||||
dry-monads
|
dry-monads
|
||||||
dry-types
|
dry-types
|
||||||
|
georuby
|
||||||
|
gnuplot
|
||||||
guard-puma (~> 0.8)
|
guard-puma (~> 0.8)
|
||||||
hanami (~> 2.0.0)
|
hanami (~> 2.0.0)
|
||||||
hanami-controller (~> 2.0.0)
|
hanami-controller (~> 2.0.0)
|
||||||
@@ -422,6 +427,7 @@ DEPENDENCIES
|
|||||||
httparty
|
httparty
|
||||||
lastfm (~> 1.27)
|
lastfm (~> 1.27)
|
||||||
mail
|
mail
|
||||||
|
matrix
|
||||||
ogpr
|
ogpr
|
||||||
pg
|
pg
|
||||||
pinboard!
|
pinboard!
|
||||||
|
27
app/actions/workouts/create.rb
Normal file
27
app/actions/workouts/create.rb
Normal file
@@ -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
|
13
app/actions/workouts/index.rb
Normal file
13
app/actions/workouts/index.rb
Normal file
@@ -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
|
19
app/commands/workouts/create.rb
Normal file
19
app/commands/workouts/create.rb
Normal file
@@ -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
|
11
app/repos/workout_repo.rb
Normal file
11
app/repos/workout_repo.rb
Normal file
@@ -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
|
12
app/templates/workouts/index.html.slim
Normal file
12
app/templates/workouts/index.html.slim
Normal file
@@ -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"
|
18
app/views/workouts/index.rb
Normal file
18
app/views/workouts/index.rb
Normal file
@@ -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
|
5
config/providers/geo.rb
Normal file
5
config/providers/geo.rb
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
Hanami.app.register_provider :geo, namespace: true do
|
||||||
|
start do
|
||||||
|
register "gpx_parser", Adamantium::Geo::GpxParser.new
|
||||||
|
end
|
||||||
|
end
|
@@ -19,6 +19,7 @@ module Adamantium
|
|||||||
get "/post/top_tracks/:slug", to: "posts.top_tracks"
|
get "/post/top_tracks/:slug", to: "posts.top_tracks"
|
||||||
get "/post/:slug", to: "posts.show"
|
get "/post/:slug", to: "posts.show"
|
||||||
get "/posts", to: "posts.index"
|
get "/posts", to: "posts.index"
|
||||||
|
get "/posts/archive/:year", to: "posts.archive"
|
||||||
|
|
||||||
get "/bookmarks", to: "bookmarks.index"
|
get "/bookmarks", to: "bookmarks.index"
|
||||||
get "/bookmarks/metadata/:id", to: "bookmarks.metadata"
|
get "/bookmarks/metadata/:id", to: "bookmarks.metadata"
|
||||||
@@ -28,6 +29,7 @@ module Adamantium
|
|||||||
get "/places", to: "places.index"
|
get "/places", to: "places.index"
|
||||||
get "/statuses", to: "statuses.index"
|
get "/statuses", to: "statuses.index"
|
||||||
|
|
||||||
|
get "/tags", to: "tags.index"
|
||||||
get "/tagged/:slug", to: "tags.show"
|
get "/tagged/:slug", to: "tags.show"
|
||||||
|
|
||||||
get "/key", to: "key.show" if Hanami.app.settings.micropub_pub_key
|
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/rss", to: "feeds.rss"
|
||||||
get "/feeds/statuses_rss", to: "feeds.statuses_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"
|
get "/:slug", to: "pages.show"
|
||||||
|
|
||||||
redirect "deploying-a-hanami-app-to-fly-io", to: "/post/deploying-a-hanami-20-app-to-flyio"
|
redirect "deploying-a-hanami-app-to-fly-io", to: "/post/deploying-a-hanami-20-app-to-flyio"
|
||||||
|
12
db/migrate/20230424120318_create_workouts.rb
Normal file
12
db/migrate/20230424120318_create_workouts.rb
Normal file
@@ -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
|
51
lib/adamantium/geo/gpx_parser.rb
Normal file
51
lib/adamantium/geo/gpx_parser.rb
Normal file
@@ -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
|
13
lib/adamantium/persistence/relations/workouts.rb
Normal file
13
lib/adamantium/persistence/relations/workouts.rb
Normal file
@@ -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
|
Reference in New Issue
Block a user